Skip to content

[FEAT] 투두 생성 모달 및 홈 투두 카드 UI 구현#138

Open
kimminna wants to merge 6 commits into
developfrom
feat/web/137-create-todo-modal
Open

[FEAT] 투두 생성 모달 및 홈 투두 카드 UI 구현#138
kimminna wants to merge 6 commits into
developfrom
feat/web/137-create-todo-modal

Conversation

@kimminna

@kimminna kimminna commented Jul 10, 2026

Copy link
Copy Markdown
Member

ISSUE 🔗

close #137



What is this PR? 🔍

홈 화면에서 할 일을 생성·조회·정렬할 수 있는 투두 생성 모달과 투두 카드 UI를 구현했습니다.

배경

  • 기존 구조: 홈 화면에 할 일을 추가할 모달이나 관련 디자인시스템 컴포넌트(아이콘/날짜/태그 선택기 등)가 없었습니다.
  • 발생 문제: 사용자가 할 일을 등록·관리할 UI 자체가 없어 홈 화면의 핵심 기능이 비어 있었습니다.
  • 해결 방향: 오버레이 기반 모달 인프라와 투두 생성에 필요한 디자인시스템 컴포넌트를 새로 만들고, 이를 조합해 투두 생성 모달과 홈 투두 카드(드래그 정렬 포함)를 구현했습니다.

의존성

  • 변경 요약: overlay-kit, @dnd-kit/modifiers 의존성을 추가했습니다.
  • 이유: 모달을 prop drilling 없이 열고 닫기 위해 overlay-kit이, 드래그를 세로 축으로 제한하기 위해 @dnd-kit/modifiers가 필요했습니다.
  • 구현 방식: overlay.open()으로 모달의 isOpen/close/unmount를 관리하고, DndContextmodifiers prop에 restrictToVerticalAxis를 전달했습니다.

디자인시스템 컴포넌트

  • 변경 요약: IconSelector/IconGraphic/DateSelector/TagNameInput/TodoToolbar를 새로 추가하고, 기존 PriorityIcon/PrioritySelector/RepeatSelector/TagSelector/TimeSelector를 모달 요구사항에 맞게 수정했습니다.
  • 이유: 투두 생성 폼에서 아이콘/날짜/태그/시간/반복/우선순위를 선택하는 UI가 design-system에 없었습니다.
  • 구현 방식: 각 컴포넌트는 값과 변경 콜백만 받는 controlled 형태로 만들어 app 레이어의 폼 라이브러리와 독립적으로 재사용 가능하게 했습니다. 투두 카테고리 아이콘 8종(SVG)도 함께 추가했습니다.

API 스키마 및 오버레이 모달 인프라

  • 변경 요약: 투두 생성 요청 zod 스키마(createTodoRequestSchema)를 정의하고, 오버레이 기반 Modal 컴포넌트와 OverlayProvider를 루트 레이아웃에 연결했습니다.
  • 이유: 아직 백엔드 API가 없어 폼 유효성 검증 기준이 필요했고, 투두/태그 생성 모달이 공통으로 쓸 오버레이 컨테이너가 없었습니다.
  • 구현 방식: date/duration 포맷을 정규식으로 검증하고, repeatType에 따라 repeatWeekdays/repeatDayOfMonth 필수 여부를 superRefine으로 처리했습니다. OverlayProviderlayout.tsxQueryProvider 내부에 배치했습니다.
  • 디자인시스템 Modal과 다르게 만든 이유: @repo/timo-design-systemModal(Modal.Root+Trigger+Panel)은 TriggerPanel이 같은 트리에 선언되고 isOpen을 내부 state로만 관리하는 비제어(uncontrolled) 컴파운드 컴포넌트입니다. 반면 이번 요구사항은 overlay.open()으로 트리거와 무관한 위치(날짜 컬럼별 추가 버튼)에서 모달을 열고, 닫힘 애니메이션이 끝난 뒤에만 unmount()를 호출해야 해서 isOpen/onClose/onExited를 외부에서 제어할 수 있는 형태가 필요했습니다. 디자인시스템 Modal은 이 "외부 제어 + exit-aware unmount" 계약을 지원하지 않아 그대로 재사용할 수 없었고, 그래서 apps/timo-web/components/modal/Modal.tsx를 앱 레이어에 별도로 만들어 overlay-kit과 연결했습니다.

홈 투두 생성 모달 및 투두 카드

  • 변경 요약: 투두 생성 모달, 태그 생성 모달, 홈 투두 카드/데이 헤더 UI를 구현하고, 관련 폴더를 todo-card/todo-modal/tag-modal 기능 단위로 재구성했습니다.
  • 이유: 기존에는 _components/_containers/_hooks가 타입별로만 분리돼 있어 하나의 기능을 수정할 때 여러 폴더를 오가야 했고, 모달 폼도 watch/setValue를 필드마다 직접 호출해 필드 하나가 바뀌어도 폼 전체가 리렌더되는 구조였습니다.
  • 구현 방식: useIconField/useTagField/useTimeField/useRepeatField 각 훅이 control을 받아 내부에서 useController로 해당 폼 필드를 직접 구독하도록 바꿔, 컨테이너 컴포넌트는 훅이 반환한 값을 JSX에 배치하기만 하면 되도록 했습니다. 홈 투두 카드는 @dnd-kit/sortableuseSortable로 정렬 가능하게 만들었고, restrictToVerticalAxis로 드래그 축을 세로로 제한했습니다. 디자인에 없던 카드 그랩(드래그 핸들) 아이콘은 제거했습니다.
  • 경계 · 제약: 투두/태그 생성은 실제 백엔드 연동 전까지 로컬 mock 데이터로 즉시 반영되며, 서버 연동은 이번 PR 범위 밖입니다.



To Reviewers

CreateTodoModalContent의 필드 훅들이 watch/setValue 대신 useController를 직접 구독하는 구조로 바뀌었으니, 필드별 리렌더링 범위가 의도대로 좁혀졌는지 봐주세요.
투두/태그 API가 아직 없어 todoId/tagId 채번과 저장이 클라이언트 로컬 상태로만 처리됩니다. 의도된 범위이며 후속 API 연동 PR에서 대체될 예정입니다.
pnpm --filter timo-web check-types가 실패하는데, origin/develop에 이미 있는 온보딩 아이콘(export 누락) 문제로 이번 PR 변경사항과는 무관함을 확인했습니다.



Screenshot 📷



Test Checklist ✔

  • pnpm --filter timo-web lint 통과
  • pnpm --filter @repo/timo-design-system check-types 통과
  • pnpm --filter @repo/timo-design-system lint 통과
  • pnpm --filter timo-web check-types — origin/develop에 이미 있는 온보딩 아이콘 export 누락 오류로 실패 (이번 PR과 무관함을 확인)
  • 브라우저에서 투두 생성 모달/드래그 정렬 동작 확인 — 미실행: 이번 세션에서 실제 브라우저 검증을 하지 않았습니다
  • pnpm build — 미실행

kimminna added 4 commits July 11, 2026 00:34
- overlay-kit, @dnd-kit/modifiers 의존성을 추가했습니다
- 아이콘 선택기, 날짜 선택기, 태그 입력, 투두 툴바 컴포넌트를 추가했습니다
- 우선순위/반복/태그/시간 선택기를 투두 모달 요구사항에 맞게 수정했습니다
- 투두 아이콘 SVG 8종을 추가했습니다
- 투두 생성 요청 스키마를 정의했습니다
- 오버레이 기반 모달 컴포넌트와 프로바이더를 추가하고 루트 레이아웃에 연결했습니다
- 투두 생성 모달과 태그 생성 모달을 구현했습니다
- 홈 투두 카드와 데이 헤더 UI를 구현하고 드래그 정렬을 연동했습니다
- 모달 폼 필드 바인딩을 useController 기반으로 구현했습니다
- 드래그 정렬을 세로 축으로 제한했습니다
- 관련 폴더를 기능 단위(todo-card/todo-modal/tag-modal)로 구성했습니다
@vercel

