Skip to content
Open
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
@@ -1,7 +1,8 @@
"use client";

import { TimerOnIcon } from "@repo/timo-design-system/icons";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useRef, useState } from "react";

import type { FocusTask } from "@/app/[locale]/(main)/focus/_types/task-type";

Expand All @@ -11,22 +12,38 @@ import { focusTaskMock } from "@/app/[locale]/(main)/focus/_mocks/task-mock";
import {
convertDateToBadgeText,
convertDateToDayNumberText,
convertDateToDayOfWeekText,
convertDateToDayOfWeekKey,
} from "@/app/[locale]/(main)/focus/_utils/date";
import { convertDurationToMinutes } from "@/app/[locale]/(main)/focus/_utils/duration";
import { Timer } from "@/components/timer/Timer";
import { TimerControls } from "@/components/timer/TimerControls";
import {
TimerSessionControls,
type TimerSessionControlsHandle,
} from "@/components/timer/TimerSessionControls";
import { convertDurationToTimeText } from "@/utils/convert-duration-to-time-text";

const SECONDS_PER_MINUTE = 60;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

SECONDS_PER_MINUTE 상수가 duration.ts와 중복 정의되어 있습니다

duration.ts에 동일한 상수(60)가 이미 정의되어 있습니다. duration.ts에서 export하여 재사용하면 값 불일치 리스크를 방지할 수 있습니다.

♻️ proposed fix

duration.ts에서 상수를 export합니다:

-const SECONDS_PER_MINUTE = 60;
+export const SECONDS_PER_MINUTE = 60;

FocusSessionContainer.tsx에서 기존 import에 추가하고 로컬 선언을 제거합니다:

 import {
   convertDurationToMinutes,
   convertDurationToTimeText,
+  SECONDS_PER_MINUTE,
 } from "`@/app/`[locale]/(main)/focus/_utils/duration";
