}
- 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}
/>
void;
children?: ReactNode;
+ ariaLabel?: string;
}
const VARIANT_MAP: Record<
@@ -32,6 +33,7 @@ export const PlayButton = ({
disabled = false,
onClick,
children,
+ ariaLabel,
}: PlayButtonProps) => {
return (