Skip to content

[FIX] 투두 생성/조회 모달 및 카드 UX 개선·버그 수정 - #240

Merged
kimminna merged 12 commits into
developfrom
fix/web/238-todo-modal-qa-fixes
Jul 15, 2026
Merged

[FIX] 투두 생성/조회 모달 및 카드 UX 개선·버그 수정#240
kimminna merged 12 commits into
developfrom
fix/web/238-todo-modal-qa-fixes

Conversation

@kimminna

Copy link
Copy Markdown
Member

ISSUE 🔗

close #238



What is this PR? 🔍

투두 생성/조회 모달과 홈 카드 전반에 걸쳐 발견된 UX 이슈와 버그를 수정했습니다.

배경

  • 기존 구조: 태그/시간/우선순위 드롭다운은 항목을 클릭하는 즉시 값이 반영되며 닫혔고, 아이콘 선택은 반대로 패널을 접었다 펴야만 반영되는 등 선택 컴포넌트마다 커밋 시점이 제각각이었습니다. 조회 모달은 react-hook-form 기반으로 필드별 즉시 PATCH를 보내는 구조였는데, 날짜 변경 시 갱신 타이밍이 어긋나 404와 화면 깜빡임이 발생했습니다.
  • 발생 문제: 드롭다운 선택 즉시 커밋으로 인해 여러 옵션을 훑어보기 어려웠고, 아이콘은 반대로 매번 패널을 접어야 해서 불편했습니다. 하위 태스크 완료는 투두 전체 PATCH로 보내고 있어 비효율적이었고, 아이콘 제거는 API 클라이언트에 NONE enum이 없어 아예 반영되지 않았습니다. 우선순위는 설정하지 않아도 서버 스키마가 MEDIUM으로 기본값을 채워 넣어 홈 카드에 항상 아이콘이 노출됐습니다.
  • 해결 방향: 드롭다운 계열은 "닫힐 때만 반영" 패턴으로, 아이콘은 반대로 "클릭 즉시 반영" 패턴으로 각각 사용성에 맞게 통일했습니다. 하위 태스크 완료는 전용 API로 분리하고, 아이콘 제거는 최신 스웨거 스펙을 확인해 NONE enum을 추가했습니다. 우선순위·반복 요일 등 값이 없을 때의 기본값 처리는 스키마 단에서 실제 상태를 그대로 보존하도록 정규화했습니다.

드롭다운 선택 컴포넌트 (태그 · 시간 · 우선순위 · 반복 · 날짜)

  • 변경 요약: 태그 · 시간 · 우선순위 드롭다운이 항목 클릭 즉시 닫히지 않고, 실제로 닫힐 때만 선택이 반영되도록 통일했습니다.
  • 이유: Dropdown.Item이 기본값 closeOnSelect=true라 클릭 즉시 패널이 닫혀버려서, 이미 적용돼 있던 "닫힐 때 커밋" 패턴(Date/Priority/Repeat 일부)이 실제로는 동작하지 않고 있었습니다.
  • 구현 방식: 각 셀렉터에 draft 상태(useState + useRef)를 두고 DropdownonOpenChange 콜백에서 열릴 때 draft를 현재 값으로 초기화, 닫힐 때 draft가 바뀌었으면 onSelect를 호출하도록 했습니다. 항목 클릭 시에는 closeOnSelect={false}를 명시해 패널이 유지되도록 했습니다. TimeSelector는 열려 있는 동안 AI 추천 응답처럼 외부에서 selected/time prop이 바뀌는 경우를 위해 useEffect로 draft를 prop에 동기화하는 로직을 추가했습니다.
  • 경계 · 제약: RepeatSelector는 WEEKLY/MONTHLY 상세 설정 화면으로 전환될 때 화살표(chevron) 회전 조건이 반대로 되어 있던 것도 함께 수정했습니다.

조회 모달 — 날짜 변경

  • 변경 요약: 날짜 변경 시 발생하던 404 에러와 모달 깜빡임을 수정했습니다.
  • 이유: 상세 조회 쿼리가 모달을 열 때 받은 날짜를 계속 고정해서 썼는데, 날짜 변경 PATCH가 성공하면 서버에서는 투두가 새 날짜로 이동합니다. 이후 캐시 무효화가 여전히 예전 날짜 쿼리 키를 대상으로 해서 즉시 재요청이 발생했고, 서버는 이미 이동한 투두를 예전 날짜로는 찾지 못해 404를 반환했습니다.
  • 구현 방식: 컨테이너에 currentDate 로컬 상태를 두고, 날짜 변경 PATCH가 성공한 시점에만 이 값을 갱신하도록 했습니다. 상세 조회는 이 currentDate를 쿼리 키로 사용하고, placeholderData: keepPreviousData를 적용해 쿼리 키가 바뀌는 동안에도 이전 데이터를 계속 보여주다가 새 데이터가 오면 자연스럽게 전환되도록 했습니다.

