From cad030f7b31996913e8e4944782353026853e83b Mon Sep 17 00:00:00 2001 From: jjangminii Date: Sat, 11 Jul 2026 01:50:47 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(web):=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C/=EC=99=84=EB=A3=8C/=EC=97=B0=EC=9E=A5=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=97=B0=EA=B2=B0=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타이머 컨트롤의 종료·시간추가 버튼을 Modal.Trigger로 전환했습니다 - 종료 모달에서 완료/연장 단계로 전환되는 TimerSessionControls를 추가했습니다 - 종료·완료·연장 각 단계의 모달 콘텐츠를 Figma 디자인대로 구현했습니다 - FocusSessionContainer와 홈 사이드바 TimerPanel에 모달 플로우를 연결했습니다 - 분 단위 변환 유틸(convertDurationToMinutes)을 추가했습니다 --- .../_containers/FocusSessionContainer.tsx | 31 +++-- .../[locale]/(main)/focus/_utils/duration.ts | 3 + .../layout/sidebar/time/TimerPanel.tsx | 16 ++- .../timer/TimerCompleteModalPanel.tsx | 45 +++++++ .../components/timer/TimerControls.tsx | 48 +++++--- .../components/timer/TimerEndModalPanel.tsx | 94 +++++++++++++++ .../timer/TimerExtendModalPanel.tsx | 108 +++++++++++++++++ .../components/timer/TimerSessionControls.tsx | 110 ++++++++++++++++++ 8 files changed, 425 insertions(+), 30 deletions(-) create mode 100644 apps/timo-web/components/timer/TimerCompleteModalPanel.tsx create mode 100644 apps/timo-web/components/timer/TimerEndModalPanel.tsx create mode 100644 apps/timo-web/components/timer/TimerExtendModalPanel.tsx create mode 100644 apps/timo-web/components/timer/TimerSessionControls.tsx diff --git a/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx b/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx index 24b12380..d1ea133c 100644 --- a/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx @@ -13,9 +13,14 @@ import { convertDateToDayNumberText, convertDateToDayOfWeekText, } from "@/app/[locale]/(main)/focus/_utils/date"; -import { convertDurationToTimeText } from "@/app/[locale]/(main)/focus/_utils/duration"; +import { + convertDurationToMinutes, + convertDurationToTimeText, +} from "@/app/[locale]/(main)/focus/_utils/duration"; import { Timer } from "@/components/timer/Timer"; -import { TimerControls } from "@/components/timer/TimerControls"; +import { TimerSessionControls } from "@/components/timer/TimerSessionControls"; + +const SECONDS_PER_MINUTE = 60; export const FocusSessionContainer = () => { const [task, setTask] = useState(focusTaskMock); @@ -26,7 +31,15 @@ export const FocusSessionContainer = () => { 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,8 +52,6 @@ export const FocusSessionContainer = () => { })); }; - const handleAddTime = () => {}; - const handleToggleCompleted = (completed: boolean) => { // TODO: API setTask((prev) => ({ @@ -64,6 +75,8 @@ export const FocusSessionContainer = () => { })); }; + const plannedMinutes = convertDurationToMinutes(task.durationSeconds); + return (
@@ -94,11 +107,13 @@ export const FocusSessionContainer = () => { size="lg" /> -
diff --git a/apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts b/apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts index 5097d596..b118289c 100644 --- a/apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts +++ b/apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts @@ -10,3 +10,6 @@ export const convertDurationToTimeText = (durationSeconds: number): string => { return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; }; + +export const convertDurationToMinutes = (durationSeconds: number): number => + Math.round(durationSeconds / SECONDS_PER_MINUTE); diff --git a/apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx b/apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx index 344dcdee..80f1df9b 100644 --- a/apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx +++ b/apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx @@ -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("PAUSED"); const handleTogglePlay = () => setStatus((prev) => (prev === "RUNNING" ? "PAUSED" : "RUNNING")); - const handleEnd = () => setStatus("PAUSED"); - const handleAddTime = () => {}; + const handleExtendTimer = () => {}; + const handleCompleteTimer = () => setStatus("PAUSED"); return (
@@ -26,11 +28,13 @@ export const TimerPanel = () => { size="sm" /> -
); diff --git a/apps/timo-web/components/timer/TimerCompleteModalPanel.tsx b/apps/timo-web/components/timer/TimerCompleteModalPanel.tsx new file mode 100644 index 00000000..18ad44da --- /dev/null +++ b/apps/timo-web/components/timer/TimerCompleteModalPanel.tsx @@ -0,0 +1,45 @@ +import { Modal } from "@repo/timo-design-system/ui"; + +export interface TimerCompleteModalPanelProps { + plannedMinutes: number; + actualMinutes: number; + feedbackText: string; + onComplete: () => void; +} + +export const TimerCompleteModalPanel = ({ + plannedMinutes, + actualMinutes, + feedbackText, + onComplete, +}: TimerCompleteModalPanelProps) => { + return ( + <> + 테스크가 완료 되었어요! +

{feedbackText}

+ +
+ +
+
+ 계획 + + {plannedMinutes}m + +
+
+ 실제 + + {actualMinutes}m + +
+
+ + + + 완료하기 + + + + ); +}; diff --git a/apps/timo-web/components/timer/TimerControls.tsx b/apps/timo-web/components/timer/TimerControls.tsx index 69c18ff2..4c92bf36 100644 --- a/apps/timo-web/components/timer/TimerControls.tsx +++ b/apps/timo-web/components/timer/TimerControls.tsx @@ -6,30 +6,40 @@ import { PlusIcon, StopIcon, } from "@repo/timo-design-system/icons"; +import { Modal } from "@repo/timo-design-system/ui"; import { TimerControlButton } from "@/components/timer/TimerControlButton"; +const MODAL_TRIGGER_CLASS = + "group bg-timo-gray-300 active:bg-timo-blue-50 flex size-14.5 items-center justify-center rounded-full"; + 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) => { return (
- } - activeIcon={} - label="종료" - onClick={onEnd} - /> + + + + + + + + - } - activeIcon={} - label="시간 추가" - onClick={onAddTime} - /> + + + + + + + +
); }; diff --git a/apps/timo-web/components/timer/TimerEndModalPanel.tsx b/apps/timo-web/components/timer/TimerEndModalPanel.tsx new file mode 100644 index 00000000..d4a46cff --- /dev/null +++ b/apps/timo-web/components/timer/TimerEndModalPanel.tsx @@ -0,0 +1,94 @@ +import { Modal, ModalButton } from "@repo/timo-design-system/ui"; + +const TimerEndIcon = () => ( + //TODO: SVG를 컴포넌트 머지 시 변경 예정 + + + + + + + + + + +); + +export interface TimerEndModalPanelProps { + onContinue: () => void; + onComplete: () => void; +} + +export const TimerEndModalPanel = ({ + onContinue, + onComplete, +}: TimerEndModalPanelProps) => { + return ( + <> + + + + 타이머 종료! + 다음 단계를 선택하세요. + + + 계속하기 + + + 완료하기 + + + + ); +}; diff --git a/apps/timo-web/components/timer/TimerExtendModalPanel.tsx b/apps/timo-web/components/timer/TimerExtendModalPanel.tsx new file mode 100644 index 00000000..e992277a --- /dev/null +++ b/apps/timo-web/components/timer/TimerExtendModalPanel.tsx @@ -0,0 +1,108 @@ +import { Modal } from "@repo/timo-design-system/ui"; +import { cn } from "@repo/timo-design-system/utils"; + +export type ExtendTimePreset = 10 | 30 | 60 | "custom"; + +const PRESET_OPTIONS: { preset: ExtendTimePreset; label: string }[] = [ + { preset: 10, label: "+10M" }, + { preset: 30, label: "+30M" }, + { preset: 60, label: "+1H" }, +]; + +const MAX_CUSTOM_MINUTES = 720; + +export interface TimerExtendModalPanelProps { + selectedPreset: ExtendTimePreset | null; + customMinutes: string; + onSelectPreset: (preset: ExtendTimePreset) => void; + onCustomMinutesChange: (value: string) => void; + onClose: () => void; + onSubmit: () => void; + canSubmit: boolean; +} + +export const TimerExtendModalPanel = ({ + selectedPreset, + customMinutes, + onSelectPreset, + onCustomMinutesChange, + onClose, + onSubmit, + canSubmit, +}: TimerExtendModalPanelProps) => { + const isCustomSelected = selectedPreset === "custom"; + + return ( + <> + 시간을 얼마나 추가할까요? + +
+ {PRESET_OPTIONS.map(({ preset, label }) => ( + + ))} + + {isCustomSelected ? ( +
+ { + const digitsOnly = e.target.value.replace(/\D/g, ""); + const clamped = + digitsOnly === "" + ? "" + : String(Math.min(Number(digitsOnly), MAX_CUSTOM_MINUTES)); + + onCustomMinutesChange(clamped); + }} + aria-label="직접 입력 시간(분)" + className="typo-headline-r-14 text-timo-black min-w-0 flex-1 text-right outline-none" + /> + + 분 + +
+ ) : ( + + )} +
+ +
+ + 닫기 + + + 시간 추가하기 + +
+ + ); +}; diff --git a/apps/timo-web/components/timer/TimerSessionControls.tsx b/apps/timo-web/components/timer/TimerSessionControls.tsx new file mode 100644 index 00000000..6a10fe4a --- /dev/null +++ b/apps/timo-web/components/timer/TimerSessionControls.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { Modal } from "@repo/timo-design-system/ui"; +import { useState } from "react"; + +import { TimerCompleteModalPanel } from "@/components/timer/TimerCompleteModalPanel"; +import { TimerControls } from "@/components/timer/TimerControls"; +import { TimerEndModalPanel } from "@/components/timer/TimerEndModalPanel"; +import { + TimerExtendModalPanel, + type ExtendTimePreset, +} from "@/components/timer/TimerExtendModalPanel"; + +type TimerModalStep = "end" | "extend" | "complete"; + +const DEFAULT_COMPLETE_FEEDBACK_TEXT = "이번 작업을 완료했어요. 수고하셨어요!"; + +export interface TimerSessionControlsProps { + isRunning: boolean; + onTogglePlay: () => void; + plannedMinutes: number; + actualMinutes: number; + feedbackText?: string; + onExtend: (minutes: number) => void; + onComplete: () => void; +} + +export const TimerSessionControls = ({ + isRunning, + onTogglePlay, + plannedMinutes, + actualMinutes, + feedbackText = DEFAULT_COMPLETE_FEEDBACK_TEXT, + onExtend, + onComplete, +}: TimerSessionControlsProps) => { + const [step, setStep] = useState("end"); + const [selectedPreset, setSelectedPreset] = useState( + null, + ); + const [customMinutes, setCustomMinutes] = useState(""); + + const resetExtendSelection = () => { + setSelectedPreset(null); + setCustomMinutes(""); + }; + + const handleSubmitExtend = () => { + const minutes = + selectedPreset === "custom" + ? Number(customMinutes) + : (selectedPreset ?? 0); + + if (minutes > 0) onExtend(minutes); + + resetExtendSelection(); + }; + + const canSubmitExtend = + selectedPreset !== null && + (selectedPreset !== "custom" || Number(customMinutes) > 0); + + return ( + + setStep("end")} + onOpenExtendModal={() => { + resetExtendSelection(); + setStep("extend"); + }} + /> + + + {step === "end" && ( + { + resetExtendSelection(); + setStep("extend"); + }} + onComplete={() => setStep("complete")} + /> + )} + {step === "extend" && ( + { + setSelectedPreset(preset); + if (preset !== "custom") setCustomMinutes(""); + }} + onCustomMinutesChange={setCustomMinutes} + onClose={resetExtendSelection} + onSubmit={handleSubmitExtend} + canSubmit={canSubmitExtend} + /> + )} + {step === "complete" && ( + + )} + + + ); +}; From b2a24147de461d3515467fb91e3991125a7575ae Mon Sep 17 00:00:00 2001 From: jjangminii Date: Sat, 11 Jul 2026 03:27:53 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(web):=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=EB=B0=8F=20=EC=A4=91=EB=8B=A8=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타이머가 실행 중일 때 종료를 누르면 뜨는 "지금 멈출까요?" 중단 모달을 추가했습니다 - 종료·완료·연장 모달과 타이머 컨트롤 라벨을 next-intl Focus 네임스페이스로 옮겨 한/영 전환되게 했습니다 - 연장 모달의 프리셋·직접입력 단위를 언어별(분/시간 vs M/H)로 분리했습니다 - 모달 버튼·타이머 컨트롤·연장 모달의 포커스링을 그림자(ring) 대신 버튼 테두리 색상 전환 방식으로 교체했습니다 --- .../timer/TimerCompleteModalPanel.tsx | 21 +++-- .../components/timer/TimerControlButton.tsx | 2 +- .../components/timer/TimerControls.tsx | 11 ++- .../components/timer/TimerEndModalPanel.tsx | 11 ++- .../timer/TimerExtendModalPanel.tsx | 48 ++++++---- .../components/timer/TimerSessionControls.tsx | 15 ++-- .../components/timer/TimerStopModalPanel.tsx | 89 +++++++++++++++++++ apps/timo-web/messages/en.json | 38 ++++++++ apps/timo-web/messages/ko.json | 38 ++++++++ .../button/modal-button/ModalButton.tsx | 7 +- 10 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 apps/timo-web/components/timer/TimerStopModalPanel.tsx diff --git a/apps/timo-web/components/timer/TimerCompleteModalPanel.tsx b/apps/timo-web/components/timer/TimerCompleteModalPanel.tsx index 18ad44da..351bd35f 100644 --- a/apps/timo-web/components/timer/TimerCompleteModalPanel.tsx +++ b/apps/timo-web/components/timer/TimerCompleteModalPanel.tsx @@ -1,9 +1,10 @@ import { Modal } from "@repo/timo-design-system/ui"; +import { useTranslations } from "next-intl"; export interface TimerCompleteModalPanelProps { plannedMinutes: number; actualMinutes: number; - feedbackText: string; + feedbackText?: string; onComplete: () => void; } @@ -13,22 +14,30 @@ export const TimerCompleteModalPanel = ({ feedbackText, onComplete, }: TimerCompleteModalPanelProps) => { + const t = useTranslations("Focus.completeModal"); + return ( <> - 테스크가 완료 되었어요! -

{feedbackText}

+ {t("title")} +

+ {feedbackText ?? t("defaultFeedback")} +

- 계획 + + {t("plannedLabel")} + {plannedMinutes}m
- 실제 + + {t("actualLabel")} + {actualMinutes}m @@ -37,7 +46,7 @@ export const TimerCompleteModalPanel = ({ - 완료하기 + {t("completeButton")} diff --git a/apps/timo-web/components/timer/TimerControlButton.tsx b/apps/timo-web/components/timer/TimerControlButton.tsx index 776aa2c8..10197e89 100644 --- a/apps/timo-web/components/timer/TimerControlButton.tsx +++ b/apps/timo-web/components/timer/TimerControlButton.tsx @@ -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 diff --git a/apps/timo-web/components/timer/TimerControls.tsx b/apps/timo-web/components/timer/TimerControls.tsx index 4c92bf36..3d4183fd 100644 --- a/apps/timo-web/components/timer/TimerControls.tsx +++ b/apps/timo-web/components/timer/TimerControls.tsx @@ -7,11 +7,12 @@ import { 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 flex size-14.5 items-center justify-center rounded-full"; + "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; @@ -26,10 +27,12 @@ export const TimerControls = ({ onOpenEndModal, onOpenExtendModal, }: TimerControlsProps) => { + const t = useTranslations("Focus.controls"); + return (
@@ -45,13 +48,13 @@ export const TimerControls = ({ icon={ isRunning ? : } - label={isRunning ? "일시정지" : "재생"} + label={isRunning ? t("pause") : t("play")} variant={isRunning ? "active" : "default"} onClick={onTogglePlay} /> diff --git a/apps/timo-web/components/timer/TimerEndModalPanel.tsx b/apps/timo-web/components/timer/TimerEndModalPanel.tsx index d4a46cff..1434fc5b 100644 --- a/apps/timo-web/components/timer/TimerEndModalPanel.tsx +++ b/apps/timo-web/components/timer/TimerEndModalPanel.tsx @@ -1,4 +1,5 @@ import { Modal, ModalButton } from "@repo/timo-design-system/ui"; +import { useTranslations } from "next-intl"; const TimerEndIcon = () => ( //TODO: SVG를 컴포넌트 머지 시 변경 예정 @@ -66,27 +67,29 @@ export const TimerEndModalPanel = ({ onContinue, onComplete, }: TimerEndModalPanelProps) => { + const t = useTranslations("Focus.endModal"); + return ( <> - 타이머 종료! - 다음 단계를 선택하세요. + {t("title")} + {t("description")} - 계속하기 + {t("continueButton")} - 완료하기 + {t("completeButton")} diff --git a/apps/timo-web/components/timer/TimerExtendModalPanel.tsx b/apps/timo-web/components/timer/TimerExtendModalPanel.tsx index e992277a..348957a6 100644 --- a/apps/timo-web/components/timer/TimerExtendModalPanel.tsx +++ b/apps/timo-web/components/timer/TimerExtendModalPanel.tsx @@ -1,16 +1,28 @@ import { Modal } from "@repo/timo-design-system/ui"; import { cn } from "@repo/timo-design-system/utils"; +import { useTranslations } from "next-intl"; export type ExtendTimePreset = 10 | 30 | 60 | "custom"; -const PRESET_OPTIONS: { preset: ExtendTimePreset; label: string }[] = [ - { preset: 10, label: "+10M" }, - { preset: 30, label: "+30M" }, - { preset: 60, label: "+1H" }, +type ExtendPresetLabelKey = + | "presetTenMin" + | "presetThirtyMin" + | "presetOneHour"; + +const PRESET_OPTIONS: { + preset: ExtendTimePreset; + labelKey: ExtendPresetLabelKey; +}[] = [ + { preset: 10, labelKey: "presetTenMin" }, + { preset: 30, labelKey: "presetThirtyMin" }, + { preset: 60, labelKey: "presetOneHour" }, ]; const MAX_CUSTOM_MINUTES = 720; +const CHIP_FOCUS_BORDER_CLASS = + "border-2 border-transparent focus-visible:border-timo-blue-300 outline-none"; + export interface TimerExtendModalPanelProps { selectedPreset: ExtendTimePreset | null; customMinutes: string; @@ -30,14 +42,15 @@ export const TimerExtendModalPanel = ({ onSubmit, canSubmit, }: TimerExtendModalPanelProps) => { + const t = useTranslations("Focus.extendModal"); const isCustomSelected = selectedPreset === "custom"; return ( <> - 시간을 얼마나 추가할까요? + {t("title")}
- {PRESET_OPTIONS.map(({ preset, label }) => ( + {PRESET_OPTIONS.map(({ preset, labelKey }) => ( ))} {isCustomSelected ? ( -
+
- 분 + {t("customUnitSuffix")}
) : ( )}
- 닫기 + {t("closeButton")} - 시간 추가하기 + {t("submitButton")}
diff --git a/apps/timo-web/components/timer/TimerSessionControls.tsx b/apps/timo-web/components/timer/TimerSessionControls.tsx index 6a10fe4a..763d9af6 100644 --- a/apps/timo-web/components/timer/TimerSessionControls.tsx +++ b/apps/timo-web/components/timer/TimerSessionControls.tsx @@ -10,10 +10,9 @@ import { TimerExtendModalPanel, type ExtendTimePreset, } from "@/components/timer/TimerExtendModalPanel"; +import { TimerStopModalPanel } from "@/components/timer/TimerStopModalPanel"; -type TimerModalStep = "end" | "extend" | "complete"; - -const DEFAULT_COMPLETE_FEEDBACK_TEXT = "이번 작업을 완료했어요. 수고하셨어요!"; +type TimerModalStep = "end" | "stop" | "extend" | "complete"; export interface TimerSessionControlsProps { isRunning: boolean; @@ -30,7 +29,7 @@ export const TimerSessionControls = ({ onTogglePlay, plannedMinutes, actualMinutes, - feedbackText = DEFAULT_COMPLETE_FEEDBACK_TEXT, + feedbackText, onExtend, onComplete, }: TimerSessionControlsProps) => { @@ -65,7 +64,7 @@ export const TimerSessionControls = ({ setStep("end")} + onOpenEndModal={() => setStep(isRunning ? "stop" : "end")} onOpenExtendModal={() => { resetExtendSelection(); setStep("extend"); @@ -82,6 +81,12 @@ export const TimerSessionControls = ({ onComplete={() => setStep("complete")} /> )} + {step === "stop" && ( + setStep("complete")} + /> + )} {step === "extend" && ( ( + //TODO: SVG를 컴포넌트 머지 시 변경 예정 + + + + + + + + + + +); + +export interface TimerStopModalPanelProps { + minutes: number; + onSwitch: () => void; +} + +export const TimerStopModalPanel = ({ + minutes, + onSwitch, +}: TimerStopModalPanelProps) => { + const t = useTranslations("Focus.stopModal"); + + return ( + <> + + + + {t("title")} + {t("description", { minutes })} + + + {t("continueButton")} + + + {t("switchButton")} + + + + ); +}; diff --git a/apps/timo-web/messages/en.json b/apps/timo-web/messages/en.json index 473c9527..81c7be00 100644 --- a/apps/timo-web/messages/en.json +++ b/apps/timo-web/messages/en.json @@ -51,5 +51,43 @@ "login": "Continue with your Google account", "connectCalendar": "Connect Google Calendar" } + }, + "Focus": { + "controls": { + "end": "End", + "pause": "Pause", + "play": "Play", + "extend": "Add Time" + }, + "endModal": { + "title": "Timer Finished!", + "description": "Please select the next step.", + "continueButton": "Continue", + "completeButton": "Exit" + }, + "stopModal": { + "title": "Shall we stop now?", + "description": "The {minutes} minutes completed so far will be recorded in the timebox.", + "continueButton": "Continue", + "switchButton": "Switch" + }, + "completeModal": { + "title": "The task has been completed!", + "defaultFeedback": "You've completed this task. Great work!", + "plannedLabel": "Plan", + "actualLabel": "Actual", + "completeButton": "Complete" + }, + "extendModal": { + "title": "How much time would you like to add?", + "presetTenMin": "+10M", + "presetThirtyMin": "+30M", + "presetOneHour": "+1H", + "customPreset": "Enter", + "customInputLabel": "Custom minutes input", + "customUnitSuffix": "M", + "closeButton": "Close", + "submitButton": "Add Time" + } } } diff --git a/apps/timo-web/messages/ko.json b/apps/timo-web/messages/ko.json index 076fe886..27553726 100644 --- a/apps/timo-web/messages/ko.json +++ b/apps/timo-web/messages/ko.json @@ -51,5 +51,43 @@ "login": "Google 계정으로 계속하기", "connectCalendar": "Google 캘린더 연결하기" } + }, + "Focus": { + "controls": { + "end": "종료", + "pause": "일시정지", + "play": "재생", + "extend": "시간 추가" + }, + "endModal": { + "title": "타이머 종료!", + "description": "다음 단계를 선택하세요.", + "continueButton": "계속하기", + "completeButton": "완료하기" + }, + "stopModal": { + "title": "지금 멈출까요?", + "description": "지금까지 수행한 {minutes}분이 타임박스에 기록됩니다.", + "continueButton": "계속 진행", + "switchButton": "전환하기" + }, + "completeModal": { + "title": "테스크가 완료 되었어요!", + "defaultFeedback": "이번 작업을 완료했어요. 수고하셨어요!", + "plannedLabel": "계획", + "actualLabel": "실제", + "completeButton": "완료하기" + }, + "extendModal": { + "title": "시간을 얼마나 추가할까요?", + "presetTenMin": "+10분", + "presetThirtyMin": "+30분", + "presetOneHour": "+1시간", + "customPreset": "직접 입력", + "customInputLabel": "직접 입력 시간(분)", + "customUnitSuffix": "분", + "closeButton": "닫기", + "submitButton": "시간 추가하기" + } } } diff --git a/packages/timo-design-system/src/components/button/modal-button/ModalButton.tsx b/packages/timo-design-system/src/components/button/modal-button/ModalButton.tsx index 055c948b..22cb7e31 100644 --- a/packages/timo-design-system/src/components/button/modal-button/ModalButton.tsx +++ b/packages/timo-design-system/src/components/button/modal-button/ModalButton.tsx @@ -5,8 +5,9 @@ import type { ButtonHTMLAttributes } from "react"; export type ModalButtonVariantTypes = "fill" | "border"; const MODAL_BUTTON_VARIANT: Record = { - fill: "bg-timo-blue-300 text-white", - border: "border border-timo-gray-500 text-timo-black", + fill: "bg-timo-blue-300 text-white focus-visible:ring-timo-blue-300 focus-visible:ring-2 focus-visible:ring-offset-2", + border: + "border border-timo-gray-500 text-timo-black focus-visible:border-timo-blue-300", }; export interface ModalButtonProps extends ButtonHTMLAttributes { @@ -23,7 +24,7 @@ export const ModalButton = ({
@@ -39,7 +44,7 @@ export const TimerCompleteModalPanel = ({ {t("actualLabel")} - {actualMinutes}m + {formatDurationLabel(actualMinutes, hourUnit, minuteUnit)}
diff --git a/apps/timo-web/components/timer/TimerSessionControls.tsx b/apps/timo-web/components/timer/TimerSessionControls.tsx index 763d9af6..59518f4a 100644 --- a/apps/timo-web/components/timer/TimerSessionControls.tsx +++ b/apps/timo-web/components/timer/TimerSessionControls.tsx @@ -1,7 +1,7 @@ "use client"; import { Modal } from "@repo/timo-design-system/ui"; -import { useState } from "react"; +import { forwardRef, useImperativeHandle, useRef, useState } from "react"; import { TimerCompleteModalPanel } from "@/components/timer/TimerCompleteModalPanel"; import { TimerControls } from "@/components/timer/TimerControls"; @@ -14,6 +14,10 @@ import { TimerStopModalPanel } from "@/components/timer/TimerStopModalPanel"; type TimerModalStep = "end" | "stop" | "extend" | "complete"; +export interface TimerSessionControlsHandle { + openStopModal: () => void; +} + export interface TimerSessionControlsProps { isRunning: boolean; onTogglePlay: () => void; @@ -24,16 +28,30 @@ export interface TimerSessionControlsProps { onComplete: () => void; } -export const TimerSessionControls = ({ - isRunning, - onTogglePlay, - plannedMinutes, - actualMinutes, - feedbackText, - onExtend, - onComplete, -}: TimerSessionControlsProps) => { +export const TimerSessionControls = forwardRef< + TimerSessionControlsHandle, + TimerSessionControlsProps +>(function TimerSessionControls( + { + isRunning, + onTogglePlay, + plannedMinutes, + actualMinutes, + feedbackText, + onExtend, + onComplete, + }, + ref, +) { const [step, setStep] = useState("end"); + const stopModalTriggerRef = useRef(null); + + useImperativeHandle(ref, () => ({ + openStopModal: () => { + setStep("stop"); + stopModalTriggerRef.current?.click(); + }, + })); const [selectedPreset, setSelectedPreset] = useState( null, ); @@ -70,6 +88,12 @@ export const TimerSessionControls = ({ setStep("extend"); }} /> +