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
113 changes: 113 additions & 0 deletions apps/timo-web/app/[locale]/(main)/focus/_components/FocusTaskItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
CalendarOnIcon,
ClockOnIcon,
PlayIcon,
StopIcon,
} from "@repo/timo-design-system/icons";
import { Checkbox, PlayButton } from "@repo/timo-design-system/ui";
import { cn } from "@repo/timo-design-system/utils";

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

export interface FocusTaskItemProps {
dayNumber: string;
dayOfWeek: string;
title: string;
completed: boolean;
dateText: string;
durationText: string;
isRunning: boolean;
subtasks: FocusTaskSubtask[];
memo?: string;
onToggleCompleted: (completed: boolean) => void;
onTogglePlay: () => void;
onToggleSubtaskCompleted: (subtaskId: number, completed: boolean) => void;
}

export const FocusTaskItem = ({
dayNumber,
dayOfWeek,
title,
completed,
dateText,
durationText,
isRunning,
subtasks,
memo,
onToggleCompleted,
onTogglePlay,
onToggleSubtaskCompleted,
}: FocusTaskItemProps) => {
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">
<Checkbox checked={completed} onChange={onToggleCompleted} />
<p className="typo-headline-b-22 text-timo-black min-w-0 flex-1 wrap-break-word">
{title}
</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">
<CalendarOnIcon width={22} height={22} />
<span className="typo-caption-r-10 text-timo-gray-900 whitespace-nowrap">
{dateText}
</span>
</div>

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

<PlayButton
variant={isRunning ? "stop" : "play"}
size="lg"
onClick={onTogglePlay}
>
{isRunning ? (
<StopIcon width={24} height={24} />
) : (
<PlayIcon width={24} height={24} />
)}
</PlayButton>
</div>

<div className="border-timo-gray-500 flex min-h-26 w-full min-w-0 flex-col gap-3 border-t pt-3">
{subtasks.map((subtask) => (
<div key={subtask.subtaskId} className="flex items-center gap-2">
<Checkbox
checked={subtask.completed}
onChange={(checked) =>
onToggleSubtaskCompleted(subtask.subtaskId, checked)
}
/>
<p
className={cn(
"typo-body-r-12 min-w-0 flex-1 wrap-break-word",
subtask.completed ? "text-timo-gray-700" : "text-timo-black",
)}
>
{subtask.content}
</p>
</div>
))}

{memo && (
<p className="typo-body-r-12 text-timo-gray-800 min-w-0 wrap-break-word">
{memo}
</p>
)}
</div>
</section>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { TimerOnIcon } from "@repo/timo-design-system/icons";
import { useState } from "react";

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

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 {
convertDateToBadgeText,
convertDateToDayNumberText,
convertDateToDayOfWeekText,
} from "@/app/[locale]/(main)/focus/_utils/date";
import { convertDurationToTimeText } from "@/app/[locale]/(main)/focus/_utils/duration";
import { Timer } from "@/components/timer/Timer";
import { TimerControls } from "@/components/timer/TimerControls";

export const FocusSessionContainer = () => {
const [task, setTask] = useState<FocusTask>(focusTaskMock);
const today = new Date();

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

const handleEnd = () => {
// TODO: API
setTask((prev) => ({
...prev,
isRunning: false,
completed: true,
subtasks: prev.subtasks.map((subtask) => ({
...subtask,
completed: true,
})),
}));
};

const handleAddTime = () => {};

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 | 💤 Low value

handleAddTime, 조용히 비어있네요 (TODO 주석마저 없이).

다른 핸들러들은 // TODO: API 주석으로 나중에 작업임을 표시해뒀는데, handleAddTime만 빈 화살괄호로 덩그러니 있어요. 리뷰어가 "이거 의도된 거 맞나?" 갸우뚱하게 만드는 포인트입니다.

-  const handleAddTime = () => {};
+  const handleAddTime = () => {
+    // TODO: 시간 추가 로직 (API 연동 및 타이머 엔진 연결 후속 작업)
+  };

원하시면 이 부분 이슈로 등록해드릴까요?

📝 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 handleAddTime = () => {};
const handleAddTime = () => {
// TODO: 시간 추가 로직 (API 연동 및 타이머 엔진 연결 후속 작업)
};
🤖 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 42, The handleAddTime placeholder in FocusSessionContainer is left as an
empty function, unlike the other handlers that clearly mark pending work with a
TODO comment. Update handleAddTime to match the existing handler pattern in
FocusSessionContainer by adding an explicit TODO marker (or otherwise making the
intentional stub obvious) so it is clear this is unfinished work rather than an
accidental no-op.


const handleToggleCompleted = (completed: boolean) => {
// TODO: API
setTask((prev) => ({
...prev,
completed,
isRunning: completed ? false : prev.isRunning,
subtasks: prev.subtasks.map((subtask) => ({ ...subtask, completed })),
}));
};

const handleToggleSubtaskCompleted = (
subtaskId: number,
completed: boolean,
) => {
// TODO: API
setTask((prev) => ({
...prev,
subtasks: prev.subtasks.map((subtask) =>
subtask.subtaskId === subtaskId ? { ...subtask, completed } : subtask,
),
}));
};

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)}
title={task.title}
completed={task.completed}
dateText={convertDateToBadgeText(task.scheduledDate)}
durationText={convertDurationToTimeText(task.durationSeconds)}
isRunning={task.isRunning}
subtasks={task.subtasks}
memo={task.memo}
onToggleCompleted={handleToggleCompleted}
onTogglePlay={handleTogglePlay}
onToggleSubtaskCompleted={handleToggleSubtaskCompleted}
/>
</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
icon={<TimerOnIcon />}
time="10:30"
plannedLabel="12M"
progress={70}
size="lg"
/>

