Skip to content

[FEAT] 타이머 종료·완료·연장·중단 모달 연결 및 다국어 지원#140

Open
jjangminii wants to merge 5 commits into
developfrom
feat/web/136-connect-modal
Open

[FEAT] 타이머 종료·완료·연장·중단 모달 연결 및 다국어 지원#140
jjangminii wants to merge 5 commits into
developfrom
feat/web/136-connect-modal

Conversation

@jjangminii

@jjangminii jjangminii commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

ISSUE 🔗

close #136



What is this PR? 🔍

Focus 뷰와 홈 사이드바 타이머에 종료·완료·연장·중단(재생 중 조기 종료) 4가지 확인 모달을 연결하고, 관련 텍스트 전체를 next-intl로 옮겨 한/영을 지원하도록 했습니다.

배경

  • 기존 구조: packages/timo-design-systemModal 컴포넌트는 이미 구현돼 있었지만 실제 타이머 화면(Focus 뷰, 홈 사이드바)에는 전혀 연결돼 있지 않았고, 종료 버튼을 눌러도 아무 동작이 없었습니다.
  • 발생 문제: 타이머 종료·완료·시간 연장 시 사용자에게 확인/입력을 받을 UI가 없어 의도치 않게 세션이 끝나거나 계획 시간을 조정할 방법이 없었고, 관련 문구도 전부 한국어로 하드코딩돼 있어 영문 로케일에서도 그대로 노출됐습니다.
  • 해결 방향: Figma 화면설계서·디자인 시스템 파일을 기준으로 종료·완료·연장·중단 4가지 모달을 하나의 Modal 인스턴스 안에서 step으로 분기하는 구조로 구현하고, 텍스트는 next-intl로 옮겨 한/영을 지원하도록 했습니다.

타이머 모달 (TimerSessionControls 외)

  • 변경 요약: 타이머 종료·완료·연장·중단 흐름을 하나의 Modal 컨트롤러(TimerSessionControls)로 통합해 신규 구현했습니다.
  • 이유: ModalModal.Trigger 클릭으로만 열 수 있는 비제어(uncontrolled) 컴포넌트라, 여러 확인 단계(종료→연장/완료)를 하나의 열린 모달 안에서 전환하려면 버튼이 아닌 지점(체크박스)에서도 여는 방법이 필요했습니다. Modal에 controlled prop을 추가하는 대신 기존 API를 유지하는 방향으로 진행했습니다.
  • 구현 방식: TimerSessionControls 내부에서 step state(end | stop | extend | complete)로 어떤 패널을 보여줄지 분기합니다. TimerControls의 종료/연장 버튼은 Modal.Trigger를 직접 사용해 클릭 시 step을 먼저 세팅한 뒤 모달이 열립니다. 상위 체크박스처럼 버튼이 아닌 지점에서 모달을 열어야 할 때를 위해 TimerSessionControlsopenStopModal imperative handle을 추가하고, 내부에 숨긴 Modal.Trigger를 ref로 프로그래밍적으로 클릭하는 방식을 택했습니다. 이를 위해 Modal.Triggerref를 받을 수 있도록 최소 수정했습니다(React 19라 forwardRef 없이 ref를 prop으로 추가).
  • 경계 · 제약: 타이머가 재생 중일 때만 종료 버튼/체크박스가 "중단" 모달을 띄우도록 isRunning 기준으로 분기했습니다. 실시간 카운트다운 로직 자체는 이번 범위 밖이라 "실제 소요 시간"은 계획 시간과 동일한 값을 임시로 사용합니다.

