[FEAT] 투두 API 연결 (상세조회 / 수정 / 삭제) - #190
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 41 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
WalkthroughTODO 생성 폼과 상세 모달을 API 모델 기반으로 개편하고, 상세 조회·수정·삭제 및 쿼리 갱신 흐름을 추가했습니다. 타이머 상태에 따른 입력 비활성화, 디바운스 수정, 시간·태그·반복·서브태스크 변환과 재생 버튼 이벤트 차단도 반영했습니다. Changes생성 폼 흐름
상세 모달 흐름
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 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
측정 커밋: |
…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
There was a problem hiding this comment.
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
📒 Files selected for processing (29)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsxapps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsxapps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsxapps/timo-web/components/todo-modal/detail/DetailTodoMemoField.tsxapps/timo-web/components/todo-modal/detail/DetailTodoModalContainer.tsxapps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsxapps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsxapps/timo-web/containers/todo-modal/create/CreateTodoModalContainer.tsxapps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsxapps/timo-web/hooks/todo-modal/common/use-tag-field.tsxapps/timo-web/hooks/todo-modal/create/use-create-todo-submit.tsapps/timo-web/hooks/todo-modal/create/use-icon-field.tsapps/timo-web/hooks/todo-modal/create/use-repeat-field.tsapps/timo-web/hooks/todo-modal/create/use-subtask-field.tsapps/timo-web/hooks/todo-modal/create/use-time-field.tsapps/timo-web/hooks/todo-modal/create/use-title-field.tsapps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.tsapps/timo-web/hooks/todo-modal/detail/use-detail-subtask-field.tsapps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.tsapps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.tsapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonapps/timo-web/utils/todo/detail-todo-update-request.tsapps/timo-web/utils/todo/todo-time.tspackages/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
There was a problem hiding this comment.
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
📒 Files selected for processing (29)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsxapps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsxapps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsxapps/timo-web/components/todo-modal/detail/DetailTodoMemoField.tsxapps/timo-web/components/todo-modal/detail/DetailTodoModalContainer.tsxapps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsxapps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsxapps/timo-web/containers/todo-modal/create/CreateTodoModalContainer.tsxapps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsxapps/timo-web/hooks/todo-modal/common/use-tag-field.tsxapps/timo-web/hooks/todo-modal/create/use-create-todo-submit.tsapps/timo-web/hooks/todo-modal/create/use-icon-field.tsapps/timo-web/hooks/todo-modal/create/use-repeat-field.tsapps/timo-web/hooks/todo-modal/create/use-subtask-field.tsapps/timo-web/hooks/todo-modal/create/use-time-field.tsapps/timo-web/hooks/todo-modal/create/use-title-field.tsapps/timo-web/hooks/todo-modal/detail/use-delete-todo-submit.tsapps/timo-web/hooks/todo-modal/detail/use-detail-subtask-field.tsapps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.tsapps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.tsapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonapps/timo-web/utils/todo/detail-todo-update-request.tsapps/timo-web/utils/todo/todo-time.tspackages/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.mdRepository: Team-Timo/Timo-client
Length of output: 1838
🏁 Script executed:
sed -n '1,260p' apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsxRepository: Team-Timo/Timo-client
Length of output: 8990
🏁 Script executed:
sed -n '260,360p' apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsxRepository: Team-Timo/Timo-client
Length of output: 2642
todo대신 폼 상태를 사용해 화면을 맞춰주세요.
dateNumber/dateLabel은detailTodoForm.date를 쓰는데, 요일 라벨은todo.dayOfWeek, 메모 아이콘은todo.memo를 봐서 수정 중에 값이 잠깐 어긋납니다.dayOfWeek와hasMemo를detailTodoForm의 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초) 안에 닫으면 마지막 수정 내용이 그대로 소실돼요.
isOpen이false로 바뀌면 대기 중인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 통신 중 에러가 발생할 때를 대비한 꼼꼼한 처리를 추가해 보면 어떨까요?
현재isError가true가 되면 컴포넌트가 렌더링되지 않아 모달 UI가 사라지게 되지만,isOpen과isMounted상태는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
title이undefined일 경우 발생할 수 있는 런타임 에러 방지
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함수에 심각한 논리적 오류가 있습니다:
- API 포맷 불일치: 사용자의
h:mm입력을 API 포맷(mm:ss)으로 변환하지 않고 폼에 그대로 저장하여, 1시간 30분(1:30) 입력 시 1분 30초로 서버에 전송됩니다.- 화면 표시 오류:
h:mm입력을formatDurationAsClockLabel에 전달하면, 해당 함수는 이를mm:ss로 간주하고 변환하여 화면에 즉시 잘못된 시간(0:01)을 표시합니다.- 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.tsRepository: 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.mdRepository: 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.tsxRepository: 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.tsxRepository: Team-Timo/Timo-client
Length of output: 2220
apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts:155-168의selectTime포맷을 기본값과 맞춰주세요.
convertDurationToTimeText와convertTimeTextToDurationSeconds는HH:MM기준인데,selectTime만convertSecondsToApiDuration으로MM:SS를 넣고 있어요. 그래서 15/30/45분 프리셋을 고르면 저장 시15:00이 15시간으로 해석될 수 있습니다.selectTime도convertDurationToTimeText로 통일하면 됩니다. 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.tsx의onAddTagClick={() => {}}가 빈 함수로 남아있어, 상세 모달에서는 새 태그를 만들 수 없는 상태예요. 생성 모달에서는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.tsRepository: 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 갱신은 잘 되어 있습니다. 다만TodayTodoListContainer는useToday()(getGetTodayQueryKey())를 직접 읽어서, 수정/삭제 직후 오늘 탭이 stale해질 수 있습니다.
apps/timo-web/hooks/todo-modal/detail/use-update-todo-submit.ts#L35-L41apps/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.
ISSUE 🔗
close #186
What is this PR? 🔍
투두 상세 모달에서 상세 조회, 삭제, 수정 API를 연결했습니다.
이번 작업에서는 단순히 API hook만 붙이는 것이 아니라, 상세 모달의 책임을 정리하면서 아래 흐름이 자연스럽게 이어지도록 구성했습니다.
todoId,date기반 상세 조회AnimatedToast재사용상세 조회 연결
상세 조회는 generated API의
useGetTodoDetail을 직접 컴포넌트에서 쓰지 않고,queries/todo/use-detail-todo.ts로 한 번 감쌌습니다.처음에는 컨테이너 안에 query 로직을 바로 둘 수도 있었지만, 다른 query 패턴과 맞추고 상세 모달 컨테이너가 API 명세에 직접 강하게 묶이지 않도록 queries 폴더로 분리했습니다.
컨테이너 구조 정리
기존에는 모달 관련 container가 components 내부에 섞여 있어서 구조가 애매했습니다.
그래서 다음처럼 UI와 container 책임을 분리했습니다.
상세 모달은 아래 구조를 따릅니다.
태그 목록 및 다국어 표시 수정
상세 모달 태그 드롭다운에서 기존에는 현재 선택된 태그 하나만 내려가고 있었습니다.
tags={[detailTodoForm.tagLabel]}이 구조 때문에 드롭다운에 운동 하나만 보이는 문제가 있었고, 생성 모달처럼 전체 태그 목록을 가져오도록 useTagField를 재사용했습니다.
또한 서버에서 기본 태그가 exercise 같은 key가 아니라 운동처럼 한국어 label로 내려오는 경우가 있어, en locale에서도 한국어로 표시되는 문제가 있었습니다.
이를 해결하기 위해 기본 태그명을 locale key로 매핑하는 유틸을 보강했습니다.
이제 기본 태그는 현재 locale에 맞게 표시됩니다.
수정 API 연결
수정 API는 generated의 useUpdateTodo를 직접 사용하지 않고, 상세 모달 전용 hook으로 감쌌습니다.
hooks/todo-modal/detail/use-update-todo-submit.ts
성공 시 별도 UI 액션은 하지 않고, 홈 목록과 상세 조회 query만 invalidate합니다.
실패 시에는 새 토스트를 만들지 않고 기존 AnimatedToast를 재사용했습니다.
수정 요청 body 변환 분리
use-detail-todo-form.ts에 form 상태 관리와 API request body 조립 로직이 함께 들어가면서 파일 책임이 커졌습니다.
그래서 API 요청 body 변환은 유틸로 분리했습니다.
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.tsTo Reviewers
이번 PR에서 의도적으로 선택한 부분들이 있습니다.
수정 성공 시 모달을 닫지 않았습니다.
성공 후 별도 액션은 필요 없다고 판단했고, query invalidate로 최신 데이터만 반영하도록 했습니다.
수정 실패 토스트는 새 컴포넌트를 만들지 않았습니다.
기존 AnimatedToast를 재사용했고, 메시지 key만 todoUpdateFailed로 추가했습니다.
상세 모달의 완료 여부는 수정 API body에 포함하지 않았습니다.
TodoUpdateRequest에는 completed 필드가 없고, 완료 상태 변경은 별도 status API 영역이라 이번 PATCH body에서는 제외했습니다.
Screenshot 📷
Test Checklist ✔
todoId,date기준 상세 조회가 되는지 확인ko/enlocale에서 기본 태그가 각각 한국어/영어로 표시되는지 확인AnimatedToast로 실패 토스트가 표시되는지 확인pnpm --filter timo-web lintpnpm --filter timo-web check-types