조회 모달 — 타이틀 · 태그 · 하위 태스크

  • 변경 요약: 타이틀 입력을 가로 한 줄로 제한하고, 태그 신규 생성과 하위 태스크 완료 토글을 각각 알맞은 API로 연동했습니다.
  • 이유: 타이틀이 <textarea>라 줄바꿈이 가능했고 Enter 키 핸들러가 없어 그냥 개행이 삽입됐습니다. 태그 추가 모달에서 새 태그를 만들어도 로컬 상태만 바뀌고 투두에 반영되지 않았고, 하위 태스크 완료는 투두 전체를 다시 PATCH하는 방식이라 백엔드에 이미 있는 전용 상태 변경 API(PATCH /todos/{id}/subtasks/{id}/status)를 활용하지 못하고 있었습니다.
  • 구현 방식: 타이틀을 <input type="text">로 바꾸고 Enter 입력 시 preventDefault 후 첫 번째 하위 태스크 입력창으로 포커스를 이동시킵니다. 태그 생성은 useTagFieldonTagCreated(tagId, syncLocal) 콜백을 추가해, 태그 생성 API 성공 후 투두 PATCH까지 마친 뒤에만 로컬 상태를 동기화하도록 했습니다. 하위 태스크 완료는 useToggleSubtaskSubmit이라는 전용 훅을 새로 만들어 changeSubtaskStatus를 호출하고, 성공 시 home/today/상세 쿼리를 무효화합니다.
  • 경계 · 제약: 하위 태스크 입력의 값 타입(DetailTodoSubtaskInput)을 훅(use-detail-subtask-field.ts)이 아니라 이를 렌더링하는 프레젠테이션 컴포넌트(DetailTodoTaskFields.tsx)가 소유하도록 옮겼습니다. 리팩토링 과정에서 한 차례 react-hook-form을 완전히 걷어내고 plain state로 옮기는 시도를 했으나, 파일이 지나치게 커지고 원래 구조(폼/PATCH 오케스트레이션/아이콘/자동저장/하위태스크로 관심사가 나뉜 5개 훅)가 이미 합리적으로 분리돼 있었다고 판단해 되돌렸습니다.

생성 모달 — 타이틀 · 하위 태스크 포커스 · 반복 요일 검증

  • 변경 요약: 조회 모달과 동일하게 타이틀을 가로 한 줄 입력으로 바꾸고, 반복 [매주] 선택 후 요일을 고르지 않아도 제출이 막히지 않도록 했습니다.
  • 이유: 반복 요일 미선택 시 createTodoRequestSchemasuperRefine이 검증 에러를 던져 제출 자체가 막혔는데, 화면에 에러 메시지가 노출되지 않아 "생성하기를 눌러도 반응이 없다"처럼 보였습니다.
  • 구현 방식: superRefine 앞에 .transform을 추가해, WEEKLY인데 요일이 비어 있으면 repeatTypeNONE으로, repeatWeekdaysnull로 정규화하도록 했습니다. MONTHLY(날짜 미선택) 검증은 이번 범위에 포함하지 않아 기존 동작(제출 차단)을 그대로 유지했습니다.

아이콘 선택 · 제거

  • 변경 요약: 아이콘 클릭 시 패널을 접었다 펴지 않아도 즉시 반영되도록 바꾸고, 제거 시 실제로 서버에 반영되도록 했습니다.
  • 이유: 조회 모달의 아이콘 훅이 "임시 선택(pendingIcon) → 패널을 닫아야 커밋" 구조였는데, 클릭 한 번으로 바로 선택되길 기대하는 다른 선택 UI들과 어긋났습니다. 아이콘 제거는 로컬로만 pendingIcon을 비웠는데, 로컬 생성된 TodoUpdateRequestIcon/TodoCreateRequestIcon enum에 NONE 값이 없어서 애초에 "제거"를 표현할 방법이 없었습니다.
  • 구현 방식: 스웨거(/v3/api-docs)를 직접 조회해 icon enum에 실제로 NONE이 존재함을 확인했습니다. 전체 pnpm gen:api 재생성은 캘린더 신규 엔드포인트 등 무관한 변경을 대량으로 끌고 와서, 두 enum 파일에 NONE: "NONE"만 최소로 수동 반영했습니다. handleSelectIcon/handleRemoveIcon을 draft 없이 즉시 onUpdate PATCH를 호출하고 성공 시 로컬 상태를 동기화 + 패널을 닫도록 재작성했습니다. 패널 열림/닫힘 애니메이션도 상단 버튼 전환 시점을 패널이 완전히 사라진 뒤로 맞춰 끊김을 줄였습니다.
  • 경계 · 제약: TodoCreateRequest.icon은 optional이라 CREATE 모달은 원래부터 아이콘 미선택 시 필드를 생략하는 방식으로 문제없이 동작하고 있어 별도로 손대지 않았습니다.

투두 카드 — 우선순위 기본값 · 호버 효과

  • 변경 요약: 실제로 설정하지 않은 우선순위가 홈 카드에 항상 노출되던 것을 제거하고, 완료된 카드의 호버 효과를 Today 탭과 통일했습니다.
  • 이유: todoSchemapriority가 없을 때 MEDIUM으로 강제 대체하고 있어, 태그 · 메모 · 반복 아이콘은 값이 있을 때만 조건부로 보이는데 우선순위만 항상 보이는 비일관성이 있었습니다.
  • 구현 방식: 스키마의 priority ?? "MEDIUM"priority ?? undefined로 바꾸고, HomeTodoCardpriority prop을 optional로 변경해 값이 있을 때만 PriorityIcon을 렌더링합니다. 완료된 카드는 isHovered 상태를 추가해 기본은 dim 처리하되 호버 중에는 일시적으로 활성 카드처럼 밝게 보이도록 했습니다(체크박스 · 재생 버튼 등 실제 기능 상태는 hover와 무관하게 유지).

우선순위 번역 키 정리

  • 변경 요약: Common.priority 번역 키를 urgent/high/medium/low에서 API enum 값(VERY_HIGH/HIGH/MEDIUM/LOW)으로 바꿨습니다.
  • 이유: HomeTodoCard에 API 값 → 번역 키를 매핑하는 별도 테이블(PRIORITY_LABEL_KEY)이 있었는데, 번역 키 자체를 API 값과 맞추면 이 매핑이 불필요해집니다.
  • 구현 방식: en.json/ko.json의 키 이름만 바꾸고, 이를 참조하던 DetailTodoModalContent.tsx · CreateTodoModalContent.tsx · HomeTodoCard.tsxtCommon("priority.xxx") 호출부와 매핑 테이블을 정리했습니다.