다국어(i18n)

  • 변경 요약: 타이머 모달·컨트롤·요일 라벨의 하드코딩된 한국어 텍스트를 next-intlFocus / Common.weekday 네임스페이스로 옮겼습니다.
  • 이유: 새로 만든 모달 텍스트가 전부 한국어로 하드코딩돼 있었고, 기존 Focus 뷰의 요일 라벨(토요일 등)도 로케일과 무관하게 고정 배열에서 가져오고 있었습니다.
  • 구현 방식: messages/{ko,en}.jsonFocus.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 방식은 흰/회색 배경 버튼에서 테두리 바깥에 별도 그림자 링이 겹쳐 보여 실제 버튼 경계와 시각적으로 불일치했습니다.
  • 구현 방식: 이미 테두리가 있는 요소(ModalButtonborder variant, 연장 모달 직접입력 인풋 컨테이너)는 focus-visible:border-timo-blue-300으로 색상만 바꿉니다. 테두리가 없는 요소(연장 모달 프리셋 칩, 타이머 컨트롤 원형 버튼)는 border-2 border-transparent를 기본값으로 깔아두고 포커스 시 색만 바꿔 레이아웃 흔들림 없이 표시합니다. 파란 배경의 fill 버튼(완료하기류)은 테두리 색을 바꿔도 배경과 구분이 안 되어 기존 ring-2 + ring-offset-2 방식을 유지했습니다.

상위 체크박스 연동

  • 변경 요약: Focus 뷰에서 타이머가 재생 중일 때 상위(부모) 태스크 체크박스를 클릭하면 즉시 완료 처리하는 대신 타이머 중단 모달을 띄우도록 했습니다.
  • 이유: 타이머가 도는 중에 체크박스만으로 작업을 완료 처리하면 진행 중인 세션이 사용자 확인 없이 끊기는 문제가 있었습니다.
  • 구현 방식: FocusSessionContainerhandleToggleCompleted에서 completed && task.isRunning이면 상태를 바꾸지 않고 timerSessionControlsRef.current?.openStopModal()만 호출합니다. Checkbox가 controlled 컴포넌트라 상태를 업데이트하지 않으면 체크 표시도 그대로 유지됩니다.



To Reviewers

Modal.Triggerref를 추가한 부분이 Modal 컴포넌트의 기존 API 방향과 맞는지 봐주세요. controlled prop 추가는 피했지만, "숨은 트리거를 프로그래밍적으로 클릭"하는 방식이 다소 우회적이라 더 나은 대안이 있다면 의견 부탁드립니다.

"실제 소요 시간"이 실시간 카운트다운 없이 계획 시간과 동일하게 표시되는 부분은 미리 알고 계셔야 할 것 같아 남깁니다. 실제 타이머 엔진 구현은 이번 PR 범위 밖입니다.

완료 모달의 버튼 개수(현재 1개)와 관련해, Figma 파일 자체가 한국어 프레임(1개 버튼)과 영문 프레임(2개, Close+Finish)이 서로 다른 상태였습니다. 우선 기존 1개 구조를 유지했고, 어느 쪽이 최신 의도인지 확인되면 후속 처리하겠습니다.

Screenshot 📷

로컬 Playwright로 /ko/focus, /en/focus에서 아래 상태들을 직접 클릭해 확인했습니다 (이미지 업로드 환경이 아니라 표는 생략합니다).

  • 재생 중 종료 클릭 → 중단 모달("지금 멈출까요? / Shall we stop now?") 노출, 소요 시간이 파란색 2시간/2h로 표시
  • 중단 모달 "전환하기/Switch" → 완료 모달로 전환, "계획/실제" 값이 2h 30m 형식으로 표시
  • 일시정지 상태에서 종료 클릭 → 종료 모달("타이머 종료! / Timer Finished!") 즉시 노출
  • 연장 모달에서 프리셋 선택·직접입력(25분/25M) 시 포커스링이 버튼 테두리 색 전환으로 표시
  • 상위 체크박스를 재생 중에 클릭 시 체크 표시가 바뀌지 않고 중단 모달만 노출, 완료 확정 후에야 체크됨
  • Focus 뷰 상단 요일 라벨이 /en/focus에서 Saturday로 표시