vercel Bot commented Jul 10, 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 10, 2026 3:44pm

@github-actions github-actions Bot added ⏰ Timo-web Timo 웹 서비스 ⌚ Timo-Design-system Timo 디자인 시스템 labels Jul 10, 2026
@github-actions github-actions Bot added ✨ Feature 새로운 기능(기능성) 구현 ♦️ 민아 민아상 labels Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: efcb067f-3026-495d-8a71-204b9d87d5f0

📥 Commits

Reviewing files that changed from the base of the PR and between 87c8f56 and 97f30e1.

📒 Files selected for processing (8)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeHeaderContainer.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-modal/CreateTodoModalContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContent.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-today-scroll.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode.ts

Walkthrough

투두 생성 API 스키마와 디자인시스템 컴포넌트가 추가되었고, 오버레이 기반 생성 모달·태그 모달·폼 훅이 홈 화면에 연결되었습니다. 생성된 Todo는 날짜별 목록에 반영되며, 우선순위·반복 타입과 관련된 UI 및 Storybook 설정도 갱신되었습니다.

Changes

투두 생성 기능

Layer / File(s) Summary
투두 계약과 도메인 변환
apps/timo-web/api/todo/todo-schema.ts, apps/timo-web/app/.../home/_types/todo-type.ts, apps/timo-web/app/.../home/_utils/*
투두 요청·응답 Zod 스키마와 반복 조건 검증, 우선순위·아이콘 타입 변경, 날짜 및 기간 변환 유틸을 추가했습니다.
생성 UI 디자인시스템 구성
packages/timo-design-system/src/components/{calendar,icon,priority,repeat,tag,time,todo}/..., packages/timo-design-system/src/components/index.ts
날짜·아이콘·태그·우선순위·반복 선택기와 TodoToolbar를 추가하고 관련 타입 및 컴포넌트를 재-export했습니다.
모달 인프라와 입력 상태
apps/timo-web/components/modal/Modal.tsx, apps/timo-web/providers/OverlayProvider.tsx, apps/timo-web/.../todo-modal/*, apps/timo-web/.../tag-modal/*
포털 기반 모달과 overlay-kit Provider를 연결하고, 생성 폼의 아이콘·시간·반복·태그 상태 훅 및 제목·서브태스크·메모 입력 필드를 구현했습니다.
투두·태그 생성 제출 흐름
apps/timo-web/.../CreateTodoModalContainer.tsx, apps/timo-web/.../CreateTodoModalContent.tsx, apps/timo-web/.../CreateTagModalContainer.tsx, apps/timo-web/.../use-create-todo-submit.ts, apps/timo-web/messages/*
폼 검증, 태그 생성, 번역 문구, 입력값 초기화, 로컬 Todo 변환 및 제출 콜백 연결을 구현했습니다.
홈 목록과 상호작용 연동
apps/timo-web/.../home/_containers/*, apps/timo-web/.../home/_hooks/*, apps/timo-web/providers/dnd/*
날짜 헤더에서 생성 모달을 열고 생성된 Todo를 날짜별 목록에 추가하며, 홈 이동·우선순위 표시·수직축 드래그 동작을 갱신했습니다.
컴포넌트 스토리 구성
packages/timo-design-system/src/components/*/*.stories.tsx
날짜, 아이콘, 태그, 우선순위, 반복 컴포넌트의 Storybook 스토리와 상호작용 예제를 추가 또는 갱신했습니다.

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

Sequence Diagram(s)

sequenceDiagram
  participant HomeDayHeaderContainer
  participant CreateTodoModalContainer
  participant CreateTodoModalContent
  participant useCreateTodoSubmit
  participant useHomeTodosByDate
  HomeDayHeaderContainer->>CreateTodoModalContainer: 날짜와 생성 콜백 전달
  CreateTodoModalContainer->>CreateTodoModalContent: 오버레이로 생성 모달 표시
  CreateTodoModalContent->>useCreateTodoSubmit: 검증된 폼 데이터 제출
  useCreateTodoSubmit->>HomeDayHeaderContainer: 로컬 Todo 전달
  HomeDayHeaderContainer->>useHomeTodosByDate: 날짜별 목록에 Todo 추가
Loading

Possibly related PRs

Suggested reviewers: yumin-kim2, jjangminii, ehye1

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning useHomeViewMode의 history 처리 변경과 today 스크롤 훅 추가는 #137 요구사항에 직접 포함되지 않은 부가 변경입니다. 이 변경은 별도 PR로 분리하고, #137 범위에는 투두·태그·모달·드래그 정렬 관련 코드만 남기세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 투두 생성 모달과 홈 투두 카드 UI 구현이라는 변경의 핵심을 정확히 요약합니다.
Description check ✅ Passed 설명이 모달, 디자인시스템, 오버레이, 드래그 정렬 등 실제 변경 범위와 잘 맞습니다.
Linked Issues check ✅ Passed 직접 링크된 #137의 핵심 요구사항인 생성 모달, 태그 모달/토스트, 디자인시스템, 오버레이, 드래그 정렬, useController 바인딩이 반영됐습니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web/137-create-todo-modal
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/web/137-create-todo-modal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Storybook Preview

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

마지막 업데이트: 2026-07-10 15:46 UTC

@github-actions

Copy link
Copy Markdown

Timo Performance Report

Bundle Size — timo-web
라우트 크기 First Load JS
/[locale] 0 B 🟡 205.79 kB
/[locale]/focus 0 B 🟡 205.79 kB
/[locale]/home 0 B 🟡 205.79 kB
/[locale]/onboarding 0 B 🟡 205.79 kB
/[locale]/settings 0 B 🟡 205.79 kB
/[locale]/settings/account 0 B 🟡 205.79 kB
/[locale]/settings/policy 0 B 🟡 205.79 kB
/[locale]/statistics 0 B 🟡 205.79 kB
/[locale]/today 0 B 🟡 205.79 kB

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

Lighthouse — timo-web

⚠️ Lighthouse 결과를 가져오지 못했습니다.

Image Optimization — timo-web

public/ 디렉토리에 이미지가 없습니다.

측정 커밋: c3aa4a7

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

