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(() => {