To Reviewers

  • 조회 모달 관련 커밋(4번)에서 react-hook-form 유지 여부를 두고 한 차례 전체를 plain state로 리팩토링했다가 되돌린 이력이 있습니다. 최종적으로는 기존 5개 훅 구조(use-detail-todo-form / use-detail-todo-patch-handlers / use-detail-todo-icon-submit / use-detail-todo-text-auto-save / use-detail-subtask-field)를 유지했는데, 이 판단이 맞는지 한 번 봐주시면 좋겠습니다.
  • 아이콘 NONE enum은 pnpm gen:api 전체 재생성 대신 두 파일에 수동으로 값을 추가했습니다. 다음에 정말 전체 재생성이 필요한 시점(예: 캘린더 기능 착수)에는 이번에 보류한 다른 API 변경사항들도 함께 반영될 수 있다는 점 참고해 주세요.
  • MONTHLY 반복(날짜 미선택) 검증은 이번 PR 범위에서 제외했습니다 — WEEKLY와 동일한 문제가 있는지는 후속으로 확인이 필요합니다.
  • 재생 버튼을 연속으로 두 번 누르면 TIMER_409가 발생하는 이슈를 세션 중 발견했으나, 수정 방향에 대한 확신이 부족해 이번 PR에는 포함하지 않았습니다. 별도 이슈로 트래킹이 필요합니다.



Screenshot 📷



Test Checklist ✔

kimminna added 9 commits July 15, 2026 23:07
- 태그/시간/우선순위/반복 드롭다운이 항목 클릭 즉시 닫히지 않고, 실제로 닫힐 때만 선택이 반영되도록 draft 상태와 closeOnSelect={false}를 적용했습니다
- 반복 셀렉터 화살표(chevron) 회전 방향이 반대로 되어 있던 것을 수정했습니다
- 시간 드롭다운이 열려 있는 상태에서 AI 추천 응답이 도착해도 draft 상태가 갱신되지 않아 선택 표시가 안 되던 것을 수정했습니다
- RepeatSelector의 onWeekdayToggle(id) prop이 onWeekdaysChange(weekdayIds[])로 바뀜에 따라 소비 측 핸들러를 갱신했습니다
- 날짜 변경 PATCH 성공 후에도 예전 날짜로 상세를 재조회해 404가 발생하던 것을, 성공 시점에 조회 기준 날짜를 함께 갱신하도록 수정했습니다
- 날짜 변경으로 쿼리 키가 바뀌는 동안 데이터가 잠깐 사라져 모달이 깜빡이던 것을 keepPreviousData로 방지했습니다
- 타이틀 입력을 textarea에서 input으로 바꿔 줄바꿈 없이 가로로만 입력되게 하고, Enter 입력 시 첫 하위 태스크로 포커스가 이동하도록 했습니다
- 태그 추가 모달에서 새 태그를 만들면 PATCH 성공 후 로컬 상태에 반영되도록 연동했습니다
- 하위 태스크 완료 토글을 기존의 전체 하위 태스크 배열 PATCH 방식 대신 전용 상태 변경 API로 호출하도록 변경했습니다
- 하위 태스크 입력의 값(value) 타입을 훅이 아닌 프레젠테이션 컴포넌트(DetailTodoTaskFields)가 소유하도록 옮겼습니다
- 타이틀 입력을 textarea에서 input으로 바꿔 줄바꿈 없이 가로로만 입력되게 했습니다
- Enter 입력 시 개행 대신 첫 번째 하위 태스크 입력창으로 포커스가 이동하도록 했습니다
- 아이콘 클릭 시 패널을 접었다 펴야 반영되던 것을, 클릭 즉시 서버에 반영되고 패널이 자동으로 닫히도록 변경했습니다
- 아이콘 제거가 로컬에서만 초기화되고 서버에 반영되지 않던 것을, 스웨거에 정의된 icon: "NONE"을 PATCH로 전송하도록 수정했습니다 (기존 생성된 API 클라이언트에 NONE이 누락되어 있어 enum에 추가했습니다)
- 아이콘 패널 열림/닫힘 시 상단 버튼과 패널 애니메이션 타이밍이 어긋나 끊겨 보이던 것을 다듬었습니다
- 생성 모달 시간 트리거의 시(hour) 자리에 2자리 패딩이 빠져 있어 "0:00"으로 보이던 것을 "00:00"으로 수정했습니다
- todoSchema에서 priority가 없을 때 "MEDIUM"으로 강제 대체하던 것을 제거해, 실제로 설정하지 않은 투두는 홈 카드에 우선순위 아이콘이 표시되지 않도록 했습니다
- 홈 투두카드에 hover 상태를 추가해, 완료된 카드가 Today 탭과 동일하게 마우스를 올리면 일시적으로 밝게 보이도록 했습니다
- createTodoRequestSchema에서 반복 [매주] 선택 후 요일을 선택하지 않으면 제출이 막히던 것을, repeatType을 NONE으로 정규화해 반복 없이 생성되도록 수정했습니다
- Common.priority 번역 키를 urgent/high/medium/low에서 VERY_HIGH/HIGH/MEDIUM/LOW로 바꿔, 여러 화면에 흩어져 있던 API enum ↔ 번역 키 매핑 테이블을 없앴습니다
@vercel

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

Request Review

@github-actions github-actions Bot added ⏰ Timo-web Timo 웹 서비스 ⌚ Timo-Design-system Timo 디자인 시스템 labels Jul 15, 2026
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 354f1e89-de94-444b-8ce1-ca390b7e8b67

