[FEAT] 타이머 종료·완료·연장·중단 모달 연결 및 다국어 지원#140
Conversation
- 타이머 컨트롤의 종료·시간추가 버튼을 Modal.Trigger로 전환했습니다 - 종료 모달에서 완료/연장 단계로 전환되는 TimerSessionControls를 추가했습니다 - 종료·완료·연장 각 단계의 모달 콘텐츠를 Figma 디자인대로 구현했습니다 - FocusSessionContainer와 홈 사이드바 TimerPanel에 모달 플로우를 연결했습니다 - 분 단위 변환 유틸(convertDurationToMinutes)을 추가했습니다
- 타이머가 실행 중일 때 종료를 누르면 뜨는 "지금 멈출까요?" 중단 모달을 추가했습니다 - 종료·완료·연장 모달과 타이머 컨트롤 라벨을 next-intl Focus 네임스페이스로 옮겨 한/영 전환되게 했습니다 - 연장 모달의 프리셋·직접입력 단위를 언어별(분/시간 vs M/H)로 분리했습니다 - 모달 버튼·타이머 컨트롤·연장 모달의 포커스링을 그림자(ring) 대신 버튼 테두리 색상 전환 방식으로 교체했습니다
- 타이머 재생 중 상위 체크박스를 누르면 완료 처리 대신 타이머 중단 모달을 띄우도록 연결했습니다 - Modal.Trigger가 ref를 받을 수 있도록 수정해 외부에서 정지 모달을 프로그래밍적으로 열 수 있게 했습니다 - 완료·중단 모달의 소요 시간을 60분 이상일 때 시/분(h m) 단위로 표기하고, 중단 모달의 시간 값을 파란색으로 강조했습니다 - 포커스 화면 상단의 요일 라벨을 next-intl Common.weekday로 교체해 영문 버전에서도 표시되게 했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 43 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Walkthrough포커스 타이머 제어가 종료·중지·연장·완료 모달을 포함하는 세션 컨트롤 구조로 변경되었습니다. 관련 화면에 번역 키, 시간 포맷터, 요일 키 변환, 모달 트리거 ref 및 포커스 스타일이 추가되었습니다. Changes포커스 타이머 모달 흐름
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant FocusSessionContainer
participant TimerSessionControls
participant Modal
participant TimerStopModalPanel
FocusSessionContainer->>TimerSessionControls: 실행 중 완료 요청
TimerSessionControls->>Modal: stop 트리거 클릭
Modal->>TimerStopModalPanel: 중지 확인 패널 표시
TimerStopModalPanel-->>TimerSessionControls: 계속 또는 전환 선택
TimerSessionControls-->>FocusSessionContainer: 세션 콜백 실행
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…to feat/web/136-connect-modal # Conflicts: # apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with 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.
Inline comments:
In
`@apps/timo-web/app/`[locale]/(main)/focus/_containers/FocusSessionContainer.tsx:
- Around line 125-126: FocusSessionContainer의 actualMinutes에 plannedMinutes를
전달하지 말고, 실제 경과 시간 필드가 추가될 때까지 0을 임시값으로 사용하거나 명확한 TODO를 추가하세요.
TimerCompleteModalPanel과 TimerStopModalPanel에 계획 시간이 실제 시간으로 표시되지 않도록 수정하고,
TimerPanel의 PLANNED_MINUTES 전달부에도 동일한 처리를 적용하세요.
- Line 27: Remove the duplicated SECONDS_PER_MINUTE declaration from
FocusSessionContainer and import the shared exported constant from duration.ts,
updating duration.ts to export it if necessary.
In `@apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx`:
- Around line 34-35: TimerPanel의 FocusSessionContainer 호출에서 actualMinutes에 계획값인
PLANNED_MINUTES를 전달하는 문제를 수정하세요. 실제 집중 세션 시간을 추적하는 상태나 계산값을 사용해 actualMinutes를
전달하고, plannedMinutes에는 계속 PLANNED_MINUTES를 유지하여 완료 모달에 계획 시간과 실제 시간이 올바르게 표시되도록
하세요.
- Line 18: TimerPanel의 handleExtendTimer가 no-op 상태이므로, FocusSessionContainer의 동일
핸들러 동작을 참고해 연장 시 durationSeconds를 업데이트하고 필요한 TODO 주석을 추가하세요.
In `@apps/timo-web/components/timer/TimerEndModalPanel.tsx`:
- Around line 61-97: The TimerEndModalPanel currently uses TimerEndIcon
directly; replace it with the finalized SVG component once available, remove the
related TODO comment, and track any pending asset work through the appropriate
issue rather than leaving inline TODO text.
- Around line 4-59: TimerEndIcon과 TimerStopModalPanel의 TimerStopIcon에 동일한 SVG
마크업이 중복되어 있습니다. SVG를 공용 아이콘 컴포넌트로 추출한 뒤 두 컴포넌트가 해당 공용 컴포넌트를 렌더링하도록 변경하고, 기존 아이콘과
동일한 크기·속성·외형을 유지하세요.
In `@apps/timo-web/components/timer/TimerExtendModalPanel.tsx`:
- Around line 73-89: Replace the input’s outline-none class in the custom
minutes field with an accessible focus-visible treatment, such as
focus-visible:outline and an appropriate focus-visible outline color, or use
outline-hidden if the parent focus-within styling is sufficient. Update the
input element in TimerExtendModalPanel’s custom minutes control while preserving
the existing parent focus-within indicator.
In `@apps/timo-web/components/timer/TimerSessionControls.tsx`:
- Around line 49-54: TimerSessionControls의 openStopModal에서 숨겨진 트리거의
stopModalTriggerRef.current?.click() 호출을 제거하고, 모달 컨텍스트가 제공하는 open 함수를 직접 사용해 모달을
열도록 변경하세요. setStep("stop")과 모달 열기 동작을 기존 순서대로 유지하되, 트리거 ref 및 관련 의존 코드는 정리하고 동일한
방식이 적용된 다른 구간도 함께 수정하세요.
In `@apps/timo-web/messages/ko.json`:
- Line 75: Update the completeModal.title Korean translation from “테스크가 완료
되었어요!” to the correctly spaced and spelled “태스크가 완료되었어요!”.
In `@packages/timo-design-system/src/components/layout/modal/Modal.tsx`:
- Around line 75-77: ModalTriggerProps를 type 교차 타입 대신 interface로 선언하도록 변경하세요.
ButtonHTMLAttributes<HTMLButtonElement>를 extends하고 기존 ref?:
Ref<HTMLButtonElement> 속성을 유지하며, ModalTriggerProps를 사용하는 코드가 동일하게 동작하는지 확인하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 20e3b7cc-9e49-406d-88fc-105f236ba739
📒 Files selected for processing (16)
apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsxapps/timo-web/app/[locale]/(main)/focus/_utils/date.tsapps/timo-web/app/[locale]/(main)/focus/_utils/duration.tsapps/timo-web/components/layout/sidebar/time/TimerPanel.tsxapps/timo-web/components/timer/TimerCompleteModalPanel.tsxapps/timo-web/components/timer/TimerControlButton.tsxapps/timo-web/components/timer/TimerControls.tsxapps/timo-web/components/timer/TimerEndModalPanel.tsxapps/timo-web/components/timer/TimerExtendModalPanel.tsxapps/timo-web/components/timer/TimerSessionControls.tsxapps/timo-web/components/timer/TimerStopModalPanel.tsxapps/timo-web/components/timer/durationLabel.tsapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonpackages/timo-design-system/src/components/button/modal-button/ModalButton.tsxpackages/timo-design-system/src/components/layout/modal/Modal.tsx
| type TimerSessionControlsHandle, | ||
| } from "@/components/timer/TimerSessionControls"; | ||
|
|
||
| const SECONDS_PER_MINUTE = 60; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
SECONDS_PER_MINUTE 상수가 duration.ts와 중복 정의되어 있습니다
duration.ts에 동일한 상수(60)가 이미 정의되어 있습니다. duration.ts에서 export하여 재사용하면 값 불일치 리스크를 방지할 수 있습니다.
♻️ proposed fix
duration.ts에서 상수를 export합니다:
-const SECONDS_PER_MINUTE = 60;
+export const SECONDS_PER_MINUTE = 60;FocusSessionContainer.tsx에서 기존 import에 추가하고 로컬 선언을 제거합니다:
import {
convertDurationToMinutes,
convertDurationToTimeText,
+ SECONDS_PER_MINUTE,
} from "`@/app/`[locale]/(main)/focus/_utils/duration";-const SECONDS_PER_MINUTE = 60;
-
export const FocusSessionContainer = () => {📝 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.
| const SECONDS_PER_MINUTE = 60; |
🤖 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 27, Remove the duplicated SECONDS_PER_MINUTE declaration from
FocusSessionContainer and import the shared exported constant from duration.ts,
updating duration.ts to export it if necessary.
| plannedMinutes={plannedMinutes} | ||
| actualMinutes={plannedMinutes} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
actualMinutes에 plannedMinutes를 그대로 전달하여 "계획"과 "실제"가 항상 동일하게 표시됩니다
FocusTask 인터페이스에 경과 시간 필드(elapsedSeconds 등)가 없어 현재 데이터 모델로는 실제 시간 추적이 불가능합니다. 하지만 actualMinutes={plannedMinutes}로 전달하면:
TimerCompleteModalPanel의 "계획"과 "실제"가 항상 같은 값을 보여줌 (예: 둘 다 "2h")TimerStopModalPanel의 "지금까지 수행한 {minutes}"에 계획 시간이 표시됨
경과 시간 추적이 구현될 때까지 TODO 주석 추가 또는 actualMinutes={0} 임시값 사용을 고려하세요. TimerPanel.tsx의 PLANNED_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 전달부에도 동일한 처리를 적용하세요.
| setStatus((prev) => (prev === "RUNNING" ? "PAUSED" : "RUNNING")); | ||
| const handleEnd = () => setStatus("PAUSED"); | ||
| const handleAddTime = () => {}; | ||
| const handleExtendTimer = () => {}; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
handleExtendTimer가 빈 구현입니다
FocusSessionContainer의 동일 핸들러는 durationSeconds 업데이트와 TODO 주석을 포함하지만, TimerPanel의 handleExtendTimer는 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 주석을 추가하세요.
| plannedMinutes={PLANNED_MINUTES} | ||
| actualMinutes={PLANNED_MINUTES} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
actualMinutes에 PLANNED_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를 유지하여 완료 모달에 계획 시간과 실제 시간이 올바르게 표시되도록
하세요.
| const TimerEndIcon = () => ( | ||
| //TODO: SVG를 컴포넌트 머지 시 변경 예정 | ||
| <svg | ||
| width="40" | ||
| height="40" | ||
| viewBox="0 0 40 40" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <circle | ||
| cx="20" | ||
| cy="20" | ||
| r="18.3333" | ||
| fill="#121212" | ||
| stroke="#121212" | ||
| strokeWidth="3.33333" | ||
| /> | ||
| <rect | ||
| x="30.7812" | ||
| y="18.3335" | ||
| width="2.96296" | ||
| height="8.14815" | ||
| transform="rotate(90 30.7812 18.3335)" | ||
| fill="white" | ||
| /> | ||
| <path | ||
| d="M17.7604 22.3774L20.6657 22.9593L18.388 34.3325L15.4827 33.7507L17.7604 22.3774Z" | ||
| fill="#4293F7" | ||
| /> | ||
| <circle cx="19.9369" cy="19.9394" r="1.791" fill="#F2FC9F" /> | ||
| <path | ||
| d="M20 1.85156V4.07378" | ||
| stroke="white" | ||
| strokeWidth="0.740741" | ||
| strokeLinecap="round" | ||
| /> | ||
| <path | ||
| d="M20 35.9258V38.148" | ||
| stroke="white" | ||
| strokeWidth="0.740741" | ||
| strokeLinecap="round" | ||
| /> | ||
| <path | ||
| d="M38.1459 20H35.9237" | ||
| stroke="white" | ||
| strokeWidth="0.740741" | ||
| strokeLinecap="round" | ||
| /> | ||
| <path | ||
| d="M4.07288 20H1.85065" | ||
| stroke="white" | ||
| strokeWidth="0.740741" | ||
| strokeLinecap="round" | ||
| /> | ||
| </svg> | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
TimerEndIcon과 TimerStopModalPanel의 TimerStopIcon이 완전히 동일한 SVG입니다.
두 파일의 SVG 마크업이 바이트 단위로 동일합니다. 공통 아이콘 컴포넌트로 추출하여 중복을 제거하세요.
♻️ 제안하는 변경
+ // shared icon file, e.g. `@/components/timer/icons/TimerSessionIcon.tsx`
+ export const TimerSessionIcon = () => (
+ <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
+ {/* ... shared SVG content ... */}
+ </svg>
+ );-const TimerEndIcon = () => (
- //TODO: SVG를 컴포넌트 머지 시 변경 예정
- <svg ...>...</svg>
-);
+import { TimerSessionIcon } from "`@/components/timer/icons/TimerSessionIcon`";🤖 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/timer/TimerEndModalPanel.tsx` around lines 4 - 59,
TimerEndIcon과 TimerStopModalPanel의 TimerStopIcon에 동일한 SVG 마크업이 중복되어 있습니다. SVG를
공용 아이콘 컴포넌트로 추출한 뒤 두 컴포넌트가 해당 공용 컴포넌트를 렌더링하도록 변경하고, 기존 아이콘과 동일한 크기·속성·외형을 유지하세요.
| export interface TimerEndModalPanelProps { | ||
| onContinue: () => void; | ||
| onComplete: () => void; | ||
| } | ||
|
|
||
| export const TimerEndModalPanel = ({ | ||
| onContinue, | ||
| onComplete, | ||
| }: TimerEndModalPanelProps) => { | ||
| const t = useTranslations("Focus.endModal"); | ||
|
|
||
| return ( | ||
| <> | ||
| <Modal.Icon> | ||
| <TimerEndIcon /> | ||
| </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> | ||
| </> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
TODO 댓글: SVG 교체 작업을 도와드릴 수 있습니다.
//TODO: SVG를 컴포넌트 머지 시 변경 예정 댓글이 남아 있습니다. SVG 아이콘 교체 작업를 진행하시려면 트래킹 이슈를 생성하고 구현을 도와드릴 수 있습니다. 필요하시면 말씀해 주세요.
🤖 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/timer/TimerEndModalPanel.tsx` around lines 61 - 97,
The TimerEndModalPanel currently uses TimerEndIcon directly; replace it with the
finalized SVG component once available, remove the related TODO comment, and
track any pending asset work through the appropriate issue rather than leaving
inline TODO text.
| <input | ||
| type="text" | ||
| inputMode="numeric" | ||
| value={customMinutes} | ||
| onChange={(e) => { | ||
| const digitsOnly = e.target.value.replace(/\D/g, ""); | ||
| const clamped = | ||
| digitsOnly === "" | ||
| ? "" | ||
| : String(Math.min(Number(digitsOnly), MAX_CUSTOM_MINUTES)); | ||
|
|
||
| onCustomMinutesChange(clamped); | ||
| }} | ||
| aria-label={t("customInputLabel")} | ||
| style={{ width: `${Math.max(customMinutes.length, 1)}ch` }} | ||
| className="typo-headline-r-14 text-timo-black shrink-0 text-center outline-none" | ||
| /> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
입력 필드에 outline-none이 직접적인 focus-visible 대체 스타일 없이 사용되었습니다.
부모 div의 focus-within:border-timo-blue-300로 간접적으로 포커스를 표시하지만, 컨벤션에서는 outline: none으로 포커스 테두리 제거를 금지하고 있습니다. 입력 요소 자체에 focus-visible 스타일을 추가하거나, outline-none 대신 outline-hidden 사용을 고려하세요.
🛡️ 제안하는 변경
className="typo-headline-r-14 text-timo-black shrink-0 text-center outline-none"
+ // focus-visible 스타일 추가 또는 outline-none 제거
/>🤖 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/timer/TimerExtendModalPanel.tsx` around lines 73 -
89, Replace the input’s outline-none class in the custom minutes field with an
accessible focus-visible treatment, such as focus-visible:outline and an
appropriate focus-visible outline color, or use outline-hidden if the parent
focus-within styling is sufficient. Update the input element in
TimerExtendModalPanel’s custom minutes control while preserving the existing
parent focus-within indicator.
Source: Path instructions
| useImperativeHandle(ref, () => ({ | ||
| openStopModal: () => { | ||
| setStep("stop"); | ||
| stopModalTriggerRef.current?.click(); | ||
| }, | ||
| })); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
히든 트리거 .click() 패턴이 동작은 하지만 fragile합니다.
openStopModal에서 setStep("stop") 직후 stopModalTriggerRef.current?.click()로 모달을 여는 패턴은 React 배칭 덕분에 동작하지만, 모달 컨텍스트의 open 함수를 직접 호출할 수 있는 방법을 고려해 보세요. 향후 Modal API가 변경되면 .click() 가정이 깨질 수 있습니다.
Also applies to: 91-96
🤖 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/timer/TimerSessionControls.tsx` around lines 49 -
54, TimerSessionControls의 openStopModal에서 숨겨진 트리거의
stopModalTriggerRef.current?.click() 호출을 제거하고, 모달 컨텍스트가 제공하는 open 함수를 직접 사용해 모달을
열도록 변경하세요. setStep("stop")과 모달 열기 동작을 기존 순서대로 유지하되, 트리거 ref 및 관련 의존 코드는 정리하고 동일한
방식이 적용된 다른 구간도 함께 수정하세요.
| export type ModalTriggerProps = ButtonHTMLAttributes<HTMLButtonElement> & { | ||
| ref?: Ref<HTMLButtonElement>; | ||
| }; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
type → interface 변경을 권장합니다.
ModalTriggerProps는 ButtonHTMLAttributes와 교차 타입으로 구성된 Props입니다. 컨벤션에 따라 Props 접미사가 붙은 타입은 interface로 선언하는 것이 권장됩니다.
♻️ 제안하는 변경
-export type ModalTriggerProps = ButtonHTMLAttributes<HTMLButtonElement> & {
- ref?: Ref<HTMLButtonElement>;
-};
+export interface ModalTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
+ ref?: Ref<HTMLButtonElement>;
+}📝 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.
| export type ModalTriggerProps = ButtonHTMLAttributes<HTMLButtonElement> & { | |
| ref?: Ref<HTMLButtonElement>; | |
| }; | |
| export interface ModalTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> { | |
| ref?: Ref<HTMLButtonElement>; | |
| } |
🤖 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 `@packages/timo-design-system/src/components/layout/modal/Modal.tsx` around
lines 75 - 77, ModalTriggerProps를 type 교차 타입 대신 interface로 선언하도록 변경하세요.
ButtonHTMLAttributes<HTMLButtonElement>를 extends하고 기존 ref?:
Ref<HTMLButtonElement> 속성을 유지하며, ModalTriggerProps를 사용하는 코드가 동일하게 동작하는지 확인하세요.
Source: Path instructions
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
- "테스크"를 "태스크"로, "완료 되었어요"를 "완료되었어요"로 맞춤법과 띄어쓰기를 수정했습니다
ISSUE 🔗
close #136
What is this PR? 🔍
Focus 뷰와 홈 사이드바 타이머에 종료·완료·연장·중단(재생 중 조기 종료) 4가지 확인 모달을 연결하고, 관련 텍스트 전체를
next-intl로 옮겨 한/영을 지원하도록 했습니다.배경
packages/timo-design-system의Modal컴포넌트는 이미 구현돼 있었지만 실제 타이머 화면(Focus 뷰, 홈 사이드바)에는 전혀 연결돼 있지 않았고, 종료 버튼을 눌러도 아무 동작이 없었습니다.Modal인스턴스 안에서 step으로 분기하는 구조로 구현하고, 텍스트는next-intl로 옮겨 한/영을 지원하도록 했습니다.타이머 모달 (TimerSessionControls 외)
Modal컨트롤러(TimerSessionControls)로 통합해 신규 구현했습니다.Modal은Modal.Trigger클릭으로만 열 수 있는 비제어(uncontrolled) 컴포넌트라, 여러 확인 단계(종료→연장/완료)를 하나의 열린 모달 안에서 전환하려면 버튼이 아닌 지점(체크박스)에서도 여는 방법이 필요했습니다.Modal에 controlled prop을 추가하는 대신 기존 API를 유지하는 방향으로 진행했습니다.TimerSessionControls내부에서stepstate(end | stop | extend | complete)로 어떤 패널을 보여줄지 분기합니다.TimerControls의 종료/연장 버튼은Modal.Trigger를 직접 사용해 클릭 시step을 먼저 세팅한 뒤 모달이 열립니다. 상위 체크박스처럼 버튼이 아닌 지점에서 모달을 열어야 할 때를 위해TimerSessionControls에openStopModalimperative handle을 추가하고, 내부에 숨긴Modal.Trigger를 ref로 프로그래밍적으로 클릭하는 방식을 택했습니다. 이를 위해Modal.Trigger가ref를 받을 수 있도록 최소 수정했습니다(React 19라forwardRef없이ref를 prop으로 추가).isRunning기준으로 분기했습니다. 실시간 카운트다운 로직 자체는 이번 범위 밖이라 "실제 소요 시간"은 계획 시간과 동일한 값을 임시로 사용합니다.다국어(i18n)
next-intl의Focus/Common.weekday네임스페이스로 옮겼습니다.토요일등)도 로케일과 무관하게 고정 배열에서 가져오고 있었습니다.messages/{ko,en}.json에Focus.controls,Focus.endModal,Focus.stopModal,Focus.extendModal,Focus.completeModal,Focus.duration키를 추가했습니다. 소요 시간은 60분 미만이면{m}분/{m}m, 정각이면{h}시간/{h}h, 그 외엔{h}시간 {m}분/{h}h {m}m로 변환하는formatDurationLabel유틸을 만들어 완료·중단 모달에서 공유합니다. 중단 모달의 소요 시간 값은t.rich()+<blue>태그로 강조색을 적용했습니다(기존TagLimitToastContainer와 동일한 패턴). 연장 모달의 프리셋 단위는 한국어 "분/시간", 영어 "M/H"로 Figma 원본 표기를 그대로 따라 언어별 문자열 자체를 다르게 관리합니다.Settings네임스페이스처럼 이미 있던 다른 미완성 i18n 갭은 이번 PR 범위에 포함하지 않았습니다.포커스 접근성
ring(box-shadow) 방식에서 버튼 자체border색상 전환 방식으로 바꿨습니다.ring-2방식은 흰/회색 배경 버튼에서 테두리 바깥에 별도 그림자 링이 겹쳐 보여 실제 버튼 경계와 시각적으로 불일치했습니다.ModalButton의bordervariant, 연장 모달 직접입력 인풋 컨테이너)는focus-visible:border-timo-blue-300으로 색상만 바꿉니다. 테두리가 없는 요소(연장 모달 프리셋 칩, 타이머 컨트롤 원형 버튼)는border-2 border-transparent를 기본값으로 깔아두고 포커스 시 색만 바꿔 레이아웃 흔들림 없이 표시합니다. 파란 배경의 fill 버튼(완료하기류)은 테두리 색을 바꿔도 배경과 구분이 안 되어 기존ring-2+ring-offset-2방식을 유지했습니다.상위 체크박스 연동
FocusSessionContainer의handleToggleCompleted에서completed && task.isRunning이면 상태를 바꾸지 않고timerSessionControlsRef.current?.openStopModal()만 호출합니다.Checkbox가 controlled 컴포넌트라 상태를 업데이트하지 않으면 체크 표시도 그대로 유지됩니다.To Reviewers
Modal.Trigger에ref를 추가한 부분이Modal컴포넌트의 기존 API 방향과 맞는지 봐주세요. controlled prop 추가는 피했지만, "숨은 트리거를 프로그래밍적으로 클릭"하는 방식이 다소 우회적이라 더 나은 대안이 있다면 의견 부탁드립니다."실제 소요 시간"이 실시간 카운트다운 없이 계획 시간과 동일하게 표시되는 부분은 미리 알고 계셔야 할 것 같아 남깁니다. 실제 타이머 엔진 구현은 이번 PR 범위 밖입니다.
완료 모달의 버튼 개수(현재 1개)와 관련해, Figma 파일 자체가 한국어 프레임(1개 버튼)과 영문 프레임(2개, Close+Finish)이 서로 다른 상태였습니다. 우선 기존 1개 구조를 유지했고, 어느 쪽이 최신 의도인지 확인되면 후속 처리하겠습니다.
Screenshot 📷
로컬 Playwright로
/ko/focus,/en/focus에서 아래 상태들을 직접 클릭해 확인했습니다 (이미지 업로드 환경이 아니라 표는 생략합니다).2시간/2h로 표시2h 30m형식으로 표시25분/25M) 시 포커스링이 버튼 테두리 색 전환으로 표시/en/focus에서Saturday로 표시Test Checklist ✔
pnpm --filter timo-web lint통과pnpm --filter @repo/timo-design-system lint통과pnpm --filter @repo/timo-design-system exec tsc --noEmit통과pnpm --filter timo-web exec tsc --noEmit— 이번 브랜치가 건드리지 않은onboarding아이콘 export 누락 에러 4건이develop에도 이미 있어 통과로 표시하지 않았습니다 (별도 확인 필요)pnpm build— 미실행