image image image



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에도 이미 있어 통과로 표시하지 않았습니다 (별도 확인 필요)
  • 위 Screenshot 항목들을 로컬 Playwright로 직접 클릭해 확인
  • pnpm build — 미실행

- 타이머 컨트롤의 종료·시간추가 버튼을 Modal.Trigger로 전환했습니다
- 종료 모달에서 완료/연장 단계로 전환되는 TimerSessionControls를 추가했습니다
- 종료·완료·연장 각 단계의 모달 콘텐츠를 Figma 디자인대로 구현했습니다
- FocusSessionContainer와 홈 사이드바 TimerPanel에 모달 플로우를 연결했습니다
- 분 단위 변환 유틸(convertDurationToMinutes)을 추가했습니다
- 타이머가 실행 중일 때 종료를 누르면 뜨는 "지금 멈출까요?" 중단 모달을 추가했습니다
- 종료·완료·연장 모달과 타이머 컨트롤 라벨을 next-intl Focus 네임스페이스로 옮겨 한/영 전환되게 했습니다
- 연장 모달의 프리셋·직접입력 단위를 언어별(분/시간 vs M/H)로 분리했습니다
- 모달 버튼·타이머 컨트롤·연장 모달의 포커스링을 그림자(ring) 대신 버튼 테두리 색상 전환 방식으로 교체했습니다
- 타이머 재생 중 상위 체크박스를 누르면 완료 처리 대신 타이머 중단 모달을 띄우도록 연결했습니다
- Modal.Trigger가 ref를 받을 수 있도록 수정해 외부에서 정지 모달을 프로그래밍적으로 열 수 있게 했습니다
- 완료·중단 모달의 소요 시간을 60분 이상일 때 시/분(h m) 단위로 표기하고, 중단 모달의 시간 값을 파란색으로 강조했습니다
- 포커스 화면 상단의 요일 라벨을 next-intl Common.weekday로 교체해 영문 버전에서도 표시되게 했습니다
@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
timo Ready Ready Preview, Comment Jul 10, 2026 8:04pm

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jjangminii, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7496a261-2174-4fe5-a8ef-bcbc99f09ccb

📥 Commits

Reviewing files that changed from the base of the PR and between e1571a4 and af6e208.

📒 Files selected for processing (4)
  • apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
  • apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts
  • apps/timo-web/messages/en.json
  • apps/timo-web/messages/ko.json

Walkthrough

포커스 타이머 제어가 종료·중지·연장·완료 모달을 포함하는 세션 컨트롤 구조로 변경되었습니다. 관련 화면에 번역 키, 시간 포맷터, 요일 키 변환, 모달 트리거 ref 및 포커스 스타일이 추가되었습니다.

Changes

포커스 타이머 모달 흐름

