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
@@ -1,6 +1,4 @@
const SECONDS_PER_HOUR = 3600;

const SECONDS_PER_MINUTE = 60;
import { SECONDS_PER_HOUR, SECONDS_PER_MINUTE } from "@/constants/time";

/**
* 초 단위 duration을 "H:MM" 형식의 텍스트로 변환한다.
Expand Down
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,37 @@ import { focusTaskMock } from "@/app/[locale]/(main)/focus/_mocks/task-mock";
import {
convertDateToBadgeText,
convertDateToDayNumberText,
convertDateToDayOfWeekText,
convertDateToDayOfWeekKey,
} from "@/app/[locale]/(main)/focus/_utils/date";
import { Timer } from "@/components/timer/Timer";
import { TimerControls } from "@/components/timer/TimerControls";
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";

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 +55,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 +83,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 +115,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 +122 to +123

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: 0 additions & 12 deletions apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts

This file was deleted.

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 "@/utils/format-duration-label";

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>
);
};
42 changes: 42 additions & 0 deletions apps/timo-web/components/timer/TimerEndModalPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import timoTimerLogo from "@repo/timo-design-system/assets/images/logo/timo-timer.svg";
import { Modal, ModalButton } from "@repo/timo-design-system/ui";
import Image from "next/image";
import { useTranslations } from "next-intl";

export interface TimerEndModalPanelProps {
onContinue: () => void;
onComplete: () => void;
}

export const TimerEndModalPanel = ({
onContinue,
onComplete,
}: TimerEndModalPanelProps) => {
const t = useTranslations("Focus.endModal");

return (
<>
<Modal.Icon>
<Image src={timoTimerLogo} alt="" width={40} height={40} />
</Modal.Icon>
<Modal.Title>{t("title")}</Modal.Title>
<Modal.Description>{t("description")}</Modal.Description>
<Modal.Footer>
<ModalButton
variant="border"
className="flex-1 px-0"
onClick={onContinue}
>
{t("continueButton")}
</ModalButton>
<ModalButton
variant="fill"
className="flex-1 px-0"
onClick={onComplete}
>
{t("completeButton")}
</ModalButton>
</Modal.Footer>
</>
);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading