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 f4524dca..28b6c677 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 2ecebfbd..cedc0fe9 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"; @@ -28,6 +30,7 @@ export interface UseHomeTodosByDateOptions { onTimerAlreadyRunning: () => void; onStopFeedback: (feedbackText: string | undefined) => void; onPlayError: (message: string | undefined) => void; + onUpdateError: (message: string | undefined) => void; } export const useHomeTodosByDate = ( @@ -37,6 +40,7 @@ export const useHomeTodosByDate = ( onTimerAlreadyRunning, onStopFeedback, onPlayError, + onUpdateError, }: UseHomeTodosByDateOptions, ) => { const [todosByDate, setTodosByDate] = useState>({}); @@ -133,8 +137,9 @@ export const useHomeTodosByDate = ( invalidateTodoDetail(dateKey, todoId); invalidateStatistics(); }, - onError: () => { + onError: (error: ErrorType) => { setTodosByDate((prev) => ({ ...prev, [dateKey]: previous })); + onUpdateError(error.response?.data.message); }, }, ); @@ -237,8 +242,9 @@ export const useHomeTodosByDate = ( invalidateTodoDetail(dateKey, todoId); invalidateStatistics(); }, - onError: () => { + onError: (error: ErrorType) => { setTodosByDate((prev) => ({ ...prev, [dateKey]: previous })); + onUpdateError(error.response?.data.message); }, }, ); @@ -270,8 +276,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 09fd520c..988f923a 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 ed7b22ef..d263c2ac 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 @@ -26,6 +26,7 @@ export interface UseTodayTodoListOptions { onTimerAlreadyRunning: () => void; onStopFeedback: (feedbackText: string | undefined) => void; onPlayError: (message: string | undefined) => void; + onUpdateError: (message: string | undefined) => void; } export const useTodayTodoList = ( @@ -35,6 +36,7 @@ export const useTodayTodoList = ( onTimerAlreadyRunning, onStopFeedback, onPlayError, + onUpdateError, }: UseTodayTodoListOptions, ) => { const [todos, setTodos] = useState(initialTodos); @@ -118,8 +120,9 @@ export const useTodayTodoList = ( invalidateTodoDetail(todoId, dateKey); invalidateFocusTodo(); }, - onError: () => { + onError: (error: ErrorType) => { setTodos(previous); + onUpdateError(error.response?.data.message); }, }, ); @@ -237,8 +240,9 @@ export const useTodayTodoList = ( invalidateTodoDetail(todoId, dateKey); invalidateFocusTodo(); }, - onError: () => { + onError: (error: ErrorType) => { setTodos(previous); + onUpdateError(error.response?.data.message); }, }, ); 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 ff2d713e..66230d81 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/components/modal/OverlayModal.tsx b/apps/timo-web/components/modal/OverlayModal.tsx index f925faaa..2f0d5972 100644 --- a/apps/timo-web/components/modal/OverlayModal.tsx +++ b/apps/timo-web/components/modal/OverlayModal.tsx @@ -114,19 +114,41 @@ export const OverlayModal = ({ }} >
- {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} +
+
, 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, }; }; 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} 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 ? (