[FEAT] 홈 화면 TODO 조회·생성·완료·순서 변경 API 및 AI 소요 시간 추천 연동 - #160
Conversation
- useSuspenseQuery 기반 useHomeView 훅을 추가하고 mock 대신 실제 API 응답을 사용하도록 홈 화면 컨테이너를 교체했습니다 - Todo 로컬 zod 스키마를 실제 백엔드 응답 스펙에 맞춰 optional 필드를 반영하고 timerStatus에 PAUSED를 추가했습니다 - optional해진 tag 필드에 맞춰 컨테이너 렌더링과 TODO 생성 시 icon null 처리를 수정했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 44 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 (3)
Walkthrough홈 화면의 Todo 데이터를 실제 API 조회 결과로 전환하고, Todo 생성·완료·시간 추천을 API 기반 흐름으로 변경했습니다. 오늘 카드 스크롤, 선택 필드 정규화, 실패 토스트 및 오버레이 닫힘 제어도 갱신했습니다. Changes홈 화면 및 Todo 흐름
오버레이 상호작용
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CreateTodoModalContent
participant TimeSelector
participant useTimeField
participant RecommendDurationAPI
CreateTodoModalContent->>TimeSelector: 시간 선택기 오픈 콜백 연결
TimeSelector->>useTimeField: 오픈 이벤트 전달
useTimeField->>RecommendDurationAPI: 제목·태그 기반 추천 시간 요청
RecommendDurationAPI-->>useTimeField: 추천 분 반환
useTimeField-->>CreateTodoModalContent: HH:MM 시간 또는 오류 토스트
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
…to feat/web/159-integrate-home-view-api
- 오늘을 배열 맨 앞으로 재정렬하던 reorderDaysTodayFirst를 제거해 과거 7일~미래 7일이 정상 순서로 보이도록 했습니다 - 오늘 카드로 스크롤하는 로직을 scrollIntoView(inline: "start") 기반으로 바꿔 오늘이 정확히 좌측 기준으로 오도록 했습니다
- mock으로 로컬에서 TODO를 생성하던 로직을 실제 POST /api/v1/todos 뮤테이션 호출로 교체했습니다 - 응답을 로컬 zod 스키마로 검증해 실제 서버 todoId를 사용하도록 했습니다 - 생성 실패 시 에러 토스트를 노출하도록 했습니다
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx:
- Around line 53-57: Update the useEffect in HomeTodoContainer that calls
scrollContainerToToday so its dependency array includes baseDate, ensuring the
scroll position is recalculated when the displayed date range changes; preserve
the existing isWeekView and scrollRef dependencies.
In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit.ts:
- Around line 60-88: Update handleSubmit’s onError callback and the unsuccessful
safeParse branch in useCreateTodoSubmit to log the API error or
schema-validation details before opening the error toast. Preserve the existing
setIsErrorToastOpen behavior and successful onCreate flow.
🪄 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: 646ceb8f-e722-44bd-b694-3e22f2085987
📒 Files selected for processing (8)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-today-scroll.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/home-view.tsapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.json
💤 Files with no reviewable changes (1)
- apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/home-view.ts
- 백엔드가 priority/tag를 값이 없을 때 필드 생략이 아닌 명시적 null로 내려줘서 zod parse가 깨지던 문제를 수정했습니다 - .optional()을 .nullish().transform(...)으로 바꿔 null도 허용하고 각각 "MEDIUM"/undefined로 정규화했습니다
- 백엔드가 icon 값이 없을 때 필드 생략이 아닌 명시적 null로 내려줘서 zod parse가 깨지던 문제를 수정했습니다 - icon 필드를 .optional()에서 .nullish().transform(...)으로 바꿔 null도 허용하고 undefined로 정규화했습니다
- 투두/하위 투두 체크박스 토글 시 실제 완료 상태 변경 API를 호출하도록 연동했습니다 - 낙관적 업데이트로 즉시 UI에 반영하고, 실패 시 이전 상태로 롤백하도록 했습니다 - 성공 시에는 응답 필드를 직접 반영하는 대신 홈 뷰 쿼리를 무효화해 서버 최신 데이터로 다시 조회하도록 했습니다
- 로컬 상태에 Todo 객체를 수동으로 조립해 넣던 방식을 홈 뷰 쿼리 invalidate로 교체해 서버 상태 중복 관리를 없앴습니다 - 더 이상 쓰이지 않는 onCreate/CreateTodoTag 전달 체인을 컨테이너에서 정리했습니다
- 시간 선택 드롭다운을 열면 현재 제목/태그 기준으로 AI 예상 소요 시간 추천 API를 호출하도록 연동했습니다 - 추천 결과를 duration 입력값에 자동 반영하고, 실패 시 토스트로 안내합니다 - duration 수동 입력은 숫자와 콜론만 허용하도록 마스킹을 추가했습니다 (시 자릿수는 제한 없음, 분은 2자리) - Dropdown 컴포넌트에 열림 이벤트(onOpenChange)를 추가해 TimeSelector/TodoToolbar를 통해 상위로 전달했습니다
- 드롭다운의 바깥 클릭 감지(mousedown)가 모달의 배경 클릭 닫기 판단(click)보다 먼저 실행되면서, 플로팅 레이어가 이미 닫힌 상태로 판단돼 모달까지 함께 닫히던 문제를 수정했습니다 - 배경 클릭 시작 시점(mousedown 캡처 단계)에 플로팅 레이어 열림 여부를 미리 스냅샷해두고, 이후 click 시점에는 그 값을 사용하도록 했습니다
- 전역 staleTime(5분) 설정 때문에 기본/주간 뷰를 토글해 이전에 방문했던 조합으로 돌아가면 캐시만 쓰고 재요청을 안 하던 문제를 수정했습니다 - 홈 뷰 쿼리에 staleTime: 0을 지정해 토글 시 항상 최신 데이터를 다시 요청하도록 했습니다
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/api/todo/todo-schema.ts`:
- Around line 68-70: Update recommendDurationResponseSchema so
recommendedMinutes uses Zod’s integer validation rather than accepting arbitrary
numbers; retain the existing numeric schema behavior while rejecting non-integer
values before formatMinutesToDuration consumes them.
In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-time-field.ts:
- Around line 64-97: Update applyRecommendedDuration so AI recommendations are
only stored in recommendedDuration and reflected in the selector display,
without calling field.onChange during handleTimeSelectorOpen. Keep the existing
handleSelectTime "ai" branch responsible for applying the stored recommendation
to the form only after the user explicitly selects the AI option.
In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts:
- Around line 44-64: Update handleToggleCompleted in
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts#L44-L64
to snapshot only the targeted todo identified by todoId and restore that item
via updateTodo in onError, preserving concurrent changes to other todos. Apply
the same item-level snapshot and rollback in handleToggleSubtaskCompleted at
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts#L89-L105
for the targeted subtask.
In `@apps/timo-web/components/modal/OverlayModal.tsx`:
- Line 32: Update the OverlayModal interaction flow to capture pointer input via
onPointerDownCapture instead of onMouseDownCapture, so mouse, touch, and pen
inputs share the snapshot path. In the onClick handler, read
wasFloatingLayerOpenRef.current for the current interaction, then immediately
reset it to false before continuing.
In `@packages/timo-design-system/src/components/layout/dropdown/Dropdown.tsx`:
- Around line 81-85: Update the Dropdown component’s isOpen synchronization so
onOpenChange is invoked from a useEffect whenever isOpen changes, covering
toggle(), outside-click, Escape-key, and close() paths. Remove the direct
callback from toggle() to avoid duplicate notifications while preserving the
existing state transitions.
🪄 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: 6ecef49a-13d0-4cd1-ba93-38980cd117ce
📒 Files selected for processing (16)
apps/timo-web/api/todo/todo-schema.tsapps/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)/home/_containers/todo-modal/CreateTodoModalContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContent.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-time-field.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-home-view.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type.tsapps/timo-web/components/modal/OverlayModal.tsxapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonpackages/timo-design-system/src/components/layout/dropdown/Dropdown.tsxpackages/timo-design-system/src/components/time/time-selector/TimeSelector.tsxpackages/timo-design-system/src/components/todo/todo-toolbar/TodoToolbar.tsx
💤 Files with no reviewable changes (1)
- apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
| const [shouldRender, setShouldRender] = useState(isOpen); | ||
| const [isVisible, setIsVisible] = useState(false); | ||
| const dialogRef = useRef<HTMLDivElement>(null); | ||
| const wasFloatingLayerOpenRef = useRef(false); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the file first, then inspect the relevant slice.
ast-grep outline apps/timo-web/components/modal/OverlayModal.tsx --view expanded
echo
echo '--- file with line numbers ---'
cat -n apps/timo-web/components/modal/OverlayModal.tsx | sed -n '1,220p'
echo
echo '--- search for related overlay/modal patterns ---'
rg -n "onMouseDownCapture|onPointerDownCapture|wasFloatingLayerOpenRef|hasOpenFloatingLayer|floating layer|OverlayModal" apps/timo-web -SRepository: Team-Timo/Timo-client
Length of output: 6154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- pointer/mouse event usage in app ---'
rg -n "onPointerDownCapture|onPointerDown|onMouseDownCapture|onMouseDown" apps/timo-web -S
echo
echo '--- focus trap / overlay click-related patterns ---'
rg -n "allowOutsideClick|clickOutsideDeactivates|escapeDeactivates|hasOpenFloatingLayer" apps/timo-web -SRepository: Team-Timo/Timo-client
Length of output: 943
🌐 Web query:
React onMouseDown touch pointer events click synthesis MDN pointer events
💡 Result:
In web development, pointer events, touch events, and mouse events represent different ways to handle user interaction. The Pointer Events API is the modern, hardware-agnostic standard designed to unify these inputs into a single model [1][2][3]. Key Concepts: Pointer Events vs. Mouse/Touch Events: - Mouse Events (e.g., mousedown, mouseup, click): Specifically designed for mouse input. They are legacy events that have been extended to support touch and other devices to maintain backward compatibility [4][5][6]. - Touch Events (e.g., touchstart, touchend): Low-level events specific to touch interfaces. They support multi-touch but are not standard on all devices and can be difficult to manage alongside mouse inputs [5][7]. - Pointer Events (e.g., pointerdown, pointerup, pointermove): A unified API that handles mouse, touch, and pen/stylus input within a single set of event types [1][2][3]. Using pointer events is the recommended approach for modern, cross-device applications [5][8]. Click Synthesis: Browsers perform "click synthesis" to ensure compatibility. When a user performs a touch gesture or a primary button mouse press and release, the browser generates a click event [6]. This synthesis allows elements designed for mouse input to function on touch devices automatically [3][6]. However, reliance on click synthesis can introduce delays (historically, some mobile browsers delayed clicks to wait for potential double-tap gestures), which is why pointer events are often preferred for responsive, performance-critical applications [9][5]. React's Event System: React provides a cross-browser wrapper called the SyntheticEvent system [10][11]. Instead of attaching event listeners to every individual DOM element, React uses event delegation, attaching a single listener to the root (or document) node to intercept native events [12]. When a native event occurs, React captures it, creates a cross-browser-compatible SyntheticEvent object, and dispatches it through its own component tree [10][12]. This system provides consistent behavior across browsers, regardless of whether the underlying input is a mouse, touch, or pointer event [9][11]. Best Practices: For modern applications, developers are encouraged to use pointer events (e.g., onPointerDown) to handle interactions. This provides a unified way to process inputs while maintaining access to the pointerType property to distinguish between devices if specific logic is needed (e.g., desktop vs. mobile interactions) [9][2]. For complex UI components like buttons, many developers use abstraction libraries (like React Aria) that handle these inconsistencies by implementing robust fallbacks and managing touch/mouse/keyboard interactions in a unified, accessible manner [9].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
- 2: https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent
- 3: https://www.w3.org/TR/2026/REC-pointerevents3-20260630/
- 4: https://developer.mozilla.org/en-US/docs/Web/API/Element/mousedown_event
- 5: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events
- 6: https://developer.mozilla.org/en-US/docs/Web/API/Element/click%5Fevent
- 7: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Using_Touch_Events
- 8: https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Using_Pointer_Events
- 9: https://react-aria.adobe.com/blog/building-a-button-part-1
- 10: https://legacy.reactjs.org/docs/events.html
- 11: https://stackoverflow.com/questions/42327331/understanding-reacts-synthetic-event-system
- 12: https://stackoverflow.com/questions/52444941/when-does-react-create-syntheticevents
포인터 입력까지 포함해 snapshot을 매 클릭마다 소비해 주세요. onMouseDownCapture만으로는 입력 경로가 제한되고, wasFloatingLayerOpenRef를 onClick 후 초기화하지 않으면 다음 상호작용에 이전 값이 남을 수 있습니다. onPointerDownCapture로 넓히고, onClick에서 값을 읽은 뒤 바로 false로 리셋해 주세요. React 이벤트 / MDN Pointer Events 문서도 같이 보면 좋습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/components/modal/OverlayModal.tsx` at line 32, Update the
OverlayModal interaction flow to capture pointer input via onPointerDownCapture
instead of onMouseDownCapture, so mouse, touch, and pen inputs share the
snapshot path. In the onClick handler, read wasFloatingLayerOpenRef.current for
the current interaction, then immediately reset it to false before continuing.
| const toggle = () => { | ||
| const next = !isOpen; | ||
| setIsOpen(next); | ||
| onOpenChange?.(next); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
onOpenChange가 모든 열림/닫힘 경로에서 호출되지 않습니다.
toggle()에서만 onOpenChange가 호출되고, 외부 클릭(Line 60), ESC 키(Line 66), close()(Line 86)에서는 호출되지 않습니다. 소비자의 상태가 드롭다운 실제 상태와 불일치하게 되어, AI 시간 추천 등 후속 로직이 누락될 수 있습니다.
useEffect로 isOpen 변경을 감지해 onOpenChange를 일관되게 호출하는 방식을 권장합니다. React 공식 문서의 Using an Effect to synchronize with an external system를 참고하세요.
🔧 제안 수정 — `useEffect`로 `onOpenChange` 호출 일원화
}, [isOpen]);
+ const prevOpenRef = useRef(false);
+ useEffect(() => {
+ if (prevOpenRef.current !== isOpen) {
+ onOpenChange?.(isOpen);
+ prevOpenRef.current = isOpen;
+ }
+ }, [isOpen, onOpenChange]);
+
const toggle = () => {
const next = !isOpen;
setIsOpen(next);
- onOpenChange?.(next);
};
const close = () => setIsOpen(false);📝 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 toggle = () => { | |
| const next = !isOpen; | |
| setIsOpen(next); | |
| onOpenChange?.(next); | |
| }; | |
| }, [isOpen]); | |
| const prevOpenRef = useRef(false); | |
| useEffect(() => { | |
| if (prevOpenRef.current !== isOpen) { | |
| onOpenChange?.(isOpen); | |
| prevOpenRef.current = isOpen; | |
| } | |
| }, [isOpen, onOpenChange]); | |
| const toggle = () => { | |
| const next = !isOpen; | |
| setIsOpen(next); | |
| }; | |
| const close = () => setIsOpen(false); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/timo-design-system/src/components/layout/dropdown/Dropdown.tsx`
around lines 81 - 85, Update the Dropdown component’s isOpen synchronization so
onOpenChange is invoked from a useEffect whenever isOpen changes, covering
toggle(), outside-click, Escape-key, and close() paths. Remove the direct
callback from toggle() to avoid duplicate notifications while preserving the
existing state transitions.
- 드래그 앤 드롭으로 순서 변경 시 mock 대신 실제 reorderTodo 뮤테이션(PATCH /api/v1/todos/{todoId}/order)을 호출하도록 연동했습니다
- 성공 시 홈 뷰 쿼리를 무효화해 서버 최신 정렬 순서를 다시 조회하고, 실패 시 이전 순서로 롤백합니다
- 더 이상 쓰이지 않는 todo-order-mock.ts를 삭제했습니다
jjangminii
left a comment
There was a problem hiding this comment.
고생하셨습니다-! 크리티컬 이슈는 아니지만 코멘트 한번 확인해주세요-!
| const toggle = () => { | ||
| const next = !isOpen; | ||
| setIsOpen(next); | ||
| onOpenChange?.(next); | ||
| }; |
There was a problem hiding this comment.
외부 클릭(close())이나 ESC 키로 닫힐 때는 onOpenChange가 호출되지 않고있어요. 지금은 TimeSelector가 열림 시점(onOpen)만 쓰고 있어 당장 문제는 아니지만, isOpen 상태 변화와 onOpenChange 알림이 항상 동기화되도록 useEffect 기반으로 일원화해두면 나중에 닫힘 시점 로직이 추가될 때 오류를 막을 수 있을 것 같다고 생각합니다-!
| onMouseDownCapture={() => { | ||
| wasFloatingLayerOpenRef.current = hasOpenFloatingLayer(); | ||
| }} |
There was a problem hiding this comment.
저희 마우스/키보드 환경만 체크하고 있는데, 태블릿 반응형도 고려해서 만들고 있으니 터치/펜슬 입력도 함께 체크해봐야 할 것 같아요-!
| queryKey: getGetHomeQueryKey({ filter, baseDate }), | ||
| queryFn: ({ signal }) => getHome({ filter, baseDate }, undefined, signal), | ||
| select: ({ data }) => homeViewDataSchema.parse(data), | ||
| staleTime: 0, |
There was a problem hiding this comment.
의도적으로 항상 최신 데이터를 받으려는 결정으로 보이는데, invalidateQueries를 이미 여러 곳에서 호출하고 있어 staleTime: 0이 캐시 이점을 거의 없애고 있다고 생각해요. 화면 전환/포커스 복귀마다 refetch가 잦아질 수 있으니 의도한 트레이드오프가 맞는지 확인해주세요-!
There was a problem hiding this comment.
staleTime: 0 은 뷰 토글 시 캐시만 쓰고 재요청 안 되는 버그를 고치기 위해 의도적으로 추가한 거여서 의도한 게 맞습니다!
- onMouseDownCapture를 onPointerDownCapture로 교체해 태블릿 터치/펜슬 입력에서도 플로팅 레이어 상태를 정확히 감지하도록 수정
ISSUE 🔗
close #159
What is this PR? 🔍
홈 화면의 일별 TODO 목록을 mock 데이터 대신 실제 백엔드 API로 조회/생성/완료 처리하도록 연동하고, 그 과정에서 발견한 날짜 범위·스크롤 버그, null 파싱 오류, 드롭다운/모달 레이스 컨디션을 함께 수정했습니다. 추가로 TODO 생성 시 AI 예상 소요 시간 추천 기능을 연동했습니다.
배경
HomeTodoContainer가getHomeViewMock으로 로컬에서 생성한 가짜 날짜/TODO 데이터를 렌더링하고, TODO 생성·완료·순서 변경도 로컬 상태에만 반영하고 있었습니다.home.zod.ts)을 확인해보니icon,durationSeconds,priority,tag,sortOrder가 모두 optional이고timerStatus에PAUSED가 추가돼 있어, 기존 로컬 zod 스키마(모두 required,PAUSED없음)로는 실제 응답이 들어오는 즉시.parse()가 깨질 상황이었습니다. 실제로 연동 후icon/priority/tag값이 없는 TODO에서 백엔드가 필드 자체를 생략하는 대신null을 명시적으로 내려주고 있어,.optional()만으로는 막지 못하는 zod 파싱 에러가 실제로 발생했습니다. 또한 API 연동 후 기본 뷰에서 오늘 기준 이전 7일이 보이지 않고 오늘부터 미래만 계속 이어지는 것처럼 보이는 버그가 있었습니다.useSuspenseQuery기반의useHomeView훅과useCreateTodo/useChangeTodoStatus/useChangeSubtaskStatus/useRecommendDuration뮤테이션으로 실제 API를 연동하고, 로컬 zod 스키마를 백엔드가 실제로 내려주는 값(명시적null포함)에 맞게 갱신했습니다. 날짜 정렬 로직도 원인을 찾아 제거했습니다.홈 화면 데이터 연동
_queries/use-home-view.ts에useHomeView훅을 추가하고,HomeTodoContainer에서getHomeViewMock호출을 이 훅으로 교체했습니다.page.tsx에서AsyncBoundary(Suspense)로 감싸져 있어useSuspenseQuery를 써야 했습니다.useGetHome은 내부적으로 일반useQuery용 타입(UseQueryOptions)으로 캐스팅되어 있어skipToken을 허용하는데, 이 옵션 객체를useSuspenseQuery에 그대로 스프레드하면UseSuspenseQueryOptions와 타입이 충돌합니다. 그래서 생성된getGetHomeQueryKey+getHomefetcher만 가져와queryKey/queryFn을 직접 조립하고,select에서BaseResponse.data를 언랩한 뒤 로컬 zod 스키마(homeViewDataSchema)로.parse()해 응답을 검증합니다. 전역queryClient(api/client/query-client.ts)에staleTime: 5분이 기본값으로 설정돼 있어, 기본/주간 뷰 토글로 이전에 방문한 filter+date 조합에 5분 내 다시 진입하면 캐시만 쓰고 재요청을 안 하는 문제가 있어 이 쿼리에는staleTime: 0을 별도로 지정했습니다.api/generated/내부 파일은 수정하지 않았습니다.날짜 범위·오늘 스크롤 버그 수정
reorderDaysTodayFirst(_utils/home-view.ts)를 제거하고, "오늘로 스크롤"하는 로직을 브라우저 네이티브scrollIntoView로 교체했습니다.reorderDaysTodayFirst가 오늘 이전 7일을 배열 끝(미래 7일보다 뒤)으로 옮기고 있어서, 화면상으로는 오늘부터 미래만 계속 이어지는 것처럼 보이고 과거 7일은 스크롤을 끝까지 넘겨야만 나타났습니다. 이후 "오늘" 버튼을 눌렀을 때도 하루 밀려서 스크롤되는 문제가 있었는데, 원인은 스크롤 위치 계산에 쓰던element.offsetLeft가 스크롤 컨테이너가 아니라 가장 가까운 positioned 조상(사실상<body>) 기준으로 계산되고 있어서였습니다. 사이드바 열림/닫힘에 따라<main>의margin-left(ml-55↔ml-5)가 바뀌는데 이 차이만큼 값이 오염되어 스크롤 목표 지점이 어긋났습니다.days배열은 API가 내려주는 과거→오늘→미래 순서를 그대로 사용합니다. 각 날짜 카드에data-today속성을 추가하고,scrollContainerToToday에서container.querySelector('[data-today="true"]')로 오늘 카드를 찾아todayElement.scrollIntoView({ inline: "start" })로 정렬합니다.scrollIntoView는 조상 요소의 margin/position과 무관하게 브라우저가 직접 정렬 위치를 계산하므로, 이전에 시도했던offsetLeft나getBoundingClientRect기반 수동 계산보다 안전합니다.MainShellContainer.tsx)의 margin 구조 자체는 변경하지 않았습니다.Todo 로컬 타입 · null 파싱 오류 수정
_types/todo-type.ts의todoSchema를 실제 백엔드 응답 스펙에 맞게 갱신했습니다 —icon/priority/tag는 백엔드가 값이 없을 때 필드를 생략하는 대신 명시적으로null을 내려주는 것을 확인해.nullish().transform(...)으로 null도 허용하도록 바꿨고(priority는"MEDIUM",icon/tag는undefined로 정규화),durationSeconds/sortOrder는 optional + 기본값,timerStatusenum에PAUSED를 추가했습니다. TODO 생성 응답 검증용todoCreateResponseSchema도 추가했습니다..optional()만으로는undefined만 허용하고null은 걸러내지 못해, 실제로 icon/priority/tag가 없는 TODO를 조회할 때마다useHomeView의homeViewDataSchema.parse()가ZodError를 던지며 화면이 통째로 에러 바운더리로 빠지는 문제가 실제로 재현됐습니다.durationSeconds,sortOrder는.default(0)을 적용해 값이 없어도Todo타입 자체는 이전처럼 non-optional하게 유지했습니다.priority/icon/tag는null/undefined를 모두 허용(.nullish())한 뒤.transform()으로 UI가 기대하는 형태(priority는 기본값"MEDIUM",icon/tag는undefined)로 정규화해,Todo타입의 출력 형태는 기존과 동일하게 유지했습니다.todoCreateResponseSchema는{ todoId: number }만 검증하는 최소 스키마입니다.icon이undefined가 되면서 폼의null값을undefined로 변환하는 코드가use-create-todo-submit.ts에 함께 필요해 같이 수정했습니다(icon: data.icon ?? undefined).TODO 생성 API 연동
use-create-todo-submit.ts에서 mock으로 로컬에 즉시 반영하던 TODO 생성 로직을 실제useCreateTodo뮤테이션(POST /api/v1/todos) 호출로 교체했습니다.Date.now()를 todoId로 쓰는 가짜 객체를 만들어 화면에만 반영하고 서버에는 아무 것도 저장하지 않았습니다.CreateTodoRequest(nullable 필드)를 generatedTodoCreateRequest(optional 필드)로 변환하는 매퍼를 추가하고(icon: data.icon ?? undefined등), 뮤테이션 성공 시 응답을todoCreateResponseSchema로.parse()한 뒤 홈 뷰 쿼리(getGetHomeQueryKey())를invalidateQueries로 무효화해 서버 최신 데이터를 다시 조회하도록 했습니다. 이전에는 로컬에서 Todo 객체를 수동으로 조립해todosByDate에 낙관적으로만 반영했는데, React Query가 서버 상태의 단일 출처가 되도록 이 방식으로 교체하면서 더 이상 필요 없어진onCreate/CreateTodoTag전달 체인(CreateTodoModalContainer→HomeDayHeaderContainer→HomeTodoContainer)을 함께 정리했습니다. 실패 시(네트워크 에러 또는 응답 형태 불일치)CreateTodoModalContainer에 추가한AnimatedToast로 에러를 알립니다. 메시지 키Toast.todoCreateFailed를 ko/en 메시지 파일에 추가했으며, 온보딩 실패 토스트(onboardingSubmitFailed)와 동일한 패턴을 따랐습니다.use-home-todos-by-date.ts에// TODO: API로 남아 있습니다.투두 및 하위 투두 완료 상태 변경 API 연동
use-home-todos-by-date.ts의handleToggleCompleted/handleToggleSubtaskCompleted에서 로컬 상태만 바꾸던 로직을 실제useChangeTodoStatus(PATCH /api/v1/todos/{todoId}/status)/useChangeSubtaskStatus(PATCH /api/v1/todos/{todoId}/subtasks/{subtaskId}/status) 뮤테이션 호출로 교체했습니다.completed/sortOrder)를 직접 반영하는 대신 홈 뷰 쿼리를invalidateQueries로 무효화해 서버가 재계산한 완료 그룹 정렬 순서까지 정확히 반영되도록 했습니다. 실패 시에는 토글 직전 상태로 롤백합니다.AI 소요 시간 추천 API 연동
TimeSelector)을 열면 현재 입력된 제목/태그를 기준으로useRecommendDuration(POST /api/v1/ai/duration) API를 호출해 추천 소요 시간을 duration 입력값에 자동 반영하도록 연동했습니다.Dropdown(디자인 시스템 공통 컴포넌트)에onOpenChange콜백을 추가하고,TimeSelector→TodoToolbar→CreateTodoModalContent까지onOpen/onTimeOpenprop으로 연결해 드롭다운이 열리는 시점을 훅으로 노출했습니다.use-time-field.ts의handleTimeSelectorOpen에서 제목이 비어있지 않고 이미 요청 중이 아닐 때만 추천 API를 호출하고, 응답의recommendedMinutes(분)를HH:MM문자열로 변환해 duration 필드에 반영하며 AI 아이콘을 선택 상태로 표시합니다. 실패 시Toast.aiDurationRecommendFailed토스트로 안내합니다. 또한 duration 수동 입력 마스크(formatDurationInput)를 숫자와 콜론만 허용(시 자릿수 제한 없음, 분은 2자리)하도록 다듬고, 검증 정규식(todo-schema.ts의DURATION_PATTERN)도 동일한 형태(\d{1,3}:[0-5]\d)로 맞췄습니다.투두 순서 변경 API 연동
use-home-todos-by-date.ts의handleReorderTodo에서 mock(patchTodoOrderMock)으로만 반영하던 드래그 앤 드롭 순서 변경을 실제useReorderTodo(PATCH /api/v1/todos/{todoId}/order) 뮤테이션 호출로 교체했습니다.todoId/newIndex/date(대상 날짜)를 실어 뮤테이션을 호출합니다. 성공 시에는 다른 완료 상태 변경 mutation들과 같은 패턴으로 홈 뷰 쿼리를invalidateQueries로 무효화해 서버가 재계산한 정렬 순서를 다시 조회하고, 실패 시 드래그 이전 순서로 롤백합니다. 더 이상 쓰이지 않는_mocks/todo-order-mock.ts는 삭제했습니다.reorderTodos유틸)은 그대로 유지했고, 완료/미완료 그룹 간 이동은 여전히 완료 상태 변경 API가 담당합니다.드롭다운 바깥 클릭 시 모달 닫힘 레이스 컨디션 수정
OverlayModal에서 드롭다운(우선순위/태그/시간/반복 선택 등)이 열려 있는 상태에서 모달 배경을 클릭해 드롭다운을 닫으려 하면, 모달까지 함께 닫혀버리던 버그를 수정했습니다.mousedown시점에 자기 자신을 닫고 공용 플로팅 레이어 카운트(hasOpenFloatingLayer)를 즉시 감소시키는데,OverlayModal의 배경 닫기 판단은 그보다 늦게 발생하는click시점에 이 카운트를 확인하고 있었습니다. 같은 한 번의 클릭 안에서mousedown이click보다 항상 먼저 발생하기 때문에, 모달이 카운트를 확인하는 시점에는 이미 드롭다운이 스스로 카운트를 감소시킨 뒤라 "열린 플로팅 레이어 없음"으로 잘못 판단해 모달까지 닫아버렸습니다.OverlayModal의 배경 요소에onMouseDownCapture를 추가해, 캡처 단계(버블 단계보다 항상 먼저 실행되는 것이 DOM 스펙상 보장됨)에서 "이 클릭이 시작되는 시점에 플로팅 레이어가 열려 있었는지"를 ref에 스냅샷해두고, 이후onClick에서는 그 스냅샷 값을 사용해 닫을지 판단하도록 했습니다.keydown이벤트만 관여해 동일한 레이스 컨디션이 없어 변경하지 않았습니다.To Reviewers
priority기본값을"MEDIUM"으로 정한 건 기존use-create-todo-submit.ts에 이미 있던 관례를 따른 것인데, 실제로 백엔드가 우선순위 없는 TODO를 의도적으로 반환하는 케이스가 있다면 이 기본값이 맞는지 확인 부탁드립니다.icon/priority/tag의 null 파싱 오류와 드롭다운/모달 레이스 컨디션은 실제 개발 중 브라우저에서 재현된 에러 로그(ZodError, 모달이 같이 닫히는 현상)를 보고 원인을 찾아 수정한 것이라 실제 케이스 기반이지만, 완료 상태 변경 API·순서 변경 API·AI 추천 API는 백엔드 CORS 설정이 아직 이 엔드포인트들(
PATCH .../status,PATCH .../order,POST /api/v1/ai/duration)에 열려 있지 않아 로컬에서 실제 응답까지 끝까지 확인하지는 못했습니다. 백엔드 CORS 설정이 열리는 대로 실제 완료 처리/순서 변경/AI 추천 흐름을 한 번 더 확인해주시면 좋겠습니다.날짜 범위/스크롤 버그는 로컬 Playwright 브라우저 확장 연결 문제로 실제 화면에서 직접 재현·확인하지 못하고 코드 레벨 분석(
offsetLeft가 positioned 조상 기준이라는 점)만으로 수정했습니다. 사이드바를 열고 닫으며 오늘 카드 정렬이 정상인지, "오늘" 버튼을 눌렀을 때 정확히 오늘로 이동하는지 리뷰어분이나 QA 단계에서 한 번 더 확인해주시면 좋겠습니다.Screenshot 📷
Test Checklist ✔
pnpm tsc --noEmit통과pnpm eslint통과 (변경 파일 기준)/home페이지 실동작 확인 — 미실행: 로컬 Playwright 브라우저 연결 문제로 미완료