Skip to content

[FEAT] 투두 API 연결 (상세조회 / 수정 / 삭제) - #190

Merged
kimminna merged 21 commits into
developfrom
feat/web/186-todo-modal-api
Jul 14, 2026
Merged

[FEAT] 투두 API 연결 (상세조회 / 수정 / 삭제)#190
kimminna merged 21 commits into
developfrom
feat/web/186-todo-modal-api

Conversation

@yumin-kim2

@yumin-kim2 yumin-kim2 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

ISSUE 🔗

close #186


What is this PR? 🔍

투두 상세 모달에서 상세 조회, 삭제, 수정 API를 연결했습니다.

이번 작업에서는 단순히 API hook만 붙이는 것이 아니라, 상세 모달의 책임을 정리하면서 아래 흐름이 자연스럽게 이어지도록 구성했습니다.

  • 홈 카드 클릭 시 상세 모달 오픈
  • 상세 모달에서 todoId, date 기반 상세 조회
  • 상세 응답값을 상세 모달 form 상태로 매핑
  • 상세 모달에서 투두 삭제 API 호출
  • 상세 모달에서 저장 버튼 클릭 시 수정 API 호출
  • 수정/삭제 성공 후 홈/상세 query invalidate
  • 수정 실패 시 기존 AnimatedToast 재사용

상세 조회 연결

상세 조회는 generated API의 useGetTodoDetail을 직접 컴포넌트에서 쓰지 않고, queries/todo/use-detail-todo.ts로 한 번 감쌌습니다.

useDetailTodo({ todoId, date })

처음에는 컨테이너 안에 query 로직을 바로 둘 수도 있었지만, 다른 query 패턴과 맞추고 상세 모달 컨테이너가 API 명세에 직접 강하게 묶이지 않도록 queries 폴더로 분리했습니다.

컨테이너 구조 정리

기존에는 모달 관련 container가 components 내부에 섞여 있어서 구조가 애매했습니다.
그래서 다음처럼 UI와 container 책임을 분리했습니다.

apps/timo-web/components/todo-modal
apps/timo-web/containers/todo-modal

상세 모달은 아래 구조를 따릅니다.

containers/todo-modal/detail/DetailTodoModalContainer.tsx
components/todo-modal/detail/DetailTodoModalContent.tsx
  • Container: 상세 조회, 삭제, 수정 API 연결
  • Content: 실제 모달 UI와 form 입력 처리

태그 목록 및 다국어 표시 수정

상세 모달 태그 드롭다운에서 기존에는 현재 선택된 태그 하나만 내려가고 있었습니다.

tags={[detailTodoForm.tagLabel]}

이 구조 때문에 드롭다운에 운동 하나만 보이는 문제가 있었고, 생성 모달처럼 전체 태그 목록을 가져오도록 useTagField를 재사용했습니다.
또한 서버에서 기본 태그가 exercise 같은 key가 아니라 운동처럼 한국어 label로 내려오는 경우가 있어, en locale에서도 한국어로 표시되는 문제가 있었습니다.
이를 해결하기 위해 기본 태그명을 locale key로 매핑하는 유틸을 보강했습니다.

  • 운동 / Exercise / exercise -> exercise
  • 과제 / Assignment / assignment -> assignment
    이제 기본 태그는 현재 locale에 맞게 표시됩니다.

수정 API 연결

수정 API는 generated의 useUpdateTodo를 직접 사용하지 않고, 상세 모달 전용 hook으로 감쌌습니다.
hooks/todo-modal/detail/use-update-todo-submit.ts
성공 시 별도 UI 액션은 하지 않고, 홈 목록과 상세 조회 query만 invalidate합니다.
실패 시에는 새 토스트를 만들지 않고 기존 AnimatedToast를 재사용했습니다.

<AnimatedToast
  isOpen={updateTodoSubmit.isErrorToastOpen}
  onClose={updateTodoSubmit.closeErrorToast}
  message={tToast("todoUpdateFailed")}
/>

수정 요청 body 변환 분리
use-detail-todo-form.ts에 form 상태 관리와 API request body 조립 로직이 함께 들어가면서 파일 책임이 커졌습니다.
그래서 API 요청 body 변환은 유틸로 분리했습니다.

utils/todo/detail-todo-update-request.ts

use-detail-todo-form.ts는 form 상태와 handler 중심으로 유지하고, TodoUpdateRequest 조립은 유틸에서 담당하도록 했습니다.

hooks 구조 정리

hooks/todo-modal 안에 파일이 많아지면서 create/detail/common 역할이 섞였습니다.
그래서 hook은 아래처럼 나눴습니다.

hooks/todo-modal/
  common/
    use-tag-field.tsx

  create/
    use-create-todo-submit.ts
    use-icon-field.ts
    use-repeat-field.ts
    use-subtask-field.ts
    use-time-field.ts
    use-title-field.ts

  detail/
    use-delete-todo-submit.ts
    use-detail-subtask-field.ts
    use-detail-todo-form.ts
    use-update-todo-submit.ts



To Reviewers

이번 PR에서 의도적으로 선택한 부분들이 있습니다.

  1. 수정 성공 시 모달을 닫지 않았습니다.
    성공 후 별도 액션은 필요 없다고 판단했고, query invalidate로 최신 데이터만 반영하도록 했습니다.

  2. 수정 실패 토스트는 새 컴포넌트를 만들지 않았습니다.
    기존 AnimatedToast를 재사용했고, 메시지 key만 todoUpdateFailed로 추가했습니다.

  3. 상세 모달의 완료 여부는 수정 API body에 포함하지 않았습니다.
    TodoUpdateRequest에는 completed 필드가 없고, 완료 상태 변경은 별도 status API 영역이라 이번 PATCH body에서는 제외했습니다.


Screenshot 📷



Test Checklist ✔

  • 상세 모달 오픈 시 todoId, date 기준 상세 조회가 되는지 확인
  • 상세 모달 상단 날짜/요일이 상세 응답 기준으로 표시되는지 확인
  • 태그 드롭다운에 현재 태그 하나가 아니라 전체 태그 목록이 표시되는지 확인
  • ko/en locale에서 기본 태그가 각각 한국어/영어로 표시되는지 확인
  • 플레이 버튼 클릭 시 상세 모달이 열리지 않는지 확인
  • 삭제 버튼 클릭 시 삭제 API가 호출되고 홈 목록이 갱신되는지 확인
  • 저장 버튼 클릭 시 수정 API가 호출되는지 확인
  • 수정 실패 시 기존 AnimatedToast로 실패 토스트가 표시되는지 확인
  • pnpm --filter timo-web lint
  • pnpm --filter timo-web check-types