🤖 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 43-76: createTodoRequestSchema의 superRefine에서 repeatType과 반복 필드의
불일치를 검증하도록 보강하세요. WEEKLY가 아닐 때 repeatWeekdays가 비어 있지 않으면 해당 값을 무효화하거나 검증 오류를
추가하고, MONTHLY가 아닐 때 repeatDayOfMonth가 null이 아니면 동일하게 처리하여 stale 데이터가 백엔드로 전송되지
않도록 하세요.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoMemoField.tsx:
- Around line 12-20: textarea in CreateTodoMemoField currently removes the focus
outline without a replacement. Replace the unconditional outline-none styling
with a focus-visible-based accessible focus indicator, using focus-visible:ring
or an equivalent visible border/outline style while preserving the default
non-focused appearance.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoTaskFields.tsx:
- Around line 22-39: title and subtask inputs in CreateTodoTaskFields use
outline-none without a visible keyboard focus indicator. Replace the
unconditional outline removal on both inputs with focus-visible styles that
provide a clear focus outline or equivalent ring while preserving the existing
appearance otherwise.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/tag-modal/CreateTagModalContainer.tsx:
- Around line 55-62: Update the close button in CreateTagModalContainer to use a
tag-modal-specific or shared close-label translation instead of
t("createModal.close"). Prefer adding and using the createTagModal.close key, or
use the existing Common.close key consistently with the translation structure.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContent.tsx:
- Around line 115-119: subtasks 매핑에서 각 항목에 동일한 Date.now() 값이 할당되지 않도록 수정하세요.
CreateTodoModalContent의 subtaskId 생성 로직을 확인해 고유한 ID를 생성하도록 인덱스나 UUID 등 안정적인 식별자를
사용하고, 토글·삭제 로직과의 일관성도 유지하세요.
- Line 122: In the reset call within CreateTodoModalContent, pass the existing
defaultDate to createDefaultValues instead of calling it without arguments, so
the form retains the selected default date after submission.
- Around line 233-237: Expose validation failures from zodResolver in
CreateTodoModalContent instead of silently ignoring them: use formState.errors
to render an inline message or provide an onInvalid callback to handleSubmit,
especially for conditions such as missing WEEKLY days. Keep the existing
title-based disabled state while ensuring users receive a clear error when
validation blocks submission.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeTodosByDate.ts:
- Around line 35-42: handleAddTodo의 TODO API를 구현해 로컬 상태만 갱신하지 않도록 하세요. Todo 생성
API를 호출하고 성공 시 서버 응답으로 상태를 반영하며, 실패 시 낙관적으로 추가한 항목을 제거하는 rollback과 사용자에게 오류를 알리는
처리를 추가하세요.

