feat: 풀스택 할일관리 앱 구현 (통합 모달 + 간트차트)

- Backend: FastAPI + MongoDB + Redis (카테고리, 할일 CRUD, 파일 첨부, 검색, 대시보드)
- Frontend: Next.js 15 + Tailwind + React Query + Zustand
- 통합 TodoModal: 생성/수정 모달 통합, 탭 구조 (기본/태그와 첨부)
- 간트차트: 카테고리별 할일 타임라인 시각화
- TodoCard: 제목/카테고리/우선순위/태그/첨부 한줄 표시
- Docker Compose 배포 (Frontend:3010, Backend:8010, MongoDB:27021, Redis:6391)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2026-02-12 15:45:03 +09:00
parent b54811ad8d
commit 074b5133bf
81 changed files with 17027 additions and 19 deletions

2648
docs/ARCHITECTURE.md Normal file

File diff suppressed because it is too large Load Diff

789
docs/FEATURE_SPEC.md Normal file
View File

@ -0,0 +1,789 @@
# 기능 정의서 (Feature Specification)
> 프로젝트: todos2 | 버전: 1.0.0 | 작성일: 2026-02-10
---
## 1. 기능 목록 (Feature Inventory)
| # | 기능명 | 우선순위 | 카테고리 | 상태 |
|---|--------|---------|---------|------|
| F-001 | 할일 생성 | Must | 할일 CRUD | 미개발 |
| F-002 | 할일 목록 조회 | Must | 할일 CRUD | 미개발 |
| F-003 | 할일 상세 조회 | Must | 할일 CRUD | 미개발 |
| F-004 | 할일 수정 | Must | 할일 CRUD | 미개발 |
| F-005 | 할일 삭제 | Must | 할일 CRUD | 미개발 |
| F-006 | 할일 완료 토글 | Must | 할일 CRUD | 미개발 |
| F-007 | 카테고리 생성 | Must | 카테고리 | 미개발 |
| F-008 | 카테고리 목록 조회 | Must | 카테고리 | 미개발 |
| F-009 | 카테고리 수정 | Must | 카테고리 | 미개발 |
| F-010 | 카테고리 삭제 | Must | 카테고리 | 미개발 |
| F-011 | 태그 부여 | Must | 태그 | 미개발 |
| F-012 | 태그별 필터링 | Must | 태그 | 미개발 |
| F-013 | 태그 목록 조회 | Must | 태그 | 미개발 |
| F-014 | 우선순위 설정 | Must | 우선순위 | 미개발 |
| F-015 | 마감일 설정 | Must | 마감일 | 미개발 |
| F-016 | 마감일 알림 표시 | Must | 마감일 | 미개발 |
| F-017 | 검색 | Should | 검색 | 미개발 |
| F-018 | 대시보드 통계 | Should | 대시보드 | 미개발 |
| F-019 | 일괄 완료 처리 | Should | 일괄 작업 | 미개발 |
| F-020 | 일괄 삭제 | Should | 일괄 작업 | 미개발 |
| F-021 | 일괄 카테고리 변경 | Should | 일괄 작업 | 미개발 |
---
## 2. 기능 상세 정의
---
### F-001: 할일 생성
- **설명**: 사용자가 제목, 내용, 카테고리, 태그, 우선순위, 마감일을 입력하여 새로운 할일을 생성한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| title | string | Y | 1~200자, 공백만 불가 | 할일 제목 |
| content | string | N | 최대 2000자 | 할일 상세 내용 |
| category_id | string | N | 유효한 카테고리 ObjectId | 분류 카테고리 |
| tags | string[] | N | 각 태그 1~30자, 최대 10개 | 태그 목록 |
| priority | enum | N | high / medium / low (기본: medium) | 우선순위 |
| due_date | datetime | N | 현재 시각 이후 (생성 시점 기준) | 마감일 |
#### 처리 규칙 (Business Rules)
1. title 앞뒤 공백을 제거(trim)한다.
2. tags 배열 내 중복 태그를 제거하고, 각 태그를 소문자로 정규화한다.
3. category_id가 지정된 경우, 해당 카테고리가 존재하는지 검증한다.
4. created_at, updated_at을 현재 시각(UTC)으로 설정한다.
5. completed는 false로 초기화한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 201 Created | 생성된 Todo 객체 (id 포함) |
| 제목 누락/검증 실패 | 422 Unprocessable Entity | 필드별 에러 메시지 |
| 카테고리 미존재 | 404 Not Found | `{"detail": "카테고리를 찾을 수 없습니다"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 제목만 입력하여 할일을 생성할 수 있다
- [ ] 모든 필드(제목, 내용, 카테고리, 태그, 우선순위, 마감일)를 입력하여 할일을 생성할 수 있다
- [ ] 제목 없이 생성 시 422 에러가 반환된다
- [ ] 201자 이상의 제목 입력 시 422 에러가 반환된다
- [ ] 존재하지 않는 카테고리 ID 입력 시 404 에러가 반환된다
- [ ] 생성 후 할일 목록에 즉시 반영된다
- [ ] 태그 중복이 자동으로 제거된다
---
### F-002: 할일 목록 조회
- **설명**: 필터링, 정렬, 페이지네이션을 적용하여 할일 목록을 조회한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| page | integer | N | >= 1, 기본: 1 | 페이지 번호 |
| limit | integer | N | 1~100, 기본: 20 | 페이지당 항목 수 |
| completed | boolean | N | true / false | 완료 상태 필터 |
| category_id | string | N | 유효한 ObjectId | 카테고리 필터 |
| priority | enum | N | high / medium / low | 우선순위 필터 |
| tag | string | N | - | 태그 필터 |
| sort | string | N | created_at / due_date / priority | 정렬 기준 (기본: created_at) |
| order | enum | N | asc / desc (기본: desc) | 정렬 방향 |
#### 처리 규칙 (Business Rules)
1. 필터 조건이 복수인 경우 AND 조건으로 적용한다.
2. 카테고리 필터 시 해당 category의 이름과 색상을 populate한다.
3. 페이지네이션 응답에 총 개수(total)와 총 페이지 수(total_pages)를 포함한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | `{"items": [...], "total": N, "page": N, "limit": N, "total_pages": N}` |
| 잘못된 파라미터 | 422 Unprocessable Entity | 필드별 에러 메시지 |
#### 수락 기준 (Acceptance Criteria)
- [ ] 필터 없이 전체 할일 목록을 조회할 수 있다
- [ ] 완료/미완료 상태로 필터링할 수 있다
- [ ] 카테고리별로 필터링할 수 있다
- [ ] 우선순위별로 필터링할 수 있다
- [ ] 태그별로 필터링할 수 있다
- [ ] 페이지네이션이 정상 동작한다 (총 개수, 총 페이지 수 포함)
- [ ] 정렬 기준(생성일, 마감일, 우선순위)과 방향(오름차순/내림차순)을 지정할 수 있다
---
### F-003: 할일 상세 조회
- **설명**: 특정 할일의 전체 정보를 조회한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| id | string | Y | 유효한 ObjectId | 할일 ID |
#### 처리 규칙 (Business Rules)
1. 카테고리가 지정된 경우, 카테고리 이름과 색상을 함께 반환한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | Todo 객체 (카테고리 정보 포함) |
| 미존재 | 404 Not Found | `{"detail": "할일을 찾을 수 없습니다"}` |
| 잘못된 ID 형식 | 422 Unprocessable Entity | `{"detail": "유효하지 않은 ID 형식입니다"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 유효한 ID로 할일 상세 정보를 조회할 수 있다
- [ ] 카테고리 정보가 함께 반환된다
- [ ] 존재하지 않는 ID로 조회 시 404가 반환된다
---
### F-004: 할일 수정
- **설명**: 기존 할일의 제목, 내용, 카테고리, 태그, 우선순위, 마감일을 수정한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| id | string (path) | Y | 유효한 ObjectId | 할일 ID |
| title | string | N | 1~200자 | 할일 제목 |
| content | string | N | 최대 2000자 | 할일 상세 내용 |
| category_id | string \| null | N | 유효한 ObjectId 또는 null | 카테고리 (null이면 해제) |
| tags | string[] | N | 각 태그 1~30자, 최대 10개 | 태그 목록 |
| priority | enum | N | high / medium / low | 우선순위 |
| due_date | datetime \| null | N | null이면 해제 | 마감일 |
#### 처리 규칙 (Business Rules)
1. 요청에 포함된 필드만 업데이트한다 (Partial Update).
2. updated_at을 현재 시각(UTC)으로 갱신한다.
3. category_id가 지정된 경우 해당 카테고리의 존재를 검증한다.
4. tags 배열이 지정된 경우 중복 제거 및 소문자 정규화를 적용한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | 수정된 Todo 객체 |
| 미존재 | 404 Not Found | `{"detail": "할일을 찾을 수 없습니다"}` |
| 검증 실패 | 422 Unprocessable Entity | 필드별 에러 메시지 |
#### 수락 기준 (Acceptance Criteria)
- [ ] 제목만 수정할 수 있다
- [ ] 카테고리를 변경할 수 있다
- [ ] 카테고리를 null로 설정하여 해제할 수 있다
- [ ] 태그를 추가/삭제할 수 있다
- [ ] 우선순위를 변경할 수 있다
- [ ] 마감일을 설정/해제할 수 있다
- [ ] 수정 후 updated_at이 갱신된다
- [ ] 존재하지 않는 할일 수정 시 404가 반환된다
---
### F-005: 할일 삭제
- **설명**: 특정 할일을 영구 삭제한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| id | string | Y | 유효한 ObjectId | 할일 ID |
#### 처리 규칙 (Business Rules)
1. 물리 삭제를 수행한다.
2. 삭제 전 확인 다이얼로그를 프론트엔드에서 표시한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 204 No Content | 빈 응답 |
| 미존재 | 404 Not Found | `{"detail": "할일을 찾을 수 없습니다"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 할일을 삭제할 수 있다
- [ ] 삭제 전 확인 다이얼로그가 표시된다
- [ ] 삭제 후 목록에서 즉시 사라진다
- [ ] 존재하지 않는 할일 삭제 시 404가 반환된다
---
### F-006: 할일 완료 토글
- **설명**: 할일의 완료/미완료 상태를 전환한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| id | string (path) | Y | 유효한 ObjectId | 할일 ID |
#### 처리 규칙 (Business Rules)
1. 현재 completed 값의 반대값으로 토글한다.
2. updated_at을 갱신한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | `{"id": "...", "completed": true/false}` |
| 미존재 | 404 Not Found | `{"detail": "할일을 찾을 수 없습니다"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 체크박스 클릭으로 완료/미완료를 토글할 수 있다
- [ ] 완료된 할일은 시각적으로 구분된다 (취소선, 흐림 처리 등)
- [ ] 토글 후 대시보드 통계가 갱신된다
---
### F-007: 카테고리 생성
- **설명**: 할일 분류를 위한 새 카테고리를 생성한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| name | string | Y | 1~50자, 고유 | 카테고리 이름 |
| color | string | N | 유효한 hex color (기본: #6B7280) | 표시 색상 |
#### 처리 규칙 (Business Rules)
1. name 앞뒤 공백을 제거(trim)한다.
2. 동일한 이름의 카테고리가 이미 존재하면 409 Conflict를 반환한다.
3. order는 현재 최대값 + 1로 자동 설정한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 201 Created | 생성된 Category 객체 |
| 이름 중복 | 409 Conflict | `{"detail": "이미 존재하는 카테고리 이름입니다"}` |
| 검증 실패 | 422 Unprocessable Entity | 필드별 에러 메시지 |
#### 수락 기준 (Acceptance Criteria)
- [ ] 이름과 색상을 지정하여 카테고리를 생성할 수 있다
- [ ] 이름만으로 생성 시 기본 색상이 적용된다
- [ ] 중복 이름 생성 시 에러가 표시된다
- [ ] 생성 후 사이드바 카테고리 목록에 즉시 반영된다
---
### F-008: 카테고리 목록 조회
- **설명**: 전체 카테고리 목록을 조회한다.
- **우선순위**: Must
#### 입력 (Inputs)
없음 (파라미터 없이 전체 조회)
#### 처리 규칙 (Business Rules)
1. order 필드 기준 오름차순으로 정렬한다.
2. 각 카테고리에 속한 할일 수(todo_count)를 함께 반환한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | `[{"_id": "...", "name": "...", "color": "...", "order": N, "todo_count": N}]` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 전체 카테고리 목록을 조회할 수 있다
- [ ] 각 카테고리에 할일 수가 표시된다
- [ ] 정렬 순서대로 표시된다
---
### F-009: 카테고리 수정
- **설명**: 기존 카테고리의 이름, 색상, 순서를 수정한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| id | string (path) | Y | 유효한 ObjectId | 카테고리 ID |
| name | string | N | 1~50자, 고유 | 카테고리 이름 |
| color | string | N | 유효한 hex color | 표시 색상 |
| order | integer | N | >= 0 | 정렬 순서 |
#### 처리 규칙 (Business Rules)
1. 요청에 포함된 필드만 업데이트한다.
2. name 변경 시 중복 검사를 수행한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | 수정된 Category 객체 |
| 미존재 | 404 Not Found | `{"detail": "카테고리를 찾을 수 없습니다"}` |
| 이름 중복 | 409 Conflict | `{"detail": "이미 존재하는 카테고리 이름입니다"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 카테고리 이름을 변경할 수 있다
- [ ] 카테고리 색상을 변경할 수 있다
- [ ] 변경 사항이 해당 카테고리의 모든 할일 표시에 반영된다
- [ ] 중복 이름으로 변경 시 에러가 표시된다
---
### F-010: 카테고리 삭제
- **설명**: 카테고리를 삭제한다. 해당 카테고리에 속한 할일의 category_id는 null로 초기화된다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| id | string (path) | Y | 유효한 ObjectId | 카테고리 ID |
#### 처리 규칙 (Business Rules)
1. 카테고리 삭제 시, 해당 카테고리에 속한 모든 할일의 category_id를 null로 변경한다.
2. 삭제 전 확인 다이얼로그에 영향받는 할일 수를 표시한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 204 No Content | 빈 응답 |
| 미존재 | 404 Not Found | `{"detail": "카테고리를 찾을 수 없습니다"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 카테고리를 삭제할 수 있다
- [ ] 삭제 확인 다이얼로그에 영향받는 할일 수가 표시된다
- [ ] 삭제 후 해당 카테고리의 할일들이 "미분류"로 표시된다
- [ ] 사이드바에서 즉시 사라진다
---
### F-011: 태그 부여
- **설명**: 할일 생성 또는 수정 시 태그를 부여한다. 콤마로 구분하여 다중 태그를 입력한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| tags | string[] | N | 각 1~30자, 최대 10개, 특수문자 불가 | 태그 배열 |
#### 처리 규칙 (Business Rules)
1. F-001(할일 생성), F-004(할일 수정)에서 tags 필드로 처리된다.
2. 입력 태그를 소문자로 정규화하고, 중복을 제거한다.
3. 기존 태그 자동완성을 위해 사용 중인 태그 목록을 제공한다.
#### 출력 (Outputs)
F-001, F-004의 출력과 동일 (Todo 객체에 tags 포함)
#### 수락 기준 (Acceptance Criteria)
- [ ] 할일 생성/수정 폼에서 태그를 입력할 수 있다
- [ ] 기존 태그 자동완성이 동작한다
- [ ] 태그가 뱃지 형태로 표시된다
- [ ] 태그를 개별 삭제할 수 있다 (x 버튼)
---
### F-012: 태그별 필터링
- **설명**: 특정 태그가 부여된 할일만 필터링하여 조회한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| tag | string | Y | 존재하는 태그 | 필터링할 태그명 |
#### 처리 규칙 (Business Rules)
1. F-002(할일 목록 조회)의 tag 파라미터로 처리된다.
2. 태그 뱃지 클릭 시 해당 태그로 필터링된다.
#### 출력 (Outputs)
F-002의 출력과 동일
#### 수락 기준 (Acceptance Criteria)
- [ ] 태그 뱃지 클릭으로 해당 태그의 할일만 필터링된다
- [ ] 필터 적용 중 태그명이 표시된다
- [ ] 필터 해제 버튼으로 전체 목록으로 돌아갈 수 있다
---
### F-013: 태그 목록 조회
- **설명**: 현재 사용 중인 모든 태그의 목록과 사용 횟수를 조회한다.
- **우선순위**: Must
#### 입력 (Inputs)
없음
#### 처리 규칙 (Business Rules)
1. todos 컬렉션에서 tags 필드를 distinct로 추출한다.
2. 각 태그의 사용 횟수(count)를 집계한다.
3. 사용 횟수 내림차순으로 정렬한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | `[{"name": "업무", "count": 5}, ...]` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 사용 중인 모든 태그 목록을 조회할 수 있다
- [ ] 각 태그의 사용 횟수가 표시된다
- [ ] 할일이 없는 태그는 목록에 표시되지 않는다
---
### F-014: 우선순위 설정
- **설명**: 할일에 높음(high), 중간(medium), 낮음(low) 3단계 우선순위를 설정한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| priority | enum | N | high / medium / low | 우선순위 (기본: medium) |
#### 처리 규칙 (Business Rules)
1. F-001(할일 생성), F-004(할일 수정)의 priority 필드로 처리된다.
2. UI에서 색상으로 구분: high=빨강, medium=노랑, low=파랑.
#### 출력 (Outputs)
F-001, F-004의 출력과 동일
#### 수락 기준 (Acceptance Criteria)
- [ ] 할일 생성/수정 시 우선순위를 선택할 수 있다
- [ ] 우선순위별 색상 구분이 된다 (high=빨강, medium=노랑, low=파랑)
- [ ] 기본 우선순위는 medium이다
- [ ] 목록에서 우선순위별 필터링이 가능하다
---
### F-015: 마감일 설정
- **설명**: 할일에 마감일을 설정한다. 달력 UI로 날짜를 선택한다.
- **우선순위**: Must
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| due_date | datetime | N | ISO 8601 형식 | 마감일 |
#### 처리 규칙 (Business Rules)
1. F-001(할일 생성), F-004(할일 수정)의 due_date 필드로 처리된다.
2. null을 전송하면 마감일을 해제한다.
#### 출력 (Outputs)
F-001, F-004의 출력과 동일
#### 수락 기준 (Acceptance Criteria)
- [ ] 달력 UI로 마감일을 선택할 수 있다
- [ ] 마감일이 설정된 할일에 날짜가 표시된다
- [ ] 마감일을 해제할 수 있다
---
### F-016: 마감일 알림 표시
- **설명**: 마감일이 임박하거나 초과한 할일에 시각적 알림을 표시한다.
- **우선순위**: Must
#### 입력 (Inputs)
없음 (프론트엔드 렌더링 로직)
#### 처리 규칙 (Business Rules)
1. 마감일 1일 이내: 주황색 "임박" 뱃지 표시
2. 마감일 초과: 빨간색 "초과" 뱃지 표시
3. 마감일 3일 이내: 노란색 "곧 마감" 표시
4. 완료된 할일은 알림을 표시하지 않는다.
#### 출력 (Outputs)
UI 렌더링 (API 응답 없음)
#### 수락 기준 (Acceptance Criteria)
- [ ] 마감 3일 이내 할일에 "곧 마감" 표시가 된다
- [ ] 마감 1일 이내 할일에 "임박" 표시가 된다
- [ ] 마감 초과 할일에 "초과" 표시가 된다
- [ ] 완료된 할일에는 알림이 표시되지 않는다
- [ ] 뱃지 색상이 긴급도에 따라 구분된다 (노랑/주황/빨강)
---
### F-017: 검색
- **설명**: 제목, 내용, 태그를 기반으로 할일을 검색한다.
- **우선순위**: Should
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| q | string | Y | 1~200자 | 검색 키워드 |
| page | integer | N | >= 1, 기본: 1 | 페이지 번호 |
| limit | integer | N | 1~100, 기본: 20 | 페이지당 항목 수 |
#### 처리 규칙 (Business Rules)
1. MongoDB text index를 활용한 전문 검색을 수행한다.
2. 검색 대상: title, content, tags 필드.
3. 검색 결과는 관련도(text score) 기준으로 정렬한다.
4. 검색어가 태그와 정확히 일치하는 경우 해당 태그의 할일을 우선 표시한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | `{"items": [...], "total": N, "query": "...", "page": N, "limit": N}` |
| 검색어 누락 | 422 Unprocessable Entity | `{"detail": "검색어를 입력해주세요"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 헤더 검색바에 키워드를 입력하여 검색할 수 있다
- [ ] 제목에 포함된 키워드로 검색된다
- [ ] 내용에 포함된 키워드로 검색된다
- [ ] 태그명으로 검색된다
- [ ] 검색 결과가 관련도순으로 정렬된다
- [ ] 검색 결과가 없을 때 "결과 없음" 메시지가 표시된다
- [ ] 검색 결과에서 검색어가 하이라이트된다
---
### F-018: 대시보드 통계
- **설명**: 할일 현황을 한눈에 파악할 수 있는 대시보드 통계를 제공한다.
- **우선순위**: Should
#### 입력 (Inputs)
없음
#### 처리 규칙 (Business Rules)
1. 통계 항목:
- 전체 할일 수, 완료 수, 미완료 수
- 완료율 (%)
- 카테고리별 할일 분포 (도넛 차트)
- 우선순위별 현황 (가로 막대 차트)
- 마감 임박 할일 목록 (상위 5개)
2. Redis에 60초 TTL로 캐싱한다.
3. 캐시 무효화: 할일 CUD 작업 시.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | 아래 JSON 구조 참고 |
```json
{
"overview": {
"total": 50,
"completed": 30,
"incomplete": 20,
"completion_rate": 60.0
},
"by_category": [
{"category_id": "...", "name": "업무", "color": "#EF4444", "count": 15}
],
"by_priority": {
"high": 10,
"medium": 25,
"low": 15
},
"upcoming_deadlines": [
{"id": "...", "title": "...", "due_date": "...", "priority": "high"}
]
}
```
#### 수락 기준 (Acceptance Criteria)
- [ ] 전체/완료/미완료 수가 카드 형태로 표시된다
- [ ] 완료율이 퍼센트로 표시된다
- [ ] 카테고리별 할일 분포가 도넛 차트로 표시된다
- [ ] 우선순위별 현황이 막대 차트로 표시된다
- [ ] 마감 임박 할일 상위 5개가 리스트로 표시된다
- [ ] 데이터가 없을 때 빈 상태 안내가 표시된다
---
### F-019: 일괄 완료 처리
- **설명**: 여러 할일을 선택하여 한번에 완료 처리한다.
- **우선순위**: Should
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| action | string | Y | "complete" | 작업 유형 |
| ids | string[] | Y | 1개 이상의 유효한 ObjectId | 대상 할일 ID 배열 |
#### 처리 규칙 (Business Rules)
1. 선택된 모든 할일의 completed를 true로 설정한다.
2. 각 항목의 updated_at을 갱신한다.
3. 존재하지 않는 ID는 무시하고 나머지를 처리한다.
4. 처리된 수와 실패 수를 반환한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | `{"action": "complete", "processed": 5, "failed": 0}` |
| ID 누락 | 422 Unprocessable Entity | `{"detail": "대상 할일을 선택해주세요"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 체크박스로 여러 할일을 선택할 수 있다
- [ ] "일괄 완료" 버튼으로 선택된 할일을 한번에 완료할 수 있다
- [ ] 처리 결과(성공 수)가 토스트로 표시된다
- [ ] 처리 후 목록이 갱신된다
---
### F-020: 일괄 삭제
- **설명**: 여러 할일을 선택하여 한번에 삭제한다.
- **우선순위**: Should
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| action | string | Y | "delete" | 작업 유형 |
| ids | string[] | Y | 1개 이상의 유효한 ObjectId | 대상 할일 ID 배열 |
#### 처리 규칙 (Business Rules)
1. 선택된 모든 할일을 물리 삭제한다.
2. 삭제 전 확인 다이얼로그를 표시한다.
3. 존재하지 않는 ID는 무시한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | `{"action": "delete", "processed": 3, "failed": 0}` |
| ID 누락 | 422 Unprocessable Entity | `{"detail": "대상 할일을 선택해주세요"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] "일괄 삭제" 버튼으로 선택된 할일을 한번에 삭제할 수 있다
- [ ] 삭제 전 "N개의 할일을 삭제하시겠습니까?" 확인 다이얼로그가 표시된다
- [ ] 처리 결과가 토스트로 표시된다
- [ ] 삭제 후 목록이 갱신된다
---
### F-021: 일괄 카테고리 변경
- **설명**: 여러 할일을 선택하여 한번에 카테고리를 변경한다.
- **우선순위**: Should
#### 입력 (Inputs)
| 필드명 | 타입 | 필수 | 검증 규칙 | 설명 |
|--------|------|------|-----------|------|
| action | string | Y | "move_category" | 작업 유형 |
| ids | string[] | Y | 1개 이상의 유효한 ObjectId | 대상 할일 ID 배열 |
| category_id | string \| null | Y | 유효한 카테고리 ObjectId 또는 null | 변경할 카테고리 |
#### 처리 규칙 (Business Rules)
1. 선택된 모든 할일의 category_id를 지정된 값으로 변경한다.
2. category_id가 null이면 "미분류"로 설정한다.
3. 대상 카테고리의 존재를 검증한다.
#### 출력 (Outputs)
| 상황 | HTTP 상태 | 응답 |
|------|-----------|------|
| 성공 | 200 OK | `{"action": "move_category", "processed": 4, "failed": 0}` |
| 카테고리 미존재 | 404 Not Found | `{"detail": "카테고리를 찾을 수 없습니다"}` |
#### 수락 기준 (Acceptance Criteria)
- [ ] 카테고리 선택 드롭다운으로 일괄 변경할 수 있다
- [ ] "미분류"로 일괄 변경할 수 있다
- [ ] 처리 결과가 토스트로 표시된다
- [ ] 변경 후 목록이 갱신된다
---
## 3. API 엔드포인트 요약
| Method | Path | 기능 ID | 설명 |
|--------|------|---------|------|
| POST | `/api/todos` | F-001 | 할일 생성 |
| GET | `/api/todos` | F-002 | 할일 목록 조회 (필터/정렬/페이지네이션) |
| GET | `/api/todos/{id}` | F-003 | 할일 상세 조회 |
| PUT | `/api/todos/{id}` | F-004 | 할일 수정 |
| DELETE | `/api/todos/{id}` | F-005 | 할일 삭제 |
| PATCH | `/api/todos/{id}/toggle` | F-006 | 할일 완료 토글 |
| POST | `/api/categories` | F-007 | 카테고리 생성 |
| GET | `/api/categories` | F-008 | 카테고리 목록 조회 |
| PUT | `/api/categories/{id}` | F-009 | 카테고리 수정 |
| DELETE | `/api/categories/{id}` | F-010 | 카테고리 삭제 |
| GET | `/api/tags` | F-013 | 태그 목록 조회 |
| GET | `/api/search` | F-017 | 검색 |
| GET | `/api/dashboard/stats` | F-018 | 대시보드 통계 |
| POST | `/api/todos/batch` | F-019, F-020, F-021 | 일괄 작업 (완료/삭제/카테고리 변경) |

259
docs/PLAN.md Normal file
View File

@ -0,0 +1,259 @@
# todos2 - 전략 기획안 (Strategic Plan)
> 버전: 1.0.0 | 작성일: 2026-02-10 | 상태: 기획 완료
## 1. 프로젝트 개요
**todos2**는 단순 할일 관리를 넘어, 카테고리/태그/우선순위/마감일/검색/대시보드 기능을 갖춘 확장형 할일 관리 애플리케이션이다.
### 1.1 목표
- 할일의 체계적 분류 및 관리 (카테고리, 태그, 우선순위)
- 마감일 기반 일정 관리 및 시각적 알림
- 검색 및 필터링을 통한 빠른 할일 탐색
- 대시보드를 통한 진행 현황 한눈에 파악
- 일괄 작업을 통한 효율적인 다중 할일 처리
### 1.2 대상 사용자
- 개인 할일 관리가 필요한 사용자
- 프로젝트별/카테고리별 작업 분류가 필요한 사용자
---
## 2. 핵심 기능
| # | 기능 | 설명 | 우선순위 |
|---|------|------|---------|
| 1 | 할일 CRUD | 할일 생성, 조회, 수정, 삭제 | Must |
| 2 | 카테고리 관리 | 카테고리 CRUD, 할일별 카테고리 분류 | Must |
| 3 | 태그 시스템 | 할일에 다중 태그 부여, 태그별 필터링 | Must |
| 4 | 우선순위 | 높음/중간/낮음 3단계 우선순위 설정 | Must |
| 5 | 마감일 관리 | 마감일 설정, 임박/초과 알림 표시 | Must |
| 6 | 검색 | 제목/내용/태그 기반 전문 검색 | Should |
| 7 | 대시보드 | 완료율, 카테고리별/우선순위별 통계 차트 | Should |
| 8 | 일괄 작업 | 다중 선택 후 완료/삭제/카테고리 변경 | Should |
---
## 3. 기술 스택
### 3.1 백엔드
| 기술 | 버전 | 용도 |
|------|------|------|
| Python | 3.11 | 런타임 |
| FastAPI | >= 0.104 | REST API 프레임워크 |
| Motor | >= 3.3 | MongoDB 비동기 드라이버 |
| Pydantic v2 | >= 2.5 | 데이터 검증 및 직렬화 |
| Uvicorn | >= 0.24 | ASGI 서버 |
| Redis (aioredis) | >= 2.0 | 캐싱 (대시보드 통계) |
### 3.2 프론트엔드
| 기술 | 버전 | 용도 |
|------|------|------|
| Next.js | 15 (App Router) | React 프레임워크 |
| TypeScript | 5.x | 타입 안정성 |
| Tailwind CSS | 4.x | 유틸리티 기반 스타일링 |
| shadcn/ui | latest | UI 컴포넌트 라이브러리 |
| Recharts | 2.x | 대시보드 차트 |
| Tanstack Query | 5.x | 서버 상태 관리 및 캐싱 |
| Zustand | 5.x | 클라이언트 상태 관리 |
### 3.3 인프라
| 기술 | 용도 |
|------|------|
| MongoDB 7.0 | 메인 데이터베이스 |
| Redis 7 | 캐싱 레이어 |
| Docker Compose | 로컬 개발 환경 |
---
## 4. 프로젝트 구조
```
todos2/
├── docker-compose.yml
├── CLAUDE.md
├── PLAN.md
├── FEATURE_SPEC.md
├── SCREEN_DESIGN.pptx
├── SCREEN_DESIGN.md
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 앱 진입점
│ ├── database.py # MongoDB 연결 설정
│ ├── models/
│ │ ├── __init__.py
│ │ ├── todo.py # Todo 모델
│ │ ├── category.py # Category 모델
│ │ └── tag.py # Tag 모델
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── todos.py # 할일 CRUD API
│ │ ├── categories.py # 카테고리 API
│ │ ├── tags.py # 태그 API
│ │ ├── search.py # 검색 API
│ │ ├── dashboard.py # 대시보드 통계 API
│ │ └── batch.py # 일괄 작업 API
│ └── services/
│ ├── __init__.py
│ ├── todo_service.py
│ ├── category_service.py
│ ├── tag_service.py
│ ├── search_service.py
│ └── dashboard_service.py
└── frontend/
├── Dockerfile
├── package.json
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── src/
├── app/
│ ├── layout.tsx # 루트 레이아웃
│ ├── page.tsx # 대시보드 (메인)
│ ├── todos/
│ │ └── page.tsx # 할일 목록
│ ├── categories/
│ │ └── page.tsx # 카테고리 관리
│ └── search/
│ └── page.tsx # 검색 결과
├── components/
│ ├── layout/
│ │ ├── Header.tsx
│ │ ├── Sidebar.tsx
│ │ └── MainLayout.tsx
│ ├── todos/
│ │ ├── TodoCard.tsx
│ │ ├── TodoForm.tsx
│ │ ├── TodoList.tsx
│ │ ├── TodoFilter.tsx
│ │ └── BatchActions.tsx
│ ├── categories/
│ │ ├── CategoryList.tsx
│ │ └── CategoryForm.tsx
│ ├── tags/
│ │ ├── TagBadge.tsx
│ │ ├── TagSelect.tsx
│ │ └── TagManager.tsx
│ ├── dashboard/
│ │ ├── StatsCards.tsx
│ │ ├── CompletionChart.tsx
│ │ ├── CategoryChart.tsx
│ │ └── PriorityChart.tsx
│ └── search/
│ ├── SearchBar.tsx
│ └── SearchResults.tsx
├── hooks/
│ ├── useTodos.ts
│ ├── useCategories.ts
│ ├── useTags.ts
│ ├── useDashboard.ts
│ └── useSearch.ts
├── lib/
│ ├── api.ts # API 클라이언트
│ └── utils.ts # 유틸리티 함수
├── store/
│ └── uiStore.ts # UI 상태 (사이드바, 필터 등)
└── types/
└── index.ts # TypeScript 타입 정의
```
---
## 5. 데이터 모델
### 5.1 Todo
```json
{
"_id": "ObjectId",
"title": "string (필수, 1~200자)",
"content": "string (선택, 최대 2000자)",
"completed": "boolean (기본: false)",
"priority": "enum: high | medium | low (기본: medium)",
"category_id": "ObjectId | null",
"tags": ["string"],
"due_date": "datetime | null",
"created_at": "datetime",
"updated_at": "datetime"
}
```
### 5.2 Category
```json
{
"_id": "ObjectId",
"name": "string (필수, 1~50자, 고유)",
"color": "string (hex color, 기본: #6B7280)",
"order": "integer (정렬 순서)",
"created_at": "datetime"
}
```
### 5.3 Tag (인라인)
태그는 별도 컬렉션 없이 Todo 문서 내 배열로 관리한다.
태그 목록은 todos 컬렉션에서 distinct 쿼리로 추출한다.
---
## 6. 개발 우선순위 (Phase별)
### Phase 1: 핵심 기반 (MVP)
> 목표: 기본 할일 관리가 가능한 최소 기능 제품
1. 프로젝트 초기 세팅 (백엔드/프론트엔드 스캐폴딩)
2. MongoDB 연결 및 데이터 모델 정의
3. 할일 CRUD API + UI
4. 카테고리 CRUD API + UI
5. 기본 레이아웃 (Header, Sidebar, MainLayout)
### Phase 2: 확장 기능
> 목표: 분류/필터/우선순위로 할일 관리 고도화
6. 태그 시스템 (태그 입력, 표시, 필터링)
7. 우선순위 설정 (높음/중간/낮음)
8. 마감일 설정 및 임박/초과 알림 UI
9. 검색 기능 (제목/내용/태그)
### Phase 3: 분석 및 효율
> 목표: 통계 및 일괄 작업으로 생산성 향상
10. 대시보드 통계 API
11. 대시보드 차트 UI (완료율, 카테고리별, 우선순위별)
12. 일괄 작업 (다중 선택, 일괄 완료/삭제/카테고리 변경)
---
## 7. API 설계 요약
| Method | Endpoint | 설명 |
|--------|----------|------|
| GET | `/api/todos` | 할일 목록 (필터/페이지네이션) |
| POST | `/api/todos` | 할일 생성 |
| GET | `/api/todos/{id}` | 할일 상세 |
| PUT | `/api/todos/{id}` | 할일 수정 |
| DELETE | `/api/todos/{id}` | 할일 삭제 |
| POST | `/api/todos/batch` | 일괄 작업 |
| GET | `/api/categories` | 카테고리 목록 |
| POST | `/api/categories` | 카테고리 생성 |
| PUT | `/api/categories/{id}` | 카테고리 수정 |
| DELETE | `/api/categories/{id}` | 카테고리 삭제 |
| GET | `/api/tags` | 사용 중인 태그 목록 |
| GET | `/api/search` | 검색 |
| GET | `/api/dashboard/stats` | 대시보드 통계 |
---
## 8. 비기능 요구사항
| 항목 | 기준 |
|------|------|
| API 응답 시간 | 95% 요청 200ms 이내 |
| 페이지네이션 | 기본 20건, 최대 100건 |
| 검색 | MongoDB text index 활용 |
| 캐싱 | 대시보드 통계 Redis 캐시 (TTL 60초) |
| CORS | 프론트엔드 도메인 허용 |
| 에러 처리 | 표준 HTTP 상태 코드 + JSON 에러 응답 |

312
docs/SCREEN_DESIGN.md Normal file
View File

@ -0,0 +1,312 @@
# todos2 — 화면설계서
> 자동 생성: `pptx_to_md.py` | 원본: `SCREEN_DESIGN.pptx`
> 생성 시각: 2026-02-10 07:12
> **이 파일을 직접 수정하지 마세요. PPTX를 수정 후 스크립트를 재실행하세요.**
## 페이지 목록
| ID | 페이지명 | 경로 | 설명 |
|-----|---------|------|------|
| P-001 | 대시보드 | `/` | 메인 페이지. 통계 카드, 차트, 마감 임박 목록 |
| P-002 | 할일 목록 | `/todos` | 할일 CRUD, 필터링, 정렬, 일괄 작업 |
| P-003 | 할일 상세/편집 | `/todos/[id]` | 할일 상세 보기 및 수정 폼 |
| P-004 | 카테고리 관리 | `/categories` | 카테고리 CRUD, 색상 지정 |
| P-005 | 검색 결과 | `/search` | 제목/내용/태그 기반 검색 결과 |
---
## P-001: 대시보드 (`/`)
### 레이아웃
[로고 todos2] | [검색바 _______________] | [알림]
● 대시보드
할일 목록
카테고리 관리
카테고리
업무 (12)
개인 (8)
학습 (5)
인기 태그
#긴급
#회의
#프로젝트
전체 할일 50
완료 30
미완료 20
완료율 60%
[카테고리별 분포 - 도넛 차트] 업무 40% | 개인 30% 학습 20% | 기타 10%
[우선순위별 현황 - 막대 차트] high: 10 | medium: 25 | low: 15
[마감 임박 할일] 1. API 문서 작성 (D-1) 2. 디자인 리뷰 (D-2) 3. 테스트 코드 (D-3)
| 컴포넌트 | 기능 | 상태 |
| --- | --- | --- |
| StatsCards | 전체/완료/미완료/완료율 카드 | loading, data, empty |
| CategoryChart | 카테고리별 도넛 차트 | loading, data, empty |
| PriorityChart | 우선순위별 막대 차트 | loading, data, empty |
| UpcomingDeadlines | 마감 임박 할일 Top 5 | loading, data, empty |
| Sidebar | 카테고리/태그 네비게이션 | default |
### 컴포넌트
| 컴포넌트 | Props | 상태 |
|---------|-------|------|
| `StatsCards` | stats | loading, empty, data |
| `CategoryChart` | categoryData | loading, empty, data |
| `PriorityChart` | priorityData | loading, empty, data |
| `UpcomingDeadlines` | deadlines, onItemClick | loading, empty, data |
| `Sidebar` | categories, tags, activePath | default |
### 인터랙션
| 트리거 | 동작 | 결과 |
|--------|------|------|
| 마감 임박 항목 클릭 | `router.push(/todos/{id})` | 해당 할일 상세 페이지로 이동 |
| 사이드바 카테고리 클릭 | `router.push(/todos?category_id={id})` | 해당 카테고리의 할일 목록으로 이동 |
| 사이드바 태그 클릭 | `router.push(/todos?tag={name})` | 해당 태그의 할일 목록으로 이동 |
### 반응형: sm, md, lg
---
## P-002: 할일 목록 (`/todos`)
### 레이아웃
[로고 todos2] | [검색바 _______________] | [알림]
대시보드
● 할일 목록
카테고리 관리
필터:
상태 ▾
우선순위 ▾
정렬 ▾
+ 새 할일
3개 선택됨
일괄 완료
카테고리 변경
일괄 삭제
API 문서 작성
업무
#긴급
D-1
[수정] [삭제]
회의록 정리
업무
#회의
D-5
[수정] [삭제]
Next.js 학습
학습
#학습
D-7
[수정] [삭제]
장보기 목록 작성
개인
#생활
-
[수정] [삭제]
< 1 2 3 4 5 >
| 컴포넌트 | 기능 | 상태 |
| --- | --- | --- |
| TodoFilter | 상태/우선순위/정렬 필터 | default, applied |
| TodoList | 할일 카드 리스트 | loading, empty, error, data |
| TodoCard | 개별 할일 행 | default, completed, overdue |
| BatchActions | 일괄 작업 바 | hidden, visible |
| TodoForm (Modal) | 할일 생성/수정 모달 | create, edit |
| Pagination | 페이지 네비게이션 | default |
### 컴포넌트
| 컴포넌트 | Props | 상태 |
|---------|-------|------|
| `TodoFilter` | filters, onFilterChange | default, applied |
| `TodoList` | todos, selectedIds, onToggle, onSelect, onEdit, onDelete | loading, empty, error, data |
| `TodoCard` | todo, isSelected, onToggle, onSelect, onEdit, onDelete | default, completed, overdue |
| `BatchActions` | selectedIds, categories, onBatchComplete, onBatchDelete, onBatchMove | hidden, visible |
| `TodoForm` | mode, todo, categories, tags, onSubmit, onClose | create, edit |
| `Pagination` | currentPage, totalPages, onPageChange | default |
### 인터랙션
| 트리거 | 동작 | 결과 |
|--------|------|------|
| "+ 새 할일" 버튼 클릭 | `openTodoForm(mode='create')` | 할일 생성 모달 열림 |
| 체크박스 클릭 | `toggleTodo(id)` | 완료 상태 토글 |
| 행 선택 체크박스 | `toggleSelect(id)` | 일괄 작업 대상에 추가/제거 |
| 필터 변경 | `applyFilter(filters)` | 목록 재조회 |
| "일괄 완료" 클릭 | `batchComplete(selectedIds)` | 선택된 할일 일괄 완료 |
| "일괄 삭제" 클릭 | `batchDelete(selectedIds)` | 확인 후 일괄 삭제 |
| "카테고리 변경" 클릭 | `batchMoveCategory(selectedIds, categoryId)` | 카테고리 선택 후 변경 |
| 태그 뱃지 클릭 | `applyFilter({tag: tagName})` | 해당 태그로 필터링 |
### 반응형: sm, md, lg
---
## P-003: 할일 상세/편집 (`/todos/[id]`)
### 레이아웃
[로고 todos2] | [검색바] | [알림]
(Sidebar)
할일 목록 > API 문서 작성
제목 *
API 문서 작성
내용
Swagger UI 기반 API 문서를 작성하고 엔드포인트별 요청/응답 예시를 추가한다.
카테고리
업무 ▾
우선순위
높음 ▾
마감일
2026-02-11 📅
태그
#긴급 ×
#문서 ×
태그 입력 (자동완성)...
취소
저장
| 컴포넌트 | 기능 | 상태 |
| --- | --- | --- |
| TodoDetailForm | 할일 상세 폼 | loading, view, edit, saving |
| CategorySelect | 카테고리 드롭다운 | default |
| PrioritySelect | 우선순위 드롭다운 (색상) | default |
| DatePicker | 달력 마감일 선택 | default, open |
| TagInput | 태그 입력 + 자동완성 + 뱃지 | default, suggesting |
### 컴포넌트
| 컴포넌트 | Props | 상태 |
|---------|-------|------|
| `TodoDetailForm` | todo, categories, tags, onSave, onCancel, onDelete | loading, view, edit, saving |
| `CategorySelect` | categories, selectedId, onChange | default |
| `PrioritySelect` | selectedPriority, onChange | default |
| `DatePicker` | selectedDate, onChange | default, open |
| `TagInput` | tags, suggestions, onAdd, onRemove | default, suggesting |
### 인터랙션
| 트리거 | 동작 | 결과 |
|--------|------|------|
| "저장" 버튼 클릭 | `updateTodo(id, formData)` | 할일 업데이트 후 성공 토스트 |
| "취소" 버튼 클릭 | `router.back()` | 이전 페이지로 이동 |
| 태그 입력 키워드 타이핑 | `fetchTagSuggestions(keyword)` | 자동완성 드롭다운 표시 |
| 태그 뱃지 × 클릭 | `removeTag(tagName)` | 태그 제거 |
| 달력 아이콘 클릭 | `openDatePicker()` | 달력 팝업 표시 |
### 반응형: sm, md, lg
---
## P-004: 카테고리 관리 (`/categories`)
### 레이아웃
[로고 todos2] | [검색바] | [알림]
대시보드
할일 목록
● 카테고리 관리
카테고리 관리
+ 새 카테고리
업무
12개 할일
[색상] [수정] [삭제]
개인
8개 할일
[색상] [수정] [삭제]
학습
5개 할일
[색상] [수정] [삭제]
건강
3개 할일
[색상] [수정] [삭제]
새 카테고리 이름...
추가
| 컴포넌트 | 기능 | 상태 |
| --- | --- | --- |
| CategoryList | 카테고리 목록 | loading, empty, data |
| CategoryItem | 개별 카테고리 행 | default, editing |
| CategoryForm | 카테고리 생성/수정 인라인 폼 | create, edit |
| ColorPicker | 카테고리 색상 선택기 | default, open |
### 컴포넌트
| 컴포넌트 | Props | 상태 |
|---------|-------|------|
| `CategoryList` | categories, onEdit, onDelete | loading, empty, data |
| `CategoryItem` | category, onEdit, onDelete | default, editing |
| `CategoryForm` | mode, category, onSubmit, onCancel | create, edit |
| `ColorPicker` | selectedColor, onChange | default, open |
### 인터랙션
| 트리거 | 동작 | 결과 |
|--------|------|------|
| "+ 새 카테고리" 버튼 클릭 | `showCategoryForm(mode='create')` | 인라인 생성 폼 표시 |
| "추가" 버튼 클릭 | `createCategory({name, color})` | 카테고리 생성 후 목록 갱신 |
| "수정" 클릭 | `showCategoryForm(mode='edit', category)` | 해당 행이 수정 폼으로 전환 |
| "삭제" 클릭 | `deleteCategory(id)` | 확인 다이얼로그 후 삭제 |
| 색상 변경 클릭 | `openColorPicker(category)` | 색상 선택기 팝업 |
### 반응형: sm, md, lg
---
## P-005: 검색 결과 (`/search`)
### 레이아웃
todos2
API 문서 [×]
[알림]
(Sidebar)
"API 문서" 검색 결과 (3건)
API 문서 작성
Swagger UI 기반 API 문서를 작성하고...
업무
D-1
API 문서 리뷰
팀원들과 API 문서 리뷰 미팅을...
업무
D-5
REST API 문서화 학습
OpenAPI 스펙과 자동화 도구를...
학습
D-14
* 결과 없을 때: "검색 결과가 없습니다. 다른 키워드로 검색해보세요."
| 컴포넌트 | 기능 | 상태 |
| --- | --- | --- |
| SearchBar | 헤더 내 검색 입력 + 클리어 | default, active, has_query |
| SearchResults | 검색 결과 리스트 | loading, empty, data |
| SearchResultItem | 개별 결과 (제목 하이라이트, 설명) | default |
| Pagination | 결과 페이지네이션 | default |
### 컴포넌트
| 컴포넌트 | Props | 상태 |
|---------|-------|------|
| `SearchBar` | query, onSearch, onClear | default, active, has_query |
| `SearchResults` | results, query, total, onItemClick | loading, empty, data |
| `SearchResultItem` | result, query, onClick | default |
| `Pagination` | currentPage, totalPages, onPageChange | default |
### 인터랙션
| 트리거 | 동작 | 결과 |
|--------|------|------|
| 검색바에 키워드 입력 후 Enter | `search(query)` | 검색 API 호출 후 결과 표시 |
| 검색바 × 버튼 클릭 | `clearSearch()` | 검색어 클리어, 이전 페이지로 이동 |
| 검색 결과 항목 클릭 | `router.push(/todos/{id})` | 해당 할일 상세 페이지로 이동 |
| 페이지 번호 클릭 | `search(query, page)` | 해당 페이지의 검색 결과 |
### 반응형: sm, md, lg

BIN
docs/SCREEN_DESIGN.pptx Normal file

Binary file not shown.

320
docs/TEST_REPORT.md Normal file
View File

@ -0,0 +1,320 @@
# todos2 -- 테스트 보고서
> 테스트 일시: 2026-02-10 10:30 KST
> 테스트 환경: macOS Darwin 25.2.0, Python 3.11, Node 20
> 테스터: Senior System Tester + DevOps (Claude Opus 4.6)
---
## 1. 백엔드 테스트
### 1.1 구문 검증 (Python AST)
| 파일 | 결과 |
|------|------|
| `app/__init__.py` | OK |
| `app/config.py` | OK |
| `app/database.py` | OK |
| `app/main.py` | OK |
| `app/models/__init__.py` | OK |
| `app/models/category.py` | OK |
| `app/models/common.py` | OK |
| `app/models/todo.py` | OK |
| `app/routers/__init__.py` | OK |
| `app/routers/categories.py` | OK |
| `app/routers/dashboard.py` | OK |
| `app/routers/search.py` | OK |
| `app/routers/tags.py` | OK |
| `app/routers/todos.py` | OK |
| `app/services/__init__.py` | OK |
| `app/services/category_service.py` | OK |
| `app/services/dashboard_service.py` | OK |
| `app/services/search_service.py` | OK |
| `app/services/todo_service.py` | OK |
**결과: 19/19 파일 통과 (100%)**
### 1.2 Import 정합성
| 파일 | Import | 참조 대상 | 결과 |
|------|--------|----------|------|
| `app/main.py` | `from contextlib import asynccontextmanager` | stdlib | OK |
| `app/main.py` | `from datetime import datetime` | stdlib | OK |
| `app/main.py` | `from fastapi import FastAPI` | fastapi (requirements.txt) | OK |
| `app/main.py` | `from fastapi.middleware.cors import CORSMiddleware` | fastapi | OK |
| `app/main.py` | `from app.config import get_settings` | app/config.py | OK |
| `app/main.py` | `from app.database import connect_db, disconnect_db` | app/database.py | OK |
| `app/main.py` | `from app.routers import todos, categories, tags, search, dashboard` | app/routers/*.py | OK |
| `app/config.py` | `from pydantic_settings import BaseSettings` | pydantic-settings (requirements.txt) | OK |
| `app/config.py` | `from pydantic import Field` | pydantic (requirements.txt) | OK |
| `app/config.py` | `from functools import lru_cache` | stdlib | OK |
| `app/database.py` | `from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase` | motor (requirements.txt) | OK |
| `app/database.py` | `import redis.asyncio as aioredis` | redis (requirements.txt) | OK |
| `app/database.py` | `from app.config import get_settings` | app/config.py | OK |
| `app/models/common.py` | `from bson import ObjectId` | pymongo (requirements.txt) | OK |
| `app/models/common.py` | `from pydantic import BaseModel, BeforeValidator` | pydantic | OK |
| `app/models/todo.py` | `from datetime import datetime` | stdlib | OK |
| `app/models/todo.py` | `from enum import Enum` | stdlib | OK |
| `app/models/todo.py` | `from pydantic import BaseModel, Field, field_validator` | pydantic | OK |
| `app/models/todo.py` | `from bson import ObjectId` (inside validator) | pymongo | OK |
| `app/models/category.py` | `from datetime import datetime` | stdlib | OK |
| `app/models/category.py` | `from pydantic import BaseModel, Field, field_validator` | pydantic | OK |
| `app/models/__init__.py` | `from app.models.common import ...` | app/models/common.py | OK |
| `app/models/__init__.py` | `from app.models.todo import ...` | app/models/todo.py | OK |
| `app/models/__init__.py` | `from app.models.category import ...` | app/models/category.py | OK |
| `app/routers/__init__.py` | `from app.routers import todos, categories, tags, search, dashboard` | app/routers/*.py | OK |
| `app/routers/todos.py` | `from fastapi import APIRouter, Depends, Query, Response, status` | fastapi | OK |
| `app/routers/todos.py` | `from app.database import get_database, get_redis` | app/database.py | OK |
| `app/routers/todos.py` | `from app.models.todo import ...` | app/models/todo.py | OK |
| `app/routers/todos.py` | `from app.services.todo_service import TodoService` | app/services/todo_service.py | OK |
| `app/routers/categories.py` | `from app.database import get_database, get_redis` | app/database.py | OK |
| `app/routers/categories.py` | `from app.models.category import ...` | app/models/category.py | OK |
| `app/routers/categories.py` | `from app.services.category_service import CategoryService` | app/services/category_service.py | OK |
| `app/routers/tags.py` | `from app.database import get_database` | app/database.py | OK |
| `app/routers/tags.py` | `from app.models.todo import TagInfo` | app/models/todo.py | OK |
| `app/routers/tags.py` | `from app.services.todo_service import TodoService` | app/services/todo_service.py | OK |
| `app/routers/search.py` | `from app.database import get_database` | app/database.py | OK |
| `app/routers/search.py` | `from app.models.todo import SearchResponse` | app/models/todo.py | OK |
| `app/routers/search.py` | `from app.services.search_service import SearchService` | app/services/search_service.py | OK |
| `app/routers/dashboard.py` | `from app.database import get_database, get_redis` | app/database.py | OK |
| `app/routers/dashboard.py` | `from app.models.todo import DashboardStats` | app/models/todo.py | OK |
| `app/routers/dashboard.py` | `from app.services.dashboard_service import DashboardService` | app/services/dashboard_service.py | OK |
| `app/services/__init__.py` | `from app.services.todo_service import TodoService` | app/services/todo_service.py | OK |
| `app/services/__init__.py` | `from app.services.category_service import CategoryService` | app/services/category_service.py | OK |
| `app/services/__init__.py` | `from app.services.search_service import SearchService` | app/services/search_service.py | OK |
| `app/services/__init__.py` | `from app.services.dashboard_service import DashboardService` | app/services/dashboard_service.py | OK |
| `app/services/todo_service.py` | `from bson import ObjectId` | pymongo | OK |
| `app/services/todo_service.py` | `from fastapi import HTTPException` | fastapi | OK |
| `app/services/todo_service.py` | `from motor.motor_asyncio import AsyncIOMotorDatabase` | motor | OK |
| `app/services/todo_service.py` | `import redis.asyncio as aioredis` | redis | OK |
| `app/services/todo_service.py` | `from app.models.todo import ...` | app/models/todo.py | OK |
| `app/services/category_service.py` | `from app.models.category import ...` | app/models/category.py | OK |
| `app/services/search_service.py` | `from app.models.todo import TodoResponse, SearchResponse` | app/models/todo.py | OK |
| `app/services/dashboard_service.py` | `import json` | stdlib | OK |
| `app/services/dashboard_service.py` | `from app.models.todo import ...` | app/models/todo.py | OK |
**결과: 53/53 import 통과 (100%)**
### 1.3 API 엔드포인트 매핑
| 기능 (FEATURE_SPEC) | API 엔드포인트 | Router 파일 | 결과 |
|---------------------|---------------|------------|------|
| F-001: 할일 생성 | `POST /api/todos` | `routers/todos.py` (L62-68) | OK |
| F-002: 할일 목록 조회 | `GET /api/todos` | `routers/todos.py` (L37-59) | OK |
| F-003: 할일 상세 조회 | `GET /api/todos/{id}` | `routers/todos.py` (L71-77) | OK |
| F-004: 할일 수정 | `PUT /api/todos/{id}` | `routers/todos.py` (L80-87) | OK |
| F-005: 할일 삭제 | `DELETE /api/todos/{id}` | `routers/todos.py` (L90-97) | OK |
| F-006: 할일 완료 토글 | `PATCH /api/todos/{id}/toggle` | `routers/todos.py` (L100-106) | OK |
| F-007: 카테고리 생성 | `POST /api/categories` | `routers/categories.py` (L29-35) | OK |
| F-008: 카테고리 목록 조회 | `GET /api/categories` | `routers/categories.py` (L21-26) | OK |
| F-009: 카테고리 수정 | `PUT /api/categories/{id}` | `routers/categories.py` (L38-45) | OK |
| F-010: 카테고리 삭제 | `DELETE /api/categories/{id}` | `routers/categories.py` (L48-55) | OK |
| F-011: 태그 부여 | F-001/F-004의 `tags` 필드 | `models/todo.py` + `services/todo_service.py` | OK |
| F-012: 태그별 필터링 | `GET /api/todos?tag=...` | `routers/todos.py` (L44) | OK |
| F-013: 태그 목록 조회 | `GET /api/tags` | `routers/tags.py` (L10-16) | OK |
| F-014: 우선순위 설정 | F-001/F-004의 `priority` 필드 | `models/todo.py` Priority enum | OK |
| F-015: 마감일 설정 | F-001/F-004의 `due_date` 필드 | `models/todo.py` | OK |
| F-016: 마감일 알림 표시 | 프론트엔드 UI 로직 | `lib/utils.ts` getDueDateStatus/Label/Color | OK |
| F-017: 검색 | `GET /api/search?q=...` | `routers/search.py` (L10-19) | OK |
| F-018: 대시보드 통계 | `GET /api/dashboard/stats` | `routers/dashboard.py` (L10-17) | OK |
| F-019: 일괄 완료 처리 | `POST /api/todos/batch` (action=complete) | `routers/todos.py` (L28-34) | OK |
| F-020: 일괄 삭제 | `POST /api/todos/batch` (action=delete) | `routers/todos.py` (L28-34) | OK |
| F-021: 일괄 카테고리 변경 | `POST /api/todos/batch` (action=move_category) | `routers/todos.py` (L28-34) | OK |
**결과: 21/21 기능 매핑 완료 (100%)**
---
## 2. 프론트엔드 테스트
### 2.1 빌드 검증
- Next.js 빌드: **OK** (이미 성공 확인됨)
### 2.2 화면설계서 컴포넌트 매핑
#### P-001: 대시보드 (`/`)
| 컴포넌트 (SCREEN_DESIGN) | 파일 | 결과 |
|--------------------------|------|------|
| `StatsCards` | `src/components/dashboard/StatsCards.tsx` | OK |
| `CategoryChart` | `src/components/dashboard/CategoryChart.tsx` | OK |
| `PriorityChart` | `src/components/dashboard/PriorityChart.tsx` | OK |
| `UpcomingDeadlines` | `src/components/dashboard/UpcomingDeadlines.tsx` | OK |
| `Sidebar` | `src/components/layout/Sidebar.tsx` | OK |
| 페이지 | `src/app/page.tsx` | OK |
#### P-002: 할일 목록 (`/todos`)
| 컴포넌트 (SCREEN_DESIGN) | 파일 | 결과 |
|--------------------------|------|------|
| `TodoFilter` | `src/components/todos/TodoFilter.tsx` | OK |
| `TodoList` | `src/components/todos/TodoList.tsx` | OK |
| `TodoCard` | `src/components/todos/TodoCard.tsx` | OK |
| `BatchActions` | `src/components/todos/BatchActions.tsx` | OK |
| `TodoForm` (Modal) | `src/components/todos/TodoForm.tsx` | OK |
| `Pagination` | `src/components/common/Pagination.tsx` | OK |
| 페이지 | `src/app/todos/page.tsx` | OK |
#### P-003: 할일 상세/편집 (`/todos/[id]`)
| 컴포넌트 (SCREEN_DESIGN) | 파일 | 결과 |
|--------------------------|------|------|
| `TodoDetailForm` | `src/components/todos/TodoDetailForm.tsx` | OK |
| `CategorySelect` (inline select) | `TodoDetailForm.tsx``<select>` (L146-158) | OK |
| `PrioritySelect` (inline select) | `TodoDetailForm.tsx``<select>` (L162-174) | OK |
| `DatePicker` | `src/components/common/DatePicker.tsx` | OK |
| `TagInput` | `src/components/common/TagInput.tsx` | OK |
| 페이지 | `src/app/todos/[id]/page.tsx` | OK |
#### P-004: 카테고리 관리 (`/categories`)
| 컴포넌트 (SCREEN_DESIGN) | 파일 | 결과 |
|--------------------------|------|------|
| `CategoryList` | `src/components/categories/CategoryList.tsx` | OK |
| `CategoryItem` | `src/components/categories/CategoryItem.tsx` | OK |
| `CategoryForm` | `src/components/categories/CategoryForm.tsx` | OK |
| `ColorPicker` | `src/components/categories/ColorPicker.tsx` | OK |
| 페이지 | `src/app/categories/page.tsx` | OK |
#### P-005: 검색 결과 (`/search`)
| 컴포넌트 (SCREEN_DESIGN) | 파일 | 결과 |
|--------------------------|------|------|
| `SearchBar` | `src/components/search/SearchBar.tsx` | OK |
| `SearchResults` | `src/components/search/SearchResults.tsx` | OK |
| `SearchResultItem` | `SearchResults.tsx` 내 인라인 구현 (검색 결과 항목 렌더링 + 하이라이트) | OK |
| `Pagination` | `src/components/common/Pagination.tsx` (공유) | OK |
| 페이지 | `src/app/search/page.tsx` | OK |
**결과: 5개 페이지, 27개 컴포넌트 모두 매핑 완료 (100%)**
### 2.3 타입 정합성
- `types/index.ts`: 14개 인터페이스/타입 정의 (Todo, TodoCreate, TodoUpdate, TodoListResponse, ToggleResponse, BatchRequest, BatchResponse, Category, CategoryCreate, CategoryUpdate, TagInfo, SearchResponse, DashboardStats, TodoFilters)
- `hooks/useTodos.ts`: types/index.ts의 Todo, TodoCreate, TodoUpdate, TodoListResponse, TodoFilters, ToggleResponse, BatchRequest, BatchResponse 사용 - **OK**
- `hooks/useCategories.ts`: types/index.ts의 Category, CategoryCreate, CategoryUpdate 사용 - **OK**
- `hooks/useDashboard.ts`: types/index.ts의 DashboardStats 사용 - **OK**
- `hooks/useSearch.ts`: types/index.ts의 SearchResponse 사용 - **OK**
- `hooks/useTags.ts`: types/index.ts의 TagInfo 사용 - **OK**
- `lib/api.ts`: types/index.ts의 ApiError 사용 - **OK**
- `store/uiStore.ts`: types/index.ts의 TodoFilters, SortField, SortOrder 사용 - **OK**
- 컴포넌트 -> hooks -> types 연결: **OK**
### 2.4 공통 컴포넌트
| 컴포넌트 | 파일 | 용도 | 결과 |
|---------|------|------|------|
| `Header` | `src/components/layout/Header.tsx` | 상단 헤더 (로고, 검색바, 알림) | OK |
| `MainLayout` | `src/components/layout/MainLayout.tsx` | 전체 레이아웃 래퍼 | OK |
| `Sidebar` | `src/components/layout/Sidebar.tsx` | 사이드바 네비게이션 | OK |
| `QueryProvider` | `src/components/providers/QueryProvider.tsx` | Tanstack Query 프로바이더 | OK |
| `Pagination` | `src/components/common/Pagination.tsx` | 페이지네이션 | OK |
| `PriorityBadge` | `src/components/common/PriorityBadge.tsx` | 우선순위 뱃지 | OK |
| `TagBadge` | `src/components/common/TagBadge.tsx` | 태그 뱃지 | OK |
| `TagInput` | `src/components/common/TagInput.tsx` | 태그 입력 + 자동완성 | OK |
| `DatePicker` | `src/components/common/DatePicker.tsx` | 날짜 선택 | OK |
---
## 3. Docker 검증
### 3.1 docker-compose.yml 검증
- **문법 검증**: `docker compose config --quiet` -- **OK** (오류 없음)
- **서비스 구성**:
| 서비스 | 이미지 | 포트 | healthcheck | volumes | networks | 결과 |
|--------|--------|------|-------------|---------|----------|------|
| mongodb | `mongo:7.0` | 27017:27017 | `mongosh --eval` | mongodb_data:/data/db | app-network | OK |
| redis | `redis:7-alpine` | 6379:6379 | `redis-cli ping` | redis_data:/data | app-network | OK |
| backend | `./backend/Dockerfile` | 8000:8000 | - | - | app-network | OK |
| frontend | `./frontend/Dockerfile` | 3000:3000 | - | - | app-network | OK |
- **환경변수**: MONGODB_URL, DB_NAME, REDIS_URL (backend) - **OK**
- **의존성**: backend -> mongodb (service_healthy), redis (service_healthy) - **OK**
- **네트워크**: `app-network` (bridge) - **OK**
- **볼륨**: `mongodb_data`, `redis_data` - **OK**
### 3.2 Dockerfile 검증
| 서비스 | Dockerfile | 베이스 이미지 | 빌드 단계 | CMD | 결과 |
|--------|-----------|------------|----------|-----|------|
| backend | `backend/Dockerfile` | `python:3.11-slim` | apt curl, pip install, COPY app/ | `uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload` | OK |
| frontend | `frontend/Dockerfile` | `node:20-alpine` | npm ci, COPY ., npm run build | `npm run start` | OK |
### 3.3 .env 파일 검증
| 변수 | 값 | 사용처 | 결과 |
|------|-----|--------|------|
| `PROJECT_NAME` | `todos2` | container_name 프리픽스 | OK |
| `MONGO_USER` | `admin` | MONGO_INITDB_ROOT_USERNAME | OK |
| `MONGO_PASSWORD` | `password123` | MONGO_INITDB_ROOT_PASSWORD | OK |
| `MONGO_PORT` | `27017` | mongodb 포트 매핑 | OK |
| `REDIS_PORT` | `6379` | redis 포트 매핑 | OK |
| `DB_NAME` | `todos2` | MongoDB 데이터베이스 이름 | OK (수정됨) |
| `BACKEND_PORT` | `8000` | backend 포트 매핑 | OK |
| `FRONTEND_PORT` | `3000` | frontend 포트 매핑 | OK |
---
## 4. 종합 결과
| 항목 | 테스트 수 | 통과 | 실패 | 통과율 |
|------|----------|------|------|--------|
| 백엔드 구문 (AST) | 19 | 19 | 0 | 100% |
| 백엔드 Import | 53 | 53 | 0 | 100% |
| API 엔드포인트 매핑 | 21 | 21 | 0 | 100% |
| 프론트엔드 빌드 | 1 | 1 | 0 | 100% |
| 화면 컴포넌트 매핑 | 27 | 27 | 0 | 100% |
| Docker 서비스 | 4 | 4 | 0 | 100% |
| Docker 파일 | 2 | 2 | 0 | 100% |
| .env 변수 | 8 | 8 | 0 | 100% |
| **합계** | **135** | **135** | **0** | **100%** |
---
## 5. 발견된 이슈 및 수정 사항
### 5.1 수정 완료
| # | 파일 | 이슈 | 수정 내역 |
|---|------|------|----------|
| 1 | `.env` | `DB_NAME=app_db` (요구사항: `todos2`) | `DB_NAME=todos2`로 수정 |
**설명**: `.env` 파일의 `DB_NAME` 값이 `app_db`로 설정되어 있었으나, 프로젝트 요구사항 및 `config.py`의 기본값(`todos2`)과 불일치. `todos2`로 수정하여 docker-compose.yml의 `DB_NAME=${DB_NAME:-app_db}` 환경변수가 올바르게 `todos2`를 참조하도록 변경.
### 5.2 참고 사항 (이슈 아님)
| # | 항목 | 설명 |
|---|------|------|
| 1 | `routers/tags.py` | `TodoService(db)` -- redis 없이 초기화. 태그 조회는 캐시 불필요하므로 정상 동작. TodoService 생성자에서 `redis_client``Optional[aioredis.Redis] = None`이므로 문제 없음. |
| 2 | `SearchService` | `_populate_categories_bulk` 메서드가 `TodoService`와 중복 존재. 리팩토링 여지가 있으나 기능상 문제 없음. |
| 3 | `SCREEN_DESIGN.md``SearchResultItem` | 별도 컴포넌트가 아닌 `SearchResults.tsx` 내부 인라인 구현. 기능 동작에는 문제 없음. |
| 4 | backend Dockerfile | `--reload` 플래그가 프로덕션에서는 제거되어야 하지만, 현재 개발 환경 설정이므로 허용. |
---
## 6. 아키텍처 준수 확인
| 아키텍처 항목 (ARCHITECTURE.md) | 구현 상태 | 결과 |
|-------------------------------|----------|------|
| Frontend: Next.js App Router | `src/app/` 디렉토리 사용 | OK |
| Frontend: TypeScript | `.tsx`, `.ts` 파일 사용 | OK |
| Frontend: Tailwind CSS | `globals.css` + className 유틸리티 | OK |
| Frontend: Recharts | `CategoryChart.tsx`, `PriorityChart.tsx`에서 사용 | OK |
| Frontend: Tanstack Query | `QueryProvider.tsx` + 5개 hooks | OK |
| Frontend: Zustand | `store/uiStore.ts` | OK |
| Backend: FastAPI | `main.py` + 5개 라우터 | OK |
| Backend: Motor (MongoDB) | `database.py` AsyncIOMotorClient | OK |
| Backend: Pydantic v2 | `models/` BaseModel, field_validator | OK |
| Backend: Redis (aioredis) | `database.py` redis.asyncio | OK |
| Backend: 3-Layer (Router -> Service -> DB) | routers/ -> services/ -> database.py | OK |
| Database: MongoDB 7.0 | docker-compose.yml `mongo:7.0` | OK |
| Database: Redis 7 | docker-compose.yml `redis:7-alpine` | OK |
| Infra: Docker Compose | `docker-compose.yml` 4 서비스 | OK |
| 캐싱: Redis TTL 60s | `dashboard_service.py` DASHBOARD_CACHE_TTL=60 | OK |
| 캐시 무효화: CUD 시 | `todo_service.py`, `category_service.py` _invalidate_cache() | OK |
| Text Search Index | `database.py` create_indexes() title/content/tags | OK |
**아키텍처 준수율: 17/17 (100%)**