-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 집중 모드(Focus) 타이머·태스크 상세 화면 구현 #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
98cef32
9503d6d
15bf149
432b3f6
efbb16b
c4a2457
aab8d25
6f0ed4e
45e5e41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 = () => {}; | ||
|
|
||
| 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> | ||
| ); | ||
| }; | ||
| 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: "회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기회의 전까지 초안 공유하기", | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| export interface FocusTaskSubtask { | ||
| subtaskId: number; | ||
| content: string; | ||
| completed: boolean; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Boolean 네이밍 컨벤션 살짝 아쉬워요 😅
As per path instructions, "Boolean 변수명: ♻️ 제안 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 AgentsSource: Path instructions |
||
| } | ||
|
|
||
| export interface FocusTask { | ||
| taskId: number; | ||
| title: string; | ||
| completed: boolean; | ||
| scheduledDate: Date; | ||
| durationSeconds: number; | ||
| isRunning: boolean; | ||
| subtasks: FocusTaskSubtask[]; | ||
| memo?: string; | ||
| } | ||
| 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}`; | ||
| }; |
| 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")}`; | ||
| }; |
| 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 />; | ||
| } |
There was a problem hiding this comment.
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만 빈 화살괄호로 덩그러니 있어요. 리뷰어가 "이거 의도된 거 맞나?" 갸우뚱하게 만드는 포인트입니다.원하시면 이 부분 이슈로 등록해드릴까요?
📝 Committable suggestion
🤖 Prompt for AI Agents