Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 (
<section className="flex w-full min-w-80 flex-col items-start gap-5 px-[34.5px] pb-8">
<div className="text-timo-gray-900 flex flex-col items-start">
<p className="typo-headline-b-30">{dayNumber}</p>
<p className="typo-headline-m-14">{dayOfWeek}</p>
</div>

<div className="flex w-full items-center gap-2.5">
<div aria-hidden="true">
<Checkbox checked disabled onChange={() => {}} />
</div>
<p className="typo-headline-b-22 text-timo-black min-w-0 flex-1 wrap-break-word">
{t("empty")}
</p>
</div>

<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center gap-0.5">
<CalendarDisableIcon width={22} height={22} />
<span className="typo-caption-r-10 text-timo-gray-700 whitespace-nowrap">
{dateText}
</span>
</div>

<div className="flex items-center gap-0.5">
<ClockDisableIcon width={22} height={22} />
<span className="typo-caption-r-10 text-timo-gray-700 w-9 text-center whitespace-nowrap">
00:00
</span>
</div>
</div>

<PlayButton
variant="play"
size="lg"
disabled
ariaLabel={t("emptyPlayDisabled")}
>
<PlayDisabledIcon width={24} height={24} />
</PlayButton>
</div>

<div className="border-timo-gray-500 min-h-26 w-full border-t" />
</section>
);
};
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -19,86 +32,192 @@ import {
TimerSessionControls,
type TimerSessionControlsHandle,
} from "@/components/timer/TimerSessionControls";
import { SECONDS_PER_MINUTE } from "@/constants/time";
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";

export const FocusSessionContainer = () => {
const [task, setTask] = useState<FocusTask>(focusTaskMock);
const today = new Date();
const queryClient = useQueryClient();
const timerSessionControlsRef = useRef<TimerSessionControlsHandle>(null);
const tWeekday = useTranslations("Common.weekday");
const tDuration = useTranslations("Focus.duration");
const tToast = useTranslations("Toast");
const [feedbackText, setFeedbackText] = useState<string | undefined>();
const [isErrorToastOpen, setIsErrorToastOpen] = useState(false);

const { data: focusView } = useFocusTodo();
const { data: activeTimer } = useActiveTimer();

const invalidateActiveTimer = () =>
queryClient.invalidateQueries({ queryKey: getGetActiveTimerQueryKey() });
const invalidateFocusTodo = () =>
queryClient.invalidateQueries({ queryKey: getGetFocusTodoQueryKey() });
const handleMutationError = () => setIsErrorToastOpen(true);

const { mutate: startTimer } = useStartTimer({
mutation: {
onSuccess: invalidateActiveTimer,
onError: handleMutationError,
},
});
const { mutate: changeStatus } = useChangeStatus({
mutation: {
onSuccess: invalidateActiveTimer,
onError: handleMutationError,
},
});
const { mutate: extendTimer } = useExtendTimer({
mutation: {
onSuccess: invalidateActiveTimer,
onError: handleMutationError,
},
});
const { mutate: completeTimer } = useCompleteTimer({
mutation: {
onSuccess: (response) => {
setFeedbackText(response.data?.aiFeedback ?? undefined);
invalidateActiveTimer();
invalidateFocusTodo();
},
onError: handleMutationError,
},
});
Comment on lines +58 to +85

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 핸들링 추가해 주세요!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토스트로 추가했습니다-!

const { mutate: changeTodoStatus } = useChangeTodoStatus({
mutation: { onSuccess: invalidateFocusTodo, onError: handleMutationError },
});
const { mutate: changeSubtaskStatus } = useChangeSubtaskStatus({
mutation: { onSuccess: invalidateFocusTodo, onError: handleMutationError },
});

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);
Comment on lines +153 to +154

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and related helpers
git ls-files 'apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx'
rg -n "convertDateToBadgeText|convertDateToDayNumberText|convertDateToDayOfWeekKey|new Date\\(focusView\\.date\\)|focusView\\.date" apps/timo-web -S

Repository: Team-Timo/Timo-client

Length of output: 1916


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the relevant lines
sed -n '120,155p' apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx

# Find helper implementations
rg -n "function convertDateToBadgeText|const convertDateToBadgeText|function convertDateToDayNumberText|const convertDateToDayNumberText|function convertDateToDayOfWeekKey|const convertDateToDayOfWeekKey" apps/timo-web -S

Repository: Team-Timo/Timo-client

Length of output: 212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Target file excerpt
sed -n '1,240p' 'apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx'

# Helper implementations and usages
rg -n "convertDateToBadgeText|convertDateToDayNumberText|convertDateToDayOfWeekKey" apps/timo-web packages -S

Repository: Team-Timo/Timo-client

Length of output: 9164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' 'apps/timo-web/app/[locale]/(main)/focus/_utils/date.ts'

node - <<'JS'
const d = new Date("2026-07-13");
console.log("toISOString:", d.toISOString());
console.log("local date getters:", d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getDay());
console.log("utc date getters:", d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), d.getUTCDay());
JS