<TimerControls
isRunning={task.isRunning}
onTogglePlay={handleTogglePlay}
onEnd={handleEnd}
onAddTime={handleAddTime}
/>
</div>
</section>
</div>
);
};
15 changes: 15 additions & 0 deletions apps/timo-web/app/[locale]/(main)/focus/_mocks/task-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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: "회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기",
};
16 changes: 16 additions & 0 deletions apps/timo-web/app/[locale]/(main)/focus/_types/task-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface FocusTaskSubtask {
subtaskId: number;
content: string;
completed: boolean;

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

Boolean 네이밍 컨벤션 살짝 아쉬워요 😅

completedis/has/should/can 접두사가 없어서 컨벤션 위반이에요. isCompleted로 바꾸면 isRunning과도 짝이 맞아서 훨씬 자연스러워집니다. 이 타입을 task-mock.tsFocusTaskCard.tsx에서도 그대로 쓰고 있어서, 한 번에 고치면 전체가 정리돼요.

As per path instructions, "Boolean 변수명: is, has, should, can 접두사".

♻️ 제안 diff
 export interface FocusTaskSubtask {
   subtaskId: number;
   content: string;
-  completed: boolean;
+  isCompleted: boolean;
 }

 export interface FocusTask {
   taskId: number;
   title: string;
-  completed: boolean;
+  isCompleted: boolean;
   scheduledDate: Date;
   durationSeconds: number;
   isRunning: boolean;
   subtasks: FocusTaskSubtask[];
   memo?: string;
 }

Also applies to: 10-10

🤖 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/_types/task-type.ts at line 4, The
boolean field name in the Task type does not follow the is/has/should/can naming
convention. Rename the `completed` property in `Task` to `isCompleted`, then
update every usage that reads or writes this field in `task-mock.ts` and
`FocusTaskCard.tsx` so the type and consumers stay aligned with the new name.

Source: Path instructions

}

export interface FocusTask {
taskId: number;
title: string;
completed: boolean;
scheduledDate: Date;
durationSeconds: number;
isRunning: boolean;
subtasks: FocusTaskSubtask[];
memo?: string;
}
25 changes: 25 additions & 0 deletions apps/timo-web/app/[locale]/(main)/focus/_utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const DAY_OF_WEEK_LABELS = [
"일요일",
"월요일",
"화요일",
"수요일",
"목요일",
"금요일",
"토요일",
];

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

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

export const convertDateToBadgeText = (date: Date): string => {
const year = `${date.getFullYear()}`.slice(-2);
const month = `${date.getMonth() + 1}`.padStart(2, "0");
const day = `${date.getDate()}`.padStart(2, "0");

return `${year}.${month}.${day}`;
};
12 changes: 12 additions & 0 deletions apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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")}`;
};
4 changes: 2 additions & 2 deletions apps/timo-web/app/[locale]/(main)/focus/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FocusHeaderContainer } from "@/app/[locale]/(main)/focus/_containers/FocusHeaderContainer";
import { FocusSessionContainer } from "@/app/[locale]/(main)/focus/_containers/FocusSessionContainer";

export default function FocusPage() {
return <FocusHeaderContainer />;
return <FocusSessionContainer />;
}
Loading