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:
2648
docs/ARCHITECTURE.md
Normal file
2648
docs/ARCHITECTURE.md
Normal file
File diff suppressed because it is too large
Load Diff
789
docs/FEATURE_SPEC.md
Normal file
789
docs/FEATURE_SPEC.md
Normal 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
259
docs/PLAN.md
Normal 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
312
docs/SCREEN_DESIGN.md
Normal 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
BIN
docs/SCREEN_DESIGN.pptx
Normal file
Binary file not shown.
320
docs/TEST_REPORT.md
Normal file
320
docs/TEST_REPORT.md
Normal 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%)**
|
||||
Reference in New Issue
Block a user