From 2b60842e2a046f72e5397918b17a9fface8bff3d Mon Sep 17 00:00:00 2001 From: kimminna Date: Thu, 16 Jul 2026 04:55:27 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix(ui):=20=EB=B0=98=EB=B3=B5=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EC=9E=90=EC=97=90=EC=84=9C=20"=EB=A7=A4=EC=9D=BC"=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=EC=A0=80=EC=9E=A5=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반복 비활성 상태에서 frequency 기본값이 "DAILY"로 채워져 있어 "매일"을 선택해도 변경 없음으로 판단되던 문제를 수정했습니다 - RepeatSelector에 isActive prop을 추가해 반복 활성 여부를 함께 판단하도록 했습니다 - 사용자가 실제로 항목을 클릭했는지 추적해, 값이 기본값과 같아도 반복을 처음 활성화하는 경우엔 반드시 커밋하도록 했습니다 - TodoToolbar가 이미 갖고 있던 isRepeatActive를 RepeatSelector로 전달하도록 했습니다 --- .../repeat/repeat-selector/RepeatSelector.tsx | 14 +++++++++++++- .../components/todo/todo-toolbar/TodoToolbar.tsx | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.tsx b/packages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.tsx index 50b36ea2..216f4649 100644 --- a/packages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.tsx +++ b/packages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.tsx @@ -44,6 +44,7 @@ export interface RepeatSelectorProps { frequencyHeading?: string; detailHeading: string; options: RepeatOption[]; + isActive?: boolean; frequency: RepeatFrequency; onFrequencyChange?: (frequency: RepeatFrequency) => void; weekly: RepeatWeeklyDetail; @@ -158,6 +159,7 @@ export const RepeatSelector = ({ frequencyHeading = "반복 일정", detailHeading, options, + isActive = true, frequency, onFrequencyChange, weekly, @@ -174,12 +176,18 @@ export const RepeatSelector = ({ const draftFrequencyRef = useRef(frequency); const draftWeekdayIdsRef = useRef(weekly.selectedWeekdayIds); const draftRepeatDayRef = useRef(monthly.repeatDay); + // 반복이 꺼져 있을 때(isActive=false) frequency는 "DAILY"라는 기본값을 표시할 + // 뿐 실제로 저장된 값이 아니다. 이 경우 "매일"을 선택해도 draft가 기본값과 + // 같아서 "변경 없음"으로 오인되므로, 사용자가 실제로 클릭했는지를 별도로 + // 추적해 그 경우엔 값이 같아도 반드시 커밋한다. + const hasSelectedFrequencyRef = useRef(false); const selectedLabel = options.find( (option) => option.frequency === draftFrequency, )?.label; const handleSelectFrequency = (value: RepeatFrequency) => { + hasSelectedFrequencyRef.current = true; draftFrequencyRef.current = value; setDraftFrequency(value); if (value !== "DAILY") setIsPicking(false); @@ -201,6 +209,7 @@ export const RepeatSelector = ({ const handleOpenChange = (isOpen: boolean) => { if (isOpen) { + hasSelectedFrequencyRef.current = false; draftFrequencyRef.current = frequency; setDraftFrequency(frequency); draftWeekdayIdsRef.current = weekly.selectedWeekdayIds; @@ -210,7 +219,10 @@ export const RepeatSelector = ({ return; } - if (draftFrequencyRef.current !== frequency) { + if ( + hasSelectedFrequencyRef.current && + (draftFrequencyRef.current !== frequency || !isActive) + ) { onFrequencyChange?.(draftFrequencyRef.current); } if ( diff --git a/packages/timo-design-system/src/components/todo/todo-toolbar/TodoToolbar.tsx b/packages/timo-design-system/src/components/todo/todo-toolbar/TodoToolbar.tsx index e8057eb8..13028893 100644 --- a/packages/timo-design-system/src/components/todo/todo-toolbar/TodoToolbar.tsx +++ b/packages/timo-design-system/src/components/todo/todo-toolbar/TodoToolbar.tsx @@ -175,6 +175,7 @@ export const TodoToolbar = ({ isOpen ? ( From ee3dc2d7951a14e5fa70427325b8f234f7a57159 Mon Sep 17 00:00:00 2001 From: kimminna Date: Thu, 16 Jul 2026 04:56:15 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix(web):=20=ED=99=88/=ED=88=AC=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=20=EB=B7=B0=20=ED=88=AC=EB=91=90=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=88=84=EB=9D=BD=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 완료 토글, 서브태스크 토글, 순서 변경 실패 시 로컬 상태만 되돌리고 사용자에게 아무 메시지도 보여주지 않던 문제를 수정했습니다 - onUpdateError 콜백을 추가해 서버가 내려준 에러 메시지(error.response.data.message)를 그대로 토스트로 노출하도록 했습니다 - 메시지가 없을 때는 todoUpdateFailed 문구로 폴백하도록 했습니다 --- .../home/_containers/HomeTodoContainer.tsx | 2 ++ .../home/_hooks/use-home-todos-by-date.ts | 13 ++++++++++--- .../today/_containers/TodayTodoListContainer.tsx | 2 ++ .../today/_hooks/useTodayTodoList.ts | 8 ++++++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx index aa7064f9..f4a0d184 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx @@ -64,6 +64,8 @@ export const HomeTodoContainer = () => { onStopFeedback: setFeedbackText, onPlayError: (message) => setPlayErrorMessage(message ?? tToast("timerStartFailed")), + onUpdateError: (message) => + setPlayErrorMessage(message ?? tToast("todoUpdateFailed")), }); const handleConfirmPendingComplete = () => { diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts index fff1170b..f5ba888f 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts @@ -3,7 +3,9 @@ import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; +import type { ErrorType } from "@/api/client/custom-instance"; import type { ApiError } from "@/api/error/api-error"; +import type { ErrorDto } from "@/api/generated/models"; import type { HomeViewDay } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/home-view-type"; import type { Todo } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type"; @@ -29,6 +31,7 @@ export interface UseHomeTodosByDateOptions { onTimerAlreadyRunning: () => void; onStopFeedback: (feedbackText: string | undefined) => void; onPlayError: (message: string | undefined) => void; + onUpdateError: (message: string | undefined) => void; } export const useHomeTodosByDate = ( @@ -38,6 +41,7 @@ export const useHomeTodosByDate = ( onTimerAlreadyRunning, onStopFeedback, onPlayError, + onUpdateError, }: UseHomeTodosByDateOptions, ) => { const [todosByDate, setTodosByDate] = useState>({}); @@ -111,8 +115,9 @@ export const useHomeTodosByDate = ( invalidateTimeBoxes(); invalidateTodoDetail(dateKey, todoId); }, - onError: () => { + onError: (error: ErrorType) => { setTodosByDate((prev) => ({ ...prev, [dateKey]: previous })); + onUpdateError(error.response?.data.message); }, }, ); @@ -207,8 +212,9 @@ export const useHomeTodosByDate = ( invalidateTimeBoxes(); invalidateTodoDetail(dateKey, todoId); }, - onError: () => { + onError: (error: ErrorType) => { setTodosByDate((prev) => ({ ...prev, [dateKey]: previous })); + onUpdateError(error.response?.data.message); }, }, ); @@ -240,8 +246,9 @@ export const useHomeTodosByDate = ( { todoId: movedTodo.todoId, data: { newIndex: toIndex, date: dateKey } }, { onSuccess: invalidateHomeAndFocus, - onError: () => { + onError: (error: ErrorType) => { setTodosByDate((prev) => ({ ...prev, [dateKey]: previous })); + onUpdateError(error.response?.data.message); }, }, ); diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx index 1cd913af..6cb87f0f 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx @@ -59,6 +59,8 @@ export const TodayTodoListContainer = () => { onStopFeedback: setFeedbackText, onPlayError: (message) => setPlayErrorMessage(message ?? tToast("timerStartFailed")), + onUpdateError: (message) => + setPlayErrorMessage(message ?? tToast("todoUpdateFailed")), }); const handleConfirmPendingComplete = () => { diff --git a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts index b4b49af2..b2abc1a4 100644 --- a/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts +++ b/apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts @@ -27,6 +27,7 @@ export interface UseTodayTodoListOptions { onTimerAlreadyRunning: () => void; onStopFeedback: (feedbackText: string | undefined) => void; onPlayError: (message: string | undefined) => void; + onUpdateError: (message: string | undefined) => void; } export const useTodayTodoList = ( @@ -36,6 +37,7 @@ export const useTodayTodoList = ( onTimerAlreadyRunning, onStopFeedback, onPlayError, + onUpdateError, }: UseTodayTodoListOptions, ) => { const [todos, setTodos] = useState(initialTodos); @@ -97,8 +99,9 @@ export const useTodayTodoList = ( invalidateTimeBoxes(); invalidateTodoDetail(todoId, dateKey); }, - onError: () => { + onError: (error: ErrorType) => { setTodos(previous); + onUpdateError(error.response?.data.message); }, }, ); @@ -208,8 +211,9 @@ export const useTodayTodoList = ( invalidateTodayView(); invalidateTodoDetail(todoId, dateKey); }, - onError: () => { + onError: (error: ErrorType) => { setTodos(previous); + onUpdateError(error.response?.data.message); }, }, ); From 42806e48de4b20f01ba1b4e216419f0ae18c1901 Mon Sep 17 00:00:00 2001 From: kimminna Date: Thu, 16 Jul 2026 05:00:39 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix(web):=20=EC=83=81=EC=84=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=98=A4=EB=B2=84=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=8B=9C=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EC=9E=98?= =?UTF-8?q?=EB=A6=BC=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모달 콘텐츠가 뷰포트보다 커지면 화면 밖으로 잘리던 문제를 수정했습니다 - 다이얼로그를 뷰포트 전체를 덮는 스크롤 래퍼와 flex 중앙 정렬 컨테이너로 감싸, 콘텐츠가 뷰포트보다 클 때만 래퍼가 스크롤되도록 했습니다 - 모달 박스 자체에는 overflow 제약을 두지 않아, 내부 드롭다운(날짜/우선순위 등)이 모달 경계에 잘리지 않고 정상적으로 열리도록 했습니다 - 배경 클릭으로 닫는 로직을 새 스크롤 래퍼로 옮기고, 가려져서 동작하지 않던 기존 backdrop의 클릭 핸들러를 정리했습니다 --- .../components/modal/OverlayModal.tsx | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/apps/timo-web/components/modal/OverlayModal.tsx b/apps/timo-web/components/modal/OverlayModal.tsx index 68ac4d4b..d8f93603 100644 --- a/apps/timo-web/components/modal/OverlayModal.tsx +++ b/apps/timo-web/components/modal/OverlayModal.tsx @@ -78,13 +78,6 @@ export const OverlayModal = ({ "bg-timo-overlay fixed inset-0 z-40 transition-opacity duration-200 ease-out", isVisible ? "opacity-100" : "opacity-0", )} - onPointerDownCapture={() => { - wasFloatingLayerOpenRef.current = hasOpenFloatingLayer(); - }} - onClick={() => { - if (wasFloatingLayerOpenRef.current) return; - onClose(); - }} aria-hidden="true" /> dialogRef.current ?? document.body, }} > -
- {children} +
+ {/* + 배경 클릭으로 모달을 닫는 영역이다. 포인터 전용 UX이며 키보드 + 사용자는 이미 Escape로 동일하게 닫을 수 있어(위 handleEscape), + 여기서는 jsx-a11y의 인터랙션 규칙을 의도적으로 예외 처리한다. + */} + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */} +
{ + wasFloatingLayerOpenRef.current = hasOpenFloatingLayer(); + }} + onClick={(event) => { + if (event.target !== event.currentTarget) return; + if (wasFloatingLayerOpenRef.current) return; + onClose(); + }} + > +
+ {children} +
+
, From 1306460e0f7eaf3b77aec54203ee08b192756117 Mon Sep 17 00:00:00 2001 From: kimminna Date: Thu, 16 Jul 2026 05:01:02 +0900 Subject: [PATCH 4/5] =?UTF-8?q?style(ui):=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=A2=8C=EC=9A=B0=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=A0=9C=EA=B1=B0=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DropdownView 아이템의 좌우 패딩(px-1.5)을 제거했습니다 --- .../src/components/dropdown-view/DropdownView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/timo-design-system/src/components/dropdown-view/DropdownView.tsx b/packages/timo-design-system/src/components/dropdown-view/DropdownView.tsx index 7d2cfa0a..2973df80 100644 --- a/packages/timo-design-system/src/components/dropdown-view/DropdownView.tsx +++ b/packages/timo-design-system/src/components/dropdown-view/DropdownView.tsx @@ -49,7 +49,7 @@ export const DropdownView = ({ role="option" aria-selected={isSelected} onClick={() => onChange(item)} - className="typo-body-m-12 text-timo-gray-900 px-1.5 py-1" + className="typo-body-m-12 text-timo-gray-900 py-1" > {item} From fe1375bb4a948f93d0f5cd0112f3ef53da2b854e Mon Sep 17 00:00:00 2001 From: kimminna Date: Thu, 16 Jul 2026 13:44:08 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix(web):=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C/=EC=A0=95=EC=A7=80=20=EC=8B=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=AA=A8=EB=8B=AC=20=EC=BA=90=EC=8B=9C=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20=EB=88=84=EB=9D=BD=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타이머 완료·정지 성공 시 투두 상세 조회 쿼리 캐시를 무효화하지 않았습니다 - 조회 모달을 열어둔 채 타이머가 끝나도 timerStatus가 갱신되지 않아 편집이 계속 막히는 문제를 해결했습니다 --- .../app/[locale]/(main)/focus/_hooks/use-focus-session.ts | 2 ++ apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx | 3 +++ apps/timo-web/hooks/timer/use-timer-query-invalidation.ts | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/apps/timo-web/app/[locale]/(main)/focus/_hooks/use-focus-session.ts b/apps/timo-web/app/[locale]/(main)/focus/_hooks/use-focus-session.ts index e7e104df..c62c7fa5 100644 --- a/apps/timo-web/app/[locale]/(main)/focus/_hooks/use-focus-session.ts +++ b/apps/timo-web/app/[locale]/(main)/focus/_hooks/use-focus-session.ts @@ -95,6 +95,7 @@ export const useFocusSession = ({ invalidateHomeView(); invalidateTimeBoxes(); invalidateTodayView(); + invalidateTodoDetail(); }, onError: onMutationError, }, @@ -108,6 +109,7 @@ export const useFocusSession = ({ invalidateHomeView(); invalidateTimeBoxes(); invalidateTodayView(); + invalidateTodoDetail(); }, onError: onMutationError, }, diff --git a/apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx b/apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx index 636562ac..10ca10f6 100644 --- a/apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx +++ b/apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx @@ -35,6 +35,7 @@ export const TimerPanel = () => { invalidateTimeBoxes, invalidateTodayView, invalidateFocusTodo, + invalidateTodoDetail, } = useTimerQueryInvalidation(); const { mutate: changeStatus } = useChangeStatus({ @@ -63,6 +64,7 @@ export const TimerPanel = () => { invalidateTimeBoxes(); invalidateTodayView(); invalidateFocusTodo(); + if (response.data?.todoId) invalidateTodoDetail(response.data.todoId); }, }, }); @@ -75,6 +77,7 @@ export const TimerPanel = () => { invalidateTimeBoxes(); invalidateTodayView(); invalidateFocusTodo(); + if (response.data?.todoId) invalidateTodoDetail(response.data.todoId); }, }, }); diff --git a/apps/timo-web/hooks/timer/use-timer-query-invalidation.ts b/apps/timo-web/hooks/timer/use-timer-query-invalidation.ts index 500c6cc3..e7c0cd05 100644 --- a/apps/timo-web/hooks/timer/use-timer-query-invalidation.ts +++ b/apps/timo-web/hooks/timer/use-timer-query-invalidation.ts @@ -9,6 +9,7 @@ import { } from "@/api/generated/endpoints/home/home"; import { getGetTimeBoxesQueryKey } from "@/api/generated/endpoints/time-box/time-box"; import { getGetActiveTimerQueryKey } from "@/api/generated/endpoints/timer/timer"; +import { getGetTodoDetailQueryKey } from "@/api/generated/endpoints/todo/todo"; import { useStatisticsQueryInvalidation } from "@/hooks/statistics/use-statistics-query-invalidation"; export const useTimerQueryInvalidation = () => { @@ -25,6 +26,10 @@ export const useTimerQueryInvalidation = () => { queryClient.invalidateQueries({ queryKey: getGetTodayQueryKey() }); const invalidateFocusTodo = () => queryClient.invalidateQueries({ queryKey: getGetFocusTodoQueryKey() }); + const invalidateTodoDetail = (todoId: number) => + queryClient.invalidateQueries({ + queryKey: getGetTodoDetailQueryKey(todoId), + }); const invalidateTimerState = () => { invalidateActiveTimer(); invalidateHomeView(); @@ -39,6 +44,7 @@ export const useTimerQueryInvalidation = () => { invalidateTimeBoxes, invalidateTodayView, invalidateFocusTodo, + invalidateTodoDetail, invalidateTimerState, }; };