Repository: Team-Timo/Timo-client

Length of output: 933


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const zones = ["UTC", "America/Los_Angeles", "America/New_York", "Asia/Tokyo"];
for (const tz of zones) {
  process.env.TZ = tz;
  const d = new Date("2026-07-13");
  console.log(
    tz,
    "=>",
    d.toString(),
    "| local:",
    d.getFullYear(),
    d.getMonth() + 1,
    d.getDate(),
    d.getDay(),
    "| badge:",
    `${d.getFullYear()}`.slice(-2) + "." +
    `${d.getMonth() + 1}`.padStart(2, "0") + "." +
    `${d.getDate()}`.padStart(2, "0")
  );
}
JS

Repository: Team-Timo/Timo-client

Length of output: 614


YYYY-MM-DD는 로컬 자정으로 파싱하세요
focusView.date는 날짜 전용 문자열이라 new Date(focusView.date)가 UTC 자정으로 해석됩니다. 그런데 getDate()/getDay() 계열은 로컬 시간을 보므로, UTC보다 뒤처진 타임존에서는 날짜/요일이 하루 밀려 보여요.

🛠️ 제안 수정
-  const today = new Date(focusView.date);
+  const today = new Date(`${focusView.date}T00:00:00`);

MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const today = new Date(focusView.date);
const dateText = convertDateToBadgeText(today);
const today = new Date(`${focusView.date}T00:00:00`);
const dateText = convertDateToBadgeText(today);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
around lines 139 - 140, Update the date construction in the
FocusSessionContainer flow before convertDateToBadgeText so the YYYY-MM-DD value
is parsed as local midnight rather than via new Date(string) UTC parsing.
Preserve focusView.date’s calendar date for local getDate()/getDay()-based
formatting.


if (!focusView.hasTodo || !todo) {
return (
<div className="flex h-full overflow-x-auto">
<AnimatedToast
isOpen={isErrorToastOpen}
onClose={() => setIsErrorToastOpen(false)}
message={tToast("focusActionFailed")}
/>
<div className="flex flex-1 flex-col gap-18">
<FocusHeaderContainer />
<FocusEmptyTaskItem
dayNumber={convertDateToDayNumberText(today)}
dayOfWeek={tWeekday(convertDateToDayOfWeekKey(today))}
dateText={dateText}
/>
</div>

<section className="border-timo-gray-500 flex h-full w-136.5 shrink-0 items-center border-l bg-white">
<div className="flex w-full flex-col items-center gap-11.25">
<Timer time="00:00" plannedLabel="0M" progress={0} size="lg" />

<TimerSessionControls
isRunning={false}
onTogglePlay={() => {}}
plannedMinutes={0}
actualMinutes={0}
onExtend={() => {}}
onComplete={() => {}}
disabled
/>
</div>
</section>
</div>
);
}

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 (
<div className="flex h-full overflow-x-auto">
<AnimatedToast
isOpen={isErrorToastOpen}
onClose={() => setIsErrorToastOpen(false)}
message={tToast("focusActionFailed")}
/>
<div className="flex flex-1 flex-col gap-18">
<FocusHeaderContainer />
<FocusTaskItem
dayNumber={convertDateToDayNumberText(today)}
dayOfWeek={tWeekday(convertDateToDayOfWeekKey(today))}
title={task.title}
completed={task.completed}
dateText={convertDateToBadgeText(task.scheduledDate)}
durationText={convertDurationToTimeText(task.durationSeconds)}
isRunning={task.isRunning}
subtasks={task.subtasks}
memo={task.memo}
title={todo.title}
completed={todo.completed}
dateText={dateText}
durationText={convertDurationToTimeText(todo.durationSeconds ?? 0)}
isRunning={isRunning}
subtasks={todo.subtasks}
onToggleCompleted={handleToggleCompleted}
onTogglePlay={handleTogglePlay}
onToggleSubtaskCompleted={handleToggleSubtaskCompleted}
Expand All @@ -109,18 +228,23 @@ export const FocusSessionContainer = () => {
<div className="flex w-full flex-col items-center gap-11.25">
<Timer
icon={<TimerOnIcon />}
time="10:30"
plannedLabel="12M"
progress={70}
time={convertDurationToTimeText(remainingSeconds)}
plannedLabel={formatDurationLabel(
plannedMinutes,
tDuration("hourUnit"),
tDuration("minuteUnit"),
)}
progress={progress}
size="lg"
/>

<TimerSessionControls
ref={timerSessionControlsRef}
isRunning={task.isRunning}
isRunning={isRunning}
onTogglePlay={handleTogglePlay}
plannedMinutes={plannedMinutes}
actualMinutes={plannedMinutes}
actualMinutes={actualMinutes}
feedbackText={feedbackText}
onExtend={handleExtendTimer}
onComplete={handleCompleteTimer}
/>
Expand Down
15 changes: 0 additions & 15 deletions apps/timo-web/app/[locale]/(main)/focus/_mocks/task-mock.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,
});
Loading
Loading