[FEAT] 투두 생성 모달 및 홈 투두 카드 UI 구현#138
Conversation
- overlay-kit, @dnd-kit/modifiers 의존성을 추가했습니다
- 아이콘 선택기, 날짜 선택기, 태그 입력, 투두 툴바 컴포넌트를 추가했습니다 - 우선순위/반복/태그/시간 선택기를 투두 모달 요구사항에 맞게 수정했습니다 - 투두 아이콘 SVG 8종을 추가했습니다
- 투두 생성 요청 스키마를 정의했습니다 - 오버레이 기반 모달 컴포넌트와 프로바이더를 추가하고 루트 레이아웃에 연결했습니다
- 투두 생성 모달과 태그 생성 모달을 구현했습니다 - 홈 투두 카드와 데이 헤더 UI를 구현하고 드래그 정렬을 연동했습니다 - 모달 폼 필드 바인딩을 useController 기반으로 구현했습니다 - 드래그 정렬을 세로 축으로 제한했습니다 - 관련 폴더를 기능 단위(todo-card/todo-modal/tag-modal)로 구성했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
Walkthrough투두 생성 API 스키마와 디자인시스템 컴포넌트가 추가되었고, 오버레이 기반 생성 모달·태그 모달·폼 훅이 홈 화면에 연결되었습니다. 생성된 Todo는 날짜별 목록에 반영되며, 우선순위·반복 타입과 관련된 UI 및 Storybook 설정도 갱신되었습니다. Changes투두 생성 기능
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 추가
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
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
측정 커밋: |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (9)
packages/timo-design-system/src/icons/source/todo-icon-daily.svgis excluded by!**/*.svgpackages/timo-design-system/src/icons/source/todo-icon-etc.svgis excluded by!**/*.svgpackages/timo-design-system/src/icons/source/todo-icon-plus.svgis excluded by!**/*.svgpackages/timo-design-system/src/icons/source/todo-icon-relationship.svgis excluded by!**/*.svgpackages/timo-design-system/src/icons/source/todo-icon-rest.svgis excluded by!**/*.svgpackages/timo-design-system/src/icons/source/todo-icon-study.svgis excluded by!**/*.svgpackages/timo-design-system/src/icons/source/todo-icon-task.svgis excluded by!**/*.svgpackages/timo-design-system/src/icons/source/todo-icon-work.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (46)
apps/timo-web/api/todo/todo-schema.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeDateInformation.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoIconField.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoMemoField.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-modal/CreateTodoTaskFields.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/tag-modal/CreateTagModalContainer.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-icon-field.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-repeat-field.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/todo-modal/use-tag-field.tsxapps/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/useHomeTodosByDate.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/useHomeViewMode.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_types/todo-type.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/todo-time.tsapps/timo-web/app/[locale]/layout.tsxapps/timo-web/components/modal/Modal.tsxapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonapps/timo-web/package.jsonapps/timo-web/providers/OverlayProvider.tsxapps/timo-web/providers/dnd/DndSortableListProvider.tsxpackages/timo-design-system/src/components/calendar/date-selector/DateSelector.stories.tsxpackages/timo-design-system/src/components/calendar/date-selector/DateSelector.tsxpackages/timo-design-system/src/components/icon/icon-graphic/IconGraphic.stories.tsxpackages/timo-design-system/src/components/icon/icon-graphic/IconGraphic.tsxpackages/timo-design-system/src/components/icon/icon-selector/IconSelector.stories.tsxpackages/timo-design-system/src/components/icon/icon-selector/IconSelector.tsxpackages/timo-design-system/src/components/index.tspackages/timo-design-system/src/components/priority/priority-icon/PriorityIcon.stories.tsxpackages/timo-design-system/src/components/priority/priority-icon/PriorityIcon.tsxpackages/timo-design-system/src/components/priority/priority-selector/PrioritySelector.stories.tsxpackages/timo-design-system/src/components/priority/priority-selector/PrioritySelector.tsxpackages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.stories.tsxpackages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.tsxpackages/timo-design-system/src/components/tag/tag-name-input/TagNameInput.stories.tsxpackages/timo-design-system/src/components/tag/tag-name-input/TagNameInput.tsxpackages/timo-design-system/src/components/tag/tag-selector/TagSelector.tsxpackages/timo-design-system/src/components/time/time-selector/TimeSelector.tsxpackages/timo-design-system/src/components/todo/todo-toolbar/TodoToolbar.tsx
| 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: "반복할 날짜를 선택해주세요.", | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
superRefine에서 반복 타입 불일치 시 stale 데이터 검증 누락
repeatType이 WEEKLY가 아닐 때 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.
| 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 데이터가 백엔드로 전송되지 않도록 하세요.
| 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" | ||
| /> | ||
| ); |
There was a problem hiding this comment.
🔒 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.
| 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
| <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> |
There was a problem hiding this comment.
🔒 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.
| <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
| <button | ||
| type="button" | ||
| aria-label={t("createModal.close")} | ||
| onClick={onClose} | ||
| className="shrink-0" | ||
| > | ||
| <DeleteIcon /> | ||
| </button> |
There was a problem hiding this comment.
📐 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.
| export interface ModalProps { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| onExited?: () => void; | ||
| children: ReactNode; | ||
| className?: string; | ||
| } |
There was a problem hiding this comment.
🔒 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 및 기타 속성 동작은 유지하세요.
| 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> |
There was a problem hiding this comment.
🩺 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}`를 추가하세요.
| export type TodoIconValue = | ||
| | "ICON_1" | ||
| | "ICON_2" | ||
| | "ICON_3" | ||
| | "ICON_4" | ||
| | "ICON_5" | ||
| | "ICON_6" | ||
| | "ICON_7" | ||
| | "ICON_8"; |
There was a problem hiding this comment.
📐 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 -20Repository: 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
아이콘 값 목록을 공용 소스로 묶어주세요
TodoIconValue와 todoIconSchema가 같은 "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(...) 스키마를 파생시키세요. 두 정의가 동일한 소스를 참조하도록 관련 선언을 정리하세요.
| <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" | ||
| /> |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
접근성: 포커스 아웃라인 제거 및 라벨 연결 누락
두 가지 접근성 문제가 있습니다:
outline-none클래스 (Line 34) — 키보드 사용자가 포커스 위치를 시각적으로 파악할 수 없습니다. Path instruction에서outline: none으로 포커스 테두리 제거를 명시적으로 금지하고 있습니다.<input>에id및<label htmlFor>연결 누락 — 스크린 리더 사용자가 입력 필드의 목적을 인지할 수 없습니다. Path instruction에서<input>에id부여 후<label htmlFor>연결을 요구합니다.
이전 PR에서 학습한 것처럼 useId()를 사용해 인스턴스별 고유 id를 생성하고, 이를 id와 aria-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.
| <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
- _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만 호출하도록 하여 폼 검증·렌더링만 담당하게 했습니다
There was a problem hiding this comment.
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에서buildTodoFromRequest가data.date를 버리고 있어서, 날짜를 수정해도 원래 컬럼에 추가됩니다.data.date를 상위로 넘겨dateKey를 다시 계산하거나,Todo에date를 포함해 저장 경로를 유지하세요. React Hook FormhandleSubmit처럼 제출값은 중간에서 잃지 않는 구조가 좋습니다: 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
📒 Files selected for processing (8)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeHeaderContainer.tsxapps/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/_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/use-home-today-scroll.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/_hooks/use-home-view-mode.ts
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: 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에서buildTodoFromRequest가data.date를 버리고 있어서, 날짜를 수정해도 원래 컬럼에 추가됩니다.data.date를 상위로 넘겨dateKey를 다시 계산하거나,Todo에date를 포함해 저장 경로를 유지하세요. React Hook FormhandleSubmit처럼 제출값은 중간에서 잃지 않는 구조가 좋습니다: 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
📒 Files selected for processing (8)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeHeaderContainer.tsxapps/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/_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/use-home-today-scroll.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/_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 || trueRepository: 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 -SRepository: 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 -nRepository: 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 -SRepository: 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 -nRepository: Team-Timo/Timo-client
Length of output: 7279
apiDays갱신 시 로컬 추가 투두를 보존하세요. 구조는 깔끔합니다. 다만handleAddTodo가 만든 항목은 로컬 상태에만 남아 있어서,filter/baseDate변경으로apiDays가 다시 들어오면useEffect가todosByDate를 통째로 덮어씁니다. 아직 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.
ISSUE 🔗
close #137
What is this PR? 🔍
홈 화면에서 할 일을 생성·조회·정렬할 수 있는 투두 생성 모달과 투두 카드 UI를 구현했습니다.
배경
의존성
overlay-kit,@dnd-kit/modifiers의존성을 추가했습니다.overlay-kit이, 드래그를 세로 축으로 제한하기 위해@dnd-kit/modifiers가 필요했습니다.overlay.open()으로 모달의isOpen/close/unmount를 관리하고,DndContext의modifiersprop에restrictToVerticalAxis를 전달했습니다.디자인시스템 컴포넌트
IconSelector/IconGraphic/DateSelector/TagNameInput/TodoToolbar를 새로 추가하고, 기존PriorityIcon/PrioritySelector/RepeatSelector/TagSelector/TimeSelector를 모달 요구사항에 맞게 수정했습니다.API 스키마 및 오버레이 모달 인프라
createTodoRequestSchema)를 정의하고, 오버레이 기반Modal컴포넌트와OverlayProvider를 루트 레이아웃에 연결했습니다.date/duration포맷을 정규식으로 검증하고,repeatType에 따라repeatWeekdays/repeatDayOfMonth필수 여부를superRefine으로 처리했습니다.OverlayProvider는layout.tsx의QueryProvider내부에 배치했습니다.@repo/timo-design-system의Modal(Modal.Root+Trigger+Panel)은Trigger와Panel이 같은 트리에 선언되고isOpen을 내부 state로만 관리하는 비제어(uncontrolled) 컴파운드 컴포넌트입니다. 반면 이번 요구사항은overlay.open()으로 트리거와 무관한 위치(날짜 컬럼별 추가 버튼)에서 모달을 열고, 닫힘 애니메이션이 끝난 뒤에만unmount()를 호출해야 해서isOpen/onClose/onExited를 외부에서 제어할 수 있는 형태가 필요했습니다. 디자인시스템 Modal은 이 "외부 제어 + exit-aware unmount" 계약을 지원하지 않아 그대로 재사용할 수 없었고, 그래서apps/timo-web/components/modal/Modal.tsx를 앱 레이어에 별도로 만들어overlay-kit과 연결했습니다.홈 투두 생성 모달 및 투두 카드
todo-card/todo-modal/tag-modal기능 단위로 재구성했습니다._components/_containers/_hooks가 타입별로만 분리돼 있어 하나의 기능을 수정할 때 여러 폴더를 오가야 했고, 모달 폼도watch/setValue를 필드마다 직접 호출해 필드 하나가 바뀌어도 폼 전체가 리렌더되는 구조였습니다.useIconField/useTagField/useTimeField/useRepeatField각 훅이control을 받아 내부에서useController로 해당 폼 필드를 직접 구독하도록 바꿔, 컨테이너 컴포넌트는 훅이 반환한 값을 JSX에 배치하기만 하면 되도록 했습니다. 홈 투두 카드는@dnd-kit/sortable의useSortable로 정렬 가능하게 만들었고,restrictToVerticalAxis로 드래그 축을 세로로 제한했습니다. 디자인에 없던 카드 그랩(드래그 핸들) 아이콘은 제거했습니다.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— 미실행