📥 Commits

Reviewing files that changed from the base of the PR and between 6778c04 and bc87ecf.

⛔ Files ignored due to path filters (3)
  • apps/timo-web/api/generated/endpoints/home/home.zod.ts is excluded by !**/generated/**
  • apps/timo-web/api/generated/models/todayTodoResponse.ts is excluded by !**/generated/**
  • apps/timo-web/api/generated/models/todoResponse.ts is excluded by !**/generated/**
📒 Files selected for processing (10)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_components/TodayTodoCard.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_mocks/today-todo-mock.ts
  • apps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsx
  • apps/timo-web/components/todo-modal/common/TodoIconField.tsx
  • apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx
  • apps/timo-web/schemas/todo/todo-schema.ts

Walkthrough

투두 카드의 완료 상태 표시, 생성·조회 모달 입력과 제출 흐름, 디자인 시스템 선택기의 드롭다운 확정 방식, 반복·우선순위 스키마 및 아이콘 패널 애니메이션이 변경되었습니다.

Changes

투두 UX 및 데이터 흐름

Layer / File(s) Summary
드롭다운 임시 선택 및 확정
packages/timo-design-system/src/components/{calendar,layout,priority,repeat,tag,time}/...
날짜·우선순위·반복·태그·시간 선택이 드롭다운을 닫을 때만 외부 콜백을 호출하도록 변경되었습니다.
생성 모달 입력 및 반복 설정
apps/timo-web/components/todo-modal/create/..., apps/timo-web/hooks/todo-modal/create/..., apps/timo-web/schemas/todo/todo-schema.ts, apps/timo-web/messages/*.json
제목 입력이 한 줄 input으로 변경되고 Enter 시 첫 서브태스크에 포커스가 이동하며, 반복 요일·시간 표시·우선순위 라벨과 반복 기본값이 갱신되었습니다.
조회 모달 UI 및 폼 연결
apps/timo-web/components/todo-modal/detail/..., apps/timo-web/hooks/todo-modal/common/..., apps/timo-web/hooks/todo-modal/detail/...
태그 토스트, 삭제 확인 트리거, 태그 생성 동기화, 아이콘 즉시 제출, 서브태스크 포커스와 관련된 조회 모달 wiring이 추가되었습니다.
조회 모달 서버 제출 및 동기화
apps/timo-web/containers/todo-modal/detail/..., apps/timo-web/hooks/todo-modal/detail/..., apps/timo-web/utils/todo/...
현재 날짜를 기준으로 상세 조회를 유지하고, 서브태스크 토글과 반복 요일 변경을 별도 제출·쿼리 무효화 흐름으로 연결했습니다.
투두 카드 표시 및 데이터 기본값
apps/timo-web/app/.../HomeTodoCard.tsx, apps/timo-web/schemas/todo/todo-schema.ts
완료 카드가 호버 시 덜 dimmed되며, 우선순위가 없는 카드에는 우선순위 아이콘이 표시되지 않습니다.
아이콘 패널 애니메이션
apps/timo-web/components/todo-modal/common/TodoIconField.tsx
아이콘 패널의 렌더링 수명과 가시성을 분리하고 열림·닫힘 transition을 적용했습니다.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DetailTodoModalContent
  participant DetailTodoModalContainer
  participant useToggleSubtaskSubmit
  participant TodoQuery
  User->>DetailTodoModalContent: 서브태스크 완료 상태 변경
  DetailTodoModalContent->>DetailTodoModalContainer: onToggleSubtask(subtaskId, completed)
  DetailTodoModalContainer->>useToggleSubtaskSubmit: handleToggle(todoId, subtaskId, date, completed)
  useToggleSubtaskSubmit->>TodoQuery: 상태 변경 요청 및 관련 쿼리 무효화
  TodoQuery-->>DetailTodoModalContainer: 갱신된 상세 데이터
  DetailTodoModalContainer-->>DetailTodoModalContent: 성공·오류 콜백 전달
Loading

Possibly related issues

Possibly related PRs

Suggested labels: ♻ Refactor

Suggested reviewers: yumin-kim2, ehye1, jjangminii

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive 대부분의 QA 항목은 반영됐지만, 아이콘 제거용 NONE enum은 generated 파일이 필터로 제외돼 완전 검증이 어렵습니다. apps/timo-web/api/generated/models/todoCreateRequestIcon.tstodoUpdateRequestIcon.ts를 포함한 diff나 생성 결과를 함께 확인해 주세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 투두 모달·카드 UX/버그 수정이라는 핵심 변경을 간결하게 담고 있습니다.
Description check ✅ Passed 설명이 변경 내용과 일치하며 모달과 카드 UX 수정 목적을 잘 설명합니다.
Out of Scope Changes check ✅ Passed 변경은 드롭다운, 모달, 타이틀 입력, 하위 태스크, 아이콘, 카드 UX 등 PR 목적과 일치합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/web/238-todo-modal-qa-fixes

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

❤️ Share

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

@github-actions github-actions Bot added 🐛 Bug 기능이 정상적으로 작동하지 않는 문제 수정 ♦️ 민아 민아상 labels Jul 15, 2026
@kimminna kimminna added the 🚬 QA QA 반영 label Jul 15, 2026
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Storybook Preview

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

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

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Timo Performance Report

Bundle Size — timo-web
라우트 크기 First Load JS
/[locale]/home 211.32 kB 🔴 417.18 kB
/[locale]/today 195.43 kB 🔴 401.29 kB
/[locale]/focus 160.58 kB 🔴 366.44 kB
/[locale]/settings 167.12 kB 🔴 372.98 kB
/[locale]/statistics 233.44 kB 🔴 439.30 kB
/[locale]/[...rest] 0 B 🟡 205.86 kB
/[locale]/login 212.96 kB 🔴 418.82 kB
/[locale]/oauth/callback 119.66 kB 🟡 325.52 kB
/[locale]/onboarding 233.42 kB 🔴 439.28 kB
/[locale] 118.97 kB 🟡 324.83 kB
/[locale]/policy 125.09 kB 🟡 330.96 kB

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

Lighthouse — timo-web
URL Perf A11y LCP CLS TBT
/en/home 🔴 62 🟢 95 🔴 15.8s 🟢 0.000 🟡 492ms
/en/today 🔴 61 🟢 95 🔴 15.6s 🟢 0.000 🟡 541ms
/en/focus 🔴 63 🟢 95 🔴 15.3s 🟢 0.000 🟡 475ms
/en/statistics 🔴 63 🟢 95 🔴 15.2s 🟢 0.000 🟡 456ms

Perf ≥ 70 / A11y ≥ 85 목표
LCP 🟢 < 2.5s 🟡 < 4s 🔴 ≥ 4s  |  CLS 🟢 < 0.1 🟡 < 0.25 🔴 ≥ 0.25  |  TBT 🟢 < 200ms 🟡 < 600ms 🔴 ≥ 600ms

Image Optimization — timo-web
파일 크기 포맷 상태
favicon.png 27.84 kB PNG ⚠️ 🟢
images/google-calendar.png 36.20 kB PNG ⚠️ 🟢
images/google-logo.png 26.79 kB PNG ⚠️ 🟢

총 3개 · 90.84 kB  |  🟢 < 200KB  |  🟡 < 500KB  |  🔴 ≥ 500KB
⚠️ 3개 파일 WebP/AVIF 변환 권장

측정 커밋: 80d1ca5

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

🤖 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/_components/todo-card/HomeTodoCard.tsx:
- Around line 167-173: Update the Todo card container in HomeTodoCard to use a
semantic article element instead of a generic div, and add onFocus/onBlur
handlers that mirror the existing hover state updates. Update handleCardClick
and handleCardKeyDown event types to HTMLElement so the article handlers remain
type-safe.

In `@apps/timo-web/components/todo-modal/common/TodoIconField.tsx`:
- Around line 79-93: Update the exiting panel wrapper around IconSelector so
that when isPanelVisible is false it is hidden from assistive technology and
removed from keyboard interaction using aria-hidden together with inert (or
propagated tabIndex={-1}), while preserving shouldRenderPanel for the exit
animation.

In `@apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx`:
- Around line 48-61: Update the input in CreateTodoTaskFields to include a
unique id and add a corresponding label using htmlFor, using an sr-only class if
the label should remain visually hidden. Preserve the existing title value,
change handling, and Enter/composition-key behavior.

In `@apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx`:
- Around line 147-157: Update the TodoIconField container and the corresponding
edit-control region around the later fields so canUpdateTodo blocks keyboard
focus and activation as well as pointer interaction. Apply inert to the disabled
regions where supported, or pass disabled state to each interactive control and
guard their handlers with canUpdateTodo, while preserving normal editing
behavior when updates are allowed.
- Line 217: Update the hasMemo prop in DetailTodoModalContent to derive from the
local form value rather than todo.memo, trimming and coercing that current value
to a boolean so the memo indicator updates while editing.

In `@apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx`:
- Around line 80-92: Update the title input in DetailTodoTaskFields by assigning
it a stable id and adding a visually hidden label whose htmlFor references that
id; replace the unconditional outline-none styling with an accessible
focus-visible indicator while preserving the existing non-focus styling and
input behavior.
- Around line 17-22: Move the shared DetailTodoSubtaskInput interface out of
DetailTodoTaskFields.tsx into a dedicated common type module, then update
use-detail-todo-text-auto-save.ts and use-detail-subtask-field.ts to import it
from that module. Affected sites:
apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx lines
17-22—remove the local interface and use the shared type;
apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts line
4—switch to the shared type import;
apps/timo-web/hooks/todo-modal/detail/use-detail-subtask-field.ts line 3—switch
to the shared type import.

In `@apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts`:
- Around line 23-55: Serialize icon update requests in handleSelectIcon and
handleRemoveIcon so a new selection or removal cannot start while a PATCH is
pending. Track the in-flight state around onUpdate, clear it in both onSuccess
and onError, and either block additional submissions or retain only the latest
pending choice for processing after completion.

In `@apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts`:
- Around line 103-113: Update handleWeekdaysChange so an empty weekdayIds array
is normalized to the no-repeat type ("NONE" or the existing equivalent) instead
of sending repeatType "WEEKLY"; preserve the filtered weekday IDs for non-empty
selections and align the behavior with the creation modal.

In `@packages/timo-design-system/src/components/layout/dropdown/Dropdown.tsx`:
- Around line 62-65: Update the Dropdown component’s setOpenState and
outside-click handling to store the latest onOpenChange callback in a ref, then
have handleOutsideClick invoke the ref’s current callback instead of a stale
closure. Keep the effect stable without relying on the disabled exhaustive-deps
warning, while preserving the existing open-state behavior.

In
`@packages/timo-design-system/src/components/repeat/repeat-selector/RepeatSelector.tsx`:
- Around line 167-230: Extract the repeated draft state/ref synchronization and
open-close commit behavior from RepeatSelector’s handleOpenChange and
corresponding TimeSelector logic into a shared useDraftValue-style hook. Use the
hook for frequency, weekday IDs, and repeat day while preserving initialization
on open, diff-only callbacks on close, and the existing selector-specific
comparison behavior.

In
`@packages/timo-design-system/src/components/time/time-selector/TimeSelector.tsx`:
- Around line 52-108: TimeSelector의 draft state/ref 동기화와 handleOpenChange 커밋 로직이
RepeatSelector와 중복되므로 공용 훅으로 추출하고 두 컴포넌트가 이를 재사용하도록 변경하세요. draftTime,
draftSelected 관리, 열릴 때 원본 값으로 재설정, 닫힐 때 변경된 값 커밋 동작은 기존과 동일하게 유지하세요.
🪄 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: c1829209-97cb-4dce-ae41-6a099d8205f3

📥 Commits

Reviewing files that changed from the base of the PR and between 590c15d and 6778c04.

⛔ Files ignored due to path filters (2)
  • apps/timo-web/api/generated/models/todoCreateRequestIcon.ts is excluded by !**/generated/**
  • apps/timo-web/api/generated/models/todoUpdateRequestIcon.ts is excluded by !**/generated/**
📒 Files selected for processing (29)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_components/todo-card/HomeTodoCard.tsx
  • apps/timo-web/components/todo-modal/common/TodoIconField.tsx
  • apps/timo-web/components/todo-modal/create/CreateTodoModalContent.tsx
  • apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx
  • apps/timo-web/components/todo-modal/detail/DetailTodoTaskFields.tsx
  • apps/timo-web/containers/todo-modal/detail/DetailTodoModalContainer.tsx
  • apps/timo-web/hooks/todo-modal/common/use-tag-field.tsx
  • apps/timo-web/hooks/todo-modal/create/use-icon-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-repeat-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-subtask-field.ts
  • apps/timo-web/hooks/todo-modal/create/use-time-field.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-subtask-field.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-todo-form.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts
  • apps/timo-web/hooks/todo-modal/detail/use-detail-todo-text-auto-save.ts
  • apps/timo-web/hooks/todo-modal/detail/use-toggle-subtask-submit.ts
  • apps/timo-web/messages/en.json
  • apps/timo-web/messages/ko.json
  • apps/timo-web/schemas/todo/todo-schema.ts
  • apps/timo-web/utils/todo/detail-todo-update-request.ts
  • packages/timo-design-system/src/components/calendar/date-selector/DateSelector.tsx
  • packages/timo-design-system/src/components/layout/dropdown/Dropdown.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-selector/TagSelector.tsx
  • packages/timo-design-system/src/components/time/time-selector/TimeSelector.tsx

Comment on lines +79 to +93
{shouldRenderPanel && (
<div
className={cn(
"origin-top",
isPanelVisible
? "translate-y-0 scale-100 opacity-100 transition-all duration-[220ms] ease-[cubic-bezier(0.16,1,0.3,1)]"
: "-translate-y-1.5 scale-95 opacity-0 transition-all duration-[160ms] ease-in",
)}
>
<IconSelector
selected={icon}
onSelect={onSelectIcon}
onRemove={onRemoveIcon}
/>
</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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

퇴장 애니메이션 중 패널이 여전히 포커스/AT에 노출됩니다.

isPanelVisiblefalse가 된 뒤에도 shouldRenderPaneltrue인 160ms 동안 IconSelector는 시각적으로만 사라지고(opacity-0) DOM/tab order에는 남아 있습니다. 키보드 사용자가 이 짧은 구간에 Tab으로 진입하면 보이지 않는 버튼에 포커스가 갈 수 있습니다. isPanelVisible이 false인 동안 aria-hiddeninert(또는 tabIndex={-1} 전파)를 함께 적용하면 안전합니다.

♿ 제안
         <div
           className={cn(
             "origin-top",
             isPanelVisible
               ? "translate-y-0 scale-100 opacity-100 transition-all duration-[220ms] ease-[cubic-bezier(0.16,1,0.3,1)]"
               : "-translate-y-1.5 scale-95 opacity-0 transition-all duration-[160ms] ease-in",
           )}
+          aria-hidden={!isPanelVisible}
+          inert={!isPanelVisible ? "" : undefined}
         >
📝 Committable suggestion

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

Suggested change
{shouldRenderPanel && (
<div
className={cn(
"origin-top",
isPanelVisible
? "translate-y-0 scale-100 opacity-100 transition-all duration-[220ms] ease-[cubic-bezier(0.16,1,0.3,1)]"
: "-translate-y-1.5 scale-95 opacity-0 transition-all duration-[160ms] ease-in",
)}
>
<IconSelector
selected={icon}
onSelect={onSelectIcon}
onRemove={onRemoveIcon}
/>
</div>
{shouldRenderPanel && (
<div
className={cn(
"origin-top",
isPanelVisible
? "translate-y-0 scale-100 opacity-100 transition-all duration-[220ms] ease-[cubic-bezier(0.16,1,0.3,1)]"
: "-translate-y-1.5 scale-95 opacity-0 transition-all duration-[160ms] ease-in",
)}
aria-hidden={!isPanelVisible}
inert={!isPanelVisible ? "" : undefined}
>
<IconSelector
selected={icon}
onSelect={onSelectIcon}
onRemove={onRemoveIcon}
/>
</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/components/todo-modal/common/TodoIconField.tsx` around lines 79
- 93, Update the exiting panel wrapper around IconSelector so that when
isPanelVisible is false it is hidden from assistive technology and removed from
keyboard interaction using aria-hidden together with inert (or propagated
tabIndex={-1}), while preserving shouldRenderPanel for the exit animation.

Comment thread apps/timo-web/components/todo-modal/create/CreateTodoTaskFields.tsx
Comment on lines +147 to +157
<div className={canUpdateTodo ? undefined : "pointer-events-none"}>
<TodoIconField
icon={iconField.icon}
isIconPanelOpen={iconField.isIconPanelOpen}
addIconLabel={tCreateModal("addIcon")}
onOpenPanel={iconField.handleOpenIconPanel}
onTogglePanel={iconField.handleToggleIconPanel}
onSelectIcon={iconField.handleSelectIcon}
onRemoveIcon={iconField.handleRemoveIcon}
/>
</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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

실행 중 편집 잠금을 키보드 입력에도 적용해 주세요.

pointer-events-none은 포인터만 차단하므로 자식 버튼은 탭으로 포커스한 뒤 Enter/Space로 실행할 수 있습니다. disabled를 각 컨트롤에 전달하거나, 영역에 inert를 적용하고 핸들러에서도 canUpdateTodo를 검사해 주세요. inert는 하위 요소의 포커스와 사용자 상호작용을 함께 차단합니다. (html.spec.whatwg.org)

Also applies to: 184-258

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

In `@apps/timo-web/components/todo-modal/detail/DetailTodoModalContent.tsx` around
lines 147 - 157, Update the TodoIconField container and the corresponding
edit-control region around the later fields so canUpdateTodo blocks keyboard
focus and activation as well as pointer interaction. Apply inert to the disabled
regions where supported, or pass disabled state to each interactive control and
guard their handlers with canUpdateTodo, while preserving normal editing
behavior when updates are allowed.

Comment on lines 23 to 55
const [isIconPanelOpen, setIsIconPanelOpen] = useState(false);
const [pendingIcon, setPendingIcon] = useState<TodoIconValue | null>(icon);

const handleSelectIcon = (nextIcon: TodoIconValue) => {
setPendingIcon(nextIcon);
};
const handleOpenIconPanel = () => setIsIconPanelOpen(true);
const handleToggleIconPanel = () => setIsIconPanelOpen((prev) => !prev);

const handleOpenIconPanel = () => {
setPendingIcon(icon);
setIsIconPanelOpen(true);
};

const handleSubmitIcon = () => {
if (!pendingIcon || pendingIcon === icon) {
const handleSelectIcon = (nextIcon: TodoIconValue) => {
if (nextIcon === icon) {
setIsIconPanelOpen(false);
return;
}

onUpdate(
{ icon: pendingIcon },
{ icon: nextIcon },
{
onSuccess: () => {
selectIcon(pendingIcon);
selectIcon(nextIcon);
setIsIconPanelOpen(false);
},
},
);
};

const handleToggleIconPanel = () => {
if (isIconPanelOpen) {
handleSubmitIcon();
return;
}

handleOpenIconPanel();
};

const handleRemoveIcon = () => {
setPendingIcon(null);
onUpdate(
{ icon: "NONE" },
{
onSuccess: () => {
removeIcon();
setIsIconPanelOpen(false);
},
},
);
};

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 | 🟠 Major | ⚡ Quick win

아이콘 업데이트 요청을 직렬화해 주세요.

첫 요청이 완료되기 전에 다른 아이콘을 선택하거나 제거하면 PATCH 요청들이 경합합니다. 응답 처리 순서에 따라 이전 선택이 마지막 선택을 덮어쓸 수 있습니다.

요청 중에는 추가 제출을 차단하거나 최신 선택을 큐에 저장하고, onSuccess/onError에서 pending 상태를 해제해 주세요.

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

In `@apps/timo-web/hooks/todo-modal/detail/use-detail-todo-icon-submit.ts` around
lines 23 - 55, Serialize icon update requests in handleSelectIcon and
handleRemoveIcon so a new selection or removal cannot start while a PATCH is
pending. Track the in-flight state around onUpdate, clear it in both onSuccess
and onError, and either block additional submissions or retain only the latest
pending choice for processing after completion.

Comment on lines +103 to 113
const handleWeekdaysChange = (weekdayIds: string[]) => {
updateTodo(
{
repeatType: "WEEKLY",
repeatWeekdays: selectedWeekdayIds.filter(isTodoUpdateRepeatWeekday),
repeatWeekdays: weekdayIds.filter(isTodoUpdateRepeatWeekday),
},
{
onSuccess: () => form.setSelectedWeekdayIds(selectedWeekdayIds),
onSuccess: () => form.setSelectedWeekdayIds(weekdayIds),
},
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

선택된 요일이 없을 때의 반복 정규화 처리를 확인해 주세요.

PR 목표에 "반복 요일 미선택 시 반복 없음으로 정규화"가 명시되어 있습니다. 조회(수정) 모달에서도 모든 요일이 해제되어 weekdayIds가 빈 배열이 될 경우, repeatType"WEEKLY"가 아닌 "NONE"(또는 해당하는 '반복 없음' 타입)으로 전송해야 하는지 검토해 보세요. 생성 모달의 기준과 동일하다면 이 부분에도 정규화 로직 추가가 필요할 수 있습니다.

💡 수정 제안 (예시)
  const handleWeekdaysChange = (weekdayIds: string[]) => {
+   const validWeekdays = weekdayIds.filter(isTodoUpdateRepeatWeekday);
    updateTodo(
      {
-       repeatType: "WEEKLY",
-       repeatWeekdays: weekdayIds.filter(isTodoUpdateRepeatWeekday),
+       repeatType: validWeekdays.length === 0 ? "NONE" : "WEEKLY",
+       repeatWeekdays: validWeekdays,
      },
      {
        onSuccess: () => form.setSelectedWeekdayIds(weekdayIds),
      },
    );
  };
📝 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
const handleWeekdaysChange = (weekdayIds: string[]) => {
updateTodo(
{
repeatType: "WEEKLY",
repeatWeekdays: selectedWeekdayIds.filter(isTodoUpdateRepeatWeekday),
repeatWeekdays: weekdayIds.filter(isTodoUpdateRepeatWeekday),
},
{
onSuccess: () => form.setSelectedWeekdayIds(selectedWeekdayIds),
onSuccess: () => form.setSelectedWeekdayIds(weekdayIds),
},
);
};
const handleWeekdaysChange = (weekdayIds: string[]) => {
const validWeekdays = weekdayIds.filter(isTodoUpdateRepeatWeekday);
updateTodo(
{
repeatType: validWeekdays.length === 0 ? "NONE" : "WEEKLY",
repeatWeekdays: validWeekdays,
},
{
onSuccess: () => form.setSelectedWeekdayIds(weekdayIds),
},
);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/timo-web/hooks/todo-modal/detail/use-detail-todo-patch-handlers.ts`
around lines 103 - 113, Update handleWeekdaysChange so an empty weekdayIds array
is normalized to the no-repeat type ("NONE" or the existing equivalent) instead
of sending repeatType "WEEKLY"; preserve the filtered weekday IDs for non-empty
selections and align the behavior with the creation modal.

Comment on lines +62 to +65
const setOpenState = (next: boolean) => {
setIsOpen(next);
onOpenChange?.(next);
};

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

useEffect 내 낡은 클로저(Stale Closure) 방지를 위한 Ref 활용

현재 useEffect의 의존성 배열에서 exhaustive-deps를 비활성화(eslint-disable-next-line)했기 때문에, 컴포넌트 외부에서 onOpenChange가 변경되더라도 handleOutsideClick 안의 close 함수가 최신 콜백을 참조하지 못하는 낡은 클로저 이슈가 발생할 수 있습니다.

드롭다운이 열려있는 동안 부모 컴포넌트가 리렌더링되어 최신 상태를 담은 onOpenChange가 전달되더라도, 바깥 영역 클릭 시에는 과거의 낡은 콜백이 실행되어 변경사항이 유실될 위험이 있습니다. 안정성을 위해 useRef를 사용하여 항상 최신의 콜백을 바라보도록 수정하는 것을 권장합니다! (React 19.2 환경이라면 useEffectEvent를 활용하는 것도 좋은 대안이 될 수 있습니다.)

💡 최신 콜백을 보장하는 수정 제안
+  const onOpenChangeRef = useRef(onOpenChange);
+  onOpenChangeRef.current = onOpenChange;
+
   const setOpenState = (next: boolean) => {
     setIsOpen(next);
-    onOpenChange?.(next);
+    onOpenChangeRef.current?.(next);
   };
📝 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
const setOpenState = (next: boolean) => {
setIsOpen(next);
onOpenChange?.(next);
};
const onOpenChangeRef = useRef(onOpenChange);
onOpenChangeRef.current = onOpenChange;
const setOpenState = (next: boolean) => {
setIsOpen(next);
onOpenChangeRef.current?.(next);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/timo-design-system/src/components/layout/dropdown/Dropdown.tsx`
around lines 62 - 65, Update the Dropdown component’s setOpenState and
outside-click handling to store the latest onOpenChange callback in a ref, then
have handleOutsideClick invoke the ref’s current callback instead of a stale
closure. Keep the effect stable without relying on the disabled exhaustive-deps
warning, while preserving the existing open-state behavior.

- 스웨거 최신 스펙에서 TodoResponse/TodayTodoResponse의 hasMemo가 hasSubtask로 바뀐 것을 확인해 todoSchema와 생성된 타입을 갱신했습니다
- HomeTodoCard/TodayTodoCard의 메모 아이콘 표시 조건을 hasSubtask 기준으로 변경했습니다
- 목업 데이터도 필드명을 맞춰 정리했습니다
- 타이틀 input이 placeholder에만 의존해 접근 가능한 이름이 없던 것을, useId로 고유 id를 부여하고 sr-only label을 연결해 해결했습니다

@jjangminii jjangminii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

체크아웃해서 이슈 개선된 것 확인했습니다~~ 확실히 전보다 더 편해진 것 같아요.

(typeof TodoUpdateRequestIcon)[keyof typeof TodoUpdateRequestIcon];

export const TodoUpdateRequestIcon = {
NONE: "NONE",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

아이콘을 이런식으로 제거할 수 있네요 👍

const handleSubmitIcon = () => {
if (!pendingIcon || pendingIcon === icon) {
const handleSelectIcon = (nextIcon: TodoIconValue) => {
if (nextIcon === icon) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

전체적으로 선택 -> 임시저장 -> 제출이라는 3단계 플로우를 선택 = 즉시 적용 1단계로 줄이면서 코드도 간결해지고 UX도 더 직관적으로 개선됐네요~~

@kimminna
kimminna merged commit dd18e8a into develop Jul 15, 2026
16 checks passed
@kimminna
kimminna deleted the fix/web/238-todo-modal-qa-fixes branch July 15, 2026 17:38

@ehye1 ehye1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

각 드롭다운에서 서버와 동기화되는 시점을 명확하게 분리하는 게 중요한 것 같아요. draft와 실제 상태를 분리함으로써 불필요한 상태 변경을 줄여서 좋은 것 같아요.
qa 반영하느라 수고많으셨어요!✌🏻

@kimminna kimminna mentioned this pull request Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⌚ Timo-Design-system Timo 디자인 시스템 ⏰ Timo-web Timo 웹 서비스 ♦️ 민아 민아상 🐛 Bug 기능이 정상적으로 작동하지 않는 문제 수정 🚬 QA QA 반영

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QA] 투두 생성/조회 모달 및 카드 UX 개선·버그 수정 QA 검증

3 participants