Layer / File(s) Summary
모달 트리거와 컨트롤 계약
apps/timo-web/components/timer/TimerControls.tsx, packages/timo-design-system/src/components/layout/modal/Modal.tsx, packages/timo-design-system/src/components/button/modal-button/ModalButton.tsx, apps/timo-web/components/timer/TimerControlButton.tsx
종료·연장 컨트롤을 Modal.Trigger 기반으로 변경하고, 트리거 ref와 focus-visible 스타일을 추가합니다.
세션 단계와 모달 패널
apps/timo-web/components/timer/TimerSessionControls.tsx, apps/timo-web/components/timer/Timer*ModalPanel.tsx, apps/timo-web/components/timer/durationLabel.ts, apps/timo-web/messages/*.json
end, stop, extend, complete 단계를 관리하며 연장 프리셋·커스텀 입력, 기간 라벨, 번역 문구와 각 모달 패널을 연결합니다.
포커스 화면과 타이머 상태 연결
apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx, apps/timo-web/app/[locale]/(main)/focus/_utils/*, apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx
타이머 세션 콜백과 계획·실제 시간을 연결하고, 실행 중 완료 요청을 중지 모달로 전환하며, 요일 키와 초 단위 기간 변환을 적용합니다.

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: 세션 콜백 실행
Loading

Possibly related PRs

Suggested labels: ✨ Feature, ⏰ Timo-web, ♠️ 정민, ⌚ Timo-Design-system

Suggested reviewers: yumin-kim2, ehye1, kimminna

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 포커스 세션 종료 모달 연결은 반영됐지만, 직접 이슈 #136의 설정 회원탈퇴용 Modal 교체는 빠져 있습니다. SettingsWithdrawalContainer의 window.confirm를 Modal로 교체하고, 필요하면 스토리북도 함께 갱신하세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed 변경은 모두 타이머 모달, 세션 제어, i18n, 접근성 범위에 머물러 있어 눈에 띄는 무관한 수정은 없습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed 주요 변경인 타이머 모달 연결과 다국어 지원을 정확히 요약합니다.
Description check ✅ Passed 모달 연결과 i18n, 포커스 스타일 등 변경 범위와 목적이 본문과 일치합니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web/136-connect-modal

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…to feat/web/136-connect-modal

# Conflicts:
#	apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
@github-actions github-actions Bot added ⏰ Timo-web Timo 웹 서비스 ⌚ Timo-Design-system Timo 디자인 시스템 labels Jul 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f18156f and e1571a4.

📒 Files selected for processing (16)
  • apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
  • apps/timo-web/app/[locale]/(main)/focus/_utils/date.ts
  • apps/timo-web/app/[locale]/(main)/focus/_utils/duration.ts
  • apps/timo-web/components/layout/sidebar/time/TimerPanel.tsx
  • apps/timo-web/components/timer/TimerCompleteModalPanel.tsx
  • apps/timo-web/components/timer/TimerControlButton.tsx
  • apps/timo-web/components/timer/TimerControls.tsx
  • apps/timo-web/components/timer/TimerEndModalPanel.tsx
  • apps/timo-web/components/timer/TimerExtendModalPanel.tsx
  • apps/timo-web/components/timer/TimerSessionControls.tsx
  • apps/timo-web/components/timer/TimerStopModalPanel.tsx
  • apps/timo-web/components/timer/durationLabel.ts
  • apps/timo-web/messages/en.json
  • apps/timo-web/messages/ko.json
  • packages/timo-design-system/src/components/button/modal-button/ModalButton.tsx
  • packages/timo-design-system/src/components/layout/modal/Modal.tsx

type TimerSessionControlsHandle,
} from "@/components/timer/TimerSessionControls";

const SECONDS_PER_MINUTE = 60;

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

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.

Suggested change
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.

Comment on lines +125 to +126
plannedMinutes={plannedMinutes}
actualMinutes={plannedMinutes}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

actualMinutesplannedMinutes를 그대로 전달하여 "계획"과 "실제"가 항상 동일하게 표시됩니다

FocusTask 인터페이스에 경과 시간 필드(elapsedSeconds 등)가 없어 현재 데이터 모델로는 실제 시간 추적이 불가능합니다. 하지만 actualMinutes={plannedMinutes}로 전달하면:

  • TimerCompleteModalPanel의 "계획"과 "실제"가 항상 같은 값을 보여줌 (예: 둘 다 "2h")
  • TimerStopModalPanel의 "지금까지 수행한 {minutes}"에 계획 시간이 표시됨

경과 시간 추적이 구현될 때까지 TODO 주석 추가 또는 actualMinutes={0} 임시값 사용을 고려하세요. TimerPanel.tsxPLANNED_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 = () => {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

handleExtendTimer가 빈 구현입니다

FocusSessionContainer의 동일 핸들러는 durationSeconds 업데이트와 TODO 주석을 포함하지만, TimerPanelhandleExtendTimer는 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 주석을 추가하세요.

Comment on lines +34 to +35
plannedMinutes={PLANNED_MINUTES}
actualMinutes={PLANNED_MINUTES}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

actualMinutesPLANNED_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를 유지하여 완료 모달에 계획 시간과 실제 시간이 올바르게 표시되도록
하세요.

Comment on lines +4 to +59
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>
);

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 | 🟠 Major | ⚡ Quick win

TimerEndIconTimerStopModalPanelTimerStopIcon이 완전히 동일한 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를
공용 아이콘 컴포넌트로 추출한 뒤 두 컴포넌트가 해당 공용 컴포넌트를 렌더링하도록 변경하고, 기존 아이콘과 동일한 크기·속성·외형을 유지하세요.

Comment on lines +61 to +97
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>
</>
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +73 to +89
<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"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

입력 필드에 outline-none이 직접적인 focus-visible 대체 스타일 없이 사용되었습니다.

부모 divfocus-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

Comment on lines +49 to +54
useImperativeHandle(ref, () => ({
openStopModal: () => {
setStep("stop");
stopModalTriggerRef.current?.click();
},
}));

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

히든 트리거 .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 및 관련 의존 코드는 정리하고 동일한
방식이 적용된 다른 구간도 함께 수정하세요.

Comment thread apps/timo-web/messages/ko.json Outdated
Comment on lines +75 to +77
export type ModalTriggerProps = ButtonHTMLAttributes<HTMLButtonElement> & {
ref?: Ref<HTMLButtonElement>;
};

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

typeinterface 변경을 권장합니다.

ModalTriggerPropsButtonHTMLAttributes와 교차 타입으로 구성된 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.

Suggested change
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

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Timo Performance Report

Bundle Size — timo-web
라우트 크기 First Load JS
/[locale]/home 73.48 kB 🟡 279.29 kB
/[locale]/today 56.88 kB 🟡 262.69 kB
/[locale]/focus 53.84 kB 🟡 259.65 kB
/[locale]/settings/account 0 B 🟡 205.81 kB
/[locale]/settings 0 B 🟡 205.81 kB
/[locale]/settings/policy 0 B 🟡 205.81 kB
/[locale]/statistics 49.87 kB 🟡 255.68 kB
/[locale]/[...rest] 0 B 🟡 205.81 kB
/[locale]/onboarding 107.39 kB 🟡 313.21 kB
/[locale] 0 B 🟡 205.81 kB

공유 번들: 205.81 kB
🟢 < 200kB  |  🟡 < 350kB  |  🔴 ≥ 350kB (First Load JS · gzip)

Lighthouse — timo-web
URL Perf A11y LCP CLS TBT
/en/home 🔴 62 🟢 96 🔴 14.1s 🟢 0.000 🟡 498ms
/en/today 🟡 71 🟡 93 🔴 13.7s 🟢 0.000 🟡 241ms
/en/focus 🟡 72 🟡 91 🔴 13.7s 🟢 0.000 🟢 197ms
/en/statistics 🔴 58 🟢 95 🔴 13.6s 🟢 0.000 🔴 688ms

Perf ≥ 70 / A11y ≥ 85 목표
LCP 🟢 < 2.5s 🟡 < 4s 🔴 ≥ 4s  |  CLS 🟢 < 0.1 🟡 < 0.25 🔴 ≥ 0.25  |  TBT 🟢 < 200ms 🟡 < 600ms 🔴 ≥ 600ms

Image Optimization — timo-web

public/ 디렉토리에 이미지가 없습니다.

측정 커밋: dffd025

- "테스크"를 "태스크"로, "완료 되었어요"를 "완료되었어요"로 맞춤법과 띄어쓰기를 수정했습니다
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Storybook Preview

항목 링크
Storybook 열기
Chromatic 빌드 확인

마지막 업데이트: 2026-07-10 20:49 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⌚ Timo-Design-system Timo 디자인 시스템 ⏰ Timo-web Timo 웹 서비스

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 설정 회원탈퇴 및 포커스 세션 종료에 Modal 컴포넌트 연결

1 participant