-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 타이머 종료·완료·연장·중단 모달 연결 및 다국어 지원 #140
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
base: develop
Are you sure you want to change the base?
Changes from all commits
cad030f
b2a2414
e1571a4
8434073
af6e208
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 |
|---|---|---|
| @@ -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"; | ||
|
|
||
|
|
@@ -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; | ||
|
|
||
| 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, | ||
|
|
@@ -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, | ||
|
|
@@ -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)} | ||
|
|
@@ -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
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
경과 시간 추적이 구현될 때까지 TODO 주석 추가 또는 🛡️ 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 |
||
| onExtend={handleExtendTimer} | ||
| onComplete={handleCompleteTimer} | ||
| /> | ||
| </div> | ||
| </section> | ||
|
|
||
| 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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = () => {}; | ||
|
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
✏️ proposed fix: TODO 주석 추가- const handleExtendTimer = () => {};
+ // TODO: 사이드바 타이머 시간 연장 로직 구현 필요
+ const handleExtendTimer = () => {};🤖 Prompt for AI Agents |
||
| const handleCompleteTimer = () => setStatus("PAUSED"); | ||
|
|
||
| return ( | ||
| <div className="flex flex-col items-center gap-11.25"> | ||
|
|
@@ -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
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🛡️ 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 |
||
| onExtend={handleExtendTimer} | ||
| onComplete={handleCompleteTimer} | ||
| /> | ||
| </div> | ||
| ); | ||
|
|
||
| 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> | ||
| </> | ||
| ); | ||
| }; |
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 | ⚡ Quick win
SECONDS_PER_MINUTE상수가duration.ts와 중복 정의되어 있습니다duration.ts에 동일한 상수(60)가 이미 정의되어 있습니다.duration.ts에서 export하여 재사용하면 값 불일치 리스크를 방지할 수 있습니다.♻️ proposed fix
duration.ts에서 상수를 export합니다:FocusSessionContainer.tsx에서 기존 import에 추가하고 로컬 선언을 제거합니다:import { convertDurationToMinutes, convertDurationToTimeText, + SECONDS_PER_MINUTE, } from "`@/app/`[locale]/(main)/focus/_utils/duration";📝 Committable suggestion
🤖 Prompt for AI Agents