@yumin-kim2 yumin-kim2 linked an issue Jul 14, 2026 that may be closed by this pull request
5 tasks
@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 3:45pm

Request Review

@yumin-kim2 yumin-kim2 changed the title [FEAT]web/186 todo modal api [FEAT] 투두 API 연결 (상세조회 / 수정 / 삭제) Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 41 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: b4c2757d-45fd-4b0a-bf29-4732e7bf3980

📥 Commits

Reviewing files that changed from the base of the PR and between fff3ab7 and 2e73a47.

📒 Files selected for processing (8)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsx
  • apps/timo-web/containers/todo-modal/create/CreateTodoModalContainer.tsx
  • apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
  • apps/timo-web/hooks/todo-modal/create/use-time-field.ts
  • apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts
  • apps/timo-web/messages/en.json
  • apps/timo-web/messages/ko.json

Walkthrough

TODO 생성 폼과 상세 모달을 API 모델 기반으로 개편하고, 상세 조회·수정·삭제 및 쿼리 갱신 흐름을 추가했습니다. 타이머 상태에 따른 입력 비활성화, 디바운스 수정, 시간·태그·반복·서브태스크 변환과 재생 버튼 이벤트 차단도 반영했습니다.

Changes

생성 폼 흐름

Layer / File(s) Summary
생성 폼 필드와 제출 흐름
apps/timo-web/hooks/todo-modal/create/*, apps/timo-web/components/todo-modal/create/*, apps/timo-web/containers/todo-modal/create/*
입력별 훅, 시간 추천, 서브태스크 조작, 요청 변환, 제출 후 응답 검증과 홈 쿼리 무효화가 추가·갱신되었습니다.

상세 모달 흐름

Layer / File(s) Summary
상세 폼과 업데이트 요청 변환
apps/timo-web/hooks/todo-modal/common/*, apps/timo-web/hooks/todo-modal/detail/*, apps/timo-web/utils/todo/*
상세 응답 기반 폼과 태그·반복·서브태스크 상태가 구성되고, 입력값이 TodoUpdateRequest로 변환됩니다.
상세 모달 편집 인터랙션
apps/timo-web/components/todo-modal/detail/*
수정 가능 상태를 타이머 상태로 제한하고, 변경 사항을 디바운스하여 저장하며 관련 입력을 비활성화합니다.
상세 모달 조회·수정·삭제 오케스트레이션
apps/timo-web/containers/todo-modal/detail/*, apps/timo-web/app/.../home/_containers/*, apps/timo-web/app/.../today/_containers/*, apps/timo-web/messages/*
상세 모달을 todoId와 날짜로 조회하고 수정·삭제 제출, 캐시 갱신, 오류 토스트 및 번역을 연결합니다.
홈 카드 재생 인터랙션
apps/timo-web/app/.../HomeTodoCard.tsx, packages/timo-design-system/src/components/button/play-button/PlayButton.tsx
재생 버튼의 포인터다운·클릭 이벤트가 상위 카드로 전파되지 않도록 버튼 계약과 사용 방식을 변경했습니다.

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

Possibly related issues

Possibly related PRs

Suggested labels: ♻ Refactor, ♦️ 민아

Suggested reviewers: ehye1, jjangminii, kimminna

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 상세조회·수정·삭제와 갱신/에러 처리는 보이지만, 실행 중인 타이머의 삭제 금지 조건은 확인되지 않아 #186을 완전히 충족하지 못했습니다. 삭제 경로에 타이머 진행 중 여부를 가드하는 로직을 추가하고, 해당 케이스를 테스트로 보강하세요.
Out of Scope Changes check ⚠️ Warning 생성 모달 훅 분리, PlayButton 확장, 메시지/레이아웃 조정 등은 #186의 상세조회·수정·삭제 범위를 넘어선 부가 변경입니다. 상세 모달 API 연동에 직접 필요한 변경만 남기고, 나머지 리팩터링은 별도 PR로 분리하세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 상세조회·수정·삭제 API 연결이라는 핵심 변경을 짧고 명확하게 담고 있습니다.
Description check ✅ Passed 설명이 PR의 상세조회·수정·삭제 연결, 구조 정리, 에러 처리와 일치합니다.
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/186-todo-modal-api

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 added ⏰ Timo-web Timo 웹 서비스 ⌚ Timo-Design-system Timo 디자인 시스템 labels Jul 14, 2026
@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.54 kB 🔴 415.41 kB
/[locale]/today 192.89 kB 🔴 398.75 kB
/[locale]/focus 159.06 kB 🔴 364.92 kB
/[locale]/settings 166.22 kB 🔴 372.08 kB
/[locale]/statistics 232.86 kB 🔴 438.73 kB
/[locale]/[...rest] 0 B 🟡 205.86 kB
/[locale]/login 212.92 kB 🔴 418.78 kB
/[locale]/oauth/callback 119.57 kB 🟡 325.43 kB
/[locale]/onboarding 232.83 kB 🔴 438.69 kB
/[locale] 118.95 kB 🟡 324.81 kB
/[locale]/policy 125.05 kB 🟡 330.92 kB

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

Lighthouse — timo-web
URL Perf A11y LCP CLS TBT
/en/home 🔴 58 🟢 95 🔴 15.5s 🟢 0.000 🔴 636ms
/en/today 🔴 58 🟢 95 🔴 15.5s 🟢 0.000 🔴 636ms
/en/focus 🔴 59 🟢 95 🔴 15.2s 🟢 0.000 🔴 612ms
/en/statistics 🔴 60 🟢 95 🔴 15.2s 🟢 0.000 🟡 558ms

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 변환 권장

측정 커밋: 176d166

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Storybook Preview

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

마지막 업데이트: 2026-07-14 15:44 UTC

@yumin-kim2 yumin-kim2 self-assigned this Jul 14, 2026
@yumin-kim2 yumin-kim2 added the ✨ Feature 새로운 기능(기능성) 구현 label Jul 14, 2026
Comment thread apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts Outdated
Comment thread apps/timo-web/utils/todo/tag-label.ts
Comment thread apps/timo-web/queries/todo/use-detail-todo.ts Outdated
Comment thread apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx Outdated
Comment thread apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx Outdated
Comment thread apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx Outdated
@yumin-kim2 yumin-kim2 added the ♣️ 유민 유민양 label Jul 14, 2026
…modal-api

# Conflicts:
#	apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
#	apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx
#	apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx
#	apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
#	apps/timo-web/utils/todo/tag-label.ts
@kimminna
kimminna merged commit acf2a2e into develop Jul 14, 2026
13 of 14 checks passed
@kimminna
kimminna deleted the feat/web/186-todo-modal-api branch July 14, 2026 15:49

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

🤖 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/components/todo-modal/detail/DetailTodoModalContent.tsx`:
- Around line 65-67: Update the dayOfWeek and memo-icon state in
DetailTodoModalContent to use the live values from detailTodoForm rather than
the todo prop, matching the existing dateNumber/dateLabel form-state behavior.
Preserve the current weekday fallback and ensure both the weekday label and
hasMemo reflect edits immediately while the form is being modified.
- Around line 89-113: Update the debounce effect around `textUpdateSignature`
and `TEXT_UPDATE_DEBOUNCE_MS` so closing the modal flushes any pending title,
memo, or subtask update before cleanup clears the timer. Preserve the existing
debounce behavior while open, prevent duplicate sends, and ensure the latest
`latestBuildUpdateRequestRef` is passed through `latestOnUpdateRef` when
`isOpen` changes to false.

In `@apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx`:
- Around line 78-88: Update the DetailTodoModalContainer error flow by adding a
useEffect that watches isError and resets both isOpen and isMounted to false
when an API error occurs, allowing the modal to close and reopen normally.
Import useEffect and keep any existing rendering behavior unchanged.
- Around line 52-59: 삭제 API 요청 실패 시 사용자에게 에러 토스트가 표시되도록 deleteTodo의 handleDelete
호출 옵션에 onError 핸들러를 추가하세요. 기존 onSuccess 동작은 유지하고, 수정 기능에서 사용하는 동일한 에러 토스트 방식과
메시지를 재사용하세요.

In `@apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx`:
- Around line 16-40: useTagField에 TName extends Path<TFieldValues> 제네릭을 추가하고,
UseTagFieldParams의 name과 useController 호출이 해당 TName을 사용하도록 연결하세요. 기존 tagId 기본값과
나머지 반환 계약은 유지하면서 호출 시 필드명이 구체적인 이름 타입으로 추론되도록 수정하세요.

In `@apps/timo-web/hooks/todo-modal/create/use-create-todo-submit.ts`:
- Line 26: Update the memo payload mapping in the todo creation submit flow to
return the trimmed memo value when it is non-empty, rather than the original
data.memo string. Preserve the existing undefined behavior for blank or absent
memos.

In `@apps/timo-web/hooks/todo-modal/create/use-subtask-field.ts`:
- Around line 57-102: Remove the syncFormValue side effect from the
setSubtaskInputs updater in handleInputChange, and use the closure’s
subtaskInputs to calculate the next entries before sequentially updating state
and synchronizing the form. Apply the same pattern in handleInputKeyDown for
Backspace deletion: compute the entries first, then setSubtaskInputs, call
syncFormValue with the updated entries, and update pendingFocusIndex without
performing side effects inside an updater.

In `@apps/timo-web/hooks/todo-modal/create/use-time-field.ts`:
- Around line 142-146: Update resetTime in the time-field hook to also reset the
form duration through the existing field.onChange handler, keeping the submitted
duration synchronized with the reset timeDisplay and selected-time state.
- Around line 82-84: Update handleTimeSelectorOpen so title is safely normalized
before calling trim, allowing undefined values from useWatch without throwing at
runtime; preserve the existing early return for an empty trimmed title or when
isRecommendingDuration is true.
- Around line 134-140: Update handleDurationInputChange to parse the user’s h:mm
input into the API’s mm:ss format before calling field.onChange, and use that
converted value for the clock display. Remove the
setRecommendedDuration(formatted) update so manual input cannot overwrite the
stored AI recommendation; preserve clearing selectedTime.

In `@apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts`:
- Line 143: Update useDetailTodoForm to expose tagField.handleAddTagClick and
the tag-limit/tag-creation-failure toast state required by
DetailTodoModalContent, then replace its empty onAddTagClick handler with the
returned callback so the detail modal can create tags like the creation modal.
- Line 88: Update the selectTime preset value formatting to use
convertDurationToTimeText instead of convertSecondsToApiDuration, matching the
HH:MM format expected by convertTimeTextToDurationSeconds and the durationText
default. Preserve the existing React Hook Form value-update flow so selected
presets are stored and displayed consistently.

In `@apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts`:
- Around line 35-41: Update the onSuccess handlers in
apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts:35-41 and
apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts:24-27 to also
invalidate the query identified by getGetTodayQueryKey(), alongside the existing
Home and detail invalidations, so TodayTodoListContainer refreshes after updates
and deletions.
🪄 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: 25bae868-9f9b-493f-9003-f58a6b867f2e

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec7ed7 and fff3ab7.

📒 Files selected for processing (29)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
  • apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx
  • apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoMemoField.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoModalContainer.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx
  • apps/timo-web/containers/todo-modal/create/CreateTodoModalContainer.tsx
  • apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
  • apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx
  • apps/timo-web/hooks/todo-modal/create/use-create-todo-submit.ts
  • apps/timo-web/hooks/todo-modal/create/use-icon-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-repeat-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-subtask-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-time-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-title-field.ts
  • apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-subtask-field.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
  • apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts
  • apps/timo-web/messages/en.json
  • apps/timo-web/messages/ko.json
  • apps/timo-web/utils/todo/detail-todo-update-request.ts
  • apps/timo-web/utils/todo/todo-time.ts
  • packages/timo-design-system/src/components/button/play-button/PlayButton.tsx
💤 Files with no reviewable changes (1)
  • apps/timo-web/components/todo-modal/detail/DetailTodoModalContainer.tsx

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 13

🤖 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/components/todo-modal/detail/DetailTodoModalContent.tsx`:
- Around line 65-67: Update the dayOfWeek and memo-icon state in
DetailTodoModalContent to use the live values from detailTodoForm rather than
the todo prop, matching the existing dateNumber/dateLabel form-state behavior.
Preserve the current weekday fallback and ensure both the weekday label and
hasMemo reflect edits immediately while the form is being modified.
- Around line 89-113: Update the debounce effect around `textUpdateSignature`
and `TEXT_UPDATE_DEBOUNCE_MS` so closing the modal flushes any pending title,
memo, or subtask update before cleanup clears the timer. Preserve the existing
debounce behavior while open, prevent duplicate sends, and ensure the latest
`latestBuildUpdateRequestRef` is passed through `latestOnUpdateRef` when
`isOpen` changes to false.

In `@apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx`:
- Around line 78-88: Update the DetailTodoModalContainer error flow by adding a
useEffect that watches isError and resets both isOpen and isMounted to false
when an API error occurs, allowing the modal to close and reopen normally.
Import useEffect and keep any existing rendering behavior unchanged.
- Around line 52-59: 삭제 API 요청 실패 시 사용자에게 에러 토스트가 표시되도록 deleteTodo의 handleDelete
호출 옵션에 onError 핸들러를 추가하세요. 기존 onSuccess 동작은 유지하고, 수정 기능에서 사용하는 동일한 에러 토스트 방식과
메시지를 재사용하세요.

In `@apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx`:
- Around line 16-40: useTagField에 TName extends Path<TFieldValues> 제네릭을 추가하고,
UseTagFieldParams의 name과 useController 호출이 해당 TName을 사용하도록 연결하세요. 기존 tagId 기본값과
나머지 반환 계약은 유지하면서 호출 시 필드명이 구체적인 이름 타입으로 추론되도록 수정하세요.

In `@apps/timo-web/hooks/todo-modal/create/use-create-todo-submit.ts`:
- Line 26: Update the memo payload mapping in the todo creation submit flow to
return the trimmed memo value when it is non-empty, rather than the original
data.memo string. Preserve the existing undefined behavior for blank or absent
memos.

In `@apps/timo-web/hooks/todo-modal/create/use-subtask-field.ts`:
- Around line 57-102: Remove the syncFormValue side effect from the
setSubtaskInputs updater in handleInputChange, and use the closure’s
subtaskInputs to calculate the next entries before sequentially updating state
and synchronizing the form. Apply the same pattern in handleInputKeyDown for
Backspace deletion: compute the entries first, then setSubtaskInputs, call
syncFormValue with the updated entries, and update pendingFocusIndex without
performing side effects inside an updater.

In `@apps/timo-web/hooks/todo-modal/create/use-time-field.ts`:
- Around line 142-146: Update resetTime in the time-field hook to also reset the
form duration through the existing field.onChange handler, keeping the submitted
duration synchronized with the reset timeDisplay and selected-time state.
- Around line 82-84: Update handleTimeSelectorOpen so title is safely normalized
before calling trim, allowing undefined values from useWatch without throwing at
runtime; preserve the existing early return for an empty trimmed title or when
isRecommendingDuration is true.
- Around line 134-140: Update handleDurationInputChange to parse the user’s h:mm
input into the API’s mm:ss format before calling field.onChange, and use that
converted value for the clock display. Remove the
setRecommendedDuration(formatted) update so manual input cannot overwrite the
stored AI recommendation; preserve clearing selectedTime.

In `@apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts`:
- Line 143: Update useDetailTodoForm to expose tagField.handleAddTagClick and
the tag-limit/tag-creation-failure toast state required by
DetailTodoModalContent, then replace its empty onAddTagClick handler with the
returned callback so the detail modal can create tags like the creation modal.
- Line 88: Update the selectTime preset value formatting to use
convertDurationToTimeText instead of convertSecondsToApiDuration, matching the
HH:MM format expected by convertTimeTextToDurationSeconds and the durationText
default. Preserve the existing React Hook Form value-update flow so selected
presets are stored and displayed consistently.

In `@apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts`:
- Around line 35-41: Update the onSuccess handlers in
apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts:35-41 and
apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts:24-27 to also
invalidate the query identified by getGetTodayQueryKey(), alongside the existing
Home and detail invalidations, so TodayTodoListContainer refreshes after updates
and deletions.
🪄 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: 25bae868-9f9b-493f-9003-f58a6b867f2e

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec7ed7 and fff3ab7.

📒 Files selected for processing (29)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
  • apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx
  • apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoMemoField.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoModalContainer.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx
  • apps/timo-web/containers/todo-modal/create/CreateTodoModalContainer.tsx
  • apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
  • apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx
  • apps/timo-web/hooks/todo-modal/create/use-create-todo-submit.ts
  • apps/timo-web/hooks/todo-modal/create/use-icon-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-repeat-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-subtask-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-time-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-title-field.ts
  • apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-subtask-field.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
  • apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts
  • apps/timo-web/messages/en.json
  • apps/timo-web/messages/ko.json
  • apps/timo-web/utils/todo/detail-todo-update-request.ts
  • apps/timo-web/utils/todo/todo-time.ts
  • packages/timo-design-system/src/components/button/play-button/PlayButton.tsx
💤 Files with no reviewable changes (1)
  • apps/timo-web/components/todo-modal/detail/DetailTodoModalContainer.tsx
🛑 Comments failed to post (13)
apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx (2)

65-67: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,260p' .agents/skills/quality/timo-review/SKILL.md

Repository: Team-Timo/Timo-client

Length of output: 1838


🏁 Script executed:

sed -n '1,260p' apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx

Repository: Team-Timo/Timo-client

Length of output: 8990


🏁 Script executed:

sed -n '260,360p' apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx

Repository: Team-Timo/Timo-client

Length of output: 2642


todo 대신 폼 상태를 사용해 화면을 맞춰주세요.
dateNumber/dateLabeldetailTodoForm.date를 쓰는데, 요일 라벨은 todo.dayOfWeek, 메모 아이콘은 todo.memo를 봐서 수정 중에 값이 잠깐 어긋납니다. dayOfWeekhasMemodetailTodoForm의 live 값으로 맞추면 됩니다. React controlled components 문서도 같이 보면 좋아요: https://react.dev/reference/react-dom/components/input

🤖 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 65 - 67, Update the dayOfWeek and memo-icon state in
DetailTodoModalContent to use the live values from detailTodoForm rather than
the todo prop, matching the existing dateNumber/dateLabel form-state behavior.
Preserve the current weekday fallback and ensure both the weekday label and
hasMemo reflect edits immediately while the form is being modified.

89-113: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

모달을 저장 debounce(3초) 안에 닫으면 마지막 수정 내용이 그대로 소실돼요.

isOpenfalse로 바뀌면 대기 중인 setTimeout이 cleanup에서 clear되고, 재실행된 effect는 !isOpen으로 바로 return 하기 때문에 그 시점까지 저장 대기 중이던 title/memo/subtask 변경이 서버로 전송되지 않아요. TEXT_UPDATE_DEBOUNCE_MS가 3초라 "입력 후 바로 닫기"는 충분히 발생할 수 있는 흐름이에요.

닫히는 순간 대기 중인 업데이트를 flush하는 처리를 추가하면 좋겠어요.

🛡️ 제안 diff
+  const pendingFlushRef = useRef<(() => void) | null>(null);
+
   useEffect(() => {
     if (!isOpen) return;
     if (!canUpdateTodo) return;

     if (!didStartTextUpdateRef.current) {
       didStartTextUpdateRef.current = true;
       return;
     }

     if (!detailTodoForm.title.trim()) return;

+    const flush = () =>
+      latestOnUpdateRef.current(latestBuildUpdateRequestRef.current());
+    pendingFlushRef.current = flush;
+
     const updateTimer = window.setTimeout(() => {
-      latestOnUpdateRef.current(latestBuildUpdateRequestRef.current());
+      flush();
+      pendingFlushRef.current = null;
     }, TEXT_UPDATE_DEBOUNCE_MS);

-    return () => window.clearTimeout(updateTimer);
+    return () => window.clearTimeout(updateTimer);
   }, [canUpdateTodo, detailTodoForm.title, isOpen, textUpdateSignature]);
+
+  useEffect(() => {
+    if (isOpen) return;
+    if (pendingFlushRef.current) {
+      pendingFlushRef.current();
+      pendingFlushRef.current = null;
+    }
+  }, [isOpen]);
📝 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 pendingFlushRef = useRef<(() => void) | null>(null);

  useEffect(() => {
    latestOnUpdateRef.current = onUpdate;
  }, [onUpdate]);

  useEffect(() => {
    latestBuildUpdateRequestRef.current = detailTodoForm.buildUpdateRequest;
  }, [detailTodoForm.buildUpdateRequest]);

  useEffect(() => {
    if (!isOpen) return;
    if (!canUpdateTodo) return;

    if (!didStartTextUpdateRef.current) {
      didStartTextUpdateRef.current = true;
      return;
    }

    if (!detailTodoForm.title.trim()) return;

    const flush = () =>
      latestOnUpdateRef.current(latestBuildUpdateRequestRef.current());
    pendingFlushRef.current = flush;

    const updateTimer = window.setTimeout(() => {
      flush();
      pendingFlushRef.current = null;
    }, TEXT_UPDATE_DEBOUNCE_MS);

    return () => window.clearTimeout(updateTimer);
  }, [canUpdateTodo, detailTodoForm.title, isOpen, textUpdateSignature]);

  useEffect(() => {
    if (isOpen) return;
    if (pendingFlushRef.current) {
      pendingFlushRef.current();
      pendingFlushRef.current = null;
    }
  }, [isOpen]);
🤖 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 89 - 113, Update the debounce effect around `textUpdateSignature` and
`TEXT_UPDATE_DEBOUNCE_MS` so closing the modal flushes any pending title, memo,
or subtask update before cleanup clears the timer. Preserve the existing
debounce behavior while open, prevent duplicate sends, and ensure the latest
`latestBuildUpdateRequestRef` is passed through `latestOnUpdateRef` when
`isOpen` changes to false.
apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx (2)

52-59: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

삭제 API 실패 시 에러 처리(Toast) 누락

수정 기능에 에러 토스트를 잘 적용해 주셨네요! 👍 삭제 API (deleteTodo) 실패 시에도 사용자에게 안정적으로 피드백을 줄 수 있도록 onError 핸들러를 추가해 주시면 완벽할 것 같습니다. 삭제 실패 토스트도 꼭 챙겨주세요! 🚀

As per PR objectives, API 요청 실패 시 에러를 처리해야 합니다.

🤖 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/containers/todo-modal/detail/DetailTodoModalContainer.tsx`
around lines 52 - 59, 삭제 API 요청 실패 시 사용자에게 에러 토스트가 표시되도록 deleteTodo의
handleDelete 호출 옵션에 onError 핸들러를 추가하세요. 기존 onSuccess 동작은 유지하고, 수정 기능에서 사용하는 동일한
에러 토스트 방식과 메시지를 재사용하세요.

78-88: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

API 에러 시 모달 상태 고착(Stuck) 방지

API 통신 중 에러가 발생할 때를 대비한 꼼꼼한 처리를 추가해 보면 어떨까요?
현재 isErrortrue가 되면 컴포넌트가 렌더링되지 않아 모달 UI가 사라지게 되지만, isOpenisMounted 상태는 true로 그대로 갇히게 된답니다. 이렇게 되면 모달을 닫을 수도 없고 다음 카드 클릭 시 다시 열 수도 없게 돼요! 😅

useEffect를 사용해 에러 발생 시 상태를 초기화(그리고 필요 시 에러 알림 제공)해 주시면 훨씬 안정적인 모달이 될 거예요. React의 useEffect 공식 문서를 활용해 보세요!

💡 제안하는 상태 초기화 코드

상단에 useEffect를 import한 뒤 컨테이너 내부에 아래 로직을 추가해 보세요:

  useEffect(() => {
    if (isError) {
      setIsOpen(false);
      setIsMounted(false);
      // 필요 시 조회 실패에 대한 에러 토스트 상태 업데이트도 이곳에 추가할 수 있습니다.
    }
  }, [isError]);
🤖 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/containers/todo-modal/detail/DetailTodoModalContainer.tsx`
around lines 78 - 88, Update the DetailTodoModalContainer error flow by adding a
useEffect that watches isError and resets both isOpen and isMounted to false
when an API error occurs, allowing the modal to close and reopen normally.
Import useEffect and keep any existing rendering behavior unchanged.
apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx (1)

16-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

name을 특정 필드로 좁히는 TName 제네릭이 없어 타입 안전성이 약해요.

useController({ name, control })에서 TName을 별도 제네릭으로 받지 않고 Path<TFieldValues> 전체 유니온을 그대로 쓰고 있어요. 그러다 보니 field.value/field.onChange가 실제로는 어떤 필드를 가리키는지와 무관하게 넓은 유니온 타입으로 추론되고, 호출자가 number 타입이 아닌 다른 필드명을 name으로 넘겨도 컴파일러가 잡아주지 못해요. 지금은 tagId(number|null) 필드만 사용해서 문제가 드러나지 않지만, 재사용 훅이라 향후 다른 폼에서 실수로 잘못된 name을 넘기면 조용히 깨질 수 있어요.

두 번째 제네릭 TName extends Path<TFieldValues>를 추가해서 name을 그 타입으로 받으면 안전해집니다. (참고: react-hook-form useController 문서, 관련 라이브러리 이슈 react-hook-form#5055)

♻️ 제안 diff
-export interface UseTagFieldParams<TFieldValues extends FieldValues> {
+export interface UseTagFieldParams<
+  TFieldValues extends FieldValues,
+  TName extends Path<TFieldValues> = Path<TFieldValues>,
+> {
   control: Control<TFieldValues>;
-  name?: Path<TFieldValues>;
+  name?: TName;
 }

-export const useTagField = <TFieldValues extends FieldValues>({
+export const useTagField = <
+  TFieldValues extends FieldValues,
+  TName extends Path<TFieldValues> = Path<TFieldValues>,
+>({
   control,
-  name = "tagId" as Path<TFieldValues>,
-}: UseTagFieldParams<TFieldValues>): UseTagFieldResult => {
+  name = "tagId" as TName,
+}: UseTagFieldParams<TFieldValues, TName>): UseTagFieldResult => {
   const tCommon = useTranslations("Common");
-  const { field } = useController({ name, control });
+  const { field } = useController<TFieldValues, TName>({ name, control });
📝 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 interface UseTagFieldParams<
  TFieldValues extends FieldValues,
  TName extends Path<TFieldValues> = Path<TFieldValues>,
> {
  control: Control<TFieldValues>;
  name?: TName;
}

export interface UseTagFieldResult {
  tagOptions: Array<{ id: number; label: string }>;
  tagLabels: string[];
  selectedTagOption?: { id: number; label: string };
  selectedTagId: number | null;
  selectedTagLabel?: string;
  isTagLimitToastOpen: boolean;
  closeTagLimitToast: () => void;
  isCreateTagErrorToastOpen: boolean;
  closeCreateTagErrorToast: () => void;
  handleSelectTag: (label: string) => void;
  handleAddTagClick: () => void;
}

export const useTagField = <
  TFieldValues extends FieldValues,
  TName extends Path<TFieldValues> = Path<TFieldValues>,
>({
  control,
  name = "tagId" as TName,
}: UseTagFieldParams<TFieldValues, TName>): UseTagFieldResult => {
  const tCommon = useTranslations("Common");
  const { field } = useController<TFieldValues, TName>({ name, control });
🤖 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/hooks/todo-modal/common/use-tag-field.tsx` around lines 16 -
40, useTagField에 TName extends Path<TFieldValues> 제네릭을 추가하고, UseTagFieldParams의
name과 useController 호출이 해당 TName을 사용하도록 연결하세요. 기존 tagId 기본값과 나머지 반환 계약은 유지하면서 호출
시 필드명이 구체적인 이름 타입으로 추론되도록 수정하세요.
apps/timo-web/hooks/todo-modal/create/use-create-todo-submit.ts (1)

26-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

메모의 공백 제거(trim) 결과 반환

현재 코드에서는 메모가 유효할 경우 원본 문자열(data.memo)을 그대로 반환하여 앞뒤 공백이 포함될 수 있습니다. 검사뿐만 아니라 실제 페이로드에도 trim()이 적용된 값을 전달하는 것이 더 안전합니다.

💡 제안하는 수정안
-  memo: data.memo?.trim() ? data.memo : undefined,
+  memo: data.memo?.trim() || 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.

  memo: data.memo?.trim() || 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/hooks/todo-modal/create/use-create-todo-submit.ts` at line 26,
Update the memo payload mapping in the todo creation submit flow to return the
trimmed memo value when it is non-empty, rather than the original data.memo
string. Preserve the existing undefined behavior for blank or absent memos.
apps/timo-web/hooks/todo-modal/create/use-subtask-field.ts (1)

57-102: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

상태 업데이트 함수 내의 사이드 이펙트 제거 및 폼 상태 동기화 보완 🐛

React의 상태 업데이트 함수(updater function) 내에서 syncFormValue(내부의 field.onChange)를 호출하거나 ref를 조작하는 것은 예기치 않은 동작을 유발할 수 있습니다. (특히 Strict Mode에서는 업데이트 함수가 두 번 호출되므로 사이드 이펙트가 중복 실행될 수 있습니다.)
또한, Backspace로 서브태스크를 삭제할 때 syncFormValue가 호출되지 않아 삭제된 서브태스크가 폼 상태에 그대로 남아있는 문제가 있습니다.

클로저에 있는 subtaskInputs를 활용하여 다음 상태를 먼저 계산한 후, 상태 업데이트와 폼 동기화 및 ref 조작을 순차적으로 수행하도록 개선해 보세요.

참고: React 공식 문서 - 상태 업데이트 함수는 순수해야 합니다

🛠️ 개선 제안
-  const handleInputChange = (index: number, value: string) => {
-    setSubtaskInputs((prev) => {
-      const next = prev.map((entry, i) =>
-        i === index ? { ...entry, value } : entry,
-      );
-      syncFormValue(next);
-      return next;
-    });
-  };
+  const handleInputChange = (index: number, value: string) => {
+    const next = subtaskInputs.map((entry, i) =>
+      i === index ? { ...entry, value } : entry,
+    );
+    setSubtaskInputs(next);
+    syncFormValue(next);
+  };
 
   const handleInputKeyDown = (
     index: number,
     event: KeyboardEvent<HTMLTextAreaElement>,
   ) => {
     if (event.key === "Enter" && !event.shiftKey) {
       if (event.nativeEvent.isComposing) return;
 
       event.preventDefault();
 
-      setSubtaskInputs((prev) => {
-        const { entries, focusIndex } = addSubtaskInputOnEnter(
-          prev,
-          index,
-          createEntry,
-          MAX_SUBTASK_COUNT,
-        );
-
-        if (focusIndex !== null) pendingFocusIndex.current = focusIndex;
-        return entries;
-      });
+      const { entries, focusIndex } = addSubtaskInputOnEnter(
+        subtaskInputs,
+        index,
+        createEntry,
+        MAX_SUBTASK_COUNT,
+      );
+
+      if (focusIndex !== null) {
+        pendingFocusIndex.current = focusIndex;
+        setSubtaskInputs(entries);
+        syncFormValue(entries);
+      }
       return;
     }
 
     if (event.key === "Backspace") {
       const { entries, focusIndex } = removeSubtaskInputOnBackspace(
         subtaskInputs,
         index,
       );
 
       if (focusIndex !== null) {
         event.preventDefault();
         pendingFocusIndex.current = focusIndex;
         setSubtaskInputs(entries);
+        syncFormValue(entries);
       }
     }
   };
📝 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 handleInputChange = (index: number, value: string) => {
    const next = subtaskInputs.map((entry, i) =>
      i === index ? { ...entry, value } : entry,
    );
    setSubtaskInputs(next);
    syncFormValue(next);
  };

  const handleInputKeyDown = (
    index: number,
    event: KeyboardEvent<HTMLTextAreaElement>,
  ) => {
    if (event.key === "Enter" && !event.shiftKey) {
      if (event.nativeEvent.isComposing) return;

      event.preventDefault();

      const { entries, focusIndex } = addSubtaskInputOnEnter(
        subtaskInputs,
        index,
        createEntry,
        MAX_SUBTASK_COUNT,
      );

      if (focusIndex !== null) {
        pendingFocusIndex.current = focusIndex;
        setSubtaskInputs(entries);
        syncFormValue(entries);
      }
      return;
    }

    if (event.key === "Backspace") {
      const { entries, focusIndex } = removeSubtaskInputOnBackspace(
        subtaskInputs,
        index,
      );

      if (focusIndex !== null) {
        event.preventDefault();
        pendingFocusIndex.current = focusIndex;
        setSubtaskInputs(entries);
        syncFormValue(entries);
      }
    }
  };
🤖 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/hooks/todo-modal/create/use-subtask-field.ts` around lines 57 -
102, Remove the syncFormValue side effect from the setSubtaskInputs updater in
handleInputChange, and use the closure’s subtaskInputs to calculate the next
entries before sequentially updating state and synchronizing the form. Apply the
same pattern in handleInputKeyDown for Backspace deletion: compute the entries
first, then setSubtaskInputs, call syncFormValue with the updated entries, and
update pendingFocusIndex without performing side effects inside an updater.
apps/timo-web/hooks/todo-modal/create/use-time-field.ts (3)

82-84: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

titleundefined일 경우 발생할 수 있는 런타임 에러 방지

useWatch로 가져온 title은 초기 렌더링 시점이나 폼 상태에 따라 undefined가 될 수 있습니다. title.trim()을 호출하기 전에 안전하게 처리해야 합니다.

💡 제안하는 수정안
   const handleTimeSelectorOpen = () => {
-    const trimmedTitle = title.trim();
+    const trimmedTitle = (title || "").trim();
     if (!trimmedTitle || isRecommendingDuration) return;
📝 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 handleTimeSelectorOpen = () => {
    const trimmedTitle = (title || "").trim();
    if (!trimmedTitle || isRecommendingDuration) return;
🤖 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/hooks/todo-modal/create/use-time-field.ts` around lines 82 -
84, Update handleTimeSelectorOpen so title is safely normalized before calling
trim, allowing undefined values from useWatch without throwing at runtime;
preserve the existing early return for an empty trimmed title or when
isRecommendingDuration is true.

134-140: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

사용자 수동 입력 처리 오류 및 AI 추천 상태 오염

handleDurationInputChange 함수에 심각한 논리적 오류가 있습니다:

  1. API 포맷 불일치: 사용자의 h:mm 입력을 API 포맷(mm:ss)으로 변환하지 않고 폼에 그대로 저장하여, 1시간 30분(1:30) 입력 시 1분 30초로 서버에 전송됩니다.
  2. 화면 표시 오류: h:mm 입력을 formatDurationAsClockLabel에 전달하면, 해당 함수는 이를 mm:ss로 간주하고 변환하여 화면에 즉시 잘못된 시간(0:01)을 표시합니다.
  3. AI 상태 오염: 수동 입력을 recommendedDuration에 저장하면, 이후 사용자가 "AI 추천" 버튼을 눌렀을 때 원래의 AI 추천값 대신 방금 수동으로 입력한 값이 복원되어 AI 기능이 망가집니다.

이를 해결하기 위해 수동 입력값을 API가 기대하는 포맷으로 변환하고, recommendedDuration 업데이트 로직을 제거해야 합니다.

💡 제안하는 수정안
   const handleDurationInputChange = (value: string) => {
     setSelectedTime(undefined);
-    const formatted = formatDurationInput(value);
-    setRecommendedDuration(formatted);
-    setTimeDisplay(formatDurationAsClockLabel(formatted));
-    field.onChange(formatted);
+    const sanitized = formatDurationInput(value);
+    setTimeDisplay(sanitized);
+
+    const [hoursText, minutesText] = sanitized.split(":");
+    const hours = Number(hoursText) || 0;
+    const minutes = Number(minutesText) || 0;
+    const totalSeconds = hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE;
+
+    field.onChange(convertSecondsToApiDuration(totalSeconds));
   };
📝 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 handleDurationInputChange = (value: string) => {
    setSelectedTime(undefined);
    const sanitized = formatDurationInput(value);
    setTimeDisplay(sanitized);

    const [hoursText, minutesText] = sanitized.split(":");
    const hours = Number(hoursText) || 0;
    const minutes = Number(minutesText) || 0;
    const totalSeconds = hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE;

    field.onChange(convertSecondsToApiDuration(totalSeconds));
  };
🤖 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/hooks/todo-modal/create/use-time-field.ts` around lines 134 -
140, Update handleDurationInputChange to parse the user’s h:mm input into the
API’s mm:ss format before calling field.onChange, and use that converted value
for the clock display. Remove the setRecommendedDuration(formatted) update so
manual input cannot overwrite the stored AI recommendation; preserve clearing
selectedTime.

142-146: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

resetTime 폼 상태 동기화 누락

resetTime 호출 시 내부 상태(timeDisplay 등)는 "00:00"으로 초기화되지만, 폼의 duration 값(field.onChange)은 업데이트되지 않아 화면과 실제 제출되는 폼 데이터 간에 불일치가 발생할 수 있습니다.

💡 제안하는 수정안
   const resetTime = () => {
     setSelectedTime(undefined);
     setTimeDisplay("00:00");
     setRecommendedDuration(undefined);
+    field.onChange("00:00");
   };
📝 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 resetTime = () => {
    setSelectedTime(undefined);
    setTimeDisplay("00:00");
    setRecommendedDuration(undefined);
    field.onChange("00:00");
  };
🤖 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/hooks/todo-modal/create/use-time-field.ts` around lines 142 -
146, Update resetTime in the time-field hook to also reset the form duration
through the existing field.onChange handler, keeping the submitted duration
synchronized with the reset timeDisplay and selected-time state.
apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts (2)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "convertTimeTextToDurationSeconds|convertSecondsToApiDuration|convertDurationToTimeText" -A 10 apps/timo-web/utils/todo/todo-time.ts

Repository: Team-Timo/Timo-client

Length of output: 1533


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' .agents/skills/quality/timo-review/SKILL.md

Repository: Team-Timo/Timo-client

Length of output: 1838


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant hook and the downstream modal wiring.
sed -n '1,320p' apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
printf '\n----\n'
sed -n '240,320p' apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx

Repository: Team-Timo/Timo-client

Length of output: 11556


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- apps/timo-web/utils/todo/todo-time.ts ---'
sed -n '1,120p' apps/timo-web/utils/todo/todo-time.ts
printf '\n%s\n' '--- apps/timo-web/hooks/todo-modal/common/use-tag-field.ts ---'
sed -n '1,260p' apps/timo-web/hooks/todo-modal/common/use-tag-field.ts
printf '\n%s\n' '--- apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx (tag area) ---'
rg -n "onAddTagClick|handleAddTagClick|tagLabels|selectedTagLabel" -A 4 -B 4 apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx

Repository: Team-Timo/Timo-client

Length of output: 2220


apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts:155-168selectTime 포맷을 기본값과 맞춰주세요.
convertDurationToTimeTextconvertTimeTextToDurationSecondsHH:MM 기준인데, selectTimeconvertSecondsToApiDuration으로 MM:SS를 넣고 있어요. 그래서 15/30/45분 프리셋을 고르면 저장 시 15:00이 15시간으로 해석될 수 있습니다. selectTimeconvertDurationToTimeText로 통일하면 됩니다. React Hook Form의 defaultValues/값 갱신 패턴도 같이 보면 좋아요: https://react-hook-form.com/docs/useform

🤖 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/hooks/todo-modal/detail/use-detail-todo-form.ts` at line 88,
Update the selectTime preset value formatting to use convertDurationToTimeText
instead of convertSecondsToApiDuration, matching the HH:MM format expected by
convertTimeTextToDurationSeconds and the durationText default. Preserve the
existing React Hook Form value-update flow so selected presets are stored and
displayed consistently.

143-143: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

상세 모달에서 "태그 추가" 버튼이 동작하지 않는 이유가 여기 있어요.

useTagField가 반환하는 handleAddTagClick(및 태그 한도/생성 실패 토스트 상태)이 useDetailTodoForm의 반환 객체에 포함되지 않아요. 그 결과 DetailTodoModalContent.tsxonAddTagClick={() => {}}가 빈 함수로 남아있어, 상세 모달에서는 새 태그를 만들 수 없는 상태예요. 생성 모달에서는 useTagField를 직접 써서 이 기능이 살아있는 것과 대비돼요.

tagField.handleAddTagClick(및 필요하다면 토스트 상태들)을 반환 객체에 추가해서 연결해주면 좋겠어요.

🔧 제안 diff
   return {
     ...
     tagLabels: tagField.tagLabels,
     selectedTagLabel: tagField.selectedTagLabel,
     handleSelectTag,
+    handleAddTagClick: tagField.handleAddTagClick,
+    isTagLimitToastOpen: tagField.isTagLimitToastOpen,
+    closeTagLimitToast: tagField.closeTagLimitToast,
+    isCreateTagErrorToastOpen: tagField.isCreateTagErrorToastOpen,
+    closeCreateTagErrorToast: tagField.closeCreateTagErrorToast,
     ...

의도된 스코프 축소(추후 PR에서 처리)인지 궁금해요 — 원하시면 DetailTodoModalContent.tsx 쪽 연결까지 같이 초안 잡아드릴 수 있어요.

Also applies to: 234-236

🤖 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/hooks/todo-modal/detail/use-detail-todo-form.ts` at line 143,
Update useDetailTodoForm to expose tagField.handleAddTagClick and the
tag-limit/tag-creation-failure toast state required by DetailTodoModalContent,
then replace its empty onAddTagClick handler with the returned callback so the
detail modal can create tags like the creation modal.
apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts (1)

35-41: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# useToday 훅이 어떤 쿼리 키를 사용하는지 검색하여 무효화 대상인지 점검합니다.
rg -A 5 "export const useToday\s*="

Repository: Team-Timo/Timo-client

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== skill =="
cat .agents/skills/quality/timo-review/SKILL.md

echo
echo "== relevant hooks =="
git ls-files 'apps/timo-web/hooks/todo-modal/detail/*' 'apps/timo-web/**/*today*' 'apps/timo-web/**/*todo*' | sed -n '1,200p'

