From d90e37de9c9cb768a4bdd369d78c913a298a3115 Mon Sep 17 00:00:00 2001 From: jjangminii Date: Mon, 13 Jul 2026 16:35:58 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(web):=20=EC=A7=91=EC=A4=91=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=A5=BC=20=EC=8B=A4=EC=A0=9C=20API=EC=99=80?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99=ED=95=9C=EB=8B=A4=20(#169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/v1/focus, GET /api/v1/timers/active를 연동해 실제 투두/타이머 데이터를 조회하도록 했습니다 - 타이머 시작/일시정지/재개/연장/완료와 TODO·하위 태스크 완료 상태 변경을 실제 API로 연동했습니다 - 오늘 집중할 투두가 없는 상태의 UI를 피그마 디자인대로 구현했습니다 - 사용하지 않게 된 mock 데이터를 제거했습니다 --- .../focus/_components/FocusEmptyTaskItem.tsx | 61 ++++++ .../_containers/FocusSessionContainer.tsx | 202 +++++++++++++----- .../[locale]/(main)/focus/_mocks/task-mock.ts | 15 -- .../(main)/focus/_queries/use-active-timer.ts | 18 ++ .../(main)/focus/_queries/use-focus-todo.ts | 16 ++ .../[locale]/(main)/focus/_types/task-type.ts | 39 ++-- .../(main)/focus/_types/timer-type.ts | 13 ++ .../app/[locale]/(main)/focus/page.tsx | 7 +- apps/timo-web/components/timer/Timer.tsx | 12 +- .../components/timer/TimerControls.tsx | 5 + .../components/timer/TimerSessionControls.tsx | 3 + apps/timo-web/messages/en.json | 3 +- apps/timo-web/messages/ko.json | 3 +- 13 files changed, 308 insertions(+), 89 deletions(-) create mode 100644 apps/timo-web/app/[locale]/(main)/focus/_components/FocusEmptyTaskItem.tsx delete mode 100644 apps/timo-web/app/[locale]/(main)/focus/_mocks/task-mock.ts create mode 100644 apps/timo-web/app/[locale]/(main)/focus/_queries/use-active-timer.ts create mode 100644 apps/timo-web/app/[locale]/(main)/focus/_queries/use-focus-todo.ts create mode 100644 apps/timo-web/app/[locale]/(main)/focus/_types/timer-type.ts diff --git a/apps/timo-web/app/[locale]/(main)/focus/_components/FocusEmptyTaskItem.tsx b/apps/timo-web/app/[locale]/(main)/focus/_components/FocusEmptyTaskItem.tsx new file mode 100644 index 00000000..e7c3ccea --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/focus/_components/FocusEmptyTaskItem.tsx @@ -0,0 +1,61 @@ +import { + CalendarDisableIcon, + ClockDisableIcon, + PlayDisabledIcon, +} from "@repo/timo-design-system/icons"; +import { Checkbox, PlayButton } from "@repo/timo-design-system/ui"; +import { useTranslations } from "next-intl"; + +export interface FocusEmptyTaskItemProps { + dayNumber: string; + dayOfWeek: string; + dateText: string; +} + +export const FocusEmptyTaskItem = ({ + dayNumber, + dayOfWeek, + dateText, +}: FocusEmptyTaskItemProps) => { + const t = useTranslations("Focus"); + + return ( +
+
+

{dayNumber}

+

{dayOfWeek}

+
+ +
+ {}} /> +

+ {t("empty")} +