-const SECONDS_PER_MINUTE = 60;
-
 export const FocusSessionContainer = () => {
📝 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 SECONDS_PER_MINUTE = 60;
🤖 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
at line 27, Remove the duplicated SECONDS_PER_MINUTE declaration from
FocusSessionContainer and import the shared exported constant from duration.ts,
updating duration.ts to export it if necessary.


export const FocusSessionContainer = () => {
const [task, setTask] = useState<FocusTask>(focusTaskMock);
const today = new Date();
const timerSessionControlsRef = useRef<TimerSessionControlsHandle>(null);
const tWeekday = useTranslations("Common.weekday");

const handleTogglePlay = () => {
// TODO: API
setTask((prev) => ({ ...prev, isRunning: !prev.isRunning }));
};

const handleEnd = () => {
const handleExtendTimer = (minutes: number) => {
// TODO: API
setTask((prev) => ({
...prev,
durationSeconds: prev.durationSeconds + minutes * SECONDS_PER_MINUTE,
}));
};

const handleCompleteTimer = () => {
// TODO: API
setTask((prev) => ({
...prev,
Expand All @@ -39,9 +56,12 @@ export const FocusSessionContainer = () => {
}));
};

const handleAddTime = () => {};

const handleToggleCompleted = (completed: boolean) => {
if (completed && task.isRunning) {
timerSessionControlsRef.current?.openStopModal();
return;
}

// TODO: API
setTask((prev) => ({
...prev,
Expand All @@ -64,13 +84,15 @@ export const FocusSessionContainer = () => {
}));
};

const plannedMinutes = convertDurationToMinutes(task.durationSeconds);

return (
<div className="flex h-full overflow-x-auto">
<div className="flex flex-1 flex-col gap-18">
<FocusHeaderContainer />
<FocusTaskItem
dayNumber={convertDateToDayNumberText(today)}
dayOfWeek={convertDateToDayOfWeekText(today)}
dayOfWeek={tWeekday(convertDateToDayOfWeekKey(today))}
title={task.title}
completed={task.completed}
dateText={convertDateToBadgeText(task.scheduledDate)}
Expand All @@ -94,11 +116,14 @@ export const FocusSessionContainer = () => {
size="lg"
/>

<TimerControls
<TimerSessionControls
ref={timerSessionControlsRef}
isRunning={task.isRunning}
onTogglePlay={handleTogglePlay}
onEnd={handleEnd}
onAddTime={handleAddTime}
plannedMinutes={plannedMinutes}
actualMinutes={plannedMinutes}
Comment on lines +123 to +124

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

actualMinutesplannedMinutes를 그대로 전달하여 "계획"과 "실제"가 항상 동일하게 표시됩니다

FocusTask 인터페이스에 경과 시간 필드(elapsedSeconds 등)가 없어 현재 데이터 모델로는 실제 시간 추적이 불가능합니다. 하지만 actualMinutes={plannedMinutes}로 전달하면:

  • TimerCompleteModalPanel의 "계획"과 "실제"가 항상 같은 값을 보여줌 (예: 둘 다 "2h")
  • TimerStopModalPanel의 "지금까지 수행한 {minutes}"에 계획 시간이 표시됨

경과 시간 추적이 구현될 때까지 TODO 주석 추가 또는 actualMinutes={0} 임시값 사용을 고려하세요. TimerPanel.tsxPLANNED_MINUTES 전달에도 동일한 패턴이 나타납니다.

🛡️ proposed fix: TODO 주석 추가
           <TimerSessionControls
             ref={timerSessionControlsRef}
             isRunning={task.isRunning}
             onTogglePlay={handleTogglePlay}
             plannedMinutes={plannedMinutes}
-            actualMinutes={plannedMinutes}
+            // TODO: actualMinutes에 실제 경과 시간 연결 필요 (FocusTask에 elapsedSeconds 필드 추가)
+            actualMinutes={0}
             onExtend={handleExtendTimer}
             onComplete={handleCompleteTimer}
           />
🤖 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 125 - 126, FocusSessionContainer의 actualMinutes에 plannedMinutes를
전달하지 말고, 실제 경과 시간 필드가 추가될 때까지 0을 임시값으로 사용하거나 명확한 TODO를 추가하세요.
TimerCompleteModalPanel과 TimerStopModalPanel에 계획 시간이 실제 시간으로 표시되지 않도록 수정하고,
TimerPanel의 PLANNED_MINUTES 전달부에도 동일한 처리를 적용하세요.

onExtend={handleExtendTimer}
onComplete={handleCompleteTimer}
/>
</div>
</section>
Expand Down
22 changes: 12 additions & 10 deletions apps/timo-web/app/[locale]/(main)/focus/_utils/date.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
const DAY_OF_WEEK_LABELS = [
"일요일",
"월요일",
"화요일",
"수요일",
"목요일",
"금요일",
"토요일",
export type WeekdayKey = "SUN" | "MON" | "TUE" | "WED" | "THU" | "FRI" | "SAT";

const WEEKDAY_KEYS: WeekdayKey[] = [
"SUN",
"MON",
"TUE",
"WED",
"THU",
"FRI",
"SAT",
];

export const convertDateToDayNumberText = (date: Date): string => {
return `${date.getDate()}`;
};

export const convertDateToDayOfWeekText = (date: Date): string => {
return DAY_OF_WEEK_LABELS[date.getDay()] ?? "";
export const convertDateToDayOfWeekKey = (date: Date): WeekdayKey => {
return WEEKDAY_KEYS[date.getDay()] ?? "SUN";
};

export const convertDateToBadgeText = (date: Date): string => {
Expand Down
12 changes: 2 additions & 10 deletions apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
const SECONDS_PER_HOUR = 3600;

const SECONDS_PER_MINUTE = 60;

export const convertDurationToTimeText = (durationSeconds: number): string => {
const hours = Math.floor(durationSeconds / SECONDS_PER_HOUR);
const minutes = Math.floor(
(durationSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE,
);

return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
};
export const convertDurationToMinutes = (durationSeconds: number): number =>
Math.round(durationSeconds / SECONDS_PER_MINUTE);
16 changes: 10 additions & 6 deletions apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { TimerOnIcon } from "@repo/timo-design-system/icons";
import { useState } from "react";

import { Timer } from "@/components/timer/Timer";
import { TimerControls } from "@/components/timer/TimerControls";
import { TimerSessionControls } from "@/components/timer/TimerSessionControls";

type TimerStatus = "RUNNING" | "PAUSED";

const PLANNED_MINUTES = 12;

export const TimerPanel = () => {
const [status, setStatus] = useState<TimerStatus>("PAUSED");

const handleTogglePlay = () =>
setStatus((prev) => (prev === "RUNNING" ? "PAUSED" : "RUNNING"));
const handleEnd = () => setStatus("PAUSED");
const handleAddTime = () => {};
const handleExtendTimer = () => {};

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 | 🟡 Minor | ⚡ Quick win

handleExtendTimer가 빈 구현입니다

FocusSessionContainer의 동일 핸들러는 durationSeconds 업데이트와 TODO 주석을 포함하지만, TimerPanelhandleExtendTimer는 no-op이고 TODO도 없습니다. 연장 모달은 표시되지만 "시간 추가하기" 버튼이 아무 동작도 수행하지 않습니다.

✏️ proposed fix: TODO 주석 추가
-  const handleExtendTimer = () => {};
+  // TODO: 사이드바 타이머 시간 연장 로직 구현 필요
+  const handleExtendTimer = () => {};
🤖 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/components/layout/sidebar/time/TimerPanel.tsx` at line 18,
TimerPanel의 handleExtendTimer가 no-op 상태이므로, FocusSessionContainer의 동일 핸들러 동작을
참고해 연장 시 durationSeconds를 업데이트하고 필요한 TODO 주석을 추가하세요.

const handleCompleteTimer = () => setStatus("PAUSED");

return (
<div className="flex flex-col items-center gap-11.25">
Expand All @@ -26,11 +28,13 @@ export const TimerPanel = () => {
size="sm"
/>

<TimerControls
<TimerSessionControls
isRunning={status === "RUNNING"}
onTogglePlay={handleTogglePlay}
onEnd={handleEnd}
onAddTime={handleAddTime}
plannedMinutes={PLANNED_MINUTES}
actualMinutes={PLANNED_MINUTES}
Comment on lines +34 to +35

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

actualMinutesPLANNED_MINUTES를 전달하여 "계획"과 "실제"가 동일하게 표시됩니다

FocusSessionContainer에서 지적한 것과 동일한 패턴입니다. actualMinutes={PLANNED_MINUTES}로 인해 완료 모달의 "계획"과 "실제"가 항상 같은 값을 보여줍니다.

🛡️ proposed fix: TODO 주석 추가
       <TimerSessionControls
         isRunning={status === "RUNNING"}
         onTogglePlay={handleTogglePlay}
         plannedMinutes={PLANNED_MINUTES}
-        actualMinutes={PLANNED_MINUTES}
+        // TODO: actualMinutes에 실제 경과 시간 연결 필요
+        actualMinutes={0}
         onExtend={handleExtendTimer}
         onComplete={handleCompleteTimer}
       />
🤖 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/components/layout/sidebar/time/TimerPanel.tsx` around lines 34
- 35, TimerPanel의 FocusSessionContainer 호출에서 actualMinutes에 계획값인
PLANNED_MINUTES를 전달하는 문제를 수정하세요. 실제 집중 세션 시간을 추적하는 상태나 계산값을 사용해 actualMinutes를
전달하고, plannedMinutes에는 계속 PLANNED_MINUTES를 유지하여 완료 모달에 계획 시간과 실제 시간이 올바르게 표시되도록
하세요.

onExtend={handleExtendTimer}
onComplete={handleCompleteTimer}
/>
</div>
);
Expand Down
59 changes: 59 additions & 0 deletions apps/timo-web/components/timer/TimerCompleteModalPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Modal } from "@repo/timo-design-system/ui";
import { useTranslations } from "next-intl";

import { formatDurationLabel } from "@/components/timer/durationLabel";

export interface TimerCompleteModalPanelProps {
plannedMinutes: number;
actualMinutes: number;
feedbackText?: string;
onComplete: () => void;
}

export const TimerCompleteModalPanel = ({
plannedMinutes,
actualMinutes,
feedbackText,
onComplete,
}: TimerCompleteModalPanelProps) => {
const t = useTranslations("Focus.completeModal");
const tDuration = useTranslations("Focus.duration");
const hourUnit = tDuration("hourUnit");
const minuteUnit = tDuration("minuteUnit");

return (
<>
<Modal.Title>{t("title")}</Modal.Title>
<p className="typo-headline-r-14 text-timo-black mt-3">
{feedbackText ?? t("defaultFeedback")}
</p>

<div className="bg-timo-gray-500 mt-4.5 h-px w-full" />

<div className="mt-4 flex gap-6.5">
<div className="flex flex-col gap-1">
<span className="typo-body-m-12 text-timo-gray-900">
{t("plannedLabel")}
</span>
<span className="typo-headline-b-20 text-timo-gray-900">
{formatDurationLabel(plannedMinutes, hourUnit, minuteUnit)}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="typo-body-m-12 text-timo-blue-300">
{t("actualLabel")}
</span>
<span className="typo-headline-m-20 text-timo-blue-300">
{formatDurationLabel(actualMinutes, hourUnit, minuteUnit)}
</span>
</div>
</div>

<Modal.Footer>
<Modal.FillButton className="flex-1 px-0" onClick={onComplete}>
{t("completeButton")}
</Modal.FillButton>
</Modal.Footer>
</>
);
};
2 changes: 1 addition & 1 deletion apps/timo-web/components/timer/TimerControlButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const TimerControlButton = ({
onClick={onClick}
disabled={disabled}
className={cn(
"group flex size-14.5 items-center justify-center rounded-full",
"group focus-visible:border-timo-blue-300 flex size-14.5 items-center justify-center rounded-full border-2 border-transparent outline-none",
isForcedActive
? "bg-timo-blue-50"
: activeIcon
Expand Down
53 changes: 36 additions & 17 deletions apps/timo-web/components/timer/TimerControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,65 @@ import {
PlusIcon,
StopIcon,
} from "@repo/timo-design-system/icons";
import { Modal } from "@repo/timo-design-system/ui";
import { useTranslations } from "next-intl";

import { TimerControlButton } from "@/components/timer/TimerControlButton";

const MODAL_TRIGGER_CLASS =
"group bg-timo-gray-300 active:bg-timo-blue-50 focus-visible:border-timo-blue-300 flex size-14.5 items-center justify-center rounded-full border-2 border-transparent outline-none";

export interface TimerControlsProps {
isRunning: boolean;
onTogglePlay: () => void;
onEnd: () => void;
onAddTime: () => void;
onOpenEndModal: () => void;
onOpenExtendModal: () => void;
}

export const TimerControls = ({
isRunning,
onTogglePlay,
onEnd,
onAddTime,
onOpenEndModal,
onOpenExtendModal,
}: TimerControlsProps) => {
const t = useTranslations("Focus.controls");

return (
<div className="flex items-center gap-5">
<TimerControlButton
icon={<EndBlackIcon />}
activeIcon={<EndBlueIcon />}
label="종료"
onClick={onEnd}
/>
<Modal.Trigger
aria-label={t("end")}
onClick={onOpenEndModal}
className={MODAL_TRIGGER_CLASS}
>
<span className="group-active:hidden">
<EndBlackIcon />
</span>
<span className="hidden group-active:block">
<EndBlueIcon />
</span>
</Modal.Trigger>

<TimerControlButton
icon={
isRunning ? <StopIcon width={24} height={24} /> : <PlayTimerIcon />
}
label={isRunning ? "일시정지" : "재생"}
label={isRunning ? t("pause") : t("play")}
variant={isRunning ? "active" : "default"}
onClick={onTogglePlay}
/>

<TimerControlButton
icon={<PlusIcon width={27} height={27} />}
activeIcon={<PlusBlueIcon />}
label="시간 추가"
onClick={onAddTime}
/>
<Modal.Trigger
aria-label={t("extend")}
onClick={onOpenExtendModal}
className={MODAL_TRIGGER_CLASS}
>
<span className="group-active:hidden">
<PlusIcon width={27} height={27} />
</span>
<span className="hidden group-active:block">
<PlusBlueIcon />
</span>
</Modal.Trigger>
</div>
);
};
Loading
Loading