In `@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_utils/date.ts:
- Around line 79-85: formatDateToIsoDate와 기존 formatDateKey가 동일한 날짜 포맷팅 로직을 중복
구현하고 있습니다. 두 함수의 의미가 동일하면 하나를 제거하고 기존 함수를 사용하며, 별도 공개 API가 필요하면
formatDateToIsoDate가 formatDateKey를 호출하도록 변경해 중복을 없애세요.

In `@apps/timo-web/components/modal/Modal.tsx`:
- Around line 11-17: ModalProps에 선택적 aria-label 프로퍼티를 추가하고, Modal 컴포넌트의
role="dialog" 요소에 해당 값을 전달하세요. 호출부에서 모달 목적을 설명하는 accessible name을 제공할 수 있도록 기본값
없이 구현하고, 기존 className 및 기타 속성 동작은 유지하세요.
- Around line 48-87: Modal의 포커스 관리가 없어 키보드 사용자가 대화상자 밖으로 이동할 수 있습니다. `Modal`
컴포넌트의 `useEffect`와 dialog 요소를 수정해 열리기 전 활성 요소를 저장하고 열린 직후 첫 번째 포커스 가능 요소(없으면
dialog 자체)로 포커스를 이동하세요. `keydown` 처리에서 Tab 이동을 dialog 내부의 포커스 가능 요소 사이로 순환시키고,
닫힐 때 저장한 이전 요소로 포커스를 복원하며 dialog에 필요한 `tabIndex={-1}`를 추가하세요.

In
`@packages/timo-design-system/src/components/icon/icon-graphic/IconGraphic.tsx`:
- Around line 15-23: TodoIconValue와 todoIconSchema의 아이콘 값이 중복 정의되어 있으므로,
IconGraphic.tsx에서 ICON_1~ICON_8을 담은 공용 상수를 먼저 만들고 이를 기반으로 TodoIconValue 타입과
z.enum(...) 스키마를 파생시키세요. 두 정의가 동일한 소스를 참조하도록 관련 선언을 정리하세요.

In `@packages/timo-design-system/src/components/index.ts`:
- Around line 22-42: 기존 소비자가 Modal 관련 타입을 import할 수 있도록
`src/components/index.ts`의 barrel export에서 `ModalButtonVariantTypes`,
`ModalButtonProps` 및 `Modal.tsx`의 관련 props 타입을 다시 re-export하세요. 기존 `Modal` 컴포넌트
export는 유지하고, 필요하면 호환 가능한 별도 `modal` 진입점도 함께 유지하세요.

In
`@packages/timo-design-system/src/components/tag/tag-name-input/TagNameInput.tsx`:
- Around line 30-35: TagNameInput의 입력 접근성을 개선하세요. TagNameInput 컴포넌트에서 useId()로
인스턴스별 고유 ID를 생성하고 input의 id와 접근 가능한 이름(aria-label 또는 연결된 label)에 사용하세요. input의
className에서 outline-none을 제거하고 키보드 포커스가 명확히 보이는 기본 또는 대체 focus 스타일을 유지하세요.
🪄 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: cfbe899b-c4cd-41e8-8eb9-6edf98da2264

📥 Commits

Reviewing files that changed from the base of the PR and between c6e0879 and 87c8f56.

⛔ Files ignored due to path filters (9)
  • packages/timo-design-system/src/icons/source/todo-icon-daily.svg is excluded by !**/*.svg
  • packages/timo-design-system/src/icons/source/todo-icon-etc.svg is excluded by !**/*.svg
  • packages/timo-design-system/src/icons/source/todo-icon-plus.svg is excluded by !**/*.svg
  • packages/timo-design-system/src/icons/source/todo-icon-relationship.svg is excluded by !**/*.svg
  • packages/timo-design-system/src/icons/source/todo-icon-rest.svg is excluded by !**/*.svg
  • packages/timo-design-system/src/icons/source/todo-icon-study.svg is excluded by !**/*.svg
  • packages/timo-design-system/src/icons/source/todo-icon-task.svg is excluded by !**/*.svg
  • packages/timo-design-system/src/icons/source/todo-icon-work.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (46)
  • apps/timo-web/api/todo/todo-schema.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeDateInformation.tsx
  • 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/_components/todo-modal/CreateTodoIconField.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoMemoField.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoTaskFields.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/tag-modal/CreateTagModalContainer.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)/home/_containers/todo-modal/CreateTodoModalContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContent.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-icon-field.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-repeat-field.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-tag-field.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-time-field.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeTodosByDate.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeViewMode.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/todo-time.ts
  • apps/timo-web/app/[locale]/layout.tsx
  • apps/timo-web/components/modal/Modal.tsx
  • apps/timo-web/messages/en.json
  • apps/timo-web/messages/ko.json
  • apps/timo-web/package.json
  • apps/timo-web/providers/OverlayProvider.tsx
  • apps/timo-web/providers/dnd/DndSortableListProvider.tsx
  • packages/timo-design-system/src/components/calendar/date-selector/DateSelector.stories.tsx
  • packages/timo-design-system/src/components/calendar/date-selector/DateSelector.tsx
  • packages/timo-design-system/src/components/icon/icon-graphic/IconGraphic.stories.tsx
  • packages/timo-design-system/src/components/icon/icon-graphic/IconGraphic.tsx
  • packages/timo-design-system/src/components/icon/icon-selector/IconSelector.stories.tsx
  • packages/timo-design-system/src/components/icon/icon-selector/IconSelector.tsx
  • packages/timo-design-system/src/components/index.ts
  • packages/timo-design-system/src/components/priority/priority-icon/PriorityIcon.stories.tsx
  • packages/timo-design-system/src/components/priority/priority-icon/PriorityIcon.tsx
  • packages/timo-design-system/src/components/priority/priority-selector/PrioritySelector.stories.tsx
  • packages/timo-design-system/src/components/priority/priority-selector/PrioritySelector.tsx
  • packages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.stories.tsx
  • packages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.tsx
  • packages/timo-design-system/src/components/tag/tag-name-input/TagNameInput.stories.tsx
  • packages/timo-design-system/src/components/tag/tag-name-input/TagNameInput.tsx
  • packages/timo-design-system/src/components/tag/tag-selector/TagSelector.tsx
  • packages/timo-design-system/src/components/time/time-selector/TimeSelector.tsx
  • packages/timo-design-system/src/components/todo/todo-toolbar/TodoToolbar.tsx

Comment on lines +43 to +76
export const createTodoRequestSchema = z
.object({
icon: todoIconSchema.nullable(),
title: z.string().trim().min(1),
subtasks: z.array(z.string().trim().min(1)).nullable(),
date: z.string().regex(DATE_PATTERN),
duration: z.string().regex(DURATION_PATTERN),
priority: todoPrioritySchema.nullable(),
tagId: z.number().int().positive().nullable(),
repeatType: todoRepeatTypeSchema,
repeatWeekdays: z.array(todoRepeatWeekdaySchema).nullable(),
repeatDayOfMonth: z.number().int().min(1).max(31).nullable(),
memo: z.string().nullable(),
})
.superRefine((value, ctx) => {
if (
value.repeatType === "WEEKLY" &&
(!value.repeatWeekdays || value.repeatWeekdays.length === 0)
) {
ctx.addIssue({
code: "custom",
path: ["repeatWeekdays"],
message: "반복할 요일을 선택해주세요.",
});
}

if (value.repeatType === "MONTHLY" && !value.repeatDayOfMonth) {
ctx.addIssue({
code: "custom",
path: ["repeatDayOfMonth"],
message: "반복할 날짜를 선택해주세요.",
});
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

superRefine에서 반복 타입 불일치 시 stale 데이터 검증 누락

repeatTypeWEEKLY가 아닐 때 repeatWeekdays가 비어있는지, MONTHLY가 아닐 때 repeatDayOfMonth가 null인지 검증하지 않습니다. 사용자가 WEEKLY → DAILY로 변경하면 repeatWeekdays에 이전 선택값이 잔존할 수 있으며, 폼 hook(use-repeat-field.ts)에서 명시적으로 초기화하지 않으면 stale 데이터가 백엔드로 전송됩니다.

🛡️ stale 데이터 방지를 위한 superRefine 보강
   .superRefine((value, ctx) => {
     if (
       value.repeatType === "WEEKLY" &&
       (!value.repeatWeekdays || value.repeatWeekdays.length === 0)
     ) {
       ctx.addIssue({
         code: "custom",
         path: ["repeatWeekdays"],
         message: "반복할 요일을 선택해주세요.",
       });
     }
 
     if (value.repeatType === "MONTHLY" && !value.repeatDayOfMonth) {
       ctx.addIssue({
         code: "custom",
         path: ["repeatDayOfMonth"],
         message: "반복할 날짜를 선택해주세요.",
       });
     }
+
+    if (value.repeatType !== "WEEKLY" && value.repeatWeekdays?.length) {
+      ctx.addIssue({
+        code: "custom",
+        path: ["repeatWeekdays"],
+        message: "반복 요일은 WEEKLY 타입에서만 설정할 수 있습니다.",
+      });
+    }
+
+    if (value.repeatType !== "MONTHLY" && value.repeatDayOfMonth != null) {
+      ctx.addIssue({
+        code: "custom",
+        path: ["repeatDayOfMonth"],
+        message: "반복 날짜는 MONTHLY 타입에서만 설정할 수 있습니다.",
+      });
+    }
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const createTodoRequestSchema = z
.object({
icon: todoIconSchema.nullable(),
title: z.string().trim().min(1),
subtasks: z.array(z.string().trim().min(1)).nullable(),
date: z.string().regex(DATE_PATTERN),
duration: z.string().regex(DURATION_PATTERN),
priority: todoPrioritySchema.nullable(),
tagId: z.number().int().positive().nullable(),
repeatType: todoRepeatTypeSchema,
repeatWeekdays: z.array(todoRepeatWeekdaySchema).nullable(),
repeatDayOfMonth: z.number().int().min(1).max(31).nullable(),
memo: z.string().nullable(),
})
.superRefine((value, ctx) => {
if (
value.repeatType === "WEEKLY" &&
(!value.repeatWeekdays || value.repeatWeekdays.length === 0)
) {
ctx.addIssue({
code: "custom",
path: ["repeatWeekdays"],
message: "반복할 요일을 선택해주세요.",
});
}
if (value.repeatType === "MONTHLY" && !value.repeatDayOfMonth) {
ctx.addIssue({
code: "custom",
path: ["repeatDayOfMonth"],
message: "반복할 날짜를 선택해주세요.",
});
}
});
export const createTodoRequestSchema = z
.object({
icon: todoIconSchema.nullable(),
title: z.string().trim().min(1),
subtasks: z.array(z.string().trim().min(1)).nullable(),
date: z.string().regex(DATE_PATTERN),
duration: z.string().regex(DURATION_PATTERN),
priority: todoPrioritySchema.nullable(),
tagId: z.number().int().positive().nullable(),
repeatType: todoRepeatTypeSchema,
repeatWeekdays: z.array(todoRepeatWeekdaySchema).nullable(),
repeatDayOfMonth: z.number().int().min(1).max(31).nullable(),
memo: z.string().nullable(),
})
.superRefine((value, ctx) => {
if (
value.repeatType === "WEEKLY" &&
(!value.repeatWeekdays || value.repeatWeekdays.length === 0)
) {
ctx.addIssue({
code: "custom",
path: ["repeatWeekdays"],
message: "반복할 요일을 선택해주세요.",
});
}
if (value.repeatType === "MONTHLY" && !value.repeatDayOfMonth) {
ctx.addIssue({
code: "custom",
path: ["repeatDayOfMonth"],
message: "반복할 날짜를 선택해주세요.",
});
}
if (value.repeatType !== "WEEKLY" && value.repeatWeekdays?.length) {
ctx.addIssue({
code: "custom",
path: ["repeatWeekdays"],
message: "반복 요일은 WEEKLY 타입에서만 설정할 수 있습니다.",
});
}
if (value.repeatType !== "MONTHLY" && value.repeatDayOfMonth != null) {
ctx.addIssue({
code: "custom",
path: ["repeatDayOfMonth"],
message: "반복 날짜는 MONTHLY 타입에서만 설정할 수 있습니다.",
});
}
});
🤖 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/api/todo/todo-schema.ts` around lines 43 - 76,
createTodoRequestSchema의 superRefine에서 repeatType과 반복 필드의 불일치를 검증하도록 보강하세요.
WEEKLY가 아닐 때 repeatWeekdays가 비어 있지 않으면 해당 값을 무효화하거나 검증 오류를 추가하고, MONTHLY가 아닐 때
repeatDayOfMonth가 null이 아니면 동일하게 처리하여 stale 데이터가 백엔드로 전송되지 않도록 하세요.