+
+ +
+
+
+ + + {dateText} + +
+ +
+ + + 00:00 + +
+
+ + + + +
+ +
+
+ ); +}; diff --git a/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx b/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx index 5ec1b43f..ec69a03f 100644 --- a/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx @@ -1,14 +1,27 @@ "use client"; import { TimerOnIcon } from "@repo/timo-design-system/icons"; +import { useQueryClient } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; import { useRef, useState } from "react"; -import type { FocusTask } from "@/app/[locale]/(main)/focus/_types/task-type"; - +import { getGetFocusTodoQueryKey } from "@/api/generated/endpoints/focus/focus"; +import { + useChangeStatus, + useCompleteTimer, + useExtendTimer, + useStartTimer, + getGetActiveTimerQueryKey, +} from "@/api/generated/endpoints/timer/timer"; +import { + useChangeSubtaskStatus, + useChangeTodoStatus, +} from "@/api/generated/endpoints/todo/todo"; +import { FocusEmptyTaskItem } from "@/app/[locale]/(main)/focus/_components/FocusEmptyTaskItem"; import { FocusTaskItem } from "@/app/[locale]/(main)/focus/_components/FocusTaskItem"; import { FocusHeaderContainer } from "@/app/[locale]/(main)/focus/_containers/FocusHeaderContainer"; -import { focusTaskMock } from "@/app/[locale]/(main)/focus/_mocks/task-mock"; +import { useActiveTimer } from "@/app/[locale]/(main)/focus/_queries/use-active-timer"; +import { useFocusTodo } from "@/app/[locale]/(main)/focus/_queries/use-focus-todo"; import { convertDateToBadgeText, convertDateToDayNumberText, @@ -19,71 +32,154 @@ import { TimerSessionControls, type TimerSessionControlsHandle, } from "@/components/timer/TimerSessionControls"; -import { SECONDS_PER_MINUTE } from "@/constants/time"; import { convertDurationToMinutes } from "@/utils/convert-duration-to-minutes"; import { convertDurationToTimeText } from "@/utils/convert-duration-to-time-text"; +import { formatDurationLabel } from "@/utils/format-duration-label"; export const FocusSessionContainer = () => { - const [task, setTask] = useState(focusTaskMock); - const today = new Date(); + const queryClient = useQueryClient(); const timerSessionControlsRef = useRef(null); const tWeekday = useTranslations("Common.weekday"); + const tDuration = useTranslations("Focus.duration"); + const [feedbackText, setFeedbackText] = useState(); + + const { data: focusView } = useFocusTodo(); + const { data: activeTimer } = useActiveTimer(); + + const invalidateActiveTimer = () => + queryClient.invalidateQueries({ queryKey: getGetActiveTimerQueryKey() }); + const invalidateFocusTodo = () => + queryClient.invalidateQueries({ queryKey: getGetFocusTodoQueryKey() }); + + const { mutate: startTimer } = useStartTimer({ + mutation: { onSuccess: invalidateActiveTimer }, + }); + const { mutate: changeStatus } = useChangeStatus({ + mutation: { onSuccess: invalidateActiveTimer }, + }); + const { mutate: extendTimer } = useExtendTimer({ + mutation: { onSuccess: invalidateActiveTimer }, + }); + const { mutate: completeTimer } = useCompleteTimer({ + mutation: { + onSuccess: (response) => { + setFeedbackText(response.data?.aiFeedback ?? undefined); + invalidateActiveTimer(); + invalidateFocusTodo(); + }, + }, + }); + const { mutate: changeTodoStatus } = useChangeTodoStatus({ + mutation: { onSuccess: invalidateFocusTodo }, + }); + const { mutate: changeSubtaskStatus } = useChangeSubtaskStatus({ + mutation: { onSuccess: invalidateFocusTodo }, + }); + + const todo = focusView.todo; + const timer = + activeTimer && todo && activeTimer.todoId === todo.todoId + ? activeTimer + : undefined; + const isRunning = timer?.status === "RUNNING"; const handleTogglePlay = () => { - // TODO: API - setTask((prev) => ({ ...prev, isRunning: !prev.isRunning })); + if (!todo) return; + + if (!timer) { + startTimer({ todoId: todo.todoId }); + return; + } + + changeStatus({ + timerId: timer.timerId, + data: { action: isRunning ? "PAUSE" : "RESUME" }, + }); }; const handleExtendTimer = (minutes: number) => { - // TODO: API - setTask((prev) => ({ - ...prev, - durationSeconds: prev.durationSeconds + minutes * SECONDS_PER_MINUTE, - })); + if (!timer) return; + + extendTimer({ timerId: timer.timerId, data: { extendMinutes: minutes } }); }; const handleCompleteTimer = () => { - // TODO: API - setTask((prev) => ({ - ...prev, - isRunning: false, - completed: true, - subtasks: prev.subtasks.map((subtask) => ({ - ...subtask, - completed: true, - })), - })); + if (!timer) return; + + completeTimer({ timerId: timer.timerId }); }; const handleToggleCompleted = (completed: boolean) => { - if (completed && task.isRunning) { + if (!todo) return; + + if (completed && isRunning) { timerSessionControlsRef.current?.openStopModal(); return; } - // TODO: API - setTask((prev) => ({ - ...prev, - completed, - isRunning: completed ? false : prev.isRunning, - subtasks: prev.subtasks.map((subtask) => ({ ...subtask, completed })), - })); + changeTodoStatus({ + todoId: todo.todoId, + data: { isCompleted: completed, date: focusView.date }, + }); }; const handleToggleSubtaskCompleted = ( subtaskId: number, completed: boolean, ) => { - // TODO: API - setTask((prev) => ({ - ...prev, - subtasks: prev.subtasks.map((subtask) => - subtask.subtaskId === subtaskId ? { ...subtask, completed } : subtask, - ), - })); + if (!todo) return; + + changeSubtaskStatus({ + todoId: todo.todoId, + subtaskId, + data: { isCompleted: completed }, + }); }; - const plannedMinutes = convertDurationToMinutes(task.durationSeconds); + const today = new Date(focusView.date); + const dateText = convertDateToBadgeText(today); + + if (!focusView.hasTodo || !todo) { + return ( +
+
+ + +
+ +
+
+ + + {}} + plannedMinutes={0} + actualMinutes={0} + onExtend={() => {}} + onComplete={() => {}} + disabled + /> +
+
+
+ ); + } + + const plannedSeconds = timer + ? timer.plannedSeconds + timer.extendedSeconds + : (todo.durationSeconds ?? 0); + const remainingSeconds = timer ? timer.remainingSeconds : plannedSeconds; + const progress = + plannedSeconds > 0 + ? ((plannedSeconds - remainingSeconds) / plannedSeconds) * 100 + : 0; + const plannedMinutes = convertDurationToMinutes(plannedSeconds); + const actualMinutes = convertDurationToMinutes(timer?.elapsedSeconds ?? 0); return (
@@ -92,13 +188,12 @@ export const FocusSessionContainer = () => { {
} - time="10:30" - plannedLabel="12M" - progress={70} + time={convertDurationToTimeText(remainingSeconds)} + plannedLabel={formatDurationLabel( + plannedMinutes, + tDuration("hourUnit"), + tDuration("minuteUnit"), + )} + progress={progress} size="lg" /> diff --git a/apps/timo-web/app/[locale]/(main)/focus/_mocks/task-mock.ts b/apps/timo-web/app/[locale]/(main)/focus/_mocks/task-mock.ts deleted file mode 100644 index b9927a4b..00000000 --- a/apps/timo-web/app/[locale]/(main)/focus/_mocks/task-mock.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { FocusTask } from "@/app/[locale]/(main)/focus/_types/task-type"; - -export const focusTaskMock: FocusTask = { - taskId: 1, - title: "앱잼 1차 과제 제출", - completed: false, - scheduledDate: new Date(2026, 6, 1), - durationSeconds: 7200, - isRunning: false, - subtasks: [ - { subtaskId: 1, content: "기획서 작성하기", completed: false }, - { subtaskId: 2, content: "와이어프레임 그리기", completed: true }, - ], - memo: "회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기", -}; diff --git a/apps/timo-web/app/[locale]/(main)/focus/_queries/use-active-timer.ts b/apps/timo-web/app/[locale]/(main)/focus/_queries/use-active-timer.ts new file mode 100644 index 00000000..8ed8920a --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/focus/_queries/use-active-timer.ts @@ -0,0 +1,18 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; + +import { + getActiveTimer, + getGetActiveTimerQueryKey, +} from "@/api/generated/endpoints/timer/timer"; +import { activeTimerSchema } from "@/app/[locale]/(main)/focus/_types/timer-type"; + +export const useActiveTimer = () => + useQuery({ + queryKey: getGetActiveTimerQueryKey(), + queryFn: ({ signal }) => getActiveTimer(undefined, signal), + select: ({ data }) => (data ? activeTimerSchema.parse(data) : undefined), + refetchInterval: (query) => + query.state.data?.data?.status === "RUNNING" ? 1000 : false, + }); diff --git a/apps/timo-web/app/[locale]/(main)/focus/_queries/use-focus-todo.ts b/apps/timo-web/app/[locale]/(main)/focus/_queries/use-focus-todo.ts new file mode 100644 index 00000000..bb81c876 --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/focus/_queries/use-focus-todo.ts @@ -0,0 +1,16 @@ +"use client"; + +import { useSuspenseQuery } from "@tanstack/react-query"; + +import { + getFocusTodo, + getGetFocusTodoQueryKey, +} from "@/api/generated/endpoints/focus/focus"; +import { focusViewSchema } from "@/app/[locale]/(main)/focus/_types/task-type"; + +export const useFocusTodo = () => + useSuspenseQuery({ + queryKey: getGetFocusTodoQueryKey(), + queryFn: ({ signal }) => getFocusTodo(undefined, signal), + select: ({ data }) => focusViewSchema.parse(data), + }); diff --git a/apps/timo-web/app/[locale]/(main)/focus/_types/task-type.ts b/apps/timo-web/app/[locale]/(main)/focus/_types/task-type.ts index 06ccb449..ee828c6d 100644 --- a/apps/timo-web/app/[locale]/(main)/focus/_types/task-type.ts +++ b/apps/timo-web/app/[locale]/(main)/focus/_types/task-type.ts @@ -1,16 +1,25 @@ -export interface FocusTaskSubtask { - subtaskId: number; - content: string; - completed: boolean; -} +import { z } from "zod"; -export interface FocusTask { - taskId: number; - title: string; - completed: boolean; - scheduledDate: Date; - durationSeconds: number; - isRunning: boolean; - subtasks: FocusTaskSubtask[]; - memo?: string; -} +export const focusTaskSubtaskSchema = z.object({ + subtaskId: z.number(), + content: z.string(), + completed: z.boolean(), +}); + +export const focusTaskSchema = z.object({ + todoId: z.number(), + title: z.string(), + completed: z.boolean(), + durationSeconds: z.number().optional(), + subtasks: z.array(focusTaskSubtaskSchema), +}); + +export const focusViewSchema = z.object({ + date: z.iso.date(), + hasTodo: z.boolean(), + todo: focusTaskSchema.nullish(), +}); + +export type FocusTaskSubtask = z.infer; +export type FocusTask = z.infer; +export type FocusView = z.infer; diff --git a/apps/timo-web/app/[locale]/(main)/focus/_types/timer-type.ts b/apps/timo-web/app/[locale]/(main)/focus/_types/timer-type.ts new file mode 100644 index 00000000..c85587bd --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/focus/_types/timer-type.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const activeTimerSchema = z.object({ + timerId: z.number(), + todoId: z.number(), + status: z.enum(["RUNNING", "PAUSED"]), + plannedSeconds: z.number(), + extendedSeconds: z.number(), + elapsedSeconds: z.number(), + remainingSeconds: z.number(), +}); + +export type ActiveTimer = z.infer; diff --git a/apps/timo-web/app/[locale]/(main)/focus/page.tsx b/apps/timo-web/app/[locale]/(main)/focus/page.tsx index 86c4e9b8..c9d745cf 100644 --- a/apps/timo-web/app/[locale]/(main)/focus/page.tsx +++ b/apps/timo-web/app/[locale]/(main)/focus/page.tsx @@ -1,5 +1,10 @@ import { FocusSessionContainer } from "@/app/[locale]/(main)/focus/_containers/FocusSessionContainer"; +import { AsyncBoundary } from "@/components/boundary/AsyncBoundary"; export default function FocusPage() { - return ; + return ( + + + + ); } diff --git a/apps/timo-web/components/timer/Timer.tsx b/apps/timo-web/components/timer/Timer.tsx index 1c67b82b..254f869d 100644 --- a/apps/timo-web/components/timer/Timer.tsx +++ b/apps/timo-web/components/timer/Timer.tsx @@ -6,7 +6,7 @@ export type TimerSize = "sm" | "lg"; export type IconType = ReactNode; export interface TimerProps { - icon: IconType; + icon?: IconType; time: string; plannedLabel: string; progress: number; @@ -92,10 +92,12 @@ export const Timer = ({
- {/*TODO: 아이콘 핸드오프시 변경 예정 */} - - {icon} - + {icon && ( + // TODO: 아이콘 핸드오프시 변경 예정 + + {icon} + + )}

void; onOpenEndModal: () => void; onOpenExtendModal: () => void; + disabled?: boolean; } export const TimerControls = ({ @@ -26,6 +27,7 @@ export const TimerControls = ({ onTogglePlay, onOpenEndModal, onOpenExtendModal, + disabled = false, }: TimerControlsProps) => { const t = useTranslations("Focus.controls"); @@ -34,6 +36,7 @@ export const TimerControls = ({ @@ -51,11 +54,13 @@ export const TimerControls = ({ label={isRunning ? t("pause") : t("play")} variant={isRunning ? "active" : "default"} onClick={onTogglePlay} + disabled={disabled} /> diff --git a/apps/timo-web/components/timer/TimerSessionControls.tsx b/apps/timo-web/components/timer/TimerSessionControls.tsx index 59518f4a..5a8cdf25 100644 --- a/apps/timo-web/components/timer/TimerSessionControls.tsx +++ b/apps/timo-web/components/timer/TimerSessionControls.tsx @@ -26,6 +26,7 @@ export interface TimerSessionControlsProps { feedbackText?: string; onExtend: (minutes: number) => void; onComplete: () => void; + disabled?: boolean; } export const TimerSessionControls = forwardRef< @@ -40,6 +41,7 @@ export const TimerSessionControls = forwardRef< feedbackText, onExtend, onComplete, + disabled = false, }, ref, ) { @@ -87,6 +89,7 @@ export const TimerSessionControls = forwardRef< resetExtendSelection(); setStep("extend"); }} + disabled={disabled} /> Date: Mon, 13 Jul 2026 19:53:28 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(ui):=20PlayButton=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=20=EC=83=81=ED=83=9C=20aria-label=EC=9D=84=20?= =?UTF-8?q?=EB=A1=9C=EC=BC=80=EC=9D=BC=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=9D=BC=EC=9D=B4=EB=93=9C=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비활성 상태에서 한국어로 고정된 "~불가" 문구가 항상 aria-label에 들어가던 문제를 수정했습니다 - ariaLabel prop을 추가해 호출부에서 로케일에 맞는 문구로 오버라이드할 수 있게 했습니다 --- .../src/components/button/play-button/PlayButton.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/timo-design-system/src/components/button/play-button/PlayButton.tsx b/packages/timo-design-system/src/components/button/play-button/PlayButton.tsx index a88908e2..d0af9e52 100644 --- a/packages/timo-design-system/src/components/button/play-button/PlayButton.tsx +++ b/packages/timo-design-system/src/components/button/play-button/PlayButton.tsx @@ -11,6 +11,7 @@ export interface PlayButtonProps { disabled?: boolean; onClick?: () => void; children?: ReactNode; + ariaLabel?: string; } const VARIANT_MAP: Record< @@ -32,6 +33,7 @@ export const PlayButton = ({ disabled = false, onClick, children, + ariaLabel, }: PlayButtonProps) => { return (

- {}} /> +

{t("empty")}

@@ -50,7 +52,12 @@ export const FocusEmptyTaskItem = ({
- +
diff --git a/apps/timo-web/messages/en.json b/apps/timo-web/messages/en.json index f768263c..69327300 100644 --- a/apps/timo-web/messages/en.json +++ b/apps/timo-web/messages/en.json @@ -226,6 +226,7 @@ "hourUnit": "h", "minuteUnit": "m" }, - "empty": "No to-dos to focus on right now" + "empty": "No to-dos to focus on right now", + "emptyPlayDisabled": "Play unavailable" } } diff --git a/apps/timo-web/messages/ko.json b/apps/timo-web/messages/ko.json index 914d6b47..ed7fb120 100644 --- a/apps/timo-web/messages/ko.json +++ b/apps/timo-web/messages/ko.json @@ -223,6 +223,7 @@ "hourUnit": "시간", "minuteUnit": "분" }, - "empty": "지금은 집중할 투두가 없어요" + "empty": "지금은 집중할 투두가 없어요", + "emptyPlayDisabled": "재생 불가" } } From 250a2e6dc1bcabba607c6f48bcdb8b24e80d70b7 Mon Sep 17 00:00:00 2001 From: jjangminii Date: Mon, 13 Jul 2026 20:27:23 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix(web):=20=EC=A7=91=EC=A4=91=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=AE=A4=ED=85=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=A7=81?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타이머/TODO 상태 변경 뮤테이션 6개에 실패 시 토스트를 띄우는 onError 핸들러를 추가했습니다 - 기존 온보딩 컨테이너의 에러 토스트 패턴(로컬 state + AnimatedToast + Toast 네임스페이스)을 그대로 따랐습니다 --- .../_containers/FocusSessionContainer.tsx | 34 ++++++++++++++++--- apps/timo-web/messages/en.json | 3 +- apps/timo-web/messages/ko.json | 3 +- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx b/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx index ec69a03f..960bc9b6 100644 --- a/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx @@ -32,6 +32,7 @@ import { TimerSessionControls, type TimerSessionControlsHandle, } from "@/components/timer/TimerSessionControls"; +import { AnimatedToast } from "@/components/toast/AnimatedToast"; import { convertDurationToMinutes } from "@/utils/convert-duration-to-minutes"; import { convertDurationToTimeText } from "@/utils/convert-duration-to-time-text"; import { formatDurationLabel } from "@/utils/format-duration-label"; @@ -41,7 +42,9 @@ export const FocusSessionContainer = () => { const timerSessionControlsRef = useRef(null); const tWeekday = useTranslations("Common.weekday"); const tDuration = useTranslations("Focus.duration"); + const tToast = useTranslations("Toast"); const [feedbackText, setFeedbackText] = useState(); + const [isErrorToastOpen, setIsErrorToastOpen] = useState(false); const { data: focusView } = useFocusTodo(); const { data: activeTimer } = useActiveTimer(); @@ -50,15 +53,25 @@ export const FocusSessionContainer = () => { queryClient.invalidateQueries({ queryKey: getGetActiveTimerQueryKey() }); const invalidateFocusTodo = () => queryClient.invalidateQueries({ queryKey: getGetFocusTodoQueryKey() }); + const handleMutationError = () => setIsErrorToastOpen(true); const { mutate: startTimer } = useStartTimer({ - mutation: { onSuccess: invalidateActiveTimer }, + mutation: { + onSuccess: invalidateActiveTimer, + onError: handleMutationError, + }, }); const { mutate: changeStatus } = useChangeStatus({ - mutation: { onSuccess: invalidateActiveTimer }, + mutation: { + onSuccess: invalidateActiveTimer, + onError: handleMutationError, + }, }); const { mutate: extendTimer } = useExtendTimer({ - mutation: { onSuccess: invalidateActiveTimer }, + mutation: { + onSuccess: invalidateActiveTimer, + onError: handleMutationError, + }, }); const { mutate: completeTimer } = useCompleteTimer({ mutation: { @@ -67,13 +80,14 @@ export const FocusSessionContainer = () => { invalidateActiveTimer(); invalidateFocusTodo(); }, + onError: handleMutationError, }, }); const { mutate: changeTodoStatus } = useChangeTodoStatus({ - mutation: { onSuccess: invalidateFocusTodo }, + mutation: { onSuccess: invalidateFocusTodo, onError: handleMutationError }, }); const { mutate: changeSubtaskStatus } = useChangeSubtaskStatus({ - mutation: { onSuccess: invalidateFocusTodo }, + mutation: { onSuccess: invalidateFocusTodo, onError: handleMutationError }, }); const todo = focusView.todo; @@ -142,6 +156,11 @@ export const FocusSessionContainer = () => { if (!focusView.hasTodo || !todo) { return (
+ setIsErrorToastOpen(false)} + message={tToast("focusActionFailed")} + />
{ return (
+ setIsErrorToastOpen(false)} + message={tToast("focusActionFailed")} + />
8 tags, and to add more, please delete existing tags.", "todoLimitLine1": "You can add up to 20 incomplete to-dos.", "todoLimitLine2": "To add a new to-do, please complete an existing one.", - "onboardingSubmitFailed": "Failed to save onboarding. Please try again." + "onboardingSubmitFailed": "Failed to save onboarding. Please try again.", + "focusActionFailed": "Failed to process the request. Please try again." }, "Login": { "animationLabel": "Login animation", diff --git a/apps/timo-web/messages/ko.json b/apps/timo-web/messages/ko.json index ed7fb120..8f1e8ba4 100644 --- a/apps/timo-web/messages/ko.json +++ b/apps/timo-web/messages/ko.json @@ -110,7 +110,8 @@ "tagLimit": "태그는 최대 8개까지 만들 수 있으며, 추가하려면 기존 태그를 삭제해 주세요.", "todoLimitLine1": "완료되지 않은 투두는 최대 20개까지 추가할 수 있어요.", "todoLimitLine2": "새로운 투두를 추가하려면 기존 투두를 완료해주세요.", - "onboardingSubmitFailed": "온보딩 저장에 실패했어요. 다시 시도해 주세요." + "onboardingSubmitFailed": "온보딩 저장에 실패했어요. 다시 시도해 주세요.", + "focusActionFailed": "요청을 처리하지 못했어요. 다시 시도해 주세요." }, "Login": { "animationLabel": "로그인 애니메이션",