[FEAT] 타이머, 타임박스 API 연동 (재생/연장/완료/종료)#184
Conversation
- 실행 중인 타이머를 정지 모달에서 완료 처리할 때 completeTimer 대신 stopTimer가 호출되도록 완료 모달 진입 경로를 구분했습니다 - FocusSessionContainer에 useStopTimer 뮤테이션과 handleStopTimer를 추가했습니다 - TimerSessionControls에 onStop prop을 추가하고, 사이드바 TimerPanel의 목업 핸들러도 함께 갱신했습니다
- 도메인 간 직접 import 금지 규칙에 맞춰 타이머 쿼리 훅을 focus 전용 위치에서 hooks/·types/로 이동했습니다 - 홈 투두 카드의 재생 버튼을 목업 대신 useStartTimer/useChangeStatus에 연동했습니다 - 사이드바 Timer 탭을 실제 useActiveTimer 데이터로 연동하고, 재생·일시정지·연장·정지·완료 컨트롤을 실제 뮤테이션에 연결했습니다 - 홈 투두 카드의 계획 시간 배지가 진행 중인 타이머의 연장 시간을 반영하도록 했습니다
- 완료 모달 진입 경로(정지 모달 vs 종료 모달)로 완료/중지 API를 나누던 로직을 계획 시간 소진 여부(remainingSeconds)로 수정했습니다 - 체크박스로 투두를 완료 처리할 때 일시정지된 타이머가 있으면 백엔드가 거부하는 문제를, 실행 중이거나 일시정지된 타이머가 있으면 정지 확인 모달을 거치도록 수정해 해결했습니다
- RUNNING 상태에서 1초마다 /api/v1/timers/active를 재요청하던 refetchInterval을 제거했습니다 - 대신 dataUpdatedAt 기준으로 클라이언트에서 매초 remainingSeconds/elapsedSeconds를 보정 계산하도록 했습니다 - refetchOnMount를 always로 설정해 화면 전환 시에는 최신 상태를 한 번 가져오도록 했습니다
- durationText가 todo.durationSeconds를 그대로 써서 활성 타이머의 연장 시간이 배지에 반영되지 않던 문제를, 이미 계산돼 있던 plannedSeconds(연장분 포함)를 쓰도록 수정했습니다
- 남은 시간이 0이 되는 순간 기존 "타이머 종료!" 모달을 자동으로 띄우도록 TimerSessionControlsHandle에 openEndModal을 추가했습니다 - 완료 모달에서 완료하기를 누르면 isTimeUp 기준으로 completeTimer/stopTimer가 자동 분기됩니다 - 시간이 다 된 뒤(모달이 뜬 뒤) 연장한 구간만 노란색 오버타임 링으로 표시하고, 시간이 다 되기 전 미리 연장한 경우는 일반 파란 링으로 유지되도록 구분했습니다 - 포커스 페이지와 사이드바 Timer 탭 둘 다 동일하게 적용했습니다
…162) - 컴포넌트 함수 하나에 뒤섞여 있던 데이터 페칭·타이머 뮤테이션·파생 계산·이펙트를 use-focus-session 훅으로 분리하고, 컨테이너는 JSX만 반환하도록 축소했습니다 - 훅 반환값을 focusSessionState/focusSessionActions로 묶어서 기존 useSettingsProfile과 동일한 상태/액션 그룹핑 컨벤션을 따르도록 했습니다 - 기능 동작 변경 없는 순수 구조 리팩터링입니다
- 오버타임 시작 지점을 타이머별로 sessionStorage에 저장하는 useTimerOvertime 훅을 추가했습니다 - FocusSessionContainer와 TimerPanel의 로컬 state 기반 오버타임 로직을 이 훅으로 교체했습니다
- 시작/상태변경/연장/완료/종료 mutation 성공 콜백에 홈 뷰 invalidate를 추가했습니다 - 사이드바 밖에서 타이머를 조작해도 홈 화면 데이터가 갱신되지 않던 문제를 수정했습니다
- formatDurationLabel 기반 시/분 번역 포맷 대신 {분}M 형태로 단순화했습니다
- --z-modal-overlay/--z-modal-panel을 --z-index-modal-overlay/--z-index-modal-panel로 변경했습니다
…to feat/web/162-timer-api-integration # Conflicts: # apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
- durationSeconds와 동일하게 timerStatus도 activeTimer와 todoId를 비교해 계산하도록 수정했습니다 - 홈 뷰 쿼리가 refetch되기 전에도 다른 화면에서 시작/정지한 타이머 상태가 바로 아이콘에 반영됩니다
- 남은 초가 있으면 분 단위로 올림하도록 convertDurationToTimeText를 수정했습니다 - 53초처럼 1분 미만 남은 시간이 00:00으로 표시되던 문제를 해결했습니다
- isTimeUp이 된 순간 changeStatus(PAUSE)를 호출해 종료 모달을 방치해도 경과 시간이 더 쌓이지 않게 했습니다 - 연장 제출 시 changeStatus(RESUME)를 함께 호출해 실제로 다시 재생되도록 했습니다 - 이를 통해 모달을 늦게 닫아도 연장 시 오버타임 링이 잘못된 진행률로 나타나던 문제를 근본적으로 줄였습니다
- 직전 렌더 이후 1.5초 넘게 벌어졌거나 isOvertime이 전환된 경우만 점프로 간주해 트랜지션을 끄도록 했습니다 - 평소 1초 간격의 정상 틱은 그대로 부드럽게 이어지고, 화면 전환·오버타임 전환처럼 값이 한번에 튀는 경우만 즉시 스냅됩니다
- stopTimer/completeTimer는 타이머 레코드만 종결할 뿐 투두의 완료 여부는 바꾸지 않아서, 체크박스가 안 바뀌던 문제를 수정했습니다 - 정지 처리 시 changeTodoStatus(isCompleted: true)도 함께 호출해 체크박스 직접 클릭과 동일하게 투두가 완료 처리되도록 했습니다 - 사이드바 TimerPanel에도 동일한 문제가 있어 오늘 날짜 기준으로 같이 반영했습니다 - 완료 모달에서는 시간 소진 시 completeTimer와 stopTimer를 모두 호출하도록 했습니다
- 일시정지 상태에서 시간이 남았는데 종료를 누르면 isRunning이 false라 종료(Timer Finished) 모달이 잘못 뜨던 문제를 수정했습니다 - isTimeUp 기준으로 분기해 시간이 남았으면 정지 확인 모달을, 다 지났으면 종료 모달을 보여주도록 했습니다
- reorderTodo 성공 시 홈 뷰 쿼리만 무효화하고 포커스 페이지 쿼리는 무효화하지 않아 실시간 반영이 안 되던 문제를 수정했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough활성 타이머 조회·시간 보정·오버타임 처리를 추가하고, 포커스 세션과 사이드바·홈 투두를 타이머 API 상태 및 변이에 연결했습니다. 시간 초과 모달, 완료 피드백, 동적 진행률과 SVG 전환 보정도 반영했습니다. Changes타이머 세션 통합
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant 사용자
participant FocusSessionContainer
participant useFocusSession
participant TimerAPI
participant TimerSessionControls
사용자->>FocusSessionContainer: 재생·연장·완료·중지 선택
FocusSessionContainer->>useFocusSession: focusSessionActions 호출
useFocusSession->>TimerAPI: 타이머 변이 요청
TimerAPI-->>useFocusSession: 타이머 상태 및 피드백 반환
useFocusSession->>TimerSessionControls: 모달·피드백 상태 갱신
Possibly related PRs
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 |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
시간당 행 높이를 50px로 늘리고, 라벨 폰트를 12px로 되돌리며 좌측 6px·상단 2px 패딩으로 배치했습니다.
ehye1
left a comment
There was a problem hiding this comment.
복잡한 로직이었는데 잘 구현해주셨네요
너무너무 수고많으셨습니다 굿굿✌🏻👍🏻
…ntainers/HomeTodoContainer.tsx Co-authored-by: 김민아 <kmina121777@naver.com>
…on.ts Co-authored-by: 김민아 <kmina121777@naver.com>
feedbackText/isErrorToastOpen을 useFocusSession 훅에서 FocusSessionContainer로 옮기고, 훅은 onMutationError/onFeedback 콜백으로 컨테이너에 알리기만 하도록 정리했습니다. 홈 화면의 useHomeTodosByDate/HomeTodoContainer와 동일한 패턴입니다.
한 줄짜리 setter 래퍼는 별도 핸들러로 추출하지 않고 onClose에 바로 인라인했습니다.
식별자 이름만으로 충분히 드러나는 주석을 정리하고, 숨은 제약·비자명한 이유를 설명하는 주석은 남겼습니다.
…om/Team-Timo/Timo-client into feat/web/162-timer-api-integration
식별자·타입만으로 드러나는 설명은 모두 제거했습니다.
…to feat/web/162-timer-api-integration # Conflicts: # apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
식별자만으로 드러나는 설명을 정리했습니다.
…름을 바꾼다 (#162) _containers 디렉터리 안의 컴포넌트는 이름이 Container로 끝나도록 컨벤션을 통일했습니다.
ISSUE 🔗
close #162
What is this PR? 🔍
FocusSessionContainer/TimerSessionControls에 mock 데이터로 채워져 있던 타이머 흐름을 실제 타이머 API(재생/일시정지·재개/연장/완료/종료/조회)와 연동하고, 연동 과정에서 실사용 중 드러난 오버타임 링·모달 분기·투두 완료 동기화 버그들을 함께 수정했습니다. 이후 코드 리뷰에서 나온 경쟁 조건·상태 관리 이슈를 추가로 정리하고, 포커스 투두 아이콘·메모 연동, 사이드바 타임박스(TimeBox) 패널의 실제 API 연동까지 이어서 진행했습니다.배경
FocusSessionContainer가 mock 데이터와// TODO: API주석으로만 채워져 있었고, 활성 타이머 조회는 폴링 방식이었습니다.sessionStorage에 지속시키고, 시간 소진 시점에 서버 타이머를 실제로 일시정지시켜 방치 시간이 쌓이지 않게 하고, 타이머 도메인 API와 투두 도메인 API를 명확히 분리해 완료 처리가 항상 투두 API를 통해 이뤄지도록 정리했습니다. 이어서 순서가 보장되지 않던 mutation 체인을 콜백으로 직렬화하고, 타임박스 API를 연동해 사이드바에서 하루의 실행 이력을 실시간으로 확인할 수 있도록 했습니다.타이머 API 연동 및 상태 관리 분리
FocusSessionContainer의 로직을use-focus-session훅으로 분리하고, 시작/일시정지·재개/연장/완료/종료/활성 타이머 조회 API를 연동했습니다.useFocusTodo(투두)와useActiveTimer(활성 타이머)를 조합해timer/isRunning/isTimeUp/progress등 파생 상태를 훅 내부에서 계산하고,focusSessionState/focusSessionActions두 객체로 묶어 반환합니다. 사이드바의TimerPanel도 동일한 패턴으로 동작하되 포커스 페이지와 무관하게 활성 타이머만으로 동작하도록 별도 구현했습니다.TimerPanel(사이드바)과use-focus-session(포커스 페이지)은 각자activeTimer/todo매칭 조건이 달라 로직을 공유하지 않고 각각 구현했습니다. 중복이 있지만, 두 화면의 관심사(전역 타이머 위젯 vs 특정 투두 상세)가 달라 의도적으로 분리했습니다.활성 타이머 폴링 → 클라이언트 카운트다운 대체
useActiveTimer가 서버를 반복 조회하는 대신, 응답 시각(dataUpdatedAt) 기준으로 매초 로컬에서remainingSeconds/elapsedSeconds를 재계산합니다.RUNNING상태일 때만 1초 간격setInterval로tickstate를 올려useMemo를 강제로 재계산시키고,elapsedSinceFetch = floor((Date.now() - dataUpdatedAt) / 1000)을 서버값에 가감합니다.PAUSED상태에서는 interval이 걸리지 않아 값이 고정됩니다.오버타임 링 지속성 및 시간 소진 시 자동 일시정지
sessionStorage에 타이머별로 저장하는useTimerOvertime훅을 추가하고, 시간 소진 시 서버 타이머를 자동으로 일시정지(PAUSE)한 뒤 연장 시 재개(RESUME)하도록 했습니다.elapsedSeconds가 계속 누적돼 연장 시 오버타임 링이 이미 100%로 보이는 문제가 있었습니다.markOvertimeStart(timerId, baseSeconds)가 기준값을 state와sessionStorage에 함께 저장해 리마운트돼도 유지되게 했습니다.isTimeUp이 처음 true가 되는 순간changeStatus({ action: "PAUSE" })를 호출해 모달을 방치해도 서버 쪽 경과 시간이 더 쌓이지 않게 했고, 연장 제출 시changeStatus({ action: "RESUME" })을 함께 호출해 실제로 다시 재생되게 했습니다. 이후 리뷰에서 두 요청을 동시에 보내면 서버가RESUME을 아직 반영하기 전에extendTimer가 도착해 실패할 수 있다는 점을 확인해,changeStatus의onSuccess콜백 안에서extendTimer를 호출하도록 순서를 강제했습니다(평소 연장 흐름은 그대로 유지). 또한 초기값을sessionStorage를 직접 읽는 lazy initializer 대신null로 시작해useEffect에서 마운트 후 로드하도록 바꿔 SSR/CSR 하이드레이션 불일치 가능성을 없앴고, 저장값 파싱에Number.isFinite검증을 추가해 손상된 값은null로 폴백하도록 방어했습니다.overtimeBaseSeconds는 타이머가 바뀌기 전까지 계속 유지되는 게 의도된 동작입니다(연장 구간 전체를 노란 링으로 표시하기 위함).remainingSeconds가 양수로 돌아온다고 바로 리셋하면 연장 직후 링이 곧바로 파란색으로 꺼져버려서, 이번 PR에서는 리셋 로직을 넣지 않았습니다.시간 표시 반올림 및 링 애니메이션 스냅 처리
convertDurationToTimeText가 1분 미만 잔여 초를 내림 대신 올림 처리하도록 수정하고, 타이머 링의stroke-dashoffset트랜지션이 값이 몰아서 바뀔 때만 꺼지도록 했습니다. 계획 시간(plannedLabel)은 60분 이상이면 시/분(H/M) 조합으로 표시합니다.00:00으로 표시돼 시간이 다 된 것처럼 보였고, 화면 전환/오버타임 전환처럼 값이 크게 점프할 때도 1초 트랜지션이 그대로 적용돼 진행률이 몰아서 스윕되는 것처럼 보였습니다. 계획 시간을 분 단위 숫자로만 보여주면 90분 같은 값이 "90분"으로만 표시돼 다른 화면의 시/분 표기와 일관되지 않았습니다.Math.ceil(durationSeconds / 60)기준으로 시/분을 재계산합니다. 링 트랜지션은 직전 렌더 시각과isOvertime값을useRef로 기록해두고, 직전 렌더 이후 1.5초 넘게 벌어졌거나isOvertime이 전환된 경우만 "점프"로 간주해 그 순간만 트랜지션 클래스를 뺍니다. 정상적인 매초 틱(~1000ms 간격)은 문턱값 안에 들어와 계속 부드럽게 이어집니다. 이 비교용ref를 렌더링 도중 직접 mutate하던 것은 React의 렌더 순수성 규칙을 위반해,useEffect로 옮겨 렌더 이후에만 갱신하도록 고쳤습니다.plannedLabel은 기존에 있던formatDurationLabel(분, "H", "M")유틸을 그대로 재사용합니다.종료/정지/완료 모달 분기 및 투두 완료 동기화
isRunning에서isTimeUp으로 수정하고, 완료 모달에서 시간 소진 시completeTimer와stopTimer를 모두 호출하며, 타이머 정지 시changeTodoStatus로 투두 완료 처리를 함께 호출하도록 했습니다.isRunning이 false라 "시간 소진" 모달이 잘못 뜨고 있었습니다. 또한stopTimer/completeTimer응답에는 투두의 완료 여부 필드가 없어 — 두 API는 타이머 레코드만 종결할 뿐 투두의completed상태는 바꾸지 않는다는 것을 실제 API 응답으로 확인했습니다 — 체크박스를 직접 클릭할 때만 쓰이던changeTodoStatus를 타이머 종료 흐름에서는 호출하지 않아 투두가 완료 처리되지 않았습니다.TimerSessionControls의onOpenEndModal을isTimeUp ? "end" : "stop"으로 분기하도록 수정했습니다.TimerCompleteModalPanel의 완료 버튼은isTimeUp이면completeTimer(피드백/모달용)와stopTimer를 모두 호출하고, 아니면stopTimer만 호출합니다.handleStopTimer(포커스 페이지, 사이드바 공통 진입점)에서stopTimer와 함께changeTodoStatus({ isCompleted: true, ... })를 호출해 체크박스 직접 클릭과 동일하게 투두를 완료 처리합니다. 이후 두 요청을 동시에 보내면 타이머 status가 아직STOPPED로 반영되기 전에changeTodoStatus가 도착해 409(타이머 실행 중인 TODO는 변경 불가)로 실패하는 걸 확인해,stopTimer의onSuccess콜백 안에서changeTodoStatus를 호출하도록 순서를 강제했습니다.TimerPanel에는 날짜 컨텍스트가 없어changeTodoStatus호출 시formatDateKey(new Date())로 오늘 날짜를 사용합니다. 다른 날짜의 투두를 사이드바에서 정지하는 상황은 이번 범위에서 고려하지 않았습니다.홈 화면 실시간 동기화 및 정지 확인 흐름
HomeTodoContainer가durationSeconds는activeTimer기준으로 계산하면서timerStatus는 홈 목록 API의 스냅샷 필드를 그대로 넘기고 있어, 다른 화면에서 타이머를 재생/정지해도 홈 뷰가 refetch되기 전까지 아이콘이 안 바뀌었습니다. 활성 타이머가 있는 투두를 홈에서 바로 체크하면changeTodoStatus가 즉시 호출돼stopTimer보다 먼저 도착해 409로 실패했고, 다른 투두 재생이 막히는 상황에서는 아무 피드백이 없었습니다. 또한 홈에서 순서를 바꾸거나 완료 처리해도 포커스 페이지 쿼리는 무효화되지 않아 실시간 반영이 안 됐고,TimerPanel/useHomeTodosByDate/use-focus-session세 곳에 흩어져 있던 쿼리 무효화 정의가 유지보수를 어렵게 했습니다.durationSeconds와 동일한 패턴(activeTimer && activeTimer.todoId === todo.todoId)으로timerStatus도 계산하도록 수정했습니다. 포커스 페이지의TimerStopModalPanel/TimerCompleteModalPanel을 재사용해 홈에서도stopTimer성공 후에만changeTodoStatus를 호출하도록 했고,startTimer가 막히는 지점에Toast.timerAlreadyRunning안내를 연결했습니다(ko/en 메시지 추가).invalidateActiveTimer/invalidateHomeView정의를useTimerQueryInvalidation훅으로,handleTogglePlay/handleExtendTimer/handleCompleteTimer/handleStopTimer로직을useTimerActions훅으로 공통화했고(mutation 설정은 각 파일에 그대로 두고timer/isTimeUp/dateKey/onNoTimer로 파라미터화), 반복되던 홈 뷰/포커스 쿼리 무효화 쌍은invalidateHomeAndFocus헬퍼로 묶었습니다. 체크박스/서브태스크 완료 처리 성공 시에도 포커스 쿼리를 함께 무효화하도록 범위를 넓혔습니다.pendingCompleteTodo/feedbackText/isTimerRunningToastOpen같은 UI 상태는 훅(useHomeTodosByDate)이 아니라 컨테이너(HomeTodoContainer)가 소유하도록 옮겨, 훅은onNeedStopConfirm/onTimerAlreadyRunning/onStopFeedback콜백으로 컨테이너에 알리기만 하고 데이터/mutation만 담당하도록 정리했습니다.포커스 투두 아이콘 · 메모 연동
TimerOnIcon대신 투두가 실제로 선택한 아이콘(IconGraphic)으로 표시하고, 포커스 투두의 메모를 연동했습니다.icon필드가 로컬 스키마가 기대하던ICON_1~8코드가 아니라 이모지 문자열로 내려오는 걸 확인해, 기존z.enum검증이 그대로 크래시를 일으키고 있었습니다.Timer컴포넌트의iconprop을TodoIconValue로 바꿔IconGraphic으로 렌더링하도록 했습니다(사이드바TimerPanel은activeTimer.iconType을 그대로 사용). 포커스 API 전용 스키마(focusTaskSchema)의icon검증은z.enum(TODO_ICON_VALUES)에서z.string()으로 완화해 이모지 문자열도 파싱되게 했습니다.memo필드를focusTaskSchema에 추가해FocusTaskItem에 그대로 전달했습니다.IconGraphic이 기대하는 코드로 어떻게 매핑할지 렌더링 방식이 아직 정해지지 않아, 포커스 페이지의Timer에는icon을 전달하지 않도록 이번 범위에서 제외했습니다. 매핑 정책이 정해지면 후속 작업으로 추가합니다.타임박스(TimeBox) 패널 연동
TimeboxPanel을 정적인 시간 눈금 + 현재 시각 표시만 있던 상태에서, 실제GET /api/v1/time-boxesAPI와 연동해 하루 동안의 타이머 실행 구간을 24시간 타임라인 위에 실시간으로 그리도록 구현했습니다.TimeboxPanel은 시간 눈금과 현재 시각 빨간 선만 그리고 있었고, 실제 타이머 실행 이력(투두별 시작/종료 구간)을 보여줄 데이터 연동이 없었습니다.sessionId/timerId/todoId/todoName/date/startedAt/startAction/endedAt/endAction/actualMinutes)을 로컬 zod 스키마(timeBoxListSchema)로 재검증하는useTimeBoxes(date)훅을 추가했습니다.TimeboxPanel이 Suspense 경계 밖에 있어 기존useActiveTimer와 동일하게useQuery(non-suspense)로 구현했습니다.ROW_HEIGHT(30px/시간)·timeToOffset좌표계를 그대로 재사용해 시작/종료 시각을 위치·높이로 변환합니다.endedAt이 없는 세션은 아직 끝나지 않은 것으로 보고, 종료 시각 대신 현재 시각을 써서 실시간으로 자라도록 했습니다.timerId+todoId가 같은 모든 세션은 진한 파랑(bg-timo-blue-300)으로, 그 외에는 연한 파랑(bg-timo-blue-65)으로 표시합니다. 연장(RESUME) 시 같은timerId로 새sessionId세션이 생성되는 걸 확인했는데, 일시정지로 한번 끊겼던 이전 구간도 같은 투두가 다시 진행 중이면 진한 파랑으로 보여야 한다는 요구사항에 맞춰, "진행 중 표시(색상)"는timerId·todoId매칭으로, "실시간으로 자라는지(끝 모서리 둥글기)"는 그 세션 자체의endedAt유무로 서로 독립적인 기준으로 분리해 판단합니다.actualMinutes는 완료(COMPLETE)로 끝난 마지막 세션에만 내려오므로, 지속 시간은 항상endedAt(또는 진행 중이면 현재 시각) -startedAt으로 클라이언트에서 계산합니다.invalidateTimeBoxes를 함께 호출하도록(useTimerQueryInvalidation에 추가) 확장해, 새로고침 없이 사이드바 타임박스가 다른 화면의 타이머 조작에도 실시간으로 갱신되도록 했습니다.기타
packages/tailwind-config/theme.css의--z-modal-overlay/--z-modal-panel을 Tailwind v4가 실제로 인식하는--z-index-*네임스페이스에 맞게 이름을 수정했습니다(기존 이름은z-modal-overlay/z-modal-panel유틸리티 클래스와 매핑되지 않는 상태였습니다).To Reviewers
useTimerOvertime의 오버타임 리셋 로직을 넣지 않은 이유(연장 구간 전체를 노란 링으로 유지하는 게 의도된 동작)를 위 "경계 · 제약"에 적어뒀는데, 이 정책이 맞는지 확인 부탁드립니다.completeTimer/stopTimer를 모두 호출하는 부분과,stopTimer→changeTodoStatus/changeStatus(RESUME)→extendTimer를 콜백 체이닝으로 순서를 강제한 부분이 백엔드의 실제 처리 순서·락 정책과 맞는지 확인이 필요합니다.TimerPanel의changeTodoStatus호출에 쓰는 날짜가 항상 "오늘"이라고 가정하고 있는데, 사이드바에서 다른 날짜의 투두 타이머를 다루는 케이스가 있는지 확인 부탁드립니다.timerId+todoId매칭으로 하고 있는데(같은 투두를 여러 번 껐다 켜도 전부 진한 파랑), 이 정책이 의도한 UX가 맞는지 확인 부탁드립니다.IconGraphic매핑)은 정책이 정해지지 않아 이번 범위에서 제외했습니다.Screenshot 📷
Test Checklist ✔
pnpm check-types:web통과pnpm lint:web통과api.timo.kr)에 연동해 재생/일시정지/연장/완료/종료 흐름 수동 테스트 (작성자 확인)