Repository: Team-Timo/Timo-client

Length of output: 7011


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search query hooks =="
rg -n "useToday|getGetHomeQueryKey|getGetTodoDetailQueryKey|invalidateQueries\\(" apps/timo-web -g '!**/dist/**' -g '!**/build/**'

echo
echo "== todo-modal detail hooks =="
cat -n apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts
echo
cat -n apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts

Repository: Team-Timo/Timo-client

Length of output: 7056


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== today-related query hooks/files =="
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' \
  "useToday|TodayTodoList|today.*QueryKey|queryKey:.*today|get.*Today.*QueryKey|today" apps/timo-web

echo
echo "== query key generators around home/detail =="
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' \
  "getGetHomeQueryKey|getGetTodoDetailQueryKey|queryKey" apps/timo-web | sed -n '1,240p'

Repository: Team-Timo/Timo-client

Length of output: 31550


Today 쿼리도 함께 무효화해 주세요
좋은 분리입니다—Home/Detail 갱신은 잘 되어 있습니다. 다만 TodayTodoListContaineruseToday()(getGetTodayQueryKey())를 직접 읽어서, 수정/삭제 직후 오늘 탭이 stale해질 수 있습니다.

  • apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts#L35-L41
  • apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts#L24-L27

getGetTodayQueryKey()도 함께 invalidateQueries에 넣어 주세요.
TanStack Query 공식 문서: https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation

📍 Affects 2 files
  • apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts#L35-L41 (this comment)
  • apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts#L24-L27
🤖 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/hooks/todo-modal/detail/use-update-todo-submit.ts` around lines
35 - 41, Update the onSuccess handlers in
apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts:35-41 and
apps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.ts:24-27 to also
invalidate the query identified by getGetTodayQueryKey(), alongside the existing
Home and detail invalidations, so TodayTodoListContainer refreshes after updates
and deletions.

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

Labels

✨ Feature 새로운 기능(기능성) 구현 ⌚ Timo-Design-system Timo 디자인 시스템 ⏰ Timo-web Timo 웹 서비스 ♣️ 유민 유민양

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 투두 모달 API 연동 (상세조회/수정/삭제)

2 participants