From 11e3e0b54de7b5ed1dd128c332755a5163dccd6c Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 03:44:24 +0900
Subject: [PATCH 01/12] =?UTF-8?q?refactor(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=EB=AA=A8=EB=8B=AC=EC=9D=84=20overlay=20?=
=?UTF-8?q?=EA=B8=B0=EB=B0=98=20API=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88?=
=?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../detail/DetailTodoModalContainer.tsx | 119 +++++++++---------
1 file changed, 58 insertions(+), 61 deletions(-)
diff --git a/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx b/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
index c506c341..404b076f 100644
--- a/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
+++ b/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
@@ -1,13 +1,11 @@
"use client";
-import { useTranslations } from "next-intl";
-import { useState } from "react";
+import { overlay } from "overlay-kit";
import type { TodoUpdateRequest } from "@/api/generated/models";
import type { ReactNode } from "react";
import { useGetTodoDetail } from "@/api/generated/endpoints/todo/todo";
-import { AnimatedToast } from "@/components/toast/AnimatedToast";
import { DetailTodoModalContent } from "@/components/todo-modal/detail/DetailTodoModalContent";
import { useDeleteTodoSubmit } from "@/hooks/todo-modal/detail/use-delete-todo-submit";
import { useUpdateTodoSubmit } from "@/hooks/todo-modal/detail/use-update-todo-submit";
@@ -21,86 +19,85 @@ export interface DetailTodoModalContainerProps {
children: (openDetailTodoModal: () => void) => ReactNode;
}
-export const DetailTodoModalContainer = ({
+interface DetailTodoModalQueryProps extends Omit<
+ DetailTodoModalContainerProps,
+ "children"
+> {
+ isOpen: boolean;
+ onClose: () => void;
+ onExited: () => void;
+}
+
+const DetailTodoModalQuery = ({
todoId,
date,
+ isOpen,
+ onClose,
+ onExited,
onTogglePlay,
onToggleCompleted,
onDelete,
- children,
-}: DetailTodoModalContainerProps) => {
- const tToast = useTranslations("Toast");
- const [isOpen, setIsOpen] = useState(false);
- const [isMounted, setIsMounted] = useState(false);
- const [updateErrorMessage, setUpdateErrorMessage] = useState(
- null,
- );
- const { data, isError } = useGetTodoDetail(
- todoId,
- { date },
- { query: { enabled: isMounted } },
- );
+}: DetailTodoModalQueryProps) => {
+ const { data, isError } = useGetTodoDetail(todoId, { date });
const { handleDelete } = useDeleteTodoSubmit();
const { handleUpdate } = useUpdateTodoSubmit();
const todo = data?.data;
- const openDetailTodoModal = () => {
- setIsMounted(true);
- setIsOpen(true);
- };
-
- const closeDetailTodoModal = () => {
- setIsOpen(false);
- };
+ if (isError || !todo) return null;
const deleteTodo = () => {
handleDelete(todoId, {
onSuccess: () => {
onDelete();
- closeDetailTodoModal();
+ onClose();
},
});
};
const updateTodo = (updateData: TodoUpdateRequest) => {
- handleUpdate(
- {
- todoId,
- date,
- data: updateData,
- },
- {
- onError: (error) => {
- setUpdateErrorMessage(
- error.response?.data.message ?? tToast("todoUpdateFailed"),
- );
- },
- },
- );
+ handleUpdate({
+ todoId,
+ date,
+ data: updateData,
+ });
};
return (
- <>
- {children(openDetailTodoModal)}
-
- {isMounted && !isError && todo ? (
- setIsMounted(false)}
- todo={todo}
- onTogglePlay={onTogglePlay}
- onToggleCompleted={onToggleCompleted}
- onDelete={deleteTodo}
- onUpdate={updateTodo}
- />
- ) : null}
+
+ );
+};
- setUpdateErrorMessage(null)}
- message={updateErrorMessage ?? ""}
+export const DetailTodoModalContainer = ({
+ todoId,
+ date,
+ onTogglePlay,
+ onToggleCompleted,
+ onDelete,
+ children,
+}: DetailTodoModalContainerProps) => {
+ const openDetailTodoModal = () => {
+ overlay.open(({ isOpen, close, unmount }) => (
+
- >
- );
+ ));
+ };
+
+ return <>{children(openDetailTodoModal)}>;
};
From fef23fd982a94b592e51e0a86af29c1fca5ca0fd Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 03:45:19 +0900
Subject: [PATCH 02/12] =?UTF-8?q?refactor(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD?=
=?UTF-8?q?=EC=9D=84=20=EB=B3=80=EA=B2=BD=20=ED=95=84=EB=93=9C=20=EB=8B=A8?=
=?UTF-8?q?=EC=9C=84=EB=A1=9C=20=EC=A0=84=EC=86=A1=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../detail/DetailTodoModalContent.tsx | 89 +++++++++++--------
.../todo-modal/detail/use-detail-todo-form.ts | 33 +------
.../utils/todo/detail-todo-update-request.ts | 89 +++++--------------
3 files changed, 74 insertions(+), 137 deletions(-)
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
index dad126c1..32b858fa 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
@@ -1,7 +1,7 @@
import { DeleteIcon, TrashOnIcon } from "@repo/timo-design-system/icons";
import { TodoToolbar } from "@repo/timo-design-system/ui";
import { useTranslations } from "next-intl";
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type {
TodoDetailResponse,
@@ -21,13 +21,18 @@ import { DetailTodoTaskFields } from "@/components/todo-modal/detail/DetailTodoT
import {
DETAIL_TODO_TIME_OPTIONS,
DETAIL_TODO_WEEKDAY_IDS,
- type DetailTodoUpdateRequestOverrides,
useDetailTodoForm,
} from "@/hooks/todo-modal/detail/use-detail-todo-form";
-import { formatShortDateLabel } from "@/utils/date/date";
+import { formatDateKey, formatShortDateLabel } from "@/utils/date/date";
+import {
+ buildDetailTodoSubtasksUpdateRequest,
+ buildDetailTodoTextUpdateRequest,
+ isTodoUpdateRepeatWeekday,
+} from "@/utils/todo/detail-todo-update-request";
+import { convertTimeTextToDurationSeconds } from "@/utils/todo/todo-time";
const DETAIL_TODO_MEMO_MAX_LENGTH = 300;
-const TEXT_UPDATE_DEBOUNCE_MS = 3000;
+const TEXT_UPDATE_DEBOUNCE_MS = 2000;
type DetailTodoWeekdayId = (typeof DETAIL_TODO_WEEKDAY_IDS)[number];
const isDetailTodoWeekdayId = (
@@ -62,7 +67,6 @@ export const DetailTodoModalContent = ({
const detailTodoForm = useDetailTodoForm({ todo });
const [selectedTime, setSelectedTime] = useState();
const [isIconPanelOpen, setIsIconPanelOpen] = useState(false);
- const canUpdateTodo = todo.timerStatus === "STOPPED";
const dateNumber = detailTodoForm.date.getDate();
const dayOfWeek = isDetailTodoWeekdayId(todo.dayOfWeek)
? todo.dayOfWeek
@@ -72,7 +76,16 @@ export const DetailTodoModalContent = ({
label: tCommon(`weekday.${weekdayId}`),
}));
const latestOnUpdateRef = useRef(onUpdate);
- const latestBuildUpdateRequestRef = useRef(detailTodoForm.buildUpdateRequest);
+ const buildTextUpdateRequest = useCallback(
+ (): TodoUpdateRequest =>
+ buildDetailTodoTextUpdateRequest({
+ title: detailTodoForm.title,
+ memo: detailTodoForm.memo,
+ subtasks: detailTodoForm.subtaskInputs,
+ }),
+ [detailTodoForm.memo, detailTodoForm.subtaskInputs, detailTodoForm.title],
+ );
+ const latestBuildTextUpdateRequestRef = useRef(buildTextUpdateRequest);
const didStartTextUpdateRef = useRef(false);
const textUpdateSignature = useMemo(
() =>
@@ -87,18 +100,16 @@ export const DetailTodoModalContent = ({
}),
[detailTodoForm.memo, detailTodoForm.subtaskInputs, detailTodoForm.title],
);
-
useEffect(() => {
latestOnUpdateRef.current = onUpdate;
}, [onUpdate]);
useEffect(() => {
- latestBuildUpdateRequestRef.current = detailTodoForm.buildUpdateRequest;
- }, [detailTodoForm.buildUpdateRequest]);
+ latestBuildTextUpdateRequestRef.current = buildTextUpdateRequest;
+ }, [buildTextUpdateRequest]);
useEffect(() => {
if (!isOpen) return;
- if (!canUpdateTodo) return;
if (!didStartTextUpdateRef.current) {
didStartTextUpdateRef.current = true;
@@ -108,18 +119,14 @@ export const DetailTodoModalContent = ({
if (!detailTodoForm.title.trim()) return;
const updateTimer = window.setTimeout(() => {
- latestOnUpdateRef.current(latestBuildUpdateRequestRef.current());
+ latestOnUpdateRef.current(latestBuildTextUpdateRequestRef.current());
}, TEXT_UPDATE_DEBOUNCE_MS);
return () => window.clearTimeout(updateTimer);
- }, [canUpdateTodo, detailTodoForm.title, isOpen, textUpdateSignature]);
-
- const updateTodo = (overrides: DetailTodoUpdateRequestOverrides = {}) => {
- if (!canUpdateTodo) return;
-
- const updateData = detailTodoForm.buildUpdateRequest(overrides);
- if (!updateData.title?.trim()) return;
+ }, [detailTodoForm.title, isOpen, textUpdateSignature]);
+ const updateTodo = (updateData: TodoUpdateRequest) => {
+ if ("title" in updateData && !updateData.title?.trim()) return;
onUpdate(updateData);
};
@@ -130,24 +137,29 @@ export const DetailTodoModalContent = ({
const handleRemoveIcon = () => {
detailTodoForm.removeIcon();
- updateTodo({ icon: null });
};
const handleSelectTime = (nextTime: TimeSelection) => {
setSelectedTime(nextTime);
const time = detailTodoForm.selectTime(nextTime);
- if (time) updateTodo({ time });
+ const durationSeconds = time
+ ? convertTimeTextToDurationSeconds(time)
+ : undefined;
+
+ if (durationSeconds) updateTodo({ durationSeconds });
};
const handleDateChange = (nextDate: Date) => {
detailTodoForm.setDate(nextDate);
- updateTodo({ date: nextDate });
+ updateTodo({ date: formatDateKey(nextDate) });
};
const handleTimeChange = (time: string) => {
detailTodoForm.setTime(time);
- updateTodo({ time });
+ const durationSeconds = convertTimeTextToDurationSeconds(time);
+
+ if (durationSeconds) updateTodo({ durationSeconds });
};
const handleSelectPriority = (priority: PriorityLevel) => {
@@ -163,24 +175,33 @@ export const DetailTodoModalContent = ({
const handleRepeatFrequencyChange = (repeatFrequency: RepeatFrequency) => {
detailTodoForm.changeRepeatFrequency(repeatFrequency);
- updateTodo({ isRepeatActive: true, repeatFrequency });
+ updateTodo({ repeatType: repeatFrequency });
};
const handleWeekdayToggle = (weekdayId: string) => {
const selectedWeekdayIds = detailTodoForm.toggleWeekday(weekdayId);
- updateTodo({ selectedWeekdayIds });
+ updateTodo({
+ repeatType: "WEEKLY",
+ repeatWeekdays: selectedWeekdayIds.filter(isTodoUpdateRepeatWeekday),
+ });
};
const handleRepeatDayChange = (repeatDay: string) => {
detailTodoForm.setRepeatDay(repeatDay);
- updateTodo({ repeatDay });
+ const repeatDayOfMonth = Number(repeatDay);
+
+ if (
+ Number.isInteger(repeatDayOfMonth) &&
+ repeatDayOfMonth >= 1 &&
+ repeatDayOfMonth <= 31
+ ) {
+ updateTodo({ repeatType: "MONTHLY", repeatDayOfMonth });
+ }
};
const handleSubtaskCompletedChange = (id: number, completed: boolean) => {
const subtasks = detailTodoForm.changeSubtaskCompleted(id, completed);
- const updateData = detailTodoForm.buildUpdateRequest({ subtasks });
- if (!updateData.title?.trim()) return;
- onUpdate(updateData);
+ updateTodo({ subtasks: buildDetailTodoSubtasksUpdateRequest(subtasks) });
};
return (
@@ -206,7 +227,7 @@ export const DetailTodoModalContent = ({
-
+
-
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
index 72c533c9..55bdaa22 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
@@ -1,11 +1,7 @@
import { TODO_ICON_VALUES } from "@repo/timo-design-system/ui";
import { useController, useForm } from "react-hook-form";
-import type {
- TodoDetailResponse,
- TodoUpdateRequest,
-} from "@/api/generated/models";
-import type { BuildDetailTodoUpdateRequestParams } from "@/utils/todo/detail-todo-update-request";
+import type { TodoDetailResponse } from "@/api/generated/models";
import type {
PriorityLevel,
RepeatFrequency,
@@ -18,7 +14,6 @@ import { SECONDS_PER_MINUTE } from "@/constants/time";
import { useTagField } from "@/hooks/todo-modal/common/use-tag-field";
import { useDetailSubtaskField } from "@/hooks/todo-modal/detail/use-detail-subtask-field";
import { parseDateKey } from "@/utils/date/date";
-import { buildDetailTodoUpdateRequest } from "@/utils/todo/detail-todo-update-request";
import {
TITLE_MAX_WEIGHTED_LENGTH,
truncateToWeightedLength,
@@ -80,9 +75,6 @@ export interface UseDetailTodoFormParams {
todo: TodoDetailResponse;
}
-export type DetailTodoUpdateRequestOverrides =
- Partial
;
-
export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
const durationText = convertDurationToTimeText(todo.durationSeconds ?? 0);
const todoIcon = todo.icon ?? null;
@@ -196,28 +188,6 @@ export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
return nextSubtasks;
};
- const buildUpdateRequest = (
- overrides: DetailTodoUpdateRequestOverrides = {},
- ): TodoUpdateRequest => {
- const params: BuildDetailTodoUpdateRequestParams = {
- icon: iconField.value,
- title: titleField.value,
- date: dateField.value,
- time: timeField.value,
- priority: priorityField.value,
- tagId: tagField.selectedTagId,
- isRepeatActive: isRepeatActiveField.value,
- repeatFrequency: repeatFrequencyField.value,
- selectedWeekdayIds: selectedWeekdayIdsField.value,
- repeatDay: repeatDayField.value,
- memo: memoField.value,
- subtasks: subtaskField.subtaskInputs,
- ...overrides,
- };
-
- return buildDetailTodoUpdateRequest(params);
- };
-
return {
date: dateField.value,
setDate: dateField.onChange,
@@ -248,7 +218,6 @@ export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
selectTime,
changeRepeatFrequency,
toggleWeekday,
- buildUpdateRequest,
handleSubmit,
dirtyFields: formState.dirtyFields,
};
diff --git a/apps/timo-web/utils/todo/detail-todo-update-request.ts b/apps/timo-web/utils/todo/detail-todo-update-request.ts
index bf669e5e..0b1de024 100644
--- a/apps/timo-web/utils/todo/detail-todo-update-request.ts
+++ b/apps/timo-web/utils/todo/detail-todo-update-request.ts
@@ -1,18 +1,11 @@
import type {
+ TodoSubtaskUpdateRequest,
TodoUpdateRequest,
TodoUpdateRequestRepeatWeekdaysItem,
} from "@/api/generated/models";
import type { DetailTodoSubtaskInput } from "@/hooks/todo-modal/detail/use-detail-subtask-field";
-import type {
- PriorityLevel,
- RepeatFrequency,
- TodoIconValue,
-} from "@repo/timo-design-system/ui";
-
-import { formatDateKey } from "@/utils/date/date";
-import { convertTimeTextToDurationSeconds } from "@/utils/todo/todo-time";
-const UPDATE_REPEAT_WEEKDAYS = [
+const DETAIL_TODO_UPDATE_WEEKDAYS = [
"MON",
"TUE",
"WED",
@@ -22,72 +15,34 @@ const UPDATE_REPEAT_WEEKDAYS = [
"SUN",
] as const;
-const isUpdateRepeatWeekday = (
+export const isTodoUpdateRepeatWeekday = (
weekdayId: string,
): weekdayId is TodoUpdateRequestRepeatWeekdaysItem =>
- (UPDATE_REPEAT_WEEKDAYS as readonly string[]).includes(weekdayId);
+ (DETAIL_TODO_UPDATE_WEEKDAYS as readonly string[]).includes(weekdayId);
+
+export const buildDetailTodoSubtasksUpdateRequest = (
+ subtasks: DetailTodoSubtaskInput[],
+): TodoSubtaskUpdateRequest[] =>
+ subtasks
+ .map((subtask) => ({
+ subtaskId: subtask.subtaskId ?? undefined,
+ content: subtask.value.trim(),
+ completed: subtask.completed,
+ }))
+ .filter((subtask) => subtask.content.length > 0);
-export interface BuildDetailTodoUpdateRequestParams {
- icon: TodoIconValue | null;
+export interface BuildDetailTodoTextUpdateRequestParams {
title: string;
- date: Date;
- time: string;
- priority: PriorityLevel;
- tagId: number | null;
- isRepeatActive: boolean;
- repeatFrequency: RepeatFrequency;
- selectedWeekdayIds: string[];
- repeatDay: string;
memo: string;
subtasks: DetailTodoSubtaskInput[];
}
-export const buildDetailTodoUpdateRequest = ({
- icon,
+export const buildDetailTodoTextUpdateRequest = ({
title,
- date,
- time,
- priority,
- tagId,
- isRepeatActive,
- repeatFrequency,
- selectedWeekdayIds,
- repeatDay,
memo,
subtasks,
-}: BuildDetailTodoUpdateRequestParams): TodoUpdateRequest => {
- const durationSeconds = convertTimeTextToDurationSeconds(time);
- const repeatType = isRepeatActive ? repeatFrequency : "NONE";
- const repeatDayOfMonth = Number(repeatDay);
- const repeatWeekdays = selectedWeekdayIds.filter(isUpdateRepeatWeekday);
- const updateSubtasks = subtasks
- .map((subtask) => ({
- subtaskId: subtask.subtaskId ?? undefined,
- content: subtask.value.trim(),
- completed: subtask.completed,
- }))
- .filter((subtask) => subtask.content.length > 0);
-
- return {
- icon: icon ?? undefined,
- title: title.trim(),
- date: formatDateKey(date),
- durationSeconds: durationSeconds > 0 ? durationSeconds : undefined,
- priority,
- tagId: tagId ?? undefined,
- repeatType,
- repeatWeekdays:
- repeatType === "WEEKLY" && repeatWeekdays.length > 0
- ? repeatWeekdays
- : undefined,
- repeatDayOfMonth:
- repeatType === "MONTHLY" &&
- Number.isInteger(repeatDayOfMonth) &&
- repeatDayOfMonth >= 1 &&
- repeatDayOfMonth <= 31
- ? repeatDayOfMonth
- : undefined,
- memo: memo.trim(),
- subtasks: updateSubtasks,
- };
-};
+}: BuildDetailTodoTextUpdateRequestParams): TodoUpdateRequest => ({
+ title: title.trim(),
+ memo: memo.trim(),
+ subtasks: buildDetailTodoSubtasksUpdateRequest(subtasks),
+});
From 73e92b714c8e232b898ffdbdc612e165c6fe364a Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 03:47:11 +0900
Subject: [PATCH 03/12] =?UTF-8?q?fix(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=98?=
=?UTF-8?q?=EC=A0=95=20=ED=9B=84=20=EB=AA=A8=EB=8B=AC=20=EB=8B=AB=EC=9D=84?=
=?UTF-8?q?=20=EB=95=8C=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=EC=9D=84=20?=
=?UTF-8?q?=EB=B0=98=EC=98=81=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../detail/DetailTodoModalContent.tsx | 36 ++++++++++++++-----
1 file changed, 28 insertions(+), 8 deletions(-)
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
index 32b858fa..840a174e 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
@@ -100,6 +100,18 @@ export const DetailTodoModalContent = ({
}),
[detailTodoForm.memo, detailTodoForm.subtaskInputs, detailTodoForm.title],
);
+ const lastSubmittedTextUpdateSignatureRef = useRef(textUpdateSignature);
+
+ const submitTextUpdate = useCallback(() => {
+ if (!detailTodoForm.title.trim()) return;
+ if (lastSubmittedTextUpdateSignatureRef.current === textUpdateSignature) {
+ return;
+ }
+
+ latestOnUpdateRef.current(latestBuildTextUpdateRequestRef.current());
+ lastSubmittedTextUpdateSignatureRef.current = textUpdateSignature;
+ }, [detailTodoForm.title, textUpdateSignature]);
+
useEffect(() => {
latestOnUpdateRef.current = onUpdate;
}, [onUpdate]);
@@ -116,14 +128,18 @@ export const DetailTodoModalContent = ({
return;
}
- if (!detailTodoForm.title.trim()) return;
-
- const updateTimer = window.setTimeout(() => {
- latestOnUpdateRef.current(latestBuildTextUpdateRequestRef.current());
- }, TEXT_UPDATE_DEBOUNCE_MS);
+ const updateTimer = window.setTimeout(
+ submitTextUpdate,
+ TEXT_UPDATE_DEBOUNCE_MS,
+ );
return () => window.clearTimeout(updateTimer);
- }, [detailTodoForm.title, isOpen, textUpdateSignature]);
+ }, [isOpen, submitTextUpdate]);
+
+ const handleClose = () => {
+ submitTextUpdate();
+ onClose();
+ };
const updateTodo = (updateData: TodoUpdateRequest) => {
if ("title" in updateData && !updateData.title?.trim()) return;
@@ -207,13 +223,17 @@ export const DetailTodoModalContent = ({
return (
-
From 3de47bca954b14f57c92783b41664793b4dbb281 Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 09:58:44 +0900
Subject: [PATCH 04/12] =?UTF-8?q?fix(web):=20=ED=99=88=20=ED=88=AC?=
=?UTF-8?q?=EB=91=90=20=EC=B9=B4=EB=93=9C=20=ED=94=8C=EB=A0=88=EC=9D=B4=20?=
=?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9D=B4=20=EB=93=9C=EB=9E=98=EA=B7=B8=20?=
=?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=97=90=20=EB=A7=89=ED=9E=88?=
=?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20?=
=?UTF-8?q?(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../_components/todo-card/HomeTodoCard.tsx | 34 +++++++++++--------
1 file changed, 20 insertions(+), 14 deletions(-)
diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
index b6f5dbcd..674a35bc 100644
--- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
+++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
@@ -104,7 +104,7 @@ export const HomeTodoCard = ({
};
const stopInteractiveEvent = (
- event: MouseEvent | PointerEvent,
+ event: MouseEvent | PointerEvent,
) => {
event.stopPropagation();
};
@@ -127,21 +127,27 @@ export const HomeTodoCard = ({
{title}
-
- {isCompleted ? (
-
- ) : isRunning ? (
-
- ) : (
-
- )}
-
+
+ {isCompleted ? (
+
+ ) : isRunning ? (
+
+ ) : (
+
+ )}
+
+
);
From b0c0e9a40aa12f784d08190bcc731c8c49267807 Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 10:08:05 +0900
Subject: [PATCH 05/12] =?UTF-8?q?fix(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=84=A0?=
=?UTF-8?q?=ED=83=9D=20=ED=9B=84=20=ED=99=95=EC=A0=95=20=EC=8B=9C=EC=97=90?=
=?UTF-8?q?=EB=A7=8C=20=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD=20=EC=A0=84?=
=?UTF-8?q?=EC=86=A1=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../detail/DetailTodoModalContent.tsx | 40 ++++++++++++++++---
1 file changed, 34 insertions(+), 6 deletions(-)
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
index 840a174e..b8f2ec93 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
@@ -67,6 +67,9 @@ export const DetailTodoModalContent = ({
const detailTodoForm = useDetailTodoForm({ todo });
const [selectedTime, setSelectedTime] = useState();
const [isIconPanelOpen, setIsIconPanelOpen] = useState(false);
+ const [pendingIcon, setPendingIcon] = useState(
+ detailTodoForm.icon,
+ );
const dateNumber = detailTodoForm.date.getDate();
const dayOfWeek = isDetailTodoWeekdayId(todo.dayOfWeek)
? todo.dayOfWeek
@@ -147,12 +150,37 @@ export const DetailTodoModalContent = ({
};
const handleSelectIcon = (nextIcon: TodoIconValue) => {
- detailTodoForm.selectIcon(nextIcon);
- updateTodo({ icon: nextIcon });
+ setPendingIcon(nextIcon);
+ };
+
+ const handleOpenIconPanel = () => {
+ setPendingIcon(detailTodoForm.icon);
+ setIsIconPanelOpen(true);
+ };
+
+ const handleSubmitIcon = () => {
+ if (!pendingIcon || pendingIcon === detailTodoForm.icon) {
+ setIsIconPanelOpen(false);
+ return;
+ }
+
+ detailTodoForm.selectIcon(pendingIcon);
+ updateTodo({ icon: pendingIcon });
+ setIsIconPanelOpen(false);
+ };
+
+ const handleToggleIconPanel = () => {
+ if (isIconPanelOpen) {
+ handleSubmitIcon();
+ return;
+ }
+
+ setPendingIcon(detailTodoForm.icon);
+ setIsIconPanelOpen(true);
};
const handleRemoveIcon = () => {
- detailTodoForm.removeIcon();
+ setPendingIcon(null);
};
const handleSelectTime = (nextTime: TimeSelection) => {
@@ -249,11 +277,11 @@ export const DetailTodoModalContent = ({
setIsIconPanelOpen(true)}
- onTogglePanel={() => setIsIconPanelOpen((prev) => !prev)}
+ onOpenPanel={handleOpenIconPanel}
+ onTogglePanel={handleToggleIconPanel}
onSelectIcon={handleSelectIcon}
onRemoveIcon={handleRemoveIcon}
/>
From 6b133bc706eb36cdce813017ee4cb1b14c32650f Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 10:08:28 +0900
Subject: [PATCH 06/12] =?UTF-8?q?fix(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=EB=AA=A8=EB=8B=AC=EC=97=90=20=ED=99=9C?=
=?UTF-8?q?=EC=84=B1=20=ED=83=80=EC=9D=B4=EB=A8=B8=20=EC=83=81=ED=83=9C=20?=
=?UTF-8?q?=EB=B0=98=EC=98=81=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../todo-modal/detail/DetailTodoModalContent.tsx | 5 ++++-
.../todo-modal/detail/DetailTodoModalContainer.tsx | 7 +++++++
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
index b8f2ec93..ccbe04d5 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type {
TodoDetailResponse,
+ TodoDetailResponseTimerStatus,
TodoUpdateRequest,
} from "@/api/generated/models";
import type {
@@ -49,6 +50,7 @@ export interface DetailTodoModalContentProps {
onToggleCompleted: (completed: boolean) => void;
onDelete: () => void;
onUpdate: (data: TodoUpdateRequest) => void;
+ timerStatus: TodoDetailResponseTimerStatus;
}
export const DetailTodoModalContent = ({
@@ -60,6 +62,7 @@ export const DetailTodoModalContent = ({
onToggleCompleted,
onDelete,
onUpdate,
+ timerStatus,
}: DetailTodoModalContentProps) => {
const t = useTranslations("Home.detailModal");
const tCreateModal = useTranslations("Home.createModal");
@@ -294,7 +297,7 @@ export const DetailTodoModalContent = ({
{
handleDelete(todoId, {
onSuccess: () => {
@@ -72,6 +78,7 @@ const DetailTodoModalQuery = ({
onToggleCompleted={onToggleCompleted}
onDelete={deleteTodo}
onUpdate={updateTodo}
+ timerStatus={timerStatus}
/>
);
};
From 8d7cd001d57145142192cd830f443acfb6cbbd74 Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 10:08:54 +0900
Subject: [PATCH 07/12] =?UTF-8?q?fix(web):=20=ED=88=AC=EB=91=90=20?=
=?UTF-8?q?=ED=94=8C=EB=A0=88=EC=9D=B4=20=EB=B2=84=ED=8A=BC=20=ED=81=B4?=
=?UTF-8?q?=EB=A6=AD=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=A0=84=ED=8C=8C=20?=
=?UTF-8?q?=EB=B0=A9=EC=A7=80=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../today/_components/TodayTodoCard.tsx | 14 ++++++++++--
.../detail/DetailTodoTaskFields.tsx | 22 +++++++++++++++----
2 files changed, 30 insertions(+), 6 deletions(-)
diff --git a/apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx b/apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx
index 6003adc2..0e68edff 100644
--- a/apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx
+++ b/apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx
@@ -13,7 +13,7 @@ import {
} from "@repo/timo-design-system/ui";
import { cn } from "@repo/timo-design-system/utils";
-import type { KeyboardEvent, ReactNode } from "react";
+import type { KeyboardEvent, MouseEvent, PointerEvent, ReactNode } from "react";
const CARD_STYLE = {
active: {
@@ -63,6 +63,12 @@ export interface TodayTodoCardProps {
const stopPropagation = (e: { stopPropagation: () => void }) =>
e.stopPropagation();
+const stopPlayEvent = (
+ event: MouseEvent | PointerEvent,
+) => {
+ event.stopPropagation();
+};
+
export const TodayTodoCard = ({
title,
isDone,
@@ -123,7 +129,11 @@ export const TodayTodoCard = ({
variant={isPlaying ? "stop" : "play"}
size="lg"
disabled={isDone}
- onClick={onPlay}
+ onClick={(event) => {
+ stopPlayEvent(event);
+ onPlay();
+ }}
+ onPointerDown={stopPlayEvent}
>
{isDone ? (
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx
index e4a5f676..4dec3ea2 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx
@@ -7,7 +7,7 @@ import { Checkbox, PlayButton } from "@repo/timo-design-system/ui";
import type { TodoDetailResponseTimerStatus } from "@/api/generated/models";
import type { DetailTodoSubtaskInput } from "@/hooks/todo-modal/detail/use-detail-subtask-field";
-import type { KeyboardEvent } from "react";
+import type { KeyboardEvent, MouseEvent, PointerEvent } from "react";
const resizeTextarea = (element: HTMLTextAreaElement | null) => {
if (!element) return;
@@ -49,6 +49,19 @@ export const DetailTodoTaskFields = ({
registerSubtaskInputRef,
onSubtaskInputKeyDown,
}: DetailTodoTaskFieldsProps) => {
+ const isRunning = timerStatus === "RUNNING";
+
+ const stopInteractiveEvent = (
+ event: MouseEvent | PointerEvent,
+ ) => {
+ event.stopPropagation();
+ };
+
+ const handlePlayClick = (event: MouseEvent) => {
+ stopInteractiveEvent(event);
+ onTogglePlay();
+ };
+
return (
@@ -69,14 +82,15 @@ export const DetailTodoTaskFields = ({
{isCompleted ? (
- ) : timerStatus === "RUNNING" ? (
+ ) : isRunning ? (
) : (
From 33380807b5193a2b2c9a057e39fc6add598bc74f Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 10:55:17 +0900
Subject: [PATCH 08/12] =?UTF-8?q?fix(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=EC=A1=B0=ED=9A=8C/=EC=88=98=EC=A0=95/?=
=?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=8B=A4=ED=8C=A8=20=ED=86=A0=EC=8A=A4?=
=?UTF-8?q?=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../detail/DetailTodoModalContent.tsx | 2 +
.../detail/DetailTodoModalContainer.tsx | 59 ++++++++++++++++---
.../detail/use-delete-todo-submit.ts | 5 +-
3 files changed, 57 insertions(+), 9 deletions(-)
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
index ccbe04d5..3aa01248 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
@@ -82,6 +82,7 @@ export const DetailTodoModalContent = ({
label: tCommon(`weekday.${weekdayId}`),
}));
const latestOnUpdateRef = useRef(onUpdate);
+
const buildTextUpdateRequest = useCallback(
(): TodoUpdateRequest =>
buildDetailTodoTextUpdateRequest({
@@ -91,6 +92,7 @@ export const DetailTodoModalContent = ({
}),
[detailTodoForm.memo, detailTodoForm.subtaskInputs, detailTodoForm.title],
);
+
const latestBuildTextUpdateRequestRef = useRef(buildTextUpdateRequest);
const didStartTextUpdateRef = useRef(false);
const textUpdateSignature = useMemo(
diff --git a/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx b/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
index d2506bd2..dd001d99 100644
--- a/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
+++ b/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
@@ -1,11 +1,15 @@
"use client";
+import { useTranslations } from "next-intl";
import { overlay } from "overlay-kit";
+import { useCallback, useEffect, useState } from "react";
-import type { TodoUpdateRequest } from "@/api/generated/models";
+import type { ErrorType } from "@/api/client/custom-instance";
+import type { ErrorDto, TodoUpdateRequest } from "@/api/generated/models";
import type { ReactNode } from "react";
import { useGetTodoDetail } from "@/api/generated/endpoints/todo/todo";
+import { AnimatedToast } from "@/components/toast/AnimatedToast";
import { DetailTodoModalContent } from "@/components/todo-modal/detail/DetailTodoModalContent";
import { useDeleteTodoSubmit } from "@/hooks/todo-modal/detail/use-delete-todo-submit";
import { useUpdateTodoSubmit } from "@/hooks/todo-modal/detail/use-update-todo-submit";
@@ -27,6 +31,7 @@ interface DetailTodoModalQueryProps extends Omit<
isOpen: boolean;
onClose: () => void;
onExited: () => void;
+ onActionError: (error: ErrorType) => void;
}
const DetailTodoModalQuery = ({
@@ -38,14 +43,21 @@ const DetailTodoModalQuery = ({
onTogglePlay,
onToggleCompleted,
onDelete,
+ onActionError,
}: DetailTodoModalQueryProps) => {
- const { data, isError } = useGetTodoDetail(todoId, { date });
+ const { data, error, isError } = useGetTodoDetail(todoId, { date });
const { handleDelete } = useDeleteTodoSubmit();
const { handleUpdate } = useUpdateTodoSubmit();
const todo = data?.data;
const { data: activeTimer } = useActiveTimer();
+ useEffect(() => {
+ if (isError && error) {
+ onActionError(error);
+ }
+ }, [error, isError, onActionError]);
+
if (isError || !todo) return null;
const timerStatus =
@@ -57,15 +69,21 @@ const DetailTodoModalQuery = ({
onDelete();
onClose();
},
+ onError: onActionError,
});
};
const updateTodo = (updateData: TodoUpdateRequest) => {
- handleUpdate({
- todoId,
- date,
- data: updateData,
- });
+ handleUpdate(
+ {
+ todoId,
+ date,
+ data: updateData,
+ },
+ {
+ onError: onActionError,
+ },
+ );
};
return (
@@ -91,6 +109,20 @@ export const DetailTodoModalContainer = ({
onDelete,
children,
}: DetailTodoModalContainerProps) => {
+ const tToast = useTranslations("Toast");
+ const [actionErrorMessage, setActionErrorMessage] = useState("");
+ const [isActionErrorToastOpen, setIsActionErrorToastOpen] = useState(false);
+
+ const showActionErrorToast = useCallback(
+ (error: ErrorType) => {
+ setActionErrorMessage(
+ error.response?.data.message ?? tToast("focusActionFailed"),
+ );
+ setIsActionErrorToastOpen(true);
+ },
+ [tToast],
+ );
+
const openDetailTodoModal = () => {
overlay.open(({ isOpen, close, unmount }) => (
));
};
- return <>{children(openDetailTodoModal)}>;
+ return (
+ <>
+ {children(openDetailTodoModal)}
+
+ setIsActionErrorToastOpen(false)}
+ message={actionErrorMessage}
+ />
+ >
+ );
};
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts b/apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts
index ac141940..0d2d0f64 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts
@@ -2,12 +2,15 @@
import { useQueryClient } from "@tanstack/react-query";
+import type { ErrorType } from "@/api/client/custom-instance";
+import type { ErrorDto } from "@/api/generated/models";
+
import { getGetHomeQueryKey } from "@/api/generated/endpoints/home/home";
import { useDeleteTodo } from "@/api/generated/endpoints/todo/todo";
export interface DeleteTodoSubmitHandlers {
onSuccess: () => void;
- onError?: () => void;
+ onError?: (error: ErrorType) => void;
}
export const useDeleteTodoSubmit = () => {
From b7694bcc4c9b32c5ab417463c9ef58b66bcbed39 Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 11:08:33 +0900
Subject: [PATCH 09/12] =?UTF-8?q?refactor(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=EB=AA=A8=EB=8B=AC=20=EC=88=98=EC=A0=95=20?=
=?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20?=
=?UTF-8?q?=EB=B6=84=EB=A6=AC=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../detail/DetailTodoModalContent.tsx | 240 +++---------------
.../todo-modal/detail/use-detail-todo-form.ts | 2 +
.../detail/use-detail-todo-icon-submit.ts | 61 +++++
.../detail/use-detail-todo-patch-handlers.ts | 112 ++++++++
.../detail/use-detail-todo-text-auto-save.ts | 90 +++++++
5 files changed, 303 insertions(+), 202 deletions(-)
create mode 100644 apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts
create mode 100644 apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
create mode 100644 apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
index 3aa01248..244bfbe8 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
@@ -1,19 +1,12 @@
import { DeleteIcon, TrashOnIcon } from "@repo/timo-design-system/icons";
import { TodoToolbar } from "@repo/timo-design-system/ui";
import { useTranslations } from "next-intl";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type {
TodoDetailResponse,
TodoDetailResponseTimerStatus,
TodoUpdateRequest,
} from "@/api/generated/models";
-import type {
- PriorityLevel,
- RepeatFrequency,
- TimeSelection,
- TodoIconValue,
-} from "@repo/timo-design-system/ui";
import { OverlayModal } from "@/components/modal/OverlayModal";
import { TodoIconField } from "@/components/todo-modal/common/TodoIconField";
@@ -24,16 +17,12 @@ import {
DETAIL_TODO_WEEKDAY_IDS,
useDetailTodoForm,
} from "@/hooks/todo-modal/detail/use-detail-todo-form";
-import { formatDateKey, formatShortDateLabel } from "@/utils/date/date";
-import {
- buildDetailTodoSubtasksUpdateRequest,
- buildDetailTodoTextUpdateRequest,
- isTodoUpdateRepeatWeekday,
-} from "@/utils/todo/detail-todo-update-request";
-import { convertTimeTextToDurationSeconds } from "@/utils/todo/todo-time";
+import { useDetailTodoIconSubmit } from "@/hooks/todo-modal/detail/use-detail-todo-icon-submit";
+import { useDetailTodoPatchHandlers } from "@/hooks/todo-modal/detail/use-detail-todo-patch-handlers";
+import { useDetailTodoTextAutoSave } from "@/hooks/todo-modal/detail/use-detail-todo-text-auto-save";
+import { formatShortDateLabel } from "@/utils/date/date";
const DETAIL_TODO_MEMO_MAX_LENGTH = 300;
-const TEXT_UPDATE_DEBOUNCE_MS = 2000;
type DetailTodoWeekdayId = (typeof DETAIL_TODO_WEEKDAY_IDS)[number];
const isDetailTodoWeekdayId = (
@@ -68,11 +57,6 @@ export const DetailTodoModalContent = ({
const tCreateModal = useTranslations("Home.createModal");
const tCommon = useTranslations("Common");
const detailTodoForm = useDetailTodoForm({ todo });
- const [selectedTime, setSelectedTime] = useState();
- const [isIconPanelOpen, setIsIconPanelOpen] = useState(false);
- const [pendingIcon, setPendingIcon] = useState(
- detailTodoForm.icon,
- );
const dateNumber = detailTodoForm.date.getDate();
const dayOfWeek = isDetailTodoWeekdayId(todo.dayOfWeek)
? todo.dayOfWeek
@@ -81,178 +65,28 @@ export const DetailTodoModalContent = ({
id: weekdayId,
label: tCommon(`weekday.${weekdayId}`),
}));
- const latestOnUpdateRef = useRef(onUpdate);
-
- const buildTextUpdateRequest = useCallback(
- (): TodoUpdateRequest =>
- buildDetailTodoTextUpdateRequest({
- title: detailTodoForm.title,
- memo: detailTodoForm.memo,
- subtasks: detailTodoForm.subtaskInputs,
- }),
- [detailTodoForm.memo, detailTodoForm.subtaskInputs, detailTodoForm.title],
- );
-
- const latestBuildTextUpdateRequestRef = useRef(buildTextUpdateRequest);
- const didStartTextUpdateRef = useRef(false);
- const textUpdateSignature = useMemo(
- () =>
- JSON.stringify({
- title: detailTodoForm.title,
- memo: detailTodoForm.memo,
- subtasks: detailTodoForm.subtaskInputs.map((subtask) => ({
- id: subtask.id,
- subtaskId: subtask.subtaskId,
- value: subtask.value,
- })),
- }),
- [detailTodoForm.memo, detailTodoForm.subtaskInputs, detailTodoForm.title],
- );
- const lastSubmittedTextUpdateSignatureRef = useRef(textUpdateSignature);
-
- const submitTextUpdate = useCallback(() => {
- if (!detailTodoForm.title.trim()) return;
- if (lastSubmittedTextUpdateSignatureRef.current === textUpdateSignature) {
- return;
- }
-
- latestOnUpdateRef.current(latestBuildTextUpdateRequestRef.current());
- lastSubmittedTextUpdateSignatureRef.current = textUpdateSignature;
- }, [detailTodoForm.title, textUpdateSignature]);
-
- useEffect(() => {
- latestOnUpdateRef.current = onUpdate;
- }, [onUpdate]);
-
- useEffect(() => {
- latestBuildTextUpdateRequestRef.current = buildTextUpdateRequest;
- }, [buildTextUpdateRequest]);
-
- useEffect(() => {
- if (!isOpen) return;
-
- if (!didStartTextUpdateRef.current) {
- didStartTextUpdateRef.current = true;
- return;
- }
-
- const updateTimer = window.setTimeout(
- submitTextUpdate,
- TEXT_UPDATE_DEBOUNCE_MS,
- );
-
- return () => window.clearTimeout(updateTimer);
- }, [isOpen, submitTextUpdate]);
+ const patchHandlers = useDetailTodoPatchHandlers({
+ form: detailTodoForm,
+ onUpdate,
+ });
+ const iconField = useDetailTodoIconSubmit({
+ icon: detailTodoForm.icon,
+ selectIcon: detailTodoForm.selectIcon,
+ onUpdate: patchHandlers.updateTodo,
+ });
+ const { submitTextUpdate } = useDetailTodoTextAutoSave({
+ isOpen,
+ title: detailTodoForm.title,
+ memo: detailTodoForm.memo,
+ subtasks: detailTodoForm.subtaskInputs,
+ onUpdate: patchHandlers.updateTodo,
+ });
const handleClose = () => {
submitTextUpdate();
onClose();
};
- const updateTodo = (updateData: TodoUpdateRequest) => {
- if ("title" in updateData && !updateData.title?.trim()) return;
- onUpdate(updateData);
- };
-
- const handleSelectIcon = (nextIcon: TodoIconValue) => {
- setPendingIcon(nextIcon);
- };
-
- const handleOpenIconPanel = () => {
- setPendingIcon(detailTodoForm.icon);
- setIsIconPanelOpen(true);
- };
-
- const handleSubmitIcon = () => {
- if (!pendingIcon || pendingIcon === detailTodoForm.icon) {
- setIsIconPanelOpen(false);
- return;
- }
-
- detailTodoForm.selectIcon(pendingIcon);
- updateTodo({ icon: pendingIcon });
- setIsIconPanelOpen(false);
- };
-
- const handleToggleIconPanel = () => {
- if (isIconPanelOpen) {
- handleSubmitIcon();
- return;
- }
-
- setPendingIcon(detailTodoForm.icon);
- setIsIconPanelOpen(true);
- };
-
- const handleRemoveIcon = () => {
- setPendingIcon(null);
- };
-
- const handleSelectTime = (nextTime: TimeSelection) => {
- setSelectedTime(nextTime);
- const time = detailTodoForm.selectTime(nextTime);
-
- const durationSeconds = time
- ? convertTimeTextToDurationSeconds(time)
- : undefined;
-
- if (durationSeconds) updateTodo({ durationSeconds });
- };
-
- const handleDateChange = (nextDate: Date) => {
- detailTodoForm.setDate(nextDate);
- updateTodo({ date: formatDateKey(nextDate) });
- };
-
- const handleTimeChange = (time: string) => {
- detailTodoForm.setTime(time);
- const durationSeconds = convertTimeTextToDurationSeconds(time);
-
- if (durationSeconds) updateTodo({ durationSeconds });
- };
-
- const handleSelectPriority = (priority: PriorityLevel) => {
- detailTodoForm.setPriority(priority);
- updateTodo({ priority });
- };
-
- const handleSelectTag = (label: string) => {
- const tagId = detailTodoForm.handleSelectTag(label);
-
- if (tagId !== null) updateTodo({ tagId });
- };
-
- const handleRepeatFrequencyChange = (repeatFrequency: RepeatFrequency) => {
- detailTodoForm.changeRepeatFrequency(repeatFrequency);
- updateTodo({ repeatType: repeatFrequency });
- };
-
- const handleWeekdayToggle = (weekdayId: string) => {
- const selectedWeekdayIds = detailTodoForm.toggleWeekday(weekdayId);
- updateTodo({
- repeatType: "WEEKLY",
- repeatWeekdays: selectedWeekdayIds.filter(isTodoUpdateRepeatWeekday),
- });
- };
-
- const handleRepeatDayChange = (repeatDay: string) => {
- detailTodoForm.setRepeatDay(repeatDay);
- const repeatDayOfMonth = Number(repeatDay);
-
- if (
- Number.isInteger(repeatDayOfMonth) &&
- repeatDayOfMonth >= 1 &&
- repeatDayOfMonth <= 31
- ) {
- updateTodo({ repeatType: "MONTHLY", repeatDayOfMonth });
- }
- };
-
- const handleSubtaskCompletedChange = (id: number, completed: boolean) => {
- const subtasks = detailTodoForm.changeSubtaskCompleted(id, completed);
- updateTodo({ subtasks: buildDetailTodoSubtasksUpdateRequest(subtasks) });
- };
-
return (
@@ -305,7 +139,9 @@ export const DetailTodoModalContent = ({
onToggleCompleted={onToggleCompleted}
onTogglePlay={onTogglePlay}
onSubtaskInputChange={detailTodoForm.changeSubtaskInput}
- onToggleSubtaskCompleted={handleSubtaskCompletedChange}
+ onToggleSubtaskCompleted={
+ patchHandlers.handleSubtaskCompletedChange
+ }
registerSubtaskInputRef={detailTodoForm.registerSubtaskInputRef}
onSubtaskInputKeyDown={detailTodoForm.handleSubtaskInputKeyDown}
/>
@@ -316,13 +152,13 @@ export const DetailTodoModalContent = ({
{}}
hasMemo={Boolean(todo.memo?.trim())}
isRepeatActive={detailTodoForm.isRepeatActive}
@@ -356,16 +192,16 @@ export const DetailTodoModalContent = ({
},
],
frequency: detailTodoForm.repeatFrequency,
- onFrequencyChange: handleRepeatFrequencyChange,
+ onFrequencyChange: patchHandlers.handleRepeatFrequencyChange,
weekly: {
weekdays,
selectedWeekdayIds: detailTodoForm.selectedWeekdayIds,
- onWeekdayToggle: handleWeekdayToggle,
+ onWeekdayToggle: patchHandlers.handleWeekdayToggle,
},
monthly: {
repeatDayLabel: t("repeatDayLabel"),
repeatDay: detailTodoForm.repeatDay,
- onRepeatDayChange: handleRepeatDayChange,
+ onRepeatDayChange: patchHandlers.handleRepeatDayChange,
},
}}
/>
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
index 55bdaa22..cb35f8db 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
@@ -222,3 +222,5 @@ export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
dirtyFields: formState.dirtyFields,
};
};
+
+export type UseDetailTodoFormReturn = ReturnType;
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts
new file mode 100644
index 00000000..79ce181a
--- /dev/null
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts
@@ -0,0 +1,61 @@
+import { useState } from "react";
+
+import type { TodoUpdateRequest } from "@/api/generated/models";
+import type { TodoIconValue } from "@repo/timo-design-system/ui";
+
+export interface UseDetailTodoIconSubmitParams {
+ icon: TodoIconValue | null;
+ selectIcon: (icon: TodoIconValue) => void;
+ onUpdate: (data: TodoUpdateRequest) => void;
+}
+
+export const useDetailTodoIconSubmit = ({
+ icon,
+ selectIcon,
+ onUpdate,
+}: UseDetailTodoIconSubmitParams) => {
+ const [isIconPanelOpen, setIsIconPanelOpen] = useState(false);
+ const [pendingIcon, setPendingIcon] = useState(icon);
+
+ const handleSelectIcon = (nextIcon: TodoIconValue) => {
+ setPendingIcon(nextIcon);
+ };
+
+ const handleOpenIconPanel = () => {
+ setPendingIcon(icon);
+ setIsIconPanelOpen(true);
+ };
+
+ const handleSubmitIcon = () => {
+ if (!pendingIcon || pendingIcon === icon) {
+ setIsIconPanelOpen(false);
+ return;
+ }
+
+ selectIcon(pendingIcon);
+ onUpdate({ icon: pendingIcon });
+ setIsIconPanelOpen(false);
+ };
+
+ const handleToggleIconPanel = () => {
+ if (isIconPanelOpen) {
+ handleSubmitIcon();
+ return;
+ }
+
+ handleOpenIconPanel();
+ };
+
+ const handleRemoveIcon = () => {
+ setPendingIcon(null);
+ };
+
+ return {
+ icon: isIconPanelOpen ? pendingIcon : icon,
+ isIconPanelOpen,
+ handleOpenIconPanel,
+ handleToggleIconPanel,
+ handleSelectIcon,
+ handleRemoveIcon,
+ };
+};
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
new file mode 100644
index 00000000..e7669f36
--- /dev/null
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
@@ -0,0 +1,112 @@
+import { useState } from "react";
+
+import type { TodoUpdateRequest } from "@/api/generated/models";
+import type { UseDetailTodoFormReturn } from "@/hooks/todo-modal/detail/use-detail-todo-form";
+import type {
+ PriorityLevel,
+ RepeatFrequency,
+ TimeSelection,
+} from "@repo/timo-design-system/ui";
+
+import { formatDateKey } from "@/utils/date/date";
+import {
+ buildDetailTodoSubtasksUpdateRequest,
+ isTodoUpdateRepeatWeekday,
+} from "@/utils/todo/detail-todo-update-request";
+import { convertTimeTextToDurationSeconds } from "@/utils/todo/todo-time";
+
+export interface UseDetailTodoPatchHandlersParams {
+ form: UseDetailTodoFormReturn;
+ onUpdate: (data: TodoUpdateRequest) => void;
+}
+
+export const useDetailTodoPatchHandlers = ({
+ form,
+ onUpdate,
+}: UseDetailTodoPatchHandlersParams) => {
+ const [selectedTime, setSelectedTime] = useState();
+
+ const updateTodo = (updateData: TodoUpdateRequest) => {
+ if ("title" in updateData && !updateData.title?.trim()) return;
+ onUpdate(updateData);
+ };
+
+ const handleSelectTime = (nextTime: TimeSelection) => {
+ setSelectedTime(nextTime);
+ const time = form.selectTime(nextTime);
+
+ const durationSeconds = time
+ ? convertTimeTextToDurationSeconds(time)
+ : undefined;
+
+ if (durationSeconds) updateTodo({ durationSeconds });
+ };
+
+ const handleDateChange = (nextDate: Date) => {
+ form.setDate(nextDate);
+ updateTodo({ date: formatDateKey(nextDate) });
+ };
+
+ const handleTimeChange = (time: string) => {
+ form.setTime(time);
+ const durationSeconds = convertTimeTextToDurationSeconds(time);
+
+ if (durationSeconds) updateTodo({ durationSeconds });
+ };
+
+ const handleSelectPriority = (priority: PriorityLevel) => {
+ form.setPriority(priority);
+ updateTodo({ priority });
+ };
+
+ const handleSelectTag = (label: string) => {
+ const tagId = form.handleSelectTag(label);
+
+ if (tagId !== null) updateTodo({ tagId });
+ };
+
+ const handleRepeatFrequencyChange = (repeatFrequency: RepeatFrequency) => {
+ form.changeRepeatFrequency(repeatFrequency);
+ updateTodo({ repeatType: repeatFrequency });
+ };
+
+ const handleWeekdayToggle = (weekdayId: string) => {
+ const selectedWeekdayIds = form.toggleWeekday(weekdayId);
+ updateTodo({
+ repeatType: "WEEKLY",
+ repeatWeekdays: selectedWeekdayIds.filter(isTodoUpdateRepeatWeekday),
+ });
+ };
+
+ const handleRepeatDayChange = (repeatDay: string) => {
+ form.setRepeatDay(repeatDay);
+ const repeatDayOfMonth = Number(repeatDay);
+
+ if (
+ Number.isInteger(repeatDayOfMonth) &&
+ repeatDayOfMonth >= 1 &&
+ repeatDayOfMonth <= 31
+ ) {
+ updateTodo({ repeatType: "MONTHLY", repeatDayOfMonth });
+ }
+ };
+
+ const handleSubtaskCompletedChange = (id: number, completed: boolean) => {
+ const subtasks = form.changeSubtaskCompleted(id, completed);
+ updateTodo({ subtasks: buildDetailTodoSubtasksUpdateRequest(subtasks) });
+ };
+
+ return {
+ selectedTime,
+ updateTodo,
+ handleSelectTime,
+ handleDateChange,
+ handleTimeChange,
+ handleSelectPriority,
+ handleSelectTag,
+ handleRepeatFrequencyChange,
+ handleWeekdayToggle,
+ handleRepeatDayChange,
+ handleSubtaskCompletedChange,
+ };
+};
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts
new file mode 100644
index 00000000..d03940da
--- /dev/null
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts
@@ -0,0 +1,90 @@
+import { useCallback, useEffect, useMemo, useRef } from "react";
+
+import type { TodoUpdateRequest } from "@/api/generated/models";
+import type { DetailTodoSubtaskInput } from "@/hooks/todo-modal/detail/use-detail-subtask-field";
+
+import { buildDetailTodoTextUpdateRequest } from "@/utils/todo/detail-todo-update-request";
+
+const TEXT_UPDATE_DEBOUNCE_MS = 2000;
+
+export interface UseDetailTodoTextAutoSaveParams {
+ isOpen: boolean;
+ title: string;
+ memo: string;
+ subtasks: DetailTodoSubtaskInput[];
+ onUpdate: (data: TodoUpdateRequest) => void;
+}
+
+export const useDetailTodoTextAutoSave = ({
+ isOpen,
+ title,
+ memo,
+ subtasks,
+ onUpdate,
+}: UseDetailTodoTextAutoSaveParams) => {
+ const latestOnUpdateRef = useRef(onUpdate);
+ const didStartTextUpdateRef = useRef(false);
+ const textUpdateSignature = useMemo(
+ () =>
+ JSON.stringify({
+ title,
+ memo,
+ subtasks: subtasks.map((subtask) => ({
+ id: subtask.id,
+ subtaskId: subtask.subtaskId,
+ value: subtask.value,
+ })),
+ }),
+ [memo, subtasks, title],
+ );
+ const lastSubmittedTextUpdateSignatureRef = useRef(textUpdateSignature);
+
+ const buildTextUpdateRequest = useCallback(
+ (): TodoUpdateRequest =>
+ buildDetailTodoTextUpdateRequest({
+ title,
+ memo,
+ subtasks,
+ }),
+ [memo, subtasks, title],
+ );
+ const latestBuildTextUpdateRequestRef = useRef(buildTextUpdateRequest);
+
+ const submitTextUpdate = useCallback(() => {
+ if (!title.trim()) return;
+ if (lastSubmittedTextUpdateSignatureRef.current === textUpdateSignature) {
+ return;
+ }
+
+ latestOnUpdateRef.current(latestBuildTextUpdateRequestRef.current());
+ lastSubmittedTextUpdateSignatureRef.current = textUpdateSignature;
+ }, [textUpdateSignature, title]);
+
+ useEffect(() => {
+ latestOnUpdateRef.current = onUpdate;
+ }, [onUpdate]);
+
+ useEffect(() => {
+ latestBuildTextUpdateRequestRef.current = buildTextUpdateRequest;
+ }, [buildTextUpdateRequest]);
+
+ useEffect(() => {
+ if (!isOpen) return;
+
+ if (!didStartTextUpdateRef.current) {
+ didStartTextUpdateRef.current = true;
+ return;
+ }
+
+ const updateTimer = window.setTimeout(
+ submitTextUpdate,
+ TEXT_UPDATE_DEBOUNCE_MS,
+ );
+
+ return () => window.clearTimeout(updateTimer);
+ }, [isOpen, submitTextUpdate]);
+
+ return {
+ submitTextUpdate,
+ };
+};
From 541d6b941c9999068728d415a338dc2fc485801d Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 12:26:00 +0900
Subject: [PATCH 10/12] =?UTF-8?q?fix(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=EC=8B=9C=EA=B0=84=20=ED=91=9C=EC=8B=9C?=
=?UTF-8?q?=EC=99=80=20=EC=A0=84=EC=86=A1=20=ED=98=95=EC=8B=9D=20=EB=B6=84?=
=?UTF-8?q?=EB=A6=AC=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../detail/DetailTodoModalContent.tsx | 6 ++++--
.../todo-modal/detail/use-detail-todo-form.ts | 7 ++-----
.../detail/use-detail-todo-patch-handlers.ts | 14 +++++++------
apps/timo-web/utils/todo/todo-time.ts | 20 +++++++++++++++++++
4 files changed, 34 insertions(+), 13 deletions(-)
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
index 244bfbe8..fc603577 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
@@ -21,6 +21,7 @@ import { useDetailTodoIconSubmit } from "@/hooks/todo-modal/detail/use-detail-to
import { useDetailTodoPatchHandlers } from "@/hooks/todo-modal/detail/use-detail-todo-patch-handlers";
import { useDetailTodoTextAutoSave } from "@/hooks/todo-modal/detail/use-detail-todo-text-auto-save";
import { formatShortDateLabel } from "@/utils/date/date";
+import { convertApiDurationToClockTimeText } from "@/utils/todo/todo-time";
const DETAIL_TODO_MEMO_MAX_LENGTH = 300;
type DetailTodoWeekdayId = (typeof DETAIL_TODO_WEEKDAY_IDS)[number];
@@ -65,6 +66,7 @@ export const DetailTodoModalContent = ({
id: weekdayId,
label: tCommon(`weekday.${weekdayId}`),
}));
+ const displayTime = convertApiDurationToClockTimeText(detailTodoForm.time);
const patchHandlers = useDetailTodoPatchHandlers({
form: detailTodoForm,
onUpdate,
@@ -153,9 +155,9 @@ export const DetailTodoModalContent = ({
dateLabel={formatShortDateLabel(detailTodoForm.date)}
date={detailTodoForm.date}
onDateChange={patchHandlers.handleDateChange}
- timeLabel={detailTodoForm.time}
+ timeLabel={displayTime}
timeOptions={DETAIL_TODO_TIME_OPTIONS}
- time={detailTodoForm.time}
+ time={displayTime}
onTimeChange={patchHandlers.handleTimeChange}
selectedTime={patchHandlers.selectedTime}
onSelectTime={patchHandlers.handleSelectTime}
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
index cb35f8db..4a04c642 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
@@ -18,10 +18,7 @@ import {
TITLE_MAX_WEIGHTED_LENGTH,
truncateToWeightedLength,
} from "@/utils/todo/text-length";
-import {
- convertDurationToTimeText,
- convertSecondsToApiDuration,
-} from "@/utils/todo/todo-time";
+import { convertSecondsToApiDuration } from "@/utils/todo/todo-time";
interface DetailTodoFormValues {
date: Date;
@@ -76,7 +73,7 @@ export interface UseDetailTodoFormParams {
}
export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
- const durationText = convertDurationToTimeText(todo.durationSeconds ?? 0);
+ const durationText = convertSecondsToApiDuration(todo.durationSeconds ?? 0);
const todoIcon = todo.icon ?? null;
const todoDate = parseDateKey(todo.date) ?? new Date();
const repeatType = isRepeatFrequency(todo.repeat.type)
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
index e7669f36..2bd14221 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
@@ -13,7 +13,10 @@ import {
buildDetailTodoSubtasksUpdateRequest,
isTodoUpdateRepeatWeekday,
} from "@/utils/todo/detail-todo-update-request";
-import { convertTimeTextToDurationSeconds } from "@/utils/todo/todo-time";
+import {
+ convertApiDurationToSeconds,
+ convertClockTimeTextToApiDuration,
+} from "@/utils/todo/todo-time";
export interface UseDetailTodoPatchHandlersParams {
form: UseDetailTodoFormReturn;
@@ -35,9 +38,7 @@ export const useDetailTodoPatchHandlers = ({
setSelectedTime(nextTime);
const time = form.selectTime(nextTime);
- const durationSeconds = time
- ? convertTimeTextToDurationSeconds(time)
- : undefined;
+ const durationSeconds = time ? convertApiDurationToSeconds(time) : 0;
if (durationSeconds) updateTodo({ durationSeconds });
};
@@ -48,8 +49,9 @@ export const useDetailTodoPatchHandlers = ({
};
const handleTimeChange = (time: string) => {
- form.setTime(time);
- const durationSeconds = convertTimeTextToDurationSeconds(time);
+ const apiDuration = convertClockTimeTextToApiDuration(time);
+ form.setTime(apiDuration);
+ const durationSeconds = convertApiDurationToSeconds(apiDuration);
if (durationSeconds) updateTodo({ durationSeconds });
};
diff --git a/apps/timo-web/utils/todo/todo-time.ts b/apps/timo-web/utils/todo/todo-time.ts
index 21b30146..93aaa78b 100644
--- a/apps/timo-web/utils/todo/todo-time.ts
+++ b/apps/timo-web/utils/todo/todo-time.ts
@@ -29,6 +29,26 @@ export const convertApiDurationToSeconds = (duration: string): number => {
return minutes * SECONDS_PER_MINUTE + seconds;
};
+export const convertApiDurationToClockTimeText = (duration: string): string => {
+ const totalSeconds = convertApiDurationToSeconds(duration);
+ const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
+ const minutes = Math.floor(
+ (totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE,
+ );
+
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
+};
+
+export const convertClockTimeTextToApiDuration = (timeText: string): string => {
+ const [hoursText = "0", minutesText = "0"] = timeText.split(":");
+ const hours = Number(hoursText) || 0;
+ const minutes = Number(minutesText) || 0;
+
+ return convertSecondsToApiDuration(
+ hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE,
+ );
+};
+
export const convertTimeTextToDurationSeconds = (timeText: string): number => {
const normalized = timeText.trim().toLowerCase();
const hourMatch = /^(\d+(?:\.\d+)?)\s*h$/.exec(normalized);
From 80d5d93a84e071453c1ffbfe7e5b1090d97a0894 Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 12:49:39 +0900
Subject: [PATCH 11/12] =?UTF-8?q?refactor(web):=20=ED=88=AC=EB=91=90=20?=
=?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B2=98?=
=?UTF-8?q?=EB=A6=AC=20=EC=A0=95=EB=A6=AC=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../home/_components/todo-card/HomeTodoCard.tsx | 1 -
.../(main)/today/_components/TodayTodoCard.tsx | 12 +++---------
2 files changed, 3 insertions(+), 10 deletions(-)
diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
index 65bc4f46..3e98a59b 100644
--- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
+++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
@@ -131,7 +131,6 @@ export const HomeTodoCard = ({
diff --git a/apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx b/apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx
index ae6a06a2..52ae7af6 100644
--- a/apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx
+++ b/apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx
@@ -13,7 +13,7 @@ import {
} from "@repo/timo-design-system/ui";
import { cn } from "@repo/timo-design-system/utils";
-import type { KeyboardEvent, MouseEvent, PointerEvent, ReactNode } from "react";
+import type { KeyboardEvent, ReactNode } from "react";
const CARD_STYLE = {
active: {
@@ -64,12 +64,6 @@ export interface TodayTodoCardProps {
const stopPropagation = (e: { stopPropagation: () => void }) =>
e.stopPropagation();
-const stopPlayEvent = (
- event: MouseEvent
| PointerEvent,
-) => {
- event.stopPropagation();
-};
-
export const TodayTodoCard = ({
title,
isDone,
@@ -133,10 +127,10 @@ export const TodayTodoCard = ({
disabled={isDone}
active={isPlayHighlighted}
onClick={(event) => {
- stopPlayEvent(event);
+ stopPropagation(event);
onPlay();
}}
- onPointerDown={stopPlayEvent}
+ onPointerDown={stopPropagation}
>
{isDone ? (
From 43e27adde10838c50a85a940b648a1debc9eec1c Mon Sep 17 00:00:00 2001
From: yumin-kim4757
Date: Wed, 15 Jul 2026 13:20:20 +0900
Subject: [PATCH 12/12] =?UTF-8?q?fix(web):=20=EC=83=81=EC=84=B8=20?=
=?UTF-8?q?=ED=88=AC=EB=91=90=20=EC=88=98=EC=A0=95=20=EC=84=B1=EA=B3=B5=20?=
=?UTF-8?q?=ED=9B=84=20=ED=8F=BC=20=EC=83=81=ED=83=9C=20=EB=B0=98=EC=98=81?=
=?UTF-8?q?=20(#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../detail/DetailTodoModalContent.tsx | 6 +-
.../detail/DetailTodoModalContainer.tsx | 12 ++-
.../hooks/todo-modal/common/use-tag-field.tsx | 6 ++
.../todo-modal/detail/use-detail-todo-form.ts | 33 +++-----
.../detail/use-detail-todo-icon-submit.ts | 18 ++++-
.../detail/use-detail-todo-patch-handlers.ts | 79 +++++++++++++------
.../detail/use-detail-todo-text-auto-save.ts | 13 ++-
7 files changed, 115 insertions(+), 52 deletions(-)
diff --git a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
index 53205702..8ce10f77 100644
--- a/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
+++ b/apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
@@ -7,6 +7,7 @@ import type {
TodoDetailResponseTimerStatus,
TodoUpdateRequest,
} from "@/api/generated/models";
+import type { UpdateTodoSubmitHandlers } from "@/hooks/todo-modal/detail/use-update-todo-submit";
import { OverlayModal } from "@/components/modal/OverlayModal";
import { TodoIconField } from "@/components/todo-modal/common/TodoIconField";
@@ -40,7 +41,10 @@ export interface DetailTodoModalContentProps {
onTogglePlay: () => void;
onToggleCompleted: (completed: boolean) => void;
onDelete: () => void;
- onUpdate: (data: TodoUpdateRequest) => void;
+ onUpdate: (
+ data: TodoUpdateRequest,
+ handlers?: UpdateTodoSubmitHandlers,
+ ) => void;
timerStatus: TodoDetailResponseTimerStatus;
}
diff --git a/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx b/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
index 47ca2d2d..93eec877 100644
--- a/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
+++ b/apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from "react";
import type { ErrorType } from "@/api/client/custom-instance";
import type { ErrorDto, TodoUpdateRequest } from "@/api/generated/models";
+import type { UpdateTodoSubmitHandlers } from "@/hooks/todo-modal/detail/use-update-todo-submit";
import type { ReactNode } from "react";
import { useGetTodoDetail } from "@/api/generated/endpoints/todo/todo";
@@ -73,7 +74,10 @@ const DetailTodoModalQuery = ({
});
};
- const updateTodo = (updateData: TodoUpdateRequest) => {
+ const updateTodo = (
+ updateData: TodoUpdateRequest,
+ handlers: UpdateTodoSubmitHandlers = {},
+ ) => {
handleUpdate(
{
todoId,
@@ -81,7 +85,11 @@ const DetailTodoModalQuery = ({
data: updateData,
},
{
- onError: onActionError,
+ onSuccess: handlers.onSuccess,
+ onError: (error) => {
+ handlers.onError?.(error);
+ onActionError(error);
+ },
},
);
};
diff --git a/apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx b/apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx
index 85ab0195..e564585e 100644
--- a/apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx
+++ b/apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx
@@ -29,6 +29,7 @@ export interface UseTagFieldResult {
isCreateTagErrorToastOpen: boolean;
closeCreateTagErrorToast: () => void;
handleSelectTag: (label: string) => void;
+ handleSelectTagById: (tagId: number) => void;
handleAddTagClick: () => void;
}
@@ -66,6 +67,10 @@ export const useTagField = ({
field.onChange(option.id);
};
+ const handleSelectTagById = (tagId: number) => {
+ field.onChange(tagId);
+ };
+
const handleAddTagClick = () => {
if (tagOptions.length >= MAX_TAG_COUNT) {
setIsTagLimitToastOpen(true);
@@ -114,6 +119,7 @@ export const useTagField = ({
isCreateTagErrorToastOpen,
closeCreateTagErrorToast: () => setIsCreateTagErrorToastOpen(false),
handleSelectTag,
+ handleSelectTagById,
handleAddTagClick,
};
};
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
index 4a04c642..f50e2a3d 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
@@ -127,6 +127,11 @@ export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
const selectIcon = (nextIcon: TodoIconValue) => iconField.onChange(nextIcon);
const removeIcon = () => iconField.onChange(null);
+ const setTagId = (tagId: number) => tagField.handleSelectTagById(tagId);
+ const setSelectedWeekdayIds = (weekdayIds: string[]) =>
+ selectedWeekdayIdsField.onChange(weekdayIds);
+ const setSubtaskCompleted = (id: number, completed: boolean) =>
+ subtaskField.handleCompletedChange(id, completed);
const changeTitle = (value: string) => {
titleField.onChange(
@@ -143,12 +148,7 @@ export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
if (!option) return undefined;
- const time = convertSecondsToApiDuration(
- option.minute * SECONDS_PER_MINUTE,
- );
- timeField.onChange(time);
-
- return time;
+ return convertSecondsToApiDuration(option.minute * SECONDS_PER_MINUTE);
};
const changeRepeatFrequency = (frequency: RepeatFrequency) => {
@@ -157,32 +157,22 @@ export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
};
const toggleWeekday = (weekdayId: string) => {
- const nextWeekdayIds = selectedWeekdayIdsField.value.includes(weekdayId)
+ return selectedWeekdayIdsField.value.includes(weekdayId)
? selectedWeekdayIdsField.value.filter((item) => item !== weekdayId)
: [...selectedWeekdayIdsField.value, weekdayId];
-
- selectedWeekdayIdsField.onChange(nextWeekdayIds);
-
- return nextWeekdayIds;
};
- const handleSelectTag = (label: string) => {
+ const getTagIdByLabel = (label: string) => {
const option = tagField.tagOptions.find((item) => item.label === label);
if (!option) return null;
- tagField.handleSelectTag(label);
-
return option.id;
};
const changeSubtaskCompleted = (id: number, completed: boolean) => {
- const nextSubtasks = subtaskField.subtaskInputs.map((subtask) =>
+ return subtaskField.subtaskInputs.map((subtask) =>
subtask.id === id ? { ...subtask, completed } : subtask,
);
-
- subtaskField.handleCompletedChange(id, completed);
-
- return nextSubtasks;
};
return {
@@ -194,10 +184,12 @@ export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
setPriority: priorityField.onChange,
tagLabels: tagField.tagLabels,
selectedTagLabel: tagField.selectedTagLabel,
- handleSelectTag,
+ getTagIdByLabel,
+ setTagId,
isRepeatActive: isRepeatActiveField.value,
repeatFrequency: repeatFrequencyField.value,
selectedWeekdayIds: selectedWeekdayIdsField.value,
+ setSelectedWeekdayIds,
repeatDay: repeatDayField.value,
setRepeatDay: repeatDayField.onChange,
title: titleField.value,
@@ -206,6 +198,7 @@ export const useDetailTodoForm = ({ todo }: UseDetailTodoFormParams) => {
registerSubtaskInputRef: subtaskField.registerInputRef,
changeSubtaskInput: subtaskField.handleInputChange,
changeSubtaskCompleted,
+ setSubtaskCompleted,
handleSubtaskInputKeyDown: subtaskField.handleInputKeyDown,
memo: memoField.value,
setMemo: memoField.onChange,
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts
index 79ce181a..d3e9b482 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts
@@ -1,12 +1,16 @@
import { useState } from "react";
import type { TodoUpdateRequest } from "@/api/generated/models";
+import type { UpdateTodoSubmitHandlers } from "@/hooks/todo-modal/detail/use-update-todo-submit";
import type { TodoIconValue } from "@repo/timo-design-system/ui";
export interface UseDetailTodoIconSubmitParams {
icon: TodoIconValue | null;
selectIcon: (icon: TodoIconValue) => void;
- onUpdate: (data: TodoUpdateRequest) => void;
+ onUpdate: (
+ data: TodoUpdateRequest,
+ handlers?: UpdateTodoSubmitHandlers,
+ ) => void;
}
export const useDetailTodoIconSubmit = ({
@@ -32,9 +36,15 @@ export const useDetailTodoIconSubmit = ({
return;
}
- selectIcon(pendingIcon);
- onUpdate({ icon: pendingIcon });
- setIsIconPanelOpen(false);
+ onUpdate(
+ { icon: pendingIcon },
+ {
+ onSuccess: () => {
+ selectIcon(pendingIcon);
+ setIsIconPanelOpen(false);
+ },
+ },
+ );
};
const handleToggleIconPanel = () => {
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
index 2bd14221..66e8f133 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
@@ -2,6 +2,7 @@ import { useState } from "react";
import type { TodoUpdateRequest } from "@/api/generated/models";
import type { UseDetailTodoFormReturn } from "@/hooks/todo-modal/detail/use-detail-todo-form";
+import type { UpdateTodoSubmitHandlers } from "@/hooks/todo-modal/detail/use-update-todo-submit";
import type {
PriorityLevel,
RepeatFrequency,
@@ -20,7 +21,10 @@ import {
export interface UseDetailTodoPatchHandlersParams {
form: UseDetailTodoFormReturn;
- onUpdate: (data: TodoUpdateRequest) => void;
+ onUpdate: (
+ data: TodoUpdateRequest,
+ handlers?: UpdateTodoSubmitHandlers,
+ ) => void;
}
export const useDetailTodoPatchHandlers = ({
@@ -29,59 +33,84 @@ export const useDetailTodoPatchHandlers = ({
}: UseDetailTodoPatchHandlersParams) => {
const [selectedTime, setSelectedTime] = useState();
- const updateTodo = (updateData: TodoUpdateRequest) => {
+ const updateTodo = (
+ updateData: TodoUpdateRequest,
+ handlers?: UpdateTodoSubmitHandlers,
+ ) => {
if ("title" in updateData && !updateData.title?.trim()) return;
- onUpdate(updateData);
+ onUpdate(updateData, handlers);
};
const handleSelectTime = (nextTime: TimeSelection) => {
- setSelectedTime(nextTime);
const time = form.selectTime(nextTime);
const durationSeconds = time ? convertApiDurationToSeconds(time) : 0;
- if (durationSeconds) updateTodo({ durationSeconds });
+ if (durationSeconds) {
+ updateTodo(
+ { durationSeconds },
+ {
+ onSuccess: () => {
+ setSelectedTime(nextTime);
+ form.setTime(time);
+ },
+ },
+ );
+ }
};
const handleDateChange = (nextDate: Date) => {
- form.setDate(nextDate);
- updateTodo({ date: formatDateKey(nextDate) });
+ updateTodo(
+ { date: formatDateKey(nextDate) },
+ { onSuccess: () => form.setDate(nextDate) },
+ );
};
const handleTimeChange = (time: string) => {
const apiDuration = convertClockTimeTextToApiDuration(time);
- form.setTime(apiDuration);
const durationSeconds = convertApiDurationToSeconds(apiDuration);
- if (durationSeconds) updateTodo({ durationSeconds });
+ if (durationSeconds) {
+ updateTodo(
+ { durationSeconds },
+ { onSuccess: () => form.setTime(apiDuration) },
+ );
+ }
};
const handleSelectPriority = (priority: PriorityLevel) => {
- form.setPriority(priority);
- updateTodo({ priority });
+ updateTodo({ priority }, { onSuccess: () => form.setPriority(priority) });
};
const handleSelectTag = (label: string) => {
- const tagId = form.handleSelectTag(label);
+ const tagId = form.getTagIdByLabel(label);
- if (tagId !== null) updateTodo({ tagId });
+ if (tagId !== null) {
+ updateTodo({ tagId }, { onSuccess: () => form.setTagId(tagId) });
+ }
};
const handleRepeatFrequencyChange = (repeatFrequency: RepeatFrequency) => {
- form.changeRepeatFrequency(repeatFrequency);
- updateTodo({ repeatType: repeatFrequency });
+ updateTodo(
+ { repeatType: repeatFrequency },
+ { onSuccess: () => form.changeRepeatFrequency(repeatFrequency) },
+ );
};
const handleWeekdayToggle = (weekdayId: string) => {
const selectedWeekdayIds = form.toggleWeekday(weekdayId);
- updateTodo({
- repeatType: "WEEKLY",
- repeatWeekdays: selectedWeekdayIds.filter(isTodoUpdateRepeatWeekday),
- });
+ updateTodo(
+ {
+ repeatType: "WEEKLY",
+ repeatWeekdays: selectedWeekdayIds.filter(isTodoUpdateRepeatWeekday),
+ },
+ {
+ onSuccess: () => form.setSelectedWeekdayIds(selectedWeekdayIds),
+ },
+ );
};
const handleRepeatDayChange = (repeatDay: string) => {
- form.setRepeatDay(repeatDay);
const repeatDayOfMonth = Number(repeatDay);
if (
@@ -89,13 +118,19 @@ export const useDetailTodoPatchHandlers = ({
repeatDayOfMonth >= 1 &&
repeatDayOfMonth <= 31
) {
- updateTodo({ repeatType: "MONTHLY", repeatDayOfMonth });
+ updateTodo(
+ { repeatType: "MONTHLY", repeatDayOfMonth },
+ { onSuccess: () => form.setRepeatDay(repeatDay) },
+ );
}
};
const handleSubtaskCompletedChange = (id: number, completed: boolean) => {
const subtasks = form.changeSubtaskCompleted(id, completed);
- updateTodo({ subtasks: buildDetailTodoSubtasksUpdateRequest(subtasks) });
+ updateTodo(
+ { subtasks: buildDetailTodoSubtasksUpdateRequest(subtasks) },
+ { onSuccess: () => form.setSubtaskCompleted(id, completed) },
+ );
};
return {
diff --git a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts
index d03940da..0ab5bd09 100644
--- a/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts
+++ b/apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
import type { TodoUpdateRequest } from "@/api/generated/models";
import type { DetailTodoSubtaskInput } from "@/hooks/todo-modal/detail/use-detail-subtask-field";
+import type { UpdateTodoSubmitHandlers } from "@/hooks/todo-modal/detail/use-update-todo-submit";
import { buildDetailTodoTextUpdateRequest } from "@/utils/todo/detail-todo-update-request";
@@ -12,7 +13,10 @@ export interface UseDetailTodoTextAutoSaveParams {
title: string;
memo: string;
subtasks: DetailTodoSubtaskInput[];
- onUpdate: (data: TodoUpdateRequest) => void;
+ onUpdate: (
+ data: TodoUpdateRequest,
+ handlers?: UpdateTodoSubmitHandlers,
+ ) => void;
}
export const useDetailTodoTextAutoSave = ({
@@ -56,8 +60,11 @@ export const useDetailTodoTextAutoSave = ({
return;
}
- latestOnUpdateRef.current(latestBuildTextUpdateRequestRef.current());
- lastSubmittedTextUpdateSignatureRef.current = textUpdateSignature;
+ latestOnUpdateRef.current(latestBuildTextUpdateRequestRef.current(), {
+ onSuccess: () => {
+ lastSubmittedTextUpdateSignatureRef.current = textUpdateSignature;
+ },
+ });
}, [textUpdateSignature, title]);
useEffect(() => {