Skip to content

[FIX] 수정 모달·오늘 탭의 타이머/완료 상태 동기화 문제 해결#203

Merged
kimminna merged 8 commits into
developfrom
feat/web/200-sync-timer-in-edit-modal
Jul 14, 2026
Merged

[FIX] 수정 모달·오늘 탭의 타이머/완료 상태 동기화 문제 해결#203
kimminna merged 8 commits into
developfrom
feat/web/200-sync-timer-in-edit-modal

Conversation

@jjangminii

Copy link
Copy Markdown
Contributor

ISSUE 🔗

close #200



What is this PR? 🔍

수정 모달과 오늘 탭에서 각기 다른 방식으로 깨져 있던 타이머·완료 상태 동기화 문제를 홈 페이지의 검증된 API 연동 패턴으로 통일했습니다.

배경

  • 기존 구조: 홈 페이지만 실제 API(useActiveTimer, changeTodoStatus 등)로 타이머/완료 상태를 관리했고, 수정 모달은 로컬 폼 스냅샷 위주, 오늘 탭은 // TODO: API mock 상태였습니다.
  • 발생 문제: 수정 모달에서 재생 버튼을 누르면 모달이 닫히고, 재생 아이콘·완료 체크박스가 실제 타이머 상태와 어긋났으며, 오늘 탭은 체크박스/재생 버튼을 눌러도 서버에 전혀 반영되지 않았습니다.
  • 해결 방향: 홈 페이지의 useHomeTodosByDate 패턴(활성 타이머 조회, 시작/일시정지/재개, 완료 시 중지 확인 다이얼로그, 관련 쿼리 무효화)을 기준으로 수정 모달과 오늘 탭을 재정렬했습니다.

수정 모달 — 재생 버튼

  • 변경 요약: 재생 버튼 클릭 시 무조건 모달을 닫던 로직을 제거했습니다.
  • 이유: onTogglePlay를 감싼 핸들러가 성공 여부와 무관하게 onClose()를 같이 호출해, 타이머는 정상 동작해도 모달이 닫혀 오작동처럼 보였습니다.
  • 구현 방식: onTogglePlay prop을 그대로 전달하도록 단순화했습니다. openTimerPanel()은 사이드바 패널을 여는 것뿐이라 모달을 닫을 이유가 없었습니다.

수정 모달 — 재생 버튼 아이콘 동기화

  • 변경 요약: 타이머 시작/토글 성공 시 모달이 구독하는 상세 조회 쿼리도 함께 무효화하도록 했습니다.
  • 이유: 기존 invalidateTimerState()는 activeTimer/home/timeBoxes만 무효화해 모달의 useGetTodoDetail 쿼리가 갱신되지 않았고, 재생 버튼 아이콘이 실제 상태와 어긋났습니다.
  • 구현 방식: use-home-todos-by-date.tshandleTogglePlay 성공 콜백에 getGetTodoDetailQueryKey(todoId, { date }) 무효화를 추가했습니다.

수정 모달 — 완료 체크박스

  • 변경 요약: 완료 체크박스의 disabled를 제거하고 실제 완료 처리 API에 연결했습니다.
  • 이유: TodoUpdateRequest에는 애초에 isCompleted 필드가 없어 체크박스가 로컬 폼 상태만 바꿀 뿐 서버에 반영되지 않았고, 타이머 진행 중엔 클릭 자체가 막혀 있었습니다.
  • 구현 방식: DetailTodoModalContaineronToggleCompleted prop을 추가해 홈의 handleToggleCompleted(실행 중이면 중지 확인, 아니면 즉시 완료)를 그대로 연결했습니다. 체크박스 표시값도 로컬 RHF 필드 대신 서버 값(todo.completed)을 직접 쓰도록 바꿔 갱신 지연 문제를 없앴습니다. 서브태스크 완료는 타이머 실행 여부와 무관하게 즉시 저장되도록 기존 canUpdateTodo 가드를 우회했습니다.
  • 경계 · 제약: 제목/서브태스크 텍스트 편집 제한([FEAT] 투두 모달 API 연동 (상세조회/수정/삭제) #186)은 그대로 유지했고, 체크박스만 예외로 뒀습니다.

오늘 탭 — 완료·재생 실동작화

  • 변경 요약: mock 상태였던 완료·재생 핸들러를 홈 페이지와 동일한 실제 API 흐름으로 재작성했습니다.
  • 이유: 오늘 탭 체크박스/재생 버튼은 로컬 상태만 뒤집고 서버 반영이 없었고, 카드 컨테이너가 props와 무관한 자체 isDone/subTodos 상태를 들고 있어 새로고침하면 항상 초기값으로 돌아갔습니다.
  • 구현 방식: useTodayTodoList.tsuseHomeTodosByDate.ts와 동일한 패턴으로 재작성했습니다. TodayTodoCardContainer는 로컬 섀도우 상태를 제거해 HomeTodoCard처럼 순수 컨트롤드 컴포넌트로 바꿨고, TodayTodoListContainer에는 홈과 동일한 중지 확인 모달/중복 실행 토스트를 연결했습니다.
  • 경계 · 제약: 서브태스크 완료·삭제는 이번 범위(상위 체크박스/재생 버튼)에서 제외하고 기존 mock 그대로 남겨뒀습니다.



To Reviewers

오늘 탭의 서브태스크 체크·삭제는 여전히 mock입니다 — 요청 범위가 상위 체크박스/재생 버튼으로 한정되어 후속 작업으로 남겼습니다.
로그인 세션이 없어 개발 서버에서 /today 라우팅과 콘솔 에러 없음까지만 확인했고, 실제 클릭 동작은 검증하지 못했습니다. 리뷰 시 실제 동작 확인 부탁드립니다.



Screenshot 📷



Test Checklist ✔

  • pnpm check-types 통과
  • pnpm lint 통과
  • 브라우저 로그인 후 실제 클릭 동작 확인 — 미실행: 테스트 계정 없음
  • pnpm build — 미실행

- 재생 버튼 클릭 시 무조건 onClose()가 호출되던 로직을 제거했습니다
- 타이머 시작/토글 성공 시 해당 투두의 상세 조회 쿼리도 함께 무효화하도록 했습니다
- 완료 체크박스에서 disabled를 제거하고, 홈 페이지의 handleToggleCompleted(실행 중이면 중지 확인, 아니면 즉시 완료)를 모달에 연결했습니다
- 서브태스크 완료도 타이머 실행 여부와 무관하게 즉시 저장되도록 했습니다
- 완료 처리 성공 시 모달의 상세 조회 쿼리도 함께 무효화하도록 했습니다
- mock(TODO: API)이던 재생·완료 핸들러를 홈 페이지와 동일한 패턴(타이머 시작/일시정지/재개, 중지 확인 다이얼로그, 사이드바 패널 연동)으로 재작성했습니다
- 카드가 로컬로만 뒤집던 완료/서브태스크 섀도우 상태를 제거하고 서버 상태를 그대로 반영하는 컨트롤드 컴포넌트로 변경했습니다
@vercel

vercel Bot commented Jul 14, 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 14, 2026 5:48pm

Request Review

@github-actions github-actions Bot added the ⏰ Timo-web Timo 웹 서비스 label Jul 14, 2026
@github-actions github-actions Bot requested review from ehye1 and kimminna July 14, 2026 17:04
@github-actions github-actions Bot added 🐛 Bug 기능이 정상적으로 작동하지 않는 문제 수정 ♠️ 정민 정민양 labels Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 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: 17 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: 902099e5-05b6-4454-bd9f-a45a6a7dc637

📥 Commits

Reviewing files that changed from the base of the PR and between 8220d09 and 1f04405.

📒 Files selected for processing (11)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoCardContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/page.tsx
  • apps/timo-web/app/[locale]/(main)/today/_components/TodayTodoCard.tsx
  • apps/timo-web/containers/timer/StopCompleteModalContainer.tsx
  • apps/timo-web/hooks/todo-modal/create/use-create-todo-submit.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-subtask-field.ts
  • packages/timo-design-system/src/components/layout/modal/Modal.tsx
  • packages/timo-design-system/src/lib/index.ts
  • packages/timo-design-system/src/lib/modal-stack-registry.ts

Walkthrough

오늘 투두의 완료·타이머 처리를 API 변이 기반으로 전환하고, 중지 후 완료 확인 모달과 실행 중 알림을 추가했습니다. 상세 모달은 완료 상태를 외부 콜백으로 처리하며, 홈·오늘 화면의 상세 쿼리 무효화를 보강했습니다.

Changes

투두 상태 흐름

Layer / File(s) Summary
상세 모달 완료 상태 계약
apps/timo-web/components/todo-modal/detail/*, apps/timo-web/containers/todo-modal/detail/*, apps/timo-web/hooks/todo-modal/detail/*, apps/timo-web/app/.../home/_containers/HomeTodoContainer.tsx
완료 상태를 상세 폼에서 제거하고 외부 onToggleCompleted 콜백으로 전달하며, 서브태스크 업데이트 요청 구성을 변경했습니다.
오늘 투두 상태 및 타이머 변이
apps/timo-web/app/.../today/_hooks/useTodayTodoList.ts
완료, 중지 후 완료, 타이머 재생·일시정지·시작을 React Query 변이와 캐시 무효화에 연결하고 반환 API를 변경했습니다.
오늘 목록 UI 및 이벤트 위임
apps/timo-web/app/.../today/_containers/*
카드 상태와 이벤트를 상위에서 관리하고, 중지 후 완료 모달 및 실행 중 타이머 알림을 렌더링하도록 변경했습니다.
홈 목록 상세 캐시 무효화
apps/timo-web/app/.../home/_hooks/use-home-todos-by-date.ts
완료, 타이머, 서브태스크 성공 시 해당 투두 상세 쿼리를 추가로 무효화합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TodayTodoListContainer
  participant useTodayTodoList
  participant TimerAPI
  participant TodoAPI
  User->>TodayTodoListContainer: 완료 토글
  TodayTodoListContainer->>useTodayTodoList: handleToggleCompleted
  useTodayTodoList->>TimerAPI: 타이머 중지
  TimerAPI-->>useTodayTodoList: 중지 결과
  useTodayTodoList->>TodoAPI: 완료 상태 변경
  TodoAPI-->>useTodayTodoList: 변경 결과
  useTodayTodoList-->>TodayTodoListContainer: 완료 상태 및 피드백 전달
Loading

Possibly related PRs

  • Team-Timo/Timo-client#184: 완료 토글을 타이머 중지 확인 및 완료 모달 흐름으로 분기한 변경과 직접 연결됩니다.
  • Team-Timo/Timo-client#190: 상세 모달에서 완료 여부를 수정 요청과 분리한 변경과 직접 연결됩니다.
  • Team-Timo/Timo-client#164: 홈 컨테이너에서 상세 모달로 완료 토글 이벤트를 전달하는 흐름과 겹칩니다.

Suggested reviewers: yumin-kim2, kimminna, ehye1

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 이슈 #200의 핵심 요구인 durationSeconds 재동기화, RUNNING 진입 제한 재검토, 실시간 planned/actualMinutes 동기화, 테스트 추가가 보이지 않습니다. use-detail-todo-form의 reset 동기화, 모달 진입 제한, TimerSessionControls tick 동기화, 관련 테스트를 추가해 #200 요구를 충족하세요.
Out of Scope Changes check ⚠️ Warning 이슈 #200 범위를 넘는 오늘 탭과 홈의 타이머·완료 API 전환이 많이 포함되어 있어, 직접 목표인 수정 모달 동기화와는 별개 작업이 섞여 있습니다. 직접 링크된 #200 범위만 남기고 오늘 탭·홈 관련 전환은 별도 PR로 분리하세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 수정 모달과 오늘 탭의 타이머·완료 상태 동기화 수정이라는 핵심 변경을 잘 요약합니다.
Description check ✅ Passed 설명이 수정 모달과 오늘 탭의 동기화 문제를 실제 API 패턴으로 정리한 변경 내용과 일치합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web/200-sync-timer-in-edit-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.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Timo Performance Report

Bundle Size — timo-web
라우트 크기 First Load JS
/[locale]/home 209.67 kB 🔴 415.53 kB
/[locale]/today 193.69 kB 🔴 399.55 kB
/[locale]/focus 160.03 kB 🔴 365.89 kB
/[locale]/settings 166.35 kB 🔴 372.20 kB
/[locale]/statistics 232.92 kB 🔴 438.77 kB
/[locale]/[...rest] 0 B 🟡 205.86 kB
/[locale]/login 212.92 kB 🔴 418.78 kB
/[locale]/oauth/callback 119.57 kB 🟡 325.42 kB
/[locale]/onboarding 232.88 kB 🔴 438.73 kB
/[locale] 118.95 kB 🟡 324.81 kB
/[locale]/policy 125.07 kB 🟡 330.93 kB

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

Lighthouse — timo-web
URL Perf A11y LCP CLS TBT
/en/home 🔴 60 🟢 95 🔴 15.6s 🟢 0.000 🟡 568ms
/en/today 🔴 69 🟢 95 🔴 15.5s 🟢 0.000 🟡 262ms
/en/focus 🔴 60 🟢 95 🔴 15.2s 🟢 0.000 🟡 547ms
/en/statistics 🔴 61 🟢 95 🔴 15.2s 🟢 0.000 🟡 515ms

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

Image Optimization — timo-web
파일 크기 포맷 상태
images/google-calendar.png 36.20 kB PNG ⚠️ 🟢
images/google-logo.png 26.79 kB PNG ⚠️ 🟢

총 2개 · 63.00 kB  |  🟢 < 200KB  |  🟡 < 500KB  |  🔴 ≥ 500KB
⚠️ 2개 파일 WebP/AVIF 변환 권장

측정 커밋: b39e1bb

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts (1)

128-140: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

상태 변경 성공 시의 캐시 무효화 처리가 아주 깔끔하게 추가되었네요! ✨

한 가지 개선을 제안드리자면, confirmStopAndComplete 함수에서도 handleToggleCompleted 함수처럼 API 요청 실패 시 UI를 이전 상태로 롤백하는 onError 처리를 추가하는 것을 추천합니다.
현재 코드에서는 updateTodo를 통해 UI 상태를 미리(Optimistic) 업데이트하고 있지만, changeTodoStatus 요청이 실패할 경우 복구 로직이 없어 클라이언트와 서버 간 상태 불일치가 발생할 수 있습니다.

React Query의 Optimistic Updates 가이드를 참고하시면 롤백 패턴에 대해 더 확인하실 수 있습니다.

🛠 제안하는 롤백 로직 추가
+          const previous = todosByDate[dateKey] ?? [];
           updateTodo(dateKey, todoId, (todo) => ({
             ...todo,
             completed: true,
           }));
           changeTodoStatus(
             { todoId, data: { isCompleted: true, date: dateKey } },
             {
               onSuccess: () => {
                 invalidateHomeAndFocus();
                 invalidateTodoDetail(dateKey, todoId);
               },
+              onError: () => {
+                setTodosByDate((prev) => ({ ...prev, [dateKey]: previous }));
+              },
             },
           );
🤖 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)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts
around lines 128 - 140, Update confirmStopAndComplete alongside
handleToggleCompleted to add an onError handler to the changeTodoStatus call.
When the optimistic update fails, restore the todo’s previous completion state
so the UI remains consistent with the server, while preserving the existing
onSuccess cache invalidation.
apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx (1)

92-97: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

서브태스크 체크박스도 잠금 상태를 따라가게 해주세요.

잠금은 텍스트박스만 지키고 있네요. disabled={disabled}가 체크박스에 빠져 있어서 canUpdateTodo=false일 때도 서브태스크 완료 토글이 들어갑니다. handleSubtaskCompletedChange에도 동일한 가드를 넣고, 가능하면 Checkbox에도 disabled를 전달해 UI와 업데이트 조건을 맞춰 주세요. MDN: https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/disabled

🤖 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/todo-modal/detail/DetailTodoTaskFields.tsx` around
lines 92 - 97, Update the subtask Checkbox in DetailTodoTaskFields to receive
disabled={disabled}, and add the same disabled guard to
handleSubtaskCompletedChange so subtask completion cannot be toggled when
canUpdateTodo is false. Keep enabled behavior unchanged.
🤖 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)/(with-time-sidebar)/today/_containers/TodayTodoCardContainer.tsx:
- Line 5: Replace the direct Home-domain import of TodoTimerStatusTypes in
TodayTodoCardContainer with the shared Today-domain type used by
TodayTodoListContainer. Reuse the existing centralized type export rather than
importing any Home-domain type directly.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx:
- Around line 4-17: TodayTodoListContainer의 HomeStopCompleteModalContainer 직접
import를 제거하고, Today 도메인 또는 공용 모듈이 제공하는 동일한 모달 컨테이너를 사용하도록 교체하세요. 기존 모달 동작과 렌더링은
유지하되 Today 코드가 home/_containers 경로에 의존하지 않도록 정리하세요.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts:
- Around line 101-127: Update confirmStopAndComplete to mirror
handleToggleCompleted’s optimistic-update rollback pattern: capture the todo’s
previous state before updateTodo, roll it back and provide error feedback when
changeTodoStatus fails, and invalidateTodayView during failure recovery as well
as success. Add onError handling for stopTimer too, ensuring no completion
update occurs when stopping the timer fails and the user receives the existing
error feedback.
- Around line 129-163: Move the willRun calculation and openTimerPanel call in
handlePlay until after the activeTimer conflict check, ensuring another todo’s
active timer triggers only onTimerAlreadyRunning without switching the panel
tab. Preserve the existing active-timer handling and startTimer behavior for
non-conflicting cases.

In `@apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx`:
- Around line 179-184: Update handleSubtaskCompletedChange to use the shared
updateTodo path instead of calling onUpdate directly, ensuring the canUpdateTodo
guard is applied while a timer is running. Preserve the existing subtask update
request construction and title validation, and route valid updates through the
common helper.

---

Outside diff comments:
In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts:
- Around line 128-140: Update confirmStopAndComplete alongside
handleToggleCompleted to add an onError handler to the changeTodoStatus call.
When the optimistic update fails, restore the todo’s previous completion state
so the UI remains consistent with the server, while preserving the existing
onSuccess cache invalidation.

In `@apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx`:
- Around line 92-97: Update the subtask Checkbox in DetailTodoTaskFields to
receive disabled={disabled}, and add the same disabled guard to
handleSubtaskCompletedChange so subtask completion cannot be toggled when
canUpdateTodo is false. Keep enabled behavior unchanged.
🪄 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: ec8873a8-660e-4bdf-b91b-b8f41b04cd37

📥 Commits

Reviewing files that changed from the base of the PR and between acf2a2e and 8220d09.

📒 Files selected for processing (9)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoCardContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts
  • apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx
  • apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
  • apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
💤 Files with no reviewable changes (1)
  • apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts

Comment on lines +101 to +127
const confirmStopAndComplete = (todoId: number) => {
if (!activeTimer) return;

const dateKey = todos.find((todo) => todo.todoId === todoId)?.date;
if (!dateKey) return;

stopTimer(
{ timerId: activeTimer.timerId },
{
onSuccess: (response) => {
onStopFeedback(response.data?.aiFeedback ?? undefined);
invalidateTimerState();

updateTodo(todoId, (todo) => ({ ...todo, completed: true }));
changeTodoStatus(
{ todoId, data: { isCompleted: true, date: dateKey } },
{
onSuccess: () => {
invalidateTodayView();
invalidateTodoDetail(todoId, dateKey);
},
},
);
},
},
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

confirmStopAndComplete에 실패 처리가 빠져 있어 로컬-서버 상태가 어긋날 수 있습니다.

stopTimerchangeTodoStatusonError가 없습니다. 특히 changeTodoStatus가 실패하면 바로 위에서 이미 updateTodo(todoId, { completed: true })로 로컬 상태를 낙관적으로 바꿔놓은 뒤라, 서버는 여전히 미완료인데 화면은 완료로 남습니다. invalidateTodayView()도 성공 콜백 안에만 있어 실패 시 재검증도 일어나지 않습니다. 바로 위 handleToggleCompleted가 이미 previous 스냅샷+onError 롤백 패턴을 쓰고 있으니 동일하게 맞춰주면 좋겠습니다.

TanStack Query의 onError 콜백 활용은 공식 가이드(Mutations 문서)에서도 낙관적 업데이트 롤백의 표준 패턴으로 안내하고 있습니다.

🩹 롤백 및 에러 피드백 추가 제안
   const confirmStopAndComplete = (todoId: number) => {
     if (!activeTimer) return;

     const dateKey = todos.find((todo) => todo.todoId === todoId)?.date;
     if (!dateKey) return;

+    const previous = todos;
+
     stopTimer(
       { timerId: activeTimer.timerId },
       {
         onSuccess: (response) => {
           onStopFeedback(response.data?.aiFeedback ?? undefined);
           invalidateTimerState();

           updateTodo(todoId, (todo) => ({ ...todo, completed: true }));
           changeTodoStatus(
             { todoId, data: { isCompleted: true, date: dateKey } },
             {
               onSuccess: () => {
                 invalidateTodayView();
                 invalidateTodoDetail(todoId, dateKey);
               },
+              onError: () => setTodos(previous),
             },
           );
         },
+        onError: () => onStopFeedback(undefined),
       },
     );
   };
📝 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 confirmStopAndComplete = (todoId: number) => {
if (!activeTimer) return;
const dateKey = todos.find((todo) => todo.todoId === todoId)?.date;
if (!dateKey) return;
stopTimer(
{ timerId: activeTimer.timerId },
{
onSuccess: (response) => {
onStopFeedback(response.data?.aiFeedback ?? undefined);
invalidateTimerState();
updateTodo(todoId, (todo) => ({ ...todo, completed: true }));
changeTodoStatus(
{ todoId, data: { isCompleted: true, date: dateKey } },
{
onSuccess: () => {
invalidateTodayView();
invalidateTodoDetail(todoId, dateKey);
},
},
);
},
},
);
};
const confirmStopAndComplete = (todoId: number) => {
if (!activeTimer) return;
const dateKey = todos.find((todo) => todo.todoId === todoId)?.date;
if (!dateKey) return;
const previous = todos;
stopTimer(
{ timerId: activeTimer.timerId },
{
onSuccess: (response) => {
onStopFeedback(response.data?.aiFeedback ?? undefined);
invalidateTimerState();
updateTodo(todoId, (todo) => ({ ...todo, completed: true }));
changeTodoStatus(
{ todoId, data: { isCompleted: true, date: dateKey } },
{
onSuccess: () => {
invalidateTodayView();
invalidateTodoDetail(todoId, dateKey);
},
onError: () => setTodos(previous),
},
);
},
onError: () => onStopFeedback(undefined),
},
);
};
🤖 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)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts
around lines 101 - 127, Update confirmStopAndComplete to mirror
handleToggleCompleted’s optimistic-update rollback pattern: capture the todo’s
previous state before updateTodo, roll it back and provide error feedback when
changeTodoStatus fails, and invalidateTodayView during failure recovery as well
as success. Add onError handling for stopTimer too, ensuring no completion
update occurs when stopping the timer fails and the user receives the existing
error feedback.

Comment on lines +129 to +163
const handlePlay = (todoId: number) => {
const dateKey = todos.find((todo) => todo.todoId === todoId)?.date;
if (!dateKey) return;

const willRun =
todos.find((todo) => todo.todoId === todoId)?.timerStatus !== "RUNNING";

if (willRun) {
openTimerPanel();
}

if (activeTimer && activeTimer.todoId === todoId) {
changeStatus(
{
timerId: activeTimer.timerId,
data: {
action: activeTimer.status === "RUNNING" ? "PAUSE" : "RESUME",
},
},
{ onSuccess: () => invalidateTodoDetail(todoId, dateKey) },
);
return;
}

// 다른 투두의 타이머가 이미 실행/일시정지 중이면 새 타이머를 시작할 수 없다(409)
if (activeTimer) {
onTimerAlreadyRunning();
return;
}

startTimer(
{ todoId },
{ onSuccess: () => invalidateTodoDetail(todoId, dateKey) },
);
};

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and read the quality review skill first
skill_file="$(git ls-files '.agents/skills/quality/timo-review/SKILL.md' | head -n 1)"
echo "SKILL_FILE=$skill_file"
if [ -n "$skill_file" ]; then
  wc -l "$skill_file"
  cat -n "$skill_file" | sed -n '1,240p'
fi

# Locate the target file and inspect surrounding context
target='apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts'
echo
echo "TARGET=$target"
wc -l "$target"
cat -n "$target" | sed -n '1,260p'

Repository: Team-Timo/Timo-client

Length of output: 9655


🏁 Script executed:

#!/bin/bash
set -euo pipefail

skill_file="$(git ls-files '.agents/skills/quality/timo-review/SKILL.md' | head -n 1)"
echo "SKILL_FILE=$skill_file"
if [ -n "$skill_file" ]; then
  wc -l "$skill_file"
  cat -n "$skill_file" | sed -n '1,240p'
fi

target='apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts'
echo
echo "TARGET=$target"
wc -l "$target"
cat -n "$target" | sed -n '1,260p'

Repository: Team-Timo/Timo-client

Length of output: 9655


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## openTimerPanel definitions"
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.next/**' "openTimerPanel" apps/timo-web src packages -A 6 -B 6

echo
echo "## time sidebar store"
store_file="$(git ls-files 'apps/timo-web/stores/time-sidebar/*' | head -n 20)"
printf '%s\n' "$store_file"
if [ -n "$store_file" ]; then
  for f in $store_file; do
    echo
    echo "FILE=$f"
    wc -l "$f"
    cat -n "$f" | sed -n '1,240p'
  done
fi

Repository: Team-Timo/Timo-client

Length of output: 8314


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## onTimerAlreadyRunning usages"
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.next/**' "onTimerAlreadyRunning" apps/timo-web -A 4 -B 4

echo
echo "## timer panel / sidebar actions"
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.next/**' "openTimerPanel|timer tab|TimerPanel|time-sidebar" apps/timo-web -A 4 -B 4

Repository: Team-Timo/Timo-client

Length of output: 48878


충돌 체크 뒤에 패널을 여는 편이 더 자연스럽습니다.
흐름은 깔끔한데, willRun일 때 openTimerPanel()activeTimer 충돌 체크보다 먼저 실행됩니다. 다른 투두의 타이머가 이미 돌아가면 onTimerAlreadyRunning()만 띄워도 되는데 패널은 timer 탭으로 열린 채 남습니다. activeTimer 분기 뒤로 옮기면 의도치 않은 탭 전환을 막을 수 있습니다. React 이벤트 처리도 같이 보면 좋습니다: https://react.dev/learn/responding-to-events

🤖 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)/(with-time-sidebar)/today/_hooks/useTodayTodoList.ts
around lines 129 - 163, Move the willRun calculation and openTimerPanel call in
handlePlay until after the activeTimer conflict check, ensuring another todo’s
active timer triggers only onTimerAlreadyRunning without switching the panel
tab. Preserve the existing active-timer handling and startTimer behavior for
non-conflicting cases.

Comment on lines 179 to 184
const handleSubtaskCompletedChange = (id: number, completed: boolean) => {
const subtasks = detailTodoForm.changeSubtaskCompleted(id, completed);
updateTodo({ subtasks });
const updateData = detailTodoForm.buildUpdateRequest({ subtasks });
if (!updateData.title?.trim()) return;
onUpdate(updateData);
};

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

서브태스크 완료 변경도 updateTodo 경로로 통일해 주세요. 타이머 실행 중에는 canUpdateTodo 가드가 같이 적용돼야 하므로, handleSubtaskCompletedChange에서 onUpdate를 직접 호출하지 말고 공통 헬퍼를 사용하거나 동일한 체크를 복원해야 합니다. 참고: MDN disabled 속성 문서.

🤖 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/todo-modal/detail/DetailTodoModalContent.tsx` around
lines 179 - 184, Update handleSubtaskCompletedChange to use the shared
updateTodo path instead of calling onUpdate directly, ensuring the canUpdateTodo
guard is applied while a timer is running. Preserve the existing subtask update
request construction and title validation, and route valid updates through the
common helper.

- 카드 너비를 w-full에서 min-w-80으로 바꾸고, 체크박스 wrapper에 inline-flex items-center를 추가해 흔들림을 없앴습니다
- 페이지 콘텐츠 영역에 min-w-[400px]를 주되, 오버플로우가 <main>으로 새지 않도록 overflow-x-auto를 함께 적용했습니다

import { useState } from "react";

import type { TodoTimerStatusTypes } from "@/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

타이머 상태 타입을 공통 타입으로 추출해서 단방향으로 정리해 주세용

- home/_types/todo-type(단순 재수출 배럴)를 거치던 3개 파일이 실제 원본인 @/api/common/todo-schema를 직접 가리키도록 정리했습니다
- Today가 home 전용 _containers 경로에 의존하던 것을 제거하고, HomeStopCompleteModalContainer를 containers/timer/StopCompleteModalContainer로 승격해 Home/Today가 공용 경로에서 가져오도록 정리했습니다
기존에는 오버레이/패널 z-index가 모든 모달 인스턴스에 고정값(40/50)이라, 중첩된 모달의 오버레이가 그 아래 모달의 패널보다 항상 낮아 아래 모달이 어두워지지 않고 그냥 겹쳐 보였습니다. 모달이 열릴 때마다 전역 카운터(acquireModalStackIndex)로 스택 인덱스를 발급받아, overlay/panel z-index를 base + stackIndex * 20으로 계산하도록 바꿔 나중에 열린 모달의 오버레이가 항상 이전 모달의 패널보다 높아지도록 했습니다.
@github-actions

Copy link
Copy Markdown

Storybook Preview

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

마지막 업데이트: 2026-07-14 17:48 UTC

@kimminna kimminna merged commit 3cc1dfa into develop Jul 14, 2026
14 checks passed
@kimminna kimminna deleted the feat/web/200-sync-timer-in-edit-modal branch July 14, 2026 17:49
@kimminna kimminna mentioned this pull request Jul 15, 2026
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 웹 서비스 ♠️ 정민 정민양 🐛 Bug 기능이 정상적으로 작동하지 않는 문제 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 수정 모달과 진행 중인 타이머 상태 동기화

2 participants