Comment on lines +12 to +20
return (
<textarea
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
rows={1}
className="typo-body-r-12 text-timo-gray-800 w-full resize-none outline-none"
/>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

outline-none으로 포커스 테두리 제거 — 접근성 위반

outline-none이 포커스 시 시각적 표시기를 제거하여 키보드 사용자가 현재 포커스 위치를 파악하기 어렵습니다. 대체 포커스 스타일이 제공되지 않고 있으므로, focus-visible 기반 스타일을 추가해 주세요.

As per coding guidelines, "outline: none으로 포커스 테두리 제거 금지" .

🛡️ 포커스 스타일 추가 제안
-      className="typo-body-r-12 text-timo-gray-800 w-full resize-none outline-none"
+      className="typo-body-r-12 text-timo-gray-800 w-full resize-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<textarea
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
rows={1}
className="typo-body-r-12 text-timo-gray-800 w-full resize-none outline-none"
/>
);
return (
<textarea
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
rows={1}
className="typo-body-r-12 text-timo-gray-800 w-full resize-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
/>
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoMemoField.tsx
around lines 12 - 20, textarea in CreateTodoMemoField currently removes the
focus outline without a replacement. Replace the unconditional outline-none
styling with a focus-visible-based accessible focus indicator, using
focus-visible:ring or an equivalent visible border/outline style while
preserving the default non-focused appearance.

Source: Path instructions

Comment on lines +22 to +39
<div className="flex w-full flex-col items-start gap-1.5">
<div className="flex w-full items-center gap-2">
<Checkbox checked={false} onChange={() => {}} disabled />
<input
{...register("title")}
placeholder={titlePlaceholder}
className="typo-headline-b-14 text-timo-black min-w-0 flex-1 outline-none"
/>
</div>
<div className="flex w-full items-center gap-2">
<Checkbox checked={false} onChange={() => {}} disabled />
<input
value={subtaskInput}
onChange={(event) => onSubtaskInputChange(event.target.value)}
placeholder={subtaskPlaceholder}
className="typo-body-r-12 text-timo-gray-800 min-w-0 flex-1 outline-none"
/>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

outline-none으로 포커스 테두리 제거 — 접근성 위반

title 입력(line 28)과 subtask 입력(line 37) 모두 outline-none을 사용하여 키보드 포커스 표시기가 사라집니다. focus-visible 기반 대체 스타일을 추가해 주세요.

As per coding guidelines, "outline: none으로 포커스 테두리 제거 금지" .

🛡️ 포커스 스타일 추가 제안
       <div className="flex w-full items-center gap-2">
         <Checkbox checked={false} onChange={() => {}} disabled />
         <input
           {...register("title")}
           placeholder={titlePlaceholder}
-          className="typo-headline-b-14 text-timo-black min-w-0 flex-1 outline-none"
+          className="typo-headline-b-14 text-timo-black min-w-0 flex-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
         />
       </div>
       <div className="flex w-full items-center gap-2">
         <Checkbox checked={false} onChange={() => {}} disabled />
         <input
           value={subtaskInput}
           onChange={(event) => onSubtaskInputChange(event.target.value)}
           placeholder={subtaskPlaceholder}
-          className="typo-body-r-12 text-timo-gray-800 min-w-0 flex-1 outline-none"
+          className="typo-body-r-12 text-timo-gray-800 min-w-0 flex-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
         />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex w-full flex-col items-start gap-1.5">
<div className="flex w-full items-center gap-2">
<Checkbox checked={false} onChange={() => {}} disabled />
<input
{...register("title")}
placeholder={titlePlaceholder}
className="typo-headline-b-14 text-timo-black min-w-0 flex-1 outline-none"
/>
</div>
<div className="flex w-full items-center gap-2">
<Checkbox checked={false} onChange={() => {}} disabled />
<input
value={subtaskInput}
onChange={(event) => onSubtaskInputChange(event.target.value)}
placeholder={subtaskPlaceholder}
className="typo-body-r-12 text-timo-gray-800 min-w-0 flex-1 outline-none"
/>
</div>
<div className="flex w-full flex-col items-start gap-1.5">
<div className="flex w-full items-center gap-2">
<Checkbox checked={false} onChange={() => {}} disabled />
<input
{...register("title")}
placeholder={titlePlaceholder}
className="typo-headline-b-14 text-timo-black min-w-0 flex-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
/>
</div>
<div className="flex w-full items-center gap-2">
<Checkbox checked={false} onChange={() => {}} disabled />
<input
value={subtaskInput}
onChange={(event) => onSubtaskInputChange(event.target.value)}
placeholder={subtaskPlaceholder}
className="typo-body-r-12 text-timo-gray-800 min-w-0 flex-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-timo-blue-500"
/>
</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoTaskFields.tsx
around lines 22 - 39, title and subtask inputs in CreateTodoTaskFields use
outline-none without a visible keyboard focus indicator. Replace the
unconditional outline removal on both inputs with focus-visible styles that
provide a clear focus outline or equivalent ring while preserving the existing
appearance otherwise.

Source: Path instructions

Comment on lines +55 to +62
<button
type="button"
aria-label={t("createModal.close")}
onClick={onClose}
className="shrink-0"
>
<DeleteIcon />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

닫기 버튼 aria-label이 태그 모달이 아닌 투두 모달의 키를 참조합니다.

t("createModal.close")를 사용하고 있는데, createTagModal 네임스페이스에 별도의 close 키가 없어 재사용한 것으로 보입니다. 스크린 리더 사용자에게는 맥락이 다소 혼동될 수 있습니다. createTagModal.close 키를 추가하거나, 공통 Common.close 키를 사용하는 것이 더 일관성 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/tag-modal/CreateTagModalContainer.tsx
around lines 55 - 62, Update the close button in CreateTagModalContainer to use
a tag-modal-specific or shared close-label translation instead of
t("createModal.close"). Prefer adding and using the createTagModal.close key, or
use the existing Common.close key consistently with the translation structure.

Comment on lines +11 to +17
export interface ModalProps {
isOpen: boolean;
onClose: () => void;
onExited?: () => void;
children: ReactNode;
className?: string;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

role="dialog"에 접근 가능한 이름(accessible name)이 없습니다.

role="dialog" 요소에 aria-label 또는 aria-labelledby가 선언되지 않아, 스크린 리더 사용자에게 모달의 목적이 전달되지 않습니다. ModalProps에 선택적 aria-label 프로퍼티를 추가하는 것을 권장합니다.

🔧 제안: aria-label prop 추가
 export interface ModalProps {
   isOpen: boolean;
   onClose: () => void;
   onExited?: () => void;
   children: ReactNode;
   className?: string;
+  ariaLabel?: string;
 }

 export const Modal = ({
   isOpen,
   onClose,
   onExited,
   children,
   className,
+  ariaLabel,
 }: ModalProps) => {

그리고 dialog 요소에 적용:

       <div
         role="dialog"
         aria-modal="true"
+        aria-label={ariaLabel}
         tabIndex={-1}
         className={cn(

Also applies to: 77-87

🤖 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/Modal.tsx` around lines 11 - 17, ModalProps에
선택적 aria-label 프로퍼티를 추가하고, Modal 컴포넌트의 role="dialog" 요소에 해당 값을 전달하세요. 호출부에서 모달
목적을 설명하는 accessible name을 제공할 수 있도록 기본값 없이 구현하고, 기존 className 및 기타 속성 동작은 유지하세요.

Comment on lines +48 to +87
useEffect(() => {
if (!shouldRender) return;

document.body.style.overflow = "hidden";

const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};

document.addEventListener("keydown", handleEscape);

return () => {
document.body.style.overflow = "";
document.removeEventListener("keydown", handleEscape);
};
}, [shouldRender, onClose]);

if (!shouldRender) return null;

return createPortal(
<>
<div
className={cn(
"bg-timo-overlay fixed inset-0 z-40 transition-opacity duration-200 ease-out",
isVisible ? "opacity-100" : "opacity-0",
)}
onClick={onClose}
aria-hidden="true"
/>
<div
role="dialog"
aria-modal="true"
className={cn(
"fixed top-1/2 left-1/2 z-50 flex -translate-x-1/2 -translate-y-1/2 flex-col rounded-[4px] bg-white transition-all duration-200 ease-out",
isVisible ? "scale-100 opacity-100" : "scale-95 opacity-0",
className,
)}
>
{children}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

포커스 트랩과 포커스 관리가 구현되어 있지 않습니다.

aria-modal="true"가 선언된 모달에서는 Tab 키가 모달 내부에서만 순환하도록 포커스 트랩이 필요합니다. 현재 구현에서는 Escape 키 처리만 있고, 모달이 열릴 때 포커스를 모달로 이동하거나 닫힐 때 이전 요소로 복원하는 로직이 없습니다. 키보드 사용자가 모달 밖으로 탭 이탈할 수 있어 접근성 보장이 되지 않습니다.

WAI-ARIA Dialog Pattern 권장 사항: https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/

🔧 제안: 포커스 트랩 및 포커스 관리 추가
 export const Modal = ({
   isOpen,
   onClose,
   onExited,
   children,
   className,
 }: ModalProps) => {
   const [shouldRender, setShouldRender] = useState(isOpen);
   const [isVisible, setIsVisible] = useState(false);
+  const dialogRef = useRef<HTMLDivElement>(null);
+  const previousFocusRef = useRef<HTMLElement | null>(null);

   useEffect(() => {
     if (!isOpen) {
       setIsVisible(false);
       const hideTimer = setTimeout(() => {
         setShouldRender(false);
         onExited?.();
       }, EXIT_ANIMATION_DURATION);
       return () => clearTimeout(hideTimer);
     }
     setShouldRender(true);
     const showFrame = requestAnimationFrame(() => setIsVisible(true));
     return () => cancelAnimationFrame(showFrame);
   }, [isOpen, onExited]);

   useEffect(() => {
     if (!shouldRender) return;

+    // 포커스 저장 및 모달로 이동
+    previousFocusRef.current = document.activeElement as HTMLElement;
+    dialogRef.current?.focus();
+
     document.body.style.overflow = "hidden";

     const handleEscape = (event: KeyboardEvent) => {
       if (event.key === "Escape") onClose();
     };

-    document.addEventListener("keydown", handleEscape);
+    const handleTab = (event: KeyboardEvent) => {
+      if (event.key !== "Tab" || !dialogRef.current) return;
+      const focusables = dialogRef.current.querySelectorAll<HTMLElement>(
+        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+      );
+      if (focusables.length === 0) return;
+      const first = focusables[0];
+      const last = focusables[focusables.length - 1];
+      if (event.shiftKey && document.activeElement === first) {
+        event.preventDefault();
+        last.focus();
+      } else if (!event.shiftKey && document.activeElement === last) {
+        event.preventDefault();
+        first.focus();
+      }
+    };
+
+    document.addEventListener("keydown", handleEscape);
+    document.addEventListener("keydown", handleTab);

     return () => {
       document.body.style.overflow = "";
       document.removeEventListener("keydown", handleEscape);
+      document.removeEventListener("keydown", handleTab);
+      previousFocusRef.current?.focus();
     };
   }, [shouldRender, onClose]);

   if (!shouldRender) return null;

   return createPortal(
     <>
       <div
         className={cn(
           "bg-timo-overlay fixed inset-0 z-40 transition-opacity duration-200 ease-out",
           isVisible ? "opacity-100" : "opacity-0",
         )}
         onClick={onClose}
         aria-hidden="true"
       />
       <div
         role="dialog"
         aria-modal="true"
+        tabIndex={-1}
+        ref={dialogRef}
         className={cn(
           "fixed top-1/2 left-1/2 z-50 flex -translate-x-1/2 -translate-y-1/2 flex-col rounded-[4px] bg-white transition-all duration-200 ease-out",
           isVisible ? "scale-100 opacity-100" : "scale-95 opacity-0",
           className,
         )}
       >
         {children}
       </div>
     </>,
     document.body,
   );
🤖 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/Modal.tsx` around lines 48 - 87, Modal의 포커스
관리가 없어 키보드 사용자가 대화상자 밖으로 이동할 수 있습니다. `Modal` 컴포넌트의 `useEffect`와 dialog 요소를 수정해
열리기 전 활성 요소를 저장하고 열린 직후 첫 번째 포커스 가능 요소(없으면 dialog 자체)로 포커스를 이동하세요. `keydown`
처리에서 Tab 이동을 dialog 내부의 포커스 가능 요소 사이로 순환시키고, 닫힐 때 저장한 이전 요소로 포커스를 복원하며 dialog에
필요한 `tabIndex={-1}`를 추가하세요.

Comment on lines +15 to +23
export type TodoIconValue =
| "ICON_1"
| "ICON_2"
| "ICON_3"
| "ICON_4"
| "ICON_5"
| "ICON_6"
| "ICON_7"
| "ICON_8";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 두 정의가 동일한 값을 가지고 있는지 교차 검증
echo "=== todo-schema.ts ===" && rg -n 'ICON_[0-9]' apps/timo-web/api/todo/todo-schema.ts | head -20
echo "=== IconGraphic.tsx ===" && rg -n 'ICON_[0-9]' packages/timo-design-system/src/components/icon/icon-graphic/IconGraphic.tsx | head -20

Repository: Team-Timo/Timo-client

Length of output: 703


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== IconGraphic.tsx ==="
cat -n packages/timo-design-system/src/components/icon/icon-graphic/IconGraphic.tsx | sed -n '1,220p'

echo "=== todo-schema.ts ==="
cat -n apps/timo-web/api/todo/todo-schema.ts | sed -n '1,220p'

echo "=== usages of TodoIconValue ==="
rg -n "TodoIconValue|todoIconSchema|ICON_GRAPHIC\\[" packages/timo-design-system apps/timo-web -g '!**/node_modules/**'

Repository: Team-Timo/Timo-client

Length of output: 7303


아이콘 값 목록을 공용 소스로 묶어주세요
TodoIconValuetodoIconSchema가 같은 "ICON_1"~"ICON_8" 값을 각각 관리하고 있어 변경 시 쉽게 어긋납니다. 공용 배열/상수 하나를 두고 z.enum(...)TodoIconValue를 거기서 파생시키면 UI와 요청 스키마의 동기화를 한 번에 잡을 수 있습니다. Zod enum 문서도 같이 보면 좋습니다: https://zod.dev/?id=enums

🤖 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/icon/icon-graphic/IconGraphic.tsx`
around lines 15 - 23, TodoIconValue와 todoIconSchema의 아이콘 값이 중복 정의되어 있으므로,
IconGraphic.tsx에서 ICON_1~ICON_8을 담은 공용 상수를 먼저 만들고 이를 기반으로 TodoIconValue 타입과
z.enum(...) 스키마를 파생시키세요. 두 정의가 동일한 소스를 참조하도록 관련 선언을 정리하세요.

Comment thread packages/timo-design-system/src/components/index.ts
Comment on lines +30 to +35
<input
value={value}
onChange={(event) => onChange(event.target.value.slice(0, maxLength))}
maxLength={maxLength}
className="typo-headline-m-14 text-timo-gray-900 min-w-0 flex-1 outline-none"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

접근성: 포커스 아웃라인 제거 및 라벨 연결 누락

두 가지 접근성 문제가 있습니다:

  1. outline-none 클래스 (Line 34) — 키보드 사용자가 포커스 위치를 시각적으로 파악할 수 없습니다. Path instruction에서 outline: none으로 포커스 테두리 제거를 명시적으로 금지하고 있습니다.
  2. <input>id<label htmlFor> 연결 누락 — 스크린 리더 사용자가 입력 필드의 목적을 인지할 수 없습니다. Path instruction에서 <input>id 부여 후 <label htmlFor> 연결을 요구합니다.

이전 PR에서 학습한 것처럼 useId()를 사용해 인스턴스별 고유 id를 생성하고, 이를 idaria-label에 연결하는 것을 권장합니다.

참고: React useId 문서

♿ 접근성 개선 제안
+import { useId } from "react";
 import { cn } from "../../../lib";
 
 const DEFAULT_MAX_LENGTH = 10;
 
 export interface TagNameInputProps {
   value: string;
   onChange: (value: string) => void;
   maxLength?: number;
   isError?: boolean;
+  label?: string;
   maxLengthHint: string;
   duplicateHint: string;
 }
 
 export const TagNameInput = ({
   value,
   onChange,
   maxLength = DEFAULT_MAX_LENGTH,
   isError = false,
+  label,
   maxLengthHint,
   duplicateHint,
 }: TagNameInputProps) => {
+  const inputId = useId();
+
   return (
     <div className="flex w-full flex-col items-start gap-2">
+      {label && (
+        <label
+          htmlFor={inputId}
+          className="typo-caption-r-10 text-timo-gray-700"
+        >
+          {label}
+        </label>
+      )}
       <div
         className={cn(
           "flex w-full items-center gap-2.5 rounded-[4px] border border-solid px-3 py-2.5",
           isError ? "border-timo-red" : "border-timo-gray-500",
         )}
       >
         <input
+          id={inputId}
+          aria-label={label ? undefined : maxLengthHint}
           value={value}
           onChange={(event) => onChange(event.target.value.slice(0, maxLength))}
           maxLength={maxLength}
-          className="typo-headline-m-14 text-timo-gray-900 min-w-0 flex-1 outline-none"
+          className="typo-headline-m-14 text-timo-gray-900 min-w-0 flex-1 focus:outline-2 focus:outline-timo-blue-300"
         />
         <span className="typo-caption-r-10 text-timo-gray-700 shrink-0 whitespace-nowrap">
           {value.length}/{maxLength}
         </span>
       </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input
value={value}
onChange={(event) => onChange(event.target.value.slice(0, maxLength))}
maxLength={maxLength}
className="typo-headline-m-14 text-timo-gray-900 min-w-0 flex-1 outline-none"
/>
<input
id={inputId}
aria-label={label ? undefined : maxLengthHint}
value={value}
onChange={(event) => onChange(event.target.value.slice(0, maxLength))}
maxLength={maxLength}
className="typo-headline-m-14 text-timo-gray-900 min-w-0 flex-1 focus:outline-2 focus:outline-timo-blue-300"
/>
🤖 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/tag/tag-name-input/TagNameInput.tsx`
around lines 30 - 35, TagNameInput의 입력 접근성을 개선하세요. TagNameInput 컴포넌트에서 useId()로
인스턴스별 고유 ID를 생성하고 input의 id와 접근 가능한 이름(aria-label 또는 연결된 label)에 사용하세요. input의
className에서 outline-none을 제거하고 키보드 포커스가 명확히 보이는 기본 또는 대체 focus 스타일을 유지하세요.

Sources: Path instructions, Learnings

kimminna added 2 commits July 11, 2026 03:50
- _hooks/useHomeTodayScroll.ts를 use-home-today-scroll.ts로 변경했습니다
- _hooks/useHomeTodosByDate.ts를 use-home-todos-by-date.ts로 변경했습니다
- _hooks/useHomeViewMode.ts를 use-home-view-mode.ts로 변경했습니다
- 네이밍 컨벤션에 맞춰 참조하는 컨테이너의 import 경로를 갱신했습니다
- 투두 생성(mock) 조립 로직을 use-create-todo-submit 훅으로 분리했습니다
- CreateTodoModalContainer가 useCreateTodoSubmit을 통해 생성 사이드 이펙트를 소유하도록 했습니다
- CreateTodoModalContent는 onSubmit prop만 호출하도록 하여 폼 검증·렌더링만 담당하게 했습니다

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

Caution

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

⚠️ Outside diff range comments (1)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx (1)

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

선택한 날짜를 handleAddTodo까지 전달하세요.
HomeTodoContainer.tsx:94에서 dateKey만 쓰면 모달에서 바꾼 data.date가 반영되지 않습니다. use-create-todo-submit.ts:8-38에서 buildTodoFromRequestdata.date를 버리고 있어서, 날짜를 수정해도 원래 컬럼에 추가됩니다. data.date를 상위로 넘겨 dateKey를 다시 계산하거나, Tododate를 포함해 저장 경로를 유지하세요. React Hook Form handleSubmit처럼 제출값은 중간에서 잃지 않는 구조가 좋습니다: https://react-hook-form.com/docs/useform/handlesubmit

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
at line 94, 모달에서 변경한 날짜가 저장 경로에서 유실되고 있습니다. HomeTodoContainer의 onCreateTodo와
handleAddTodo 호출 흐름에서 제출된 data.date를 상위로 전달하고, use-create-todo-submit의
buildTodoFromRequest가 해당 날짜를 보존하도록 수정하세요. 필요하면 data.date로 dateKey를 재계산해 올바른 날짜
컬럼에 추가되게 하며, React Hook Form handleSubmit처럼 제출값을 중간에서 버리지 않도록 전체 호출 체인을 점검하세요.
🤖 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/_hooks/todo-modal/use-create-todo-submit.ts:
- Line 12: use-create-todo-submit.ts의 서브태스크 생성 로직에서 .map() 내부의 Date.now() 기반
todoId가 모든 항목에 동일하게 생성되고 상위 todoId와 충돌할 수 있습니다. 고유 ID 생성 유틸리티 또는 충돌을 방지하는 안정적인
ID 생성 방식을 사용해 각 서브태스크의 todoId를 독립적으로 생성하고, 관련 23–24행의 ID 할당도 동일하게 수정하세요.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts:
- Around line 16-20: When apiDays changes, the useEffect currently replaces
todosByDate and discards todos added locally by handleAddTodo. Preserve pending
local additions by tracking them in a separate overlay state and merging it with
the API-derived entries, or by merging additions into the incoming apiDays
result; update handleAddTodo and the apiDays synchronization logic consistently
so filter/baseDate refreshes retain unsynced todos.

---

Outside diff comments:
In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx:
- Line 94: 모달에서 변경한 날짜가 저장 경로에서 유실되고 있습니다. HomeTodoContainer의 onCreateTodo와
handleAddTodo 호출 흐름에서 제출된 data.date를 상위로 전달하고, use-create-todo-submit의
buildTodoFromRequest가 해당 날짜를 보존하도록 수정하세요. 필요하면 data.date로 dateKey를 재계산해 올바른 날짜
컬럼에 추가되게 하며, React Hook Form handleSubmit처럼 제출값을 중간에서 버리지 않도록 전체 호출 체인을 점검하세요.
🪄 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: efcb067f-3026-495d-8a71-204b9d87d5f0

📥 Commits

Reviewing files that changed from the base of the PR and between 87c8f56 and 97f30e1.

📒 Files selected for processing (8)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeHeaderContainer.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-modal/CreateTodoModalContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContent.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-today-scroll.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode.ts

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

Caution

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

⚠️ Outside diff range comments (1)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx (1)

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

선택한 날짜를 handleAddTodo까지 전달하세요.
HomeTodoContainer.tsx:94에서 dateKey만 쓰면 모달에서 바꾼 data.date가 반영되지 않습니다. use-create-todo-submit.ts:8-38에서 buildTodoFromRequestdata.date를 버리고 있어서, 날짜를 수정해도 원래 컬럼에 추가됩니다. data.date를 상위로 넘겨 dateKey를 다시 계산하거나, Tododate를 포함해 저장 경로를 유지하세요. React Hook Form handleSubmit처럼 제출값은 중간에서 잃지 않는 구조가 좋습니다: https://react-hook-form.com/docs/useform/handlesubmit

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
at line 94, 모달에서 변경한 날짜가 저장 경로에서 유실되고 있습니다. HomeTodoContainer의 onCreateTodo와
handleAddTodo 호출 흐름에서 제출된 data.date를 상위로 전달하고, use-create-todo-submit의
buildTodoFromRequest가 해당 날짜를 보존하도록 수정하세요. 필요하면 data.date로 dateKey를 재계산해 올바른 날짜
컬럼에 추가되게 하며, React Hook Form handleSubmit처럼 제출값을 중간에서 버리지 않도록 전체 호출 체인을 점검하세요.
🤖 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/_hooks/todo-modal/use-create-todo-submit.ts:
- Line 12: use-create-todo-submit.ts의 서브태스크 생성 로직에서 .map() 내부의 Date.now() 기반
todoId가 모든 항목에 동일하게 생성되고 상위 todoId와 충돌할 수 있습니다. 고유 ID 생성 유틸리티 또는 충돌을 방지하는 안정적인
ID 생성 방식을 사용해 각 서브태스크의 todoId를 독립적으로 생성하고, 관련 23–24행의 ID 할당도 동일하게 수정하세요.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts:
- Around line 16-20: When apiDays changes, the useEffect currently replaces
todosByDate and discards todos added locally by handleAddTodo. Preserve pending
local additions by tracking them in a separate overlay state and merging it with
the API-derived entries, or by merging additions into the incoming apiDays
result; update handleAddTodo and the apiDays synchronization logic consistently
so filter/baseDate refreshes retain unsynced todos.

---

Outside diff comments:
In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx:
- Line 94: 모달에서 변경한 날짜가 저장 경로에서 유실되고 있습니다. HomeTodoContainer의 onCreateTodo와
handleAddTodo 호출 흐름에서 제출된 data.date를 상위로 전달하고, use-create-todo-submit의
buildTodoFromRequest가 해당 날짜를 보존하도록 수정하세요. 필요하면 data.date로 dateKey를 재계산해 올바른 날짜
컬럼에 추가되게 하며, React Hook Form handleSubmit처럼 제출값을 중간에서 버리지 않도록 전체 호출 체인을 점검하세요.
🪄 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: efcb067f-3026-495d-8a71-204b9d87d5f0

📥 Commits

Reviewing files that changed from the base of the PR and between 87c8f56 and 97f30e1.

📒 Files selected for processing (8)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeHeaderContainer.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-modal/CreateTodoModalContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-modal/CreateTodoModalContent.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-today-scroll.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode.ts
🛑 Comments failed to post (2)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit.ts (1)

12-12: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Date.now() 기반 ID 충돌 — 서브태스크가 모두 동일한 subtaskId를 갖습니다.

이전 리뷰에서 CreateTodoModalContent.tsx의 동일한 문제를 지적했고 수정되었으나, 리팩토링으로 로직이 이 파일로 이동하면서 수정이 유실되었습니다. .map()Date.now()가 동기 호출되어 모든 서브태스크가 같은 ID를 갖게 되고, todoId와도 충돌할 수 있습니다.

 ): Todo => ({
-  todoId: Date.now(),
+  todoId: Date.now(),
   icon: data.icon,
   title: data.title,
   completed: false,
   durationSeconds: convertApiDurationToSeconds(data.duration),
   priority: data.priority ?? "MEDIUM",
   tag: { tagId: tag.id, name: tag.name },
   hasMemo: Boolean(data.memo?.trim()),
   isRepeated: data.repeatType !== "NONE",
   timerStatus: "STOPPED",
   sortOrder: 0,
   subtasks: (data.subtasks ?? []).map((content, index) => ({
-    subtaskId: Date.now(),
+    subtaskId: Date.now() + index,
     content,
     completed: false,
   })),
 }),

참고: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now

Also applies to: 23-24

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-create-todo-submit.ts
at line 12, use-create-todo-submit.ts의 서브태스크 생성 로직에서 .map() 내부의 Date.now() 기반
todoId가 모든 항목에 동일하게 생성되고 상위 todoId와 충돌할 수 있습니다. 고유 ID 생성 유틸리티 또는 충돌을 방지하는 안정적인
ID 생성 방식을 사용해 각 서브태스크의 todoId를 독립적으로 생성하고, 관련 23–24행의 ID 할당도 동일하게 수정하세요.
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts (1)

16-20: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts'

echo "== file exists =="
git ls-files "$FILE"

echo "== outline =="
ast-grep outline "$FILE" --view expanded || true

echo "== file contents =="
cat -n "$FILE"

Repository: Team-Timo/Timo-client

Length of output: 4349


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find related usages and the hook's consumers to understand state ownership.
rg -n "useHomeTodosByDate|handleAddTodo|todosByDate|apiDays" apps/timo-web/app/[locale]/\(main\)/\(with-time-sidebar\)/home -S

# Show nearby files if present.
fd -a "use-home-todos-by-date" apps/timo-web/app/[locale]/\(main\)/\(with-time-sidebar\)/home || true

Repository: Team-Timo/Timo-client

Length of output: 2864


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts'

# Show only the hook and its immediate neighborhood with line numbers.
sed -n '1,220p' "$FILE" | cat -n

echo
echo "== related search =="
rg -n "setTodosByDate|handleAddTodo|apiDays" "$FILE" -n -S

Repository: Team-Timo/Timo-client

Length of output: 4629


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx'
sed -n '1,180p' "$FILE" | cat -n

Repository: Team-Timo/Timo-client

Length of output: 6861


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether apiDays is stable/memoized and how todosByDate is consumed.
rg -n "useMemo|useHomeTodosByDate|todosByDate\[dateKey\]|day\.todos|isWeekView" \
  apps/timo-web/app/[locale]/\(main\)/\(with-time-sidebar\)/home/_containers/HomeTodoContainer.tsx -n -S

Repository: Team-Timo/Timo-client

Length of output: 850


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx'
echo "== file exists =="
git ls-files "$FILE"

echo "== outline =="
ast-grep outline "$FILE" --view expanded || true

echo "== contents =="
sed -n '1,220p' "$FILE" | cat -n

Repository: Team-Timo/Timo-client

Length of output: 7279


apiDays 갱신 시 로컬 추가 투두를 보존하세요. 구조는 깔끔합니다. 다만 handleAddTodo가 만든 항목은 로컬 상태에만 남아 있어서, filter/baseDate 변경으로 apiDays가 다시 들어오면 useEffecttodosByDate를 통째로 덮어씁니다. 아직 API에 반영되지 않은 추가분은 별도 오버레이 상태로 분리하거나 새 apiDays와 병합해 주세요. React의 derived state 안내도 참고할 만합니다: https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-todos-by-date.ts
around lines 16 - 20, When apiDays changes, the useEffect currently replaces
todosByDate and discards todos added locally by handleAddTodo. Preserve pending
local additions by tracking them in a separate overlay state and merging it with
the API-derived entries, or by merging additions into the incoming apiDays
result; update handleAddTodo and the apiDays synchronization logic consistently
so filter/baseDate refreshes retain unsynced todos.

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] 투두 생성 모달 및 홈 투두 카드 UI 구현

1 participant