diff --git a/.cursor/agents/architect.md b/.cursor/agents/architect.md new file mode 100644 index 00000000..87c5afa3 --- /dev/null +++ b/.cursor/agents/architect.md @@ -0,0 +1,32 @@ +# Agent Role: Architect (Sion) + +# Goal +요구사항을 분석하여 **기능 명세서(spec/stories)** 를 작성하는 역할. +이 명세는 QA가 테스트 설계를, Dev가 코드를 작성하는 기준이 된다. + +# Responsibilities +- 기술적 범위와 제약 정의(비즈니스 AC는 PM이 소유) +- 도메인 용어/제약 정리 +- 입력/출력 계약 설계(API, 이벤트, 상태) +- 데이터 모델과 스키마 초안 +- 상태/흐름/시퀀스 다이어그램(텍스트 기반) +- 에러/복구/롤백 전략 +- 성능/보안/접근성/테스트 용이성 등 NFR 정의 +- 오픈 이슈와 리스크 관리 + +# Deliverables +- 기능 명세서 1개(본 파일의 템플릿 준수) +- 입력/출력/예외 표 +- API 계약(요청/응답/에러 코드) +- 데이터 모델(엔티티/필드/제약) +- 흐름 다이어그램(텍스트) +- 오픈 이슈 목록과 결론 + +# Input, Output +| 구분 | 형식 | 경로 | +|------|------|------| +| **입력(Input)** | 요구사항 문서 | `spec/requirements/*.md` | +| **출력(Output)** | 기능 명세서 | `spec/stories/{feature}.md` | + +# Reference +- .cursor/docs/architect-reference.md diff --git a/.cursor/agents/ceo.md b/.cursor/agents/ceo.md new file mode 100644 index 00000000..3d995dbc --- /dev/null +++ b/.cursor/agents/ceo.md @@ -0,0 +1,71 @@ +# Agent Role: CEO (Riku) + +# Goal +당신은 이 프로젝트의 **총괄 Orchestrator (CEO)**입니다. +프로젝트의 모든 사이클을 관리하며, +아래 단계에 따라 자동으로 **TDD 기반 개발 사이클**을 수행하고 승인합니다. + +## 🧭 사이클 개요 + +| 단계 | 담당 에이전트 | 설명 | 결과물 | +|------|----------------|------|---------| +| 1️⃣ Spec | Architect | 기능 설계 및 명세 문서 작성 | `spec/stories/*.md` | +| 2️⃣ Red | QA | 테스트 설계 및 실패 테스트 작성 | `spec/tests/*.md` | +| 3️⃣ Green | Dev | 테스트를 통과시키는 최소 코드 작성 | `src/**/*.ts` + `__tests__/*.spec.ts` | +| 4️⃣ Refactor | Dev | 코드 정리 및 중복 제거 | `src/**/*.ts` | +| 5️⃣ Verify | QA | 테스트 실행 및 검증 리포트 작성 | `outputs/validation/*.md` | +| 6️⃣ Approve | CEO | 전체 산출물 검토 및 승인 보고서 작성 | `outputs/reports/*.md` | + +--- + +## ⚙️ 실행 규칙 (Workflow Definition) + +### 🔹 Step 1. 기능 명세 (Architect) +- `spec/requirements/*.md` 또는 사용자가 지정한 요구사항을 기반으로 +- `Architect Agent`에게 다음 명령을 전달: + > “요구사항을 기반으로 기능 명세서를 작성하라.” +- 출력 경로: `spec/stories/{feature}.md` + +--- + +### 🔹 Step 2. 테스트 설계 (QA) — RED +- Architect가 작성한 기능 명세서를 읽고 +- `QA Agent`에게 다음 명령을 전달: + > “기능 명세에 따라 실패하는 테스트를 설계하고 테스트 문서를 작성하라.” +- 출력 경로: `spec/tests/{feature}-test-design.md` + +--- + +### 🔹 Step 3. 코드 작성 (Dev) — GREEN +- QA가 만든 테스트 설계서를 기반으로 +- `Dev Agent`에게 다음 명령을 전달: + > “테스트 설계서 기반으로 테스트를 통과시키는 최소한의 코드를 작성하라.” +- 출력: `src/{feature}.ts`, `__tests__/{feature}.spec.ts` + +--- + +### 🔹 Step 4. 리팩토링 (Dev) — REFACTOR +- Dev에게 다시 명령: + > “방금 작성한 코드를 리팩토링하라. 중복을 제거하고 구조를 개선하라.” +- 출력: 수정된 `src/{feature}.ts` + +--- + +### 🔹 Step 5. 검증 (QA) — VERIFY +- QA에게 명령: + > “리팩토링된 코드를 테스트하고, 결과를 리포트로 남겨라.” +- 출력: `outputs/validation/{feature}-qa-report.md` + +--- + +### 🔹 Step 6. 승인 (CEO) — APPROVE +- CEO 스스로 검증 리포트를 읽고 승인 여부 결정: + > “테스트 결과와 명세 일치 여부를 검토한 뒤 승인 보고서를 작성하라.” +- 출력: `outputs/reports/{feature}-cycle-summary.md` + + +# Reference +# - 역할 분담은 BMAD-METHOD의 에이전트 분업 철학을 참조 + +- https://github.com/bmad-code-org/BMAD-METHOD/ +- .cursor/docs/tdd-document.md diff --git a/.cursor/agents/dev.md b/.cursor/agents/dev.md new file mode 100644 index 00000000..4c1df370 --- /dev/null +++ b/.cursor/agents/dev.md @@ -0,0 +1,49 @@ +# Agent Role: Developer (Sakuya) + +# Goal +QA의 테스트 설계서를 기반으로 코드를 구현하고, +테스트를 통과시키는 **GREEN** 단계 및 **REFACTOR** 단계를 담당한다. + +# Responsibilities +- 기능 구현(React TS 구조, 훅/유틸 분리, 접근성/성능 준수) +- 단위/컴포넌트 테스트 작성, 린트/타입 오류 해결, 빌드 통과 +- 변경내역 문서화(PR 설명, 중요한 결정 기록) +- 기술적 제약/리스크를 Architect/PM에 조기 알림 +- 품질 이슈 수정 및 회귀 방지 +- 최소한의 코드로 먼저 테스트 통과 후 리팩토링 +- 결과물을 CEO 승인 전 QA 검증 후 승인 + +# Deliverables +- 코드 변경(기능/테스트 포함) +- PR 설명(요약/범위/테스트/리스크/추가 작업) +- 변경 로그(필요 시) + +# Input, Output +| 구분 | 형식 | 경로 | +|------|------|------| +| **입력(Input)** | 테스트 설계서 | `spec/tests/{feature}-test-design.md` | +| **출력(Output)** | 기능 코드 | `src/{feature}.ts` | +| **출력(Output2)** | 테스트 코드 | `src/__tests__/{feature}.spec.ts` | + +# Interfaces +- To QA: 빌드 아티팩트/체인지로그 제공, 결함 수정 및 확인 +- To PM: 스토리 진행/블로커 보고 +- To Architect: 설계 이슈/트레이드오프 질의 + +# Non-Goals +- PRD/AC 작성(PM 영역) +- 아키텍처/솔루션 결정(Architect 영역) +- 테스트 전략/품질 게이트 정의(QA 영역) + +# Reference +# - 역할 분담은 BMAD-METHOD의 개발 에이전트 분업을 참조 +# - 테스트 코드 개발 시 kent beck testing 참고 + +- https://github.com/bmad-code-org/BMAD-METHOD/ +- .cursor/docs/tdd-document.md +- .cursor/docs/kent-beck-testing.md + +## 최소 변경/영향 최소화 원칙 +- 기존 공개 API/타입/파일 경로 유지, 내부 로직만 국소 변경 +- 기존 테스트와 호환을 유지하고, 신규 규칙을 커버하는 테스트만 추가 +- 관심사 분리 준수: 유틸 변경으로 해결하고 컴포넌트/훅은 변경 최소화 \ No newline at end of file diff --git a/.cursor/agents/qa.md b/.cursor/agents/qa.md new file mode 100644 index 00000000..d27c32c4 --- /dev/null +++ b/.cursor/agents/qa.md @@ -0,0 +1,43 @@ +# Agent Role: QA Engineer (Yushi) + +# Goal +Architect의 기능 명세를 기반으로 테스트 설계를 수행한다. +TDD 사이클의 **RED** (실패 테스트)와 **VERIFY** (검증) 단계를 담당한다. + +# Responsibilities +- 테스트 전략/계획 수립(범위, 기법, 환경, 데이터, 게이트) +- AC 기반 테스트 케이스 설계, NFR 기반 비기능 테스트(성능/접근성/보안) 계획 +- 테스트 환경/데이터 관리, 실행 및 자동화 파이프라인 연계 +- 결함 리포팅(재현 절차/로그/우선순위) 및 트라이애지 주도 +- 품질 게이트 정의 및 릴리스 사전/사후 검증, 서명 + +# Deliverables +- Test Plan(전략/범위/환경/게이트) +- Test Case Matrix(스토리/AC 매핑, 엣지/회귀 포함) +- Test Execution Report 및 Release Sign-off + +# Input, Output +| 구분 | 형식 | 경로 | +|------|------|------| +| **입력(Input)** | 기능 명세서 | `spec/stories/{feature}.md` | +| **출력(Output)** | 검증 리포트 | `outputs/{feature}-qa-report.md` | + +# Interfaces +- To Dev: 결함 리포트/재현 절차/우선순위 전달, 수정 확인 +- To PM: 품질 상태/AC 충족 여부 보고, 출시 리스크 공유 +- To CEO: 릴리스 권고 요약 +- From Architect: 테스트 가능성 관련 제약/리스크 인수 + +# Reference +# - 역할 분담은 BMAD-METHOD의 QA/품질 게이트 철학을 참조 +# - 테스트 코드 설계 시 kent beck testing 참고 + +- .cursor/docs/testing-library-queries-priority.md +- https://github.com/bmad-code-org/BMAD-METHOD/ +- .cursor/docs/kent-beck-testing.md + +## 최소 변경/영향 최소화 테스트 전략 +- 구현 변경을 강제하지 않는 테스트 작성(기존 공개 API/시그니처 유지) +- 새로운 규칙을 드러내는 케이스만 추가하고, 기존 케이스와 충돌하지 않도록 구성 +- 겹침(overlap) 미검증 정책을 전제(겹침 관련 단언 금지) +- .cursor/docs/tdd-document.md diff --git a/.cursor/checklist/ceo-approval-checklist.md b/.cursor/checklist/ceo-approval-checklist.md new file mode 100644 index 00000000..7fff0198 --- /dev/null +++ b/.cursor/checklist/ceo-approval-checklist.md @@ -0,0 +1,60 @@ +# 🧾 CEO 승인 체크리스트 + +> Purpose: 각 에이전트의 1차 자체 점검 결과를 종합 검토하여 최종 승인/보류/반려를 결정합니다. + +--- + +## 1️⃣ 산출물 수령 확인 +| 에이전트 | 산출물 | 링크/경로 | 자체 점검 결과 | 확인 | +|---------|--------|-----------|----------------|------| +| 테스트설계 | 테스트 시나리오 명세 · 체크리스트 | `.cursor/outputs/test-design-review.md` | ✅/⚠️ | ☐ | +| 테스트코드 | 테스트 코드 · 커버리지 | `.cursor/outputs/test-code-review.md` | ✅/⚠️ | ☐ | +| 코드작성 | 변경 코드 · 타입/상수 | `.cursor/outputs/code-implementation-review.md` | ✅/⚠️ | ☐ | +| 리팩토링 | 변경 전/후 요약 · 회귀 목록 | `.cursor/outputs/refactoring-review.md` | ✅/⚠️ | ☐ | +| 오케스트레이션 | 파이프라인 로그 · 링크 모음 | `.cursor/outputs/orchestration-review.md` | ✅/⚠️ | ☐ | + +--- + +## 2️⃣ 품질 게이트 +| 항목 | 기준 | 증빙 | 통과 | +|------|------|------|------| +| 린트 | `pnpm lint` 경고/에러 0 | 로그 | ☐ | +| 테스트 | `pnpm test` 전부 통과, 커버리지 목표 | 리포트 | ☐ | +| 빌드 | `pnpm build` 성공 | 로그 | ☐ | + +--- + +## 3️⃣ 도메인·규칙 준수 +| 항목 | 기준 | 통과 | +|------|------|------| +| 날짜/시간 | ISO/UTC/24시간, 윤년/월경계 | ☐ | +| 반복 일정 | 종료일 제한, 31일/2월29일, 단일/전체 수정/삭제 | ☐ | +| 접근성 | id/aria-label/시맨틱, Material-UI 패턴 | ☐ | +| 타입 | any 금지, 명시적 타입, JSDoc | ☐ | +| 아키텍처 | 훅/유틸/컴포넌트 경계 | ☐ | + +--- + +## 4️⃣ 리스크 및 의사결정 +| 리스크 | 영향 | 대응 | 상태 | +|--------|------|------|------| +| | | | | + +--- + +## 5️⃣ 최종 결정 +- 승인/보류/반려: +- 비고: +- 승인자: CEO +- 일자: + +--- + +## 📚 참고 문서 +- `.cursor/docs/tdd-document.md` +- `.cursorrules` + +## 🧾 제출 지침 +- 최종 승인 결과는 `.cursor/outputs/ceo-approval.md`에 기록합니다. + + diff --git a/.cursor/checklist/code-implementation-agent-checklist.md b/.cursor/checklist/code-implementation-agent-checklist.md new file mode 100644 index 00000000..b5af70a8 --- /dev/null +++ b/.cursor/checklist/code-implementation-agent-checklist.md @@ -0,0 +1,95 @@ +# 🧩 코드작성 에이전트 검증 체크리스트 + +> Purpose: 테스트 주도 구현 시, 아키텍처·타입·접근성·성능·도메인 규칙을 준수했는지 검증합니다. (1차 자체 점검 → CEO 승인) + +--- + +## 1️⃣ 요구사항 명확성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 1-1 | TDD 흐름 | 실패 테스트 → 최소 구현 → 리팩토링 순서 준수 | ☐ | +| 1-2 | 도메인 적합 | 캘린더 용어/규칙(ISO/UTC/24시간/윤년/월경계) 반영 | ☐ | + +--- + +## 2️⃣ 입력·출력 정의 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 2-1 | 명시적 타입 | 인터페이스/유니온/반환 타입 명시, any 금지 | ☐ | +| 2-2 | API I/O | 오류 처리/로딩 상태/사용자 피드백 일관 | ☐ | + +--- + +## 3️⃣ 처리 로직 구체성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 3-1 | 관심사 분리 | 훅=로직, 유틸=순수함수, 컴포넌트=UI | ☐ | +| 3-2 | 훅 패턴 | 객체 반환, 로딩/에러 상태, useCallback/useMemo 사용 | ☐ | +| 3-3 | 유틸 품질 | 순수성, 명명된 상수, 50줄 이하, 중복 제거 | ☐ | +| 3-4 | 반복 일정 | 종료일 제한, 31일/2월29일, 그룹 수정/삭제 처리 | ☐ | +| 3-5 | 겹침/시간 검증 | 시작<종료, 겹침 탐지, 잘못된 조합 금지 | ☐ | + +--- + +## 4️⃣ 에러 및 예외 처리 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 4-1 | 사용자 피드백 | 폼 에러 헬퍼텍스트/다이얼로그, 접근성 속성 제공 | ☐ | +| 4-2 | 로깅 | 개발 단계 콘솔/에러 로깅 기준 일관 | ☐ | + +--- + +## 5️⃣ 테스트 기반성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 5-1 | 테스트 통과 | 신규/기존 테스트 모두 통과 | ☐ | +| 5-2 | 커버리지 영향 | 의미 있는 라인 증가, 미달 사유 기록 | ☐ | + +--- + +## 6️⃣ 문서 품질·코드 스타일 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 6-1 | JSDoc | 복잡 함수에 JSDoc, 반환/파라미터 타입 기술 | ☐ | +| 6-2 | ESLint/TS | 경고/에러 0, 엄격 TS 규칙 준수 | ☐ | +| 6-3 | Import 순서 | 외부 → 내부(hooks/utils/types) 순서 | ☐ | + +--- + +## 7️⃣ 성능·접근성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 7-1 | 렌더 최적화 | 불필요 리렌더 방지, 핸들러 useCallback | ☐ | +| 7-2 | 접근성 | id/aria-label/시맨틱 요소, Material-UI 패턴 준수 | ☐ | + +--- + +## 8️⃣ 프롬프트 품질 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 8-1 | 산출물 명시 | 변경 파일, 타입 정의, 훅/유틸 경계 설명 | ☐ | +| 8-2 | 금지사항 | 컴포넌트에 비즈니스 로직, any, 매직 넘버 금지 | ☐ | + +--- + +## 📎 산출물 +- 변경된 소스 목록과 요약(아키텍처 영향 포함) +- 타입/상수 추가 목록 +- 사용자 피드백(에러/로딩) 확인 캡처 또는 설명 + +--- + +## 📚 참고 문서 +- `.cursor/docs/tdd-document.md` — TDD 사이클과 구조/행동 커밋 분리 +- `.cursorrules` — 훅/유틸/컴포넌트 경계·접근성·타입·도메인 규칙 + +## 🧾 제출 지침 +- 본 체크리스트로 1차 자체 점검 완료 후 결과 보고서를 다음 경로에 작성: + - `.cursor/outputs/code-implementation-review.md` +- 보고서에는 다음을 포함: + - 변경 파일 목록과 영향 범위 + - 규칙 준수 근거(코드 스니펫/라인 참조) + - 명령/로그: `pnpm lint`, `pnpm build` + + + diff --git a/.cursor/checklist/feature-verification.md b/.cursor/checklist/feature-verification.md new file mode 100644 index 00000000..9eb83b5b --- /dev/null +++ b/.cursor/checklist/feature-verification.md @@ -0,0 +1,92 @@ +# 🧩 기능설계 에이전트 검증 템플릿 + +> **Purpose** +> This template verifies whether the output from the **Feature Design Agent (기능설계 에이전트)** +> meets TDD-friendly standards of clarity, testability, and system consistency. + +--- + +## 1️⃣ 요구사항 명확성 (Requirement Clarity) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 1-1 | 요구사항이 명시적으로 표현되어 있는가 | “무엇을 해야 하는가”가 구체적으로 기술되어 있는가 | ☐ | +| 1-2 | 비기능 요구사항이 포함되어 있는가 | 예: 보안, 성능, 오류 처리 등 시스템 제약사항 포함 여부 | ☐ | +| 1-3 | 테스트 가능한 형태로 변환 가능한가 | 결과가 “확인 가능한 조건”으로 서술되어 있는가 | ☐ | + +--- + +## 2️⃣ 입력·출력 정의 (I/O Specification) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 2-1 | 입력값이 명시되어 있는가 | 파라미터명, 타입, 필수 여부가 정의되어 있는가 | ☐ | +| 2-2 | 출력값이 명시되어 있는가 | 반환 데이터 구조나 응답 형식이 정의되어 있는가 | ☐ | +| 2-3 | 입력과 출력이 논리적으로 일관적인가 | 입력 조건 변화에 따른 출력이 일관성 있는가 | ☐ | +| 2-4 | 데이터 타입 정의가 명확한가 | string/number/boolean 등 타입이 누락되지 않았는가 | ☐ | + +--- + +## 3️⃣ 처리 로직 구체성 (Process Logic) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 3-1 | 처리 단계가 순서대로 정의되어 있는가 | 입력 검증 → 처리 → 결과 반환 등 단계적 구조인가 | ☐ | +| 3-2 | 각 단계가 테스트 가능하게 서술되어 있는가 | 결과를 검증할 수 있을 정도로 조건과 결과가 명확한가 | ☐ | +| 3-3 | 조건 분기(예외 흐름)가 포함되어 있는가 | 정상 흐름 외에도 실패·예외 케이스가 포함되어 있는가 | ☐ | + +--- + +## 4️⃣ 에러 및 예외 처리 (Error Handling) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 4-1 | 주요 실패 케이스가 정의되어 있는가 | 입력 누락, 검증 실패, 비즈니스 예외 등 | ☐ | +| 4-2 | 에러 응답 형식이 일관된가 | 코드·메시지 구조가 명확하고 통일되어 있는가 | ☐ | +| 4-3 | 성공/실패 응답 구분이 명확한가 | 응답 필드가 중복되거나 모호하지 않은가 | ☐ | + +--- + +## 5️⃣ 테스트 기반성 (Testability) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 5-1 | 기능 단위가 작고 독립적인가 | 다른 기능과 결합 없이 테스트 가능한가 | ☐ | +| 5-2 | 각 입력에 대한 기대 결과가 명확한가 | 예상 결과가 단일하고 구체적인가 | ☐ | +| 5-3 | 명세에서 테스트 시나리오를 도출할 수 있는가 | “Given-When-Then” 구조로 변환 가능성이 있는가 | ☐ | + +--- + +## 6️⃣ 명세 문서 품질 (Documentation Quality) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 6-1 | 섹션 구조가 일관성 있는가 | Feature / Input / Process / Output / Error 순서 유지 | ☐ | +| 6-2 | 표(Table) 형식이 깨지지 않았는가 | 마크다운 표가 정상적으로 렌더링되는가 | ☐ | +| 6-3 | 중복·모순된 내용이 없는가 | 동일 내용이 반복되거나 충돌되지 않는가 | ☐ | +| 6-4 | 요약이 명확한가 | 첫 문단에서 이 기능의 목적이 드러나는가 | ☐ | + +--- + +## 7️⃣ 프로젝트 맥락 및 영향 분석 (Context & Impact) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 7-1 | 도메인 용어가 일관된가 | 일정, 이벤트, 날짜, 사용자 등 프로젝트 용어에 맞는가 | ☐ | +| 7-2 | 기존 기능과 충돌하지 않는가 | 기존 기능(조회, 수정, 삭제 등)과 로직적으로 겹치지 않는가 | ☐ | +| 7-3 | 새로운 기능의 경계가 명확한가 | 기존 기능과의 입력·출력, 책임 구분이 명확한가 | ☐ | +| 7-4 | 기존 기능의 동작에 영향이 없는가 | 기존 API, DB 스키마, UI 로직이 깨질 위험이 없는가 | ☐ | +| 7-5 | 영향 범위가 정의되어 있는가 | 수정된 모듈이 어디에 영향을 미치는지 명시되어 있는가 | ☐ | +| 7-6 | 회귀 테스트 필요성이 식별되었는가 | 기존 기능 중 재검증이 필요한 부분이 언급되었는가 | ☐ | + +--- + +## 8️⃣ 에이전트 품질 점검 (Prompt Quality) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 8-1 | 에이전트 프롬프트가 일관된 형식을 요구하는가 | Feature / Input / Process / Output / Error 구조 유지 | ☐ | +| 8-2 | 언어·포맷 지시가 명확한가 | “표로 작성”, “마크다운 형식” 등 출력 형식 명시 | ☐ | +| 8-3 | 일반적 서술을 제한했는가 | 추상적 지시(“간단히 설명”)가 없는가 | ☐ | +| 8-4 | Rules(.cursorrules)와 일관되는가 | 언어, 테스트 프레임워크, 코드 스타일이 규칙과 맞는가 | ☐ | + +--- + +## 🧾 제출 지침 +- 본 체크리스트로 1차 자체 점검 완료 후 결과 보고서를 다음 경로에 작성: + - `.cursor/outputs/feature-verification-review.md` +- 보고서에는 다음을 포함: + - 각 항목별 점검 결과 및 피드백 + - 최종 검증 결과 요약 diff --git a/.cursor/checklist/orchestration-agent-checklist.md b/.cursor/checklist/orchestration-agent-checklist.md new file mode 100644 index 00000000..3bbc2b1b --- /dev/null +++ b/.cursor/checklist/orchestration-agent-checklist.md @@ -0,0 +1,66 @@ +# 🧵 오케스트레이션 에이전트 검증 체크리스트 + +> Purpose: 테스트설계 → 테스트코드 → 코드작성 → 리팩토링의 순서를 관리하고, 승인 게이트와 산출물 연결성을 검증합니다. (1차 자체 점검 → CEO 승인) + +--- + +## 1️⃣ 흐름/게이트 설계 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 1-1 | 단계 정의 | 4단계 흐름과 각 단계 산출물/입력/출력 정의 | ☐ | +| 1-2 | 승인 게이트 | 각 단계별 자체 체크리스트 ✅ 후에만 다음 단계 진행 | ☐ | +| 1-3 | 롤백 계획 | 실패 시 이전 안정 지점으로 복귀 절차 | ☐ | + +--- + +## 2️⃣ 입력·출력 연결성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 2-1 | 산출물 참조 | 다음 단계가 이전 단계 산출물을 정확히 참조 | ☐ | +| 2-2 | 추적성 | 시나리오 ↔ 테스트 ↔ 코드 변경 추적 가능 | ☐ | + +--- + +## 3️⃣ 품질 게이트 실행 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 3-1 | 명령어 파이프라인 | `pnpm lint` → `pnpm test` → `pnpm build` 자동/수동 실행 | ☐ | +| 3-2 | 보고 | 실패 원인/영향/대응계획을 간결히 리포팅 | ☐ | + +--- + +## 4️⃣ 리스크/의사결정 관리 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 4-1 | 리스크 레지스터 | 성능/접근성/도메인 규칙 위반 리스크 추적 | ☐ | +| 4-2 | 의사결정 기록 | 대안/선택 근거, 승인자, 날짜 기록 | ☐ | + +--- + +## 5️⃣ 커뮤니케이션/문서화 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 5-1 | 변경 로그 | 주요 변경과 영향 범위 요약 | ☐ | +| 5-2 | PR 템플릿 | 체크리스트 링크/증빙 첨부, 리스크·테스트 증거 포함 | ☐ | + +--- + +## 📎 산출물 +- 단계별 체크리스트 링크 모음 +- 빌드/테스트 로그 요약 +- 리스크·의사결정 기록 + +--- + +## 📚 참고 문서 +- `.cursor/docs/tdd-document.md` — 단계별 게이트와 커밋 분리 원칙 반영 +- `.cursorrules` — 품질 게이트 명령/도메인 규칙 준수 확인 + +## 🧾 제출 지침 +- 본 체크리스트로 1차 자체 점검 완료 후 결과 보고서를 다음 경로에 작성: + - `.cursor/outputs/orchestration-review.md` +- 보고서에는 다음을 포함: + - 단계별 산출물 경로 체크 및 추적성 표 + - 품질 게이트 실행 로그 집계 + + diff --git a/.cursor/checklist/refactoring-agent-checklist.md b/.cursor/checklist/refactoring-agent-checklist.md new file mode 100644 index 00000000..2d8d00ce --- /dev/null +++ b/.cursor/checklist/refactoring-agent-checklist.md @@ -0,0 +1,75 @@ +# 🔧 리팩토링 에이전트 검증 체크리스트 + +> Purpose: 외부 동작을 유지하면서 가독성·성능·구조를 개선했는지, 테스트 안정성을 해치지 않았는지 검증합니다. (1차 자체 점검 → CEO 승인) + +--- + +## 1️⃣ 요구사항 명확성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 1-1 | 리팩토링 범위 | 대상 파일/모듈과 비대상 명확화(스코프 크립 방지) | ☐ | +| 1-2 | 목표 정의 | 중복 제거, 명명 개선, 구조화, 성능 개선 등 목적 명시 | ☐ | + +--- + +## 2️⃣ 입력·출력/행동 동일성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 2-1 | 퍼블릭 API 불변 | 함수 시그니처/반환 타입/훅 반환 객체 불변 | ☐ | +| 2-2 | 테스트 무변경 통과 | 기존 테스트 수정 없이 통과(필요 시 합리적 근거) | ☐ | + +--- + +## 3️⃣ 처리 로직 개선 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 3-1 | 분해/추출 | 50줄 이상 함수 분해, 유틸로 추출, 이름 개선 | ☐ | +| 3-2 | 성능 | 불필요 연산/렌더 제거, useMemo/useCallback 적용 | ☐ | +| 3-3 | 타입 강화 | any 제거, 유니온/인터페이스 명확화 | ☐ | + +--- + +## 4️⃣ 에러/예외·회귀 안전망 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 4-1 | 회귀 테스트 | 영향 영역 테스트 추가/보강 | ☐ | +| 4-2 | 로깅·에러 | 기존 에러 처리 흐름 유지, 메시지 일관 | ☐ | + +--- + +## 5️⃣ 문서·스타일 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 5-1 | JSDoc/주석 | 비자명한 의도/제약 조건만 간결히 문서화 | ☐ | +| 5-2 | ESLint/TS | 경고 0, 엄격 TS, import 순서 준수 | ☐ | + +--- + +## 6️⃣ 맥락·영향 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 6-1 | 경계 준수 | 훅/유틸/컴포넌트 경계 유지, 의존 역전 지키기 | ☐ | +| 6-2 | 변경 영향 | 영향 범위/리스크/롤백 계획 기록 | ☐ | + +--- + +## 📎 산출물 +- 변경 전/후 요약(목적-효과-영향) +- 영향 범위 회귀 테스트 목록 +- 퍼블릭 API 변경 없음 확인 근거 + +--- + +## 📚 참고 문서 +- `.cursor/docs/tdd-document.md` — Green 상태에서만 리팩토링, Tidy First +- `.cursorrules` — 50줄 제한/중복 제거/명명 규칙/성능·접근성 규칙 + +## 🧾 제출 지침 +- 본 체크리스트로 1차 자체 점검 완료 후 결과 보고서를 다음 경로에 작성: + - `.cursor/outputs/refactoring-review.md` +- 보고서에는 다음을 포함: + - 전/후 비교와 각 리팩토링의 의도·효과 + - 회귀 안전성 증빙(테스트 통과 로그) + + + diff --git a/.cursor/checklist/test-code-agent-checklist.md b/.cursor/checklist/test-code-agent-checklist.md new file mode 100644 index 00000000..4bbd5a88 --- /dev/null +++ b/.cursor/checklist/test-code-agent-checklist.md @@ -0,0 +1,123 @@ +# 🧪 테스트코드 작성 에이전트 검증 체크리스트 + +> Purpose: 테스트 설계 명세를 실행 가능한 테스트로 구현할 때, 타입 안전성·접근성·결정성을 보장하고 프로젝트 규칙을 준수했는지 검증합니다. (1차 자체 점검 → CEO 승인) + +--- + +## 1️⃣ 요구사항 명확성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 1-1 | 시나리오 매핑 | 모든 시나리오가 테스트 파일/케이스로 일대일 매핑 | ☐ | +| 1-2 | 명명 규칙 | 테스트 이름이 한글 설명, 시나리오 서술형 | ☐ | +| 1-3 | 커버리지 목표 | 파일/라인/브랜치 목표와 미달 사유 기록 | ☐ | + +--- + +## 2️⃣ 입력·출력 정의 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 2-1 | 고정된 테스트 데이터 | 타임존 UTC, 시드 고정, ISO/24시간 형식 사용 | ☐ | +| 2-2 | MSW 핸들러 | `src/__mocks__/handlers.ts` 활용, API 경계 모킹 | ☐ | +| 2-3 | 관찰 포인트 | 화면 텍스트/role/aria, 훅 상태, 유틸 반환값 검증 | ☐ | + +--- + +## 3️⃣ 처리 로직 구체성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 3-1 | AAA 패턴 | Arrange-Act-Assert 구조 준수 | ☐ | +| 3-2 | 구현 세부 회피 | 내부 구현이 아닌 공개 API/사용자 관점 검증 | ☐ | +| 3-3 | 동기/비동기 처리 | `await`/`findBy*`/`waitFor` 적절 사용 | ☐ | +| 3-4 | 훅 테스트 | `renderHook`, `act` 사용, 상태·에러·로딩 검증 | ☐ | + +--- + +## ♾️ 전역 엣지 케이스 커버리지 게이트 (Global Edge-Case Coverage Gate) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| EC-1 | 경계값 테스트 | min/max/빈 값/잘못된 형식/오버플로우 케이스 구현 | ☐ | +| EC-2 | 달력 도메인 경계 | 월 경계, 윤년 2/29, 주 계산, 타임존 고정 테스트 | ☐ | +| EC-3 | 에러 플로우 | API 4xx/5xx/네트워크 실패, 사용자 피드백 단언 | ☐ | +| EC-4 | 접근성 실패 | id/aria 누락 시 스크린리더 관찰 포인트 검증 | ☐ | +| EC-5 | 성능 상한 | 최대 데이터셋에서 렌더/검색 시간 과도하지 않음 | ☐ | +| EC-6 | 트레이스 매핑 | 요구사항 ↔ 테스트케이스 표를 리포트에 포함 | ☐ | +| EC-7 | 결정성 | `vi.setSystemTime`/`useFakeTimers` 등으로 결정성 보장 | ☐ | + +--- + +## 4️⃣ 에러 및 예외 처리 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 4-1 | 폼 검증 에러 | 필수값/시간 순서/겹침 오류 UI 검증 | ☐ | +| 4-2 | API 오류 | HTTP 상태별 에러 메시지/재시도 UX 검증 | ☐ | +| 4-3 | 접근성 실패 | 누락된 `id`/`aria-label` 검출 테스트 포함 | ☐ | + +--- + +## 5️⃣ 테스트 기반성 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 5-1 | 결정성 | 타이머/시간 고정(`vi.useFakeTimers` 등) | ☐ | +| 5-2 | 독립성 | 케이스 간 상태 공유 금지, 파일 격리 | ☐ | +| 5-3 | 성능 | 과도한 렌더/대기 회피, 최소한의 렌더로 검증 | ☐ | + +--- + +## 6️⃣ 문서 품질 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 6-1 | 디렉토리 구조 | `src/__tests__/hooks|unit|integration` 위치 준수 | ☐ | +| 6-2 | 파일명 규칙 | `*.spec.ts(x)` 네이밍, 대상과 1:1 매칭 | ☐ | +| 6-3 | 주석 최소화 | 테스트 의도가 코드와 설명으로 충분히 드러남 | ☐ | + +--- + +## 7️⃣ 프로젝트 맥락·영향 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 7-1 | 규칙 준수 | any 금지, 명시적 타입, 접근성, 성능 규칙 반영 | ☐ | +| 7-2 | 회귀 테스트 | 영향 범위의 기존 테스트 보강 여부 | ☐ | + +--- + +## 8️⃣ 프롬프트 품질 +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 8-1 | 출력 형식 | 통과/실패 기준과 증빙(스크린샷/로그) 안내 | ☐ | +| 8-2 | 금지사항 | 구현 세부 검사, 과도한 mock 금지 안내 | ☐ | + +--- + +## 📎 산출물 +- 테스트 코드(PR 포함) +- 커버리지 리포트 +- MSW 핸들러/목 데이터 업데이트 목록 + +--- + +## 📚 참고 문서 +- `.cursor/docs/tdd-document.md` — Red/Green/Refactor, Tidy First 커밋 분리 +- `.cursorrules` — 테스트 구조/한글 설명/AAA/UTC·ISO 규칙 준수 + +## 🧾 제출 지침 +- 본 체크리스트로 1차 자체 점검 완료 후 결과 보고서를 다음 경로에 작성: + - `.cursor/outputs/test-code-review.md` +- 보고서에는 다음을 포함: + - 시나리오 ↔ 테스트 케이스 매핑 표 + - 커버리지 스냅샷 및 미달 항목과 사유 + - 명령/로그: `pnpm test`, `pnpm test:coverage` + + +--- + +## 🚦 단계 게이트 (TDD & 승인) + +- [ ] Red: 실패 테스트 커밋 완료(필요 시 테스트 파일 상단 임시 ESLint 비활성화 주석 포함) + - 예: `/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */` +- [ ] Green: 최소 구현으로 전체 테스트 통과, 임시 린트 예외 제거 +- [ ] Refactor: 테스트 코드 정리(중복 제거/명명 개선) +- [ ] 자체 점검 체크리스트 전 항목 확인(✅) +- [ ] CEO 승인 완료(`.cursor/outputs/ceo-approval.md` 반영) +- [ ] 다음 단계로 전환: 코드작성 에이전트 + + diff --git a/.cursor/checklist/test-design-agent-checklist.md b/.cursor/checklist/test-design-agent-checklist.md new file mode 100644 index 00000000..0b022409 --- /dev/null +++ b/.cursor/checklist/test-design-agent-checklist.md @@ -0,0 +1,126 @@ +# 🧪 테스트설계 에이전트 검증 체크리스트 + +> Purpose: 기능설계 산출물과 프로젝트 규칙을 기반으로, 테스트 설계 산출물이 TDD·도메인·접근성·성능 요구사항을 충분히 반영하는지 사전 검증합니다. (1차 자체 점검 → CEO 승인) + +--- + +## 1️⃣ 요구사항 명확성 (Requirement Clarity) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 1-1 | 테스트 범위 정의 | 유닛(유틸), 훅, 컴포넌트, 통합 중 범위를 명확히 구분했는가 | ✅ | +| 1-2 | 기능 시나리오 식별 | 사용자 플로우(생성/수정/삭제/검색/반복)별 Given-When-Then 정의 | ✅ | +| 1-3 | 비기능 요구 반영 | 접근성, 성능(메모·렌더링), 에러/로딩 UX 요구 포함 | ✅ | +| 1-4 | 승인 기준 명시 | 각 시나리오의 통과 조건과 실패 기준을 구체화 | ✅ | + +--- + +## 2️⃣ 입력·출력 정의 (I/O Specification) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 2-1 | 입력 데이터와 타입 | 이벤트/반복/알림/검색 입력 구조와 타입, 필수값 명시 | ✅ | +| 2-2 | 외부 경계/목 전략 | `fetchHolidays` 등 API 경계를 MSW/핸들러 전략과 함께 정의 | ✅ | +| 2-3 | 출력 및 관찰 지표 | 화면/상태 변화, 접근성 속성, 알림 트리거 등 관찰 가능한 출력 | ✅ | +| 2-4 | 시간·날짜 규칙 | UTC, ISO 날짜, 24시간, 윤년/월 경계/반복 종료일 포함 | ✅ | + +--- + +## 3️⃣ 처리 로직 구체성 (Process Logic) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 3-1 | 우선순위/커버리지 | 핵심 경로 우선, 엣지 케이스 포함, 커버리지 목표 제시 | ✅ | +| 3-2 | 실패/예외 흐름 | 검증 실패, 겹침, API 오류, 네트워크 오류 시나리오 포함 | ✅ | +| 3-3 | 의존성 분리 | 훅·유틸·컴포넌트 관심사 분리 가정으로 테스트 가능 상태 | ✅ | +| 3-4 | 반복 일정 규칙 | 종료일 제한, 31일/2월29일 규칙, 단일/전체 수정/삭제 흐름 | ✅ | + +--- + +## ♾️ 전역 엣지 케이스 커버리지 게이트 (Global Edge-Case Coverage Gate) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| EC-1 | 요구사항 엣지 케이스 추출 | `requirements`/`stories`에서 각 요구사항별 엣지 케이스 목록화 | ☐ | +| EC-2 | 트레이스 매트릭스 | 요구사항 → 테스트 케이스 매핑 표 작성(누락 0개) | ☐ | +| EC-3 | 경계값·형식·범위 | min/max, 포맷 오류, 빈 값, 월/윤년/주 경계 포함 | ☐ | +| EC-4 | 오류/네트워크 | HTTP 오류, 타임아웃, 재시도/중단 기준 설계 | ☐ | +| EC-5 | 접근성 엣지 | id/aria 누락, 라벨-컨트롤 연결 실패 시나리오 포함 | ☐ | +| EC-6 | 성능 상한 | 데이터 최대 개수·렌더 비용 경계 및 UX 영향 정의 | ☐ | +| EC-7 | 결정성 확보 | 시간/시드 고정, 비결정성 제거 전략 명시 | ☐ | +| EC-8 | 승인 기준 | 각 엣지 케이스의 통과/실패 기준과 관찰 지표 명문화 | ☐ | + +--- + +## 4️⃣ 에러 및 예외 처리 (Error Handling) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 4-1 | 오류 메시지 정책 | 코드/메시지 일관, 사용자 피드백(헬퍼텍스트/다이얼로그) 검증 | ✅ | +| 4-2 | API 오류 매핑 | HTTP 상태별 처리, 재시도/중단 기준 정의 | ✅ | +| 4-3 | 폼 검증 실패 | 필수값, 시간 순서, 겹침 검증 케이스 정의 | ✅ | + +--- + +## 5️⃣ 테스트 기반성 (Testability) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 5-1 | AAA 패턴 | 시나리오가 Arrange-Act-Assert로 변환 가능 | ✅ | +| 5-2 | 결정적 테스트 | 타임존/시드 고정, 비결정성 제거 전략 명시 | ✅ | +| 5-3 | 독립성 | 케이스 간 공유 상태/순서 의존 제거 | ✅ | +| 5-4 | 접근성 검증 | id/aria-label/시맨틱 요소 관찰 포인트 포함 | ✅ | + +--- + +## 6️⃣ 명세 문서 품질 (Documentation Quality) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 6-1 | 구조 일관성 | Feature/Input/Process/Output/Error 구조 유지 | ✅ | +| 6-2 | 표 정합성 | 표 렌더링, 중복·모순 없음 | ✅ | +| 6-3 | 용어 일관성 | 캘린더 도메인 용어 사용(이벤트, 반복, 알림 등) | ✅ | + +--- + +## 7️⃣ 프로젝트 맥락 및 영향 분석 (Context & Impact) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 7-1 | 아키텍처 적합성 | 훅/유틸/컴포넌트 경계 준수 가정 | ✅ | +| 7-2 | 회귀 범위 도출 | 영향 범위와 회귀 테스트 리스트 제시 | ✅ | +| 7-3 | 성능 고려 | 메모·가상화·렌더링 최적화 검증 포인트 정의 | ✅ | + +--- + +## 8️⃣ 에이전트 프롬프트 품질 (Prompt Quality) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 8-1 | 출력 형식 고정 | 표/체크박스 등 형식 요구 명확 | ✅ | +| 8-2 | 금지사항 반영 | 구현 세부 테스트 금지, any 금지 등 규칙 반영 | ✅ | +| 8-3 | 승인 게이트 | "자체 점검 ✅ 후 CEO 승인" 절차 포함 | ✅ | + +--- + +## 📎 산출물 (Deliverables) +- 테스트 시나리오 명세서(본 체크리스트와 함께 제출) +- 엣지 케이스 목록(윤년, 월경계, 반복 규칙) +- 목/핸들러 전략서(MSW) + +--- + +## 📚 참고 문서 +- `.cursor/docs/tdd-document.md` — TDD 사이클 및 커밋 원칙 준수 +- `.cursorrules` — 타입/접근성/도메인/아키텍처 규칙 전면 준수 + +## 🧾 제출 지침 +- 본 체크리스트로 1차 자체 점검 완료 후 결과 보고서를 다음 경로에 작성: + - `.cursor/outputs/test-design-review.md` +- 보고서에는 다음을 포함: + - 체크 항목별 결과(✅/⚠️)와 근거(파일/라인/로그) + - 커버리지 목표/엣지 케이스 목록/모킹 전략 요약 + - 실행 명령과 로그: `pnpm lint`, `pnpm test`, `pnpm build` + +--- + +## 🚦 단계 게이트 (TDD & 승인) + +- [x] Red: 실패 테스트 설계 완료, 린트 예외 정책 문서화(테스트 파일 국한) +- [x] Green: 구현 가이드(최소 구현 범위)와 통과 기준 명시 +- [x] 자체 점검 체크리스트 전 항목 확인(✅) +- [x] CEO 승인 완료(`.cursor/outputs/ceo-approval.md` 반영) +- [x] 다음 단계로 전환: 테스트코드 에이전트 + + diff --git a/.cursor/docs/architect-reference.md b/.cursor/docs/architect-reference.md new file mode 100644 index 00000000..32537e3a --- /dev/null +++ b/.cursor/docs/architect-reference.md @@ -0,0 +1,36 @@ +# Architect Reference Guide + +## 📌 목적 +본 문서는 기능 설계 담당 Architect 에이전트가 시스템 설계 시 따라야 할 원칙과 문서화 기준을 제공합니다. + +## 🧩 주요 설계 원칙 +- 시스템 구성 요소(Component)를 명확히 정의하고, 책임(Responsibility)을 분리한다. +- 인터페이스(Interface)와 경계(Boundary)를 설계 초기부터 고려한다. +- 비기능 요구사항(성능, 확장성, 보안 등)을 설계 문서에 반영한다. +- 설계 결정(Architectural Decision)을 문서화하여 이유(Rationale)와 대안(Alternative)을 남긴다. +- 설계 문서를 최신 상태로 유지하며 변경 이력을 기록한다. + +## 📁 문서화 구조 제안 +1. 개요 및 목표 (Overview) +2. 시스템 컨텍스트 및 외부 연계 (Context) +3. 구성요소(Container) 및 컴포넌트(Component) 구조 +4. 데이터 설계 (Data Model) +5. 인터페이스 및 API 설계 +6. 주요 설계 결정 및 트레이드오프 (Architectural Decisions) +7. 비기능 요구사항 및 품질 속성 +8. 운영·배포 아키텍처 및 인프라 (Deployment) +9. 용어집 및 참고자료 (Glossary & References) + +## ✅ 설계 체크리스트 +- [ ] 기능 명세서의 요구사항을 설계에 반영했는가? +- [ ] 각 구성요소의 책임이 명확히 정의되어 있는가? +- [ ] 컴포넌트 간 의존성이 낮게 설계되었는가? +- [ ] 인터페이스 명세가 명확하며 변경에 유연한가? +- [ ] 성능/보안/확장성 등의 비기능 요구사항이 반영되었는가? +- [ ] 설계 문서 내 설계 결정이 이유와 대안을 포함하고 있는가? +- [ ] 문서가 최신 상태이며 버전/변경 이력이 남아 있는가? + +## 📎 참고 문서 및 자료 +- Software architecture documentation guide – Document360. +- Best practices for software architecture design – Lucidchart. +- Software architecture documentation: The ultimate guide – Working Software. \ No newline at end of file diff --git a/.cursor/docs/kent-beck-testing.md b/.cursor/docs/kent-beck-testing.md new file mode 100644 index 00000000..04d38efa --- /dev/null +++ b/.cursor/docs/kent-beck-testing.md @@ -0,0 +1,89 @@ +# 🧠 Kent Beck's Test Design Philosophy (TDD Core Guide) + +> "Tests are a **design tool for software**, the **specification of behavior**, and a **bulwark against regression**." — Kent Beck + +--- + +## 1️⃣ Purpose of Testing + +In TDD, testing is **not a means to find bugs, but a compass to guide correct design**. + +| Category | Description | +|------|------| +| 🎯 **Specification Test** | Tests that define "what the function should do" | +| 🔍 **Regression Test** | Tests that ensure already working functionality is not broken | +| 🧩 **Design Driver Test** | Tests that drive the implementation toward a simpler, more modular direction | + +--- + +## 2️⃣ 3 Stages of Test Design (Red → Green → Refactor) + +| Stage | Description | Action Example | +|------|------|-----------| +| 🔴 **Red** | Write a failing test first. | Declare, "This feature must work," first. | +| 🟢 **Green** | Write the minimum code necessary to pass the test. | Defer complex logic; focus on 'pass' for now. | +| 🟣 **Refactor** | Remove duplication, improve clarity. | Refactor the code to make the test maintainable. | + +> 💬 "Tests make you design the code, and Refactoring makes you clean up the design." + +--- + +## 3️⃣ 5 Principles of Good Tests (Kent Beck + xUnit Patterns) + +| Principle | Description | +|------|------| +| **Fast** | Tests should run quickly. (Provide immediate feedback) | +| **Independent** | Tests should not affect each other. | +| **Repeatable** | Running the test anytime, anywhere must yield the same result. | +| **Self-validating** | The result must be clearly defined as pass/fail. | +| **Timely** | Tests should be written *just before* writing the code. | + +> ✅ Mnemonic: **F.I.R.S.T. Principles** + +--- + +## 4️⃣ F.I.R.S.T. Principles and Code Examples + +| Principle | Description (Emphasis) | Bad Test ❌ (Anti-Pattern) | Good Test ✅ (Best Practice) | +|------|------|------|------| +| **Fast** | Tests must execute quickly. | Tests that connect to the **network/database** every time. (Takes seconds) | Tests using **Mocking** or an **In-memory DB**. (Takes milliseconds) | +| **Independent** | Tests should not depend on the order or state of others. | A test where `testDeleteUser()` only succeeds if `testCreateUser()` runs first. | Ensures an **isolated environment** by performing independent data setup (Setup/Teardown) for each test. | +| **Repeatable** | Must guarantee the same result regardless of when run. | Tests that rely on the current **time** or **random numbers**, changing results upon execution. | Mocks the `Time Provider` or uses a fixed Seed to ensure only **predictable values** are used. | +| **Self-validating** | The result must clearly be `PASS` or `FAIL`. | A test that requires **manual checking** of logs or files after execution. | Returns an **explicit boolean result** like `Assert.Equals(expected, actual)`. | +| **Timely** | Tests must be written *just before* coding. | Tests written to **meet coverage goals** after functionality is complete or just before release. | Starts in a **Red (failing) state**, acting as the **functional specification** before design is finished. | + +--- + +## 5️⃣ Test Design Checklist + +| Category | Key Question | Example | +|------|------------|------| +| **Functional Scope** | Are all functional requirements expressed as tests? | Includes the case of "handling the 31st when selecting a recurring schedule" | +| **Input/Output Definition** | Are the input values and expected results clear? | Specify date, recurrence type, end date, etc. | +| **Edge Cases** | Inclusion of boundary values / exceptional cases | February 29th on a leap year, invalid end date, etc. | +| **Independence** | Is there no dependency between tests? | DB initialization or using Mocks | +| **Clarity** | Does the test name read like a specification? | `it('31st is only created in months that have 31 days')` | + +--- + +## 6️⃣ Test Naming Convention (Behavior Driven) + +> Test names are written from the "**user's perspective**." + +| Example | Bad Example ❌ | Good Example ✅ | +|------|-------------|------------| +| Recurrence End Validation | `testEndDateLogic()` | `it('should return an error if the end date is before the start date')` | +| Recurrence Interval Validation | `testInterval()` | `it('should fail validation if the recurrence interval is less than 1')` | + +> 💬 "One should be able to understand the functionality just by reading the test names." + +--- + +## 7️⃣ Test Design Approaches + +| Approach | Description | Example | +|------|------|------| +| **Example-Driven Design** | Define tests based on concrete examples | 31st $\rightarrow$ created only in specific months | +| **Boundary Testing** | Test minimum, maximum, and boundary values | `2025-12-31`, `2024-02-29` | +| **Error-Driven Design** | Write failure cases first | Invalid date $\rightarrow$ error occurs | +| **Goal-Oriented Design** | Group tests by requirement units | Group cases by recurrence type \ No newline at end of file diff --git a/.cursor/docs/tdd-document.md b/.cursor/docs/tdd-document.md new file mode 100644 index 00000000..ab191ba4 --- /dev/null +++ b/.cursor/docs/tdd-document.md @@ -0,0 +1,77 @@ +Always follow the instructions in plan.md. When I say "go", find the next unmarked test in plan.md, implement the test, then implement only enough code to make that test pass. + +# ROLE AND EXPERTISE + +You are a senior software engineer who follows Kent Beck's Test-Driven Development (TDD) and Tidy First principles. Your purpose is to guide development following these methodologies precisely. + +# CORE DEVELOPMENT PRINCIPLES + +- Always follow the TDD cycle: Red → Green → Refactor +- Write the simplest failing test first +- Implement the minimum code needed to make tests pass +- Refactor only after tests are passing +- Follow Beck's "Tidy First" approach by separating structural changes from behavioral changes +- Maintain high code quality throughout development + +# TDD METHODOLOGY GUIDANCE + +- Start by writing a failing test that defines a small increment of functionality +- Use meaningful test names that describe behavior (e.g., "shouldSumTwoPositiveNumbers") +- Make test failures clear and informative +- Write just enough code to make the test pass - no more +- Once tests pass, consider if refactoring is needed +- Repeat the cycle for new functionality + +# TIDY FIRST APPROACH + +- Separate all changes into two distinct types: + 1. STRUCTURAL CHANGES: Rearranging code without changing behavior (renaming, extracting methods, moving code) + 2. BEHAVIORAL CHANGES: Adding or modifying actual functionality +- Never mix structural and behavioral changes in the same commit +- Always make structural changes first when both are needed +- Validate structural changes do not alter behavior by running tests before and after + +# COMMIT DISCIPLINE + +- Only commit when: + 1. ALL tests are passing + 2. ALL compiler/linter warnings have been resolved + 3. The change represents a single logical unit of work + 4. Commit messages clearly state whether the commit contains structural or behavioral changes +- Use small, frequent commits rather than large, infrequent ones + +# CODE QUALITY STANDARDS + +- Eliminate duplication ruthlessly +- Express intent clearly through naming and structure +- Make dependencies explicit +- Keep methods small and focused on a single responsibility +- Minimize state and side effects +- Use the simplest solution that could possibly work + +# REFACTORING GUIDELINES + +- Refactor only when tests are passing (in the "Green" phase) +- Use established refactoring patterns with their proper names +- Make one refactoring change at a time +- Run tests after each refactoring step +- Prioritize refactorings that remove duplication or improve clarity + +# EXAMPLE WORKFLOW + +When approaching a new feature: +1. Write a simple failing test for a small part of the feature +2. Implement the bare minimum to make it pass +3. Run tests to confirm they pass (Green) +4. Make any necessary structural changes (Tidy First), running tests after each change +5. Commit structural changes separately +6. Add another test for the next small increment of functionality +7. Repeat until the feature is complete, committing behavioral changes separately from structural ones + +Follow this process precisely, always prioritizing clean, well-tested code over quick implementation. + +Always write one test at a time, make it run, then improve structure. Always run all the tests (except long-running tests) each time. + +# Rust-specific + +Prefer functional programming style over imperative style in Rust. Use Option and Result combinators (map, and_then, unwrap_or, etc.) instead of pattern matching with if let or match when possible. diff --git a/.cursor/docs/testing-library-queries-priority.md b/.cursor/docs/testing-library-queries-priority.md new file mode 100644 index 00000000..dd07457b --- /dev/null +++ b/.cursor/docs/testing-library-queries-priority.md @@ -0,0 +1,50 @@ +# Testing Library Query Priority Guide + +## 📚 Overview + +Testing Library provides several query methods for selecting DOM elements. This document explains **which query should be used first (priority)** and the **characteristics of each query**. + +--- + +## 🔍 Query Types and Characteristics + +### ✅ Basic Query Types + +* `getBy...`: Must find exactly one matching element; throws an error if none or multiple are found. +* `queryBy...`: Returns `null` if no element is found; throws an error if multiple are found. +* `findBy...`: Finds elements asynchronously and returns a Promise. +* `getAllBy...`, `queryAllBy...`, `findAllBy...`: Versions for finding multiple elements. + +### 🎯 Priority (based on query predicate) + +Testing Library recommends query methods that are as **semantic and accessibility-friendly** as possible. + +The general priority is as follows: + +1. `getByRole(…, { name: … })` +2. `getByLabelText(…)` +3. `getByPlaceholderText(…)` +4. `getByText(…)` +5. `getByDisplayValue(…)` +6. `getByAltText(…)`, `getByTitle(…)` +7. `getByTestId(…)` — Use as a last resort whenever possible + +> Note: Refer to the “Which query should I use?” section for more details. + +--- + +## 🧠 Why is Role the Priority? + +* From an **Accessibility** perspective, elements with a designated role are linked with screen readers and other assistive technologies. +* The combination of `getByRole` + `name` option can appropriately select a wide range of elements. +* Queries that rely on `testId`, class names, or IDs are easily broken by implementation changes and are therefore considered the last option. + +--- + +## ⚙️ Practical Guidelines + +* Prioritize **Role**-based queries whenever possible. + +```ts +// Recommended +screen.getByRole('button', { name: /submit/i }); \ No newline at end of file diff --git a/.cursor/outputs/ceo-approval.md b/.cursor/outputs/ceo-approval.md new file mode 100644 index 00000000..7e3640b2 --- /dev/null +++ b/.cursor/outputs/ceo-approval.md @@ -0,0 +1,42 @@ +# 🧾 CEO 최종 승인 기록 + +## 메타데이터 +- 승인자: CEO +- 날짜: 2025-10-28 +- 관련 커밋/PR: +- 참고 문서: `.cursor/docs/tdd-document.md`, `.cursorrules` + +--- + +## 단계별 결과 수집 +| 단계 | 보고서 경로 | 자체 점검 | CEO 판단 | 코멘트 | +|------|-------------|-----------|----------|--------| +| 기능설계 검증 | `.cursor/outputs/feature-verification-review.md` | ✅ | 승인 | 요구사항/입출력/로직/예외/테스트/맥락 모두 적합 | +| 테스트설계 | `.cursor/outputs/test-design-review.md` | ✅ | 승인 | 설계 문서 기준 11개 케이스 커버, 엣지/에러/게이트 명확 | +| 테스트코드 | `.cursor/outputs/test-code-review.md` | ✅/⚠️ | 승인/보류/반려 | | +| 코드작성 | `.cursor/outputs/code-implementation-review.md` | ✅/⚠️ | 승인/보류/반려 | | +| 리팩토링 | `.cursor/outputs/refactoring-review.md` | ✅/⚠️ | 승인/보류/반려 | | +| 오케스트레이션 | `.cursor/outputs/orchestration-review.md` | ✅/⚠️ | 승인/보류/반려 | | + +--- + +## 품질 게이트 확인 +- `pnpm lint`: 통과/실패 (로그 링크/요약) +- `pnpm test`: 통과/실패, 커버리지 (요약) +- `pnpm build`: 통과/실패 (로그) + +--- + +## 도메인·규칙 준수 판단 +- 날짜/시간: ISO(YYYY-MM-DD), 24h, UTC 검증 기준 반영 +- 반복 일정: 종료일 제한, 31일/윤년, 그룹 관리 규칙 준수 +- 접근성: ARIA 라벨·시맨틱 HTML·MUI 사용 원칙 반영 +- 타입/아키텍처: 엄격 TS, 관심사 분리, 하위호환 타입 보강 확인 + +--- + +## 최종 결정 +- 전체 상태: 승인(기능설계, 테스트설계 단계) +- 조건/비고: 테스트코드/코드작성/리팩토링/오케스트레이션은 별도 승인. + + diff --git a/.cursor/outputs/code-implementation-review.md b/.cursor/outputs/code-implementation-review.md new file mode 100644 index 00000000..06c0b61a --- /dev/null +++ b/.cursor/outputs/code-implementation-review.md @@ -0,0 +1,51 @@ +# 🧩 코드작성 결과 보고서 (스토리 02/03/04/05 진행) + +## 메타데이터 +- 에이전트: 코드작성 +- 날짜: 2025-10-30 +- 참고 문서: `.cursor/checklist/code-implementation-agent-checklist.md` + +--- + +## 요약 +- 주요 변경 파일: + - `src/App.tsx` (반복 아이콘 표시, 반복 수정/삭제 다이얼로그) + - `src/hooks/useEventOperations.ts` (저장/삭제 분기 옵션 추가) + - `src/utils/repeatGeneration.ts` (종료일 상수/유틸 도입) + - 테스트: UI/훅/유닛 추가·보완 +- 테스트 통과 여부: 타깃 테스트 Green(반복 UI·수정·삭제, 종료일 유틸) +- 빌드: 생략(요청에 따라 린트/빌드 스킵) + +--- + +## 규칙 준수 근거 +| 영역 | 규칙 | 근거 | 코멘트 | +|------|------|------|--------| +| 타입 | any 금지/명시적 타입 | `types.ts` 유니온/인터페이스 | any 미사용 | +| 구조 | 훅/유틸/컴포넌트 경계 | 폴더 구조 준수 | 관심사 분리 유지 | +| 접근성 | id/aria/시맨틱 | `App.tsx` 아이콘/버튼 라벨 | `aria-label` 적용 | +| 도메인 | ISO/UTC/24h/윤년/월경계 | `repeatGeneration.ts` | 상수화/클램프 | +| 반복 | 수정/삭제 분기 | 훅/통합 테스트 | 단일/전체 분기 구현 | + +--- + +## 실행 로그(요약) +```bash +# 스토리 02/03 +pnpm test -t "반복 아이콘 표시" +pnpm test -t "clampToSystemMaxEndDate|getEffectiveEndDate" + +# 스토리 04 +pnpm test -t "반복 일정 수정 플로우|반복 일정 수정 분기" + +# 스토리 05 +pnpm test -t "삭제 분기" +``` + +--- + +## 결론 +- 상태: 통과(타깃 테스트) +- 후속 조치: 삭제 UI 다이얼로그 테스트 추가 및 리팩터 기회 재평가 + + diff --git a/.cursor/outputs/feature-verification-review.md b/.cursor/outputs/feature-verification-review.md new file mode 100644 index 00000000..4388366f --- /dev/null +++ b/.cursor/outputs/feature-verification-review.md @@ -0,0 +1,93 @@ +# 기능설계 검증 보고서 - 반복 일정 표시 (스토리 02) + +- 문서 범위: 스토리 02(반복 일정 표시)만 해당 +- 기준 문서: `.cursor/checklist/feature-verification.md` +- 참조 스토리: `.cursor/spec/stories/02-repeat-event-display.md` +- 대상 브랜치: main +- 작성일: 2025-10-30 +- 작성자: Sion(architect) + +## 1️⃣ 요구사항 명확성 (Requirement Clarity) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 1-1 | 요구사항이 명시적으로 표현되어 있는가 | 반복 일정은 주/월 뷰와 우측 리스트에서 아이콘으로 구분 | ✅ | +| 1-2 | 비기능 요구사항이 포함되어 있는가 | 접근성 `aria-label="반복 일정"`, 테마 색상 사용 명시 | ✅ | +| 1-3 | 테스트 가능한 형태로 변환 가능한가 | 주/월/리스트에서 아이콘 노출 여부를 질의로 검증 가능 | ✅ | + +- 비고: 아이콘 우선순위(알림 > 반복)도 구체적으로 명시됨. + +## 2️⃣ 입력·출력 정의 (I/O Specification) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 2-1 | 입력값이 명시되어 있는가 | `event.repeat.type !== 'none'` 조건을 입력으로 간주 | ✅ | +| 2-2 | 출력값이 명시되어 있는가 | UI에 Repeat 아이콘 렌더(주/월/리스트) | ✅ | +| 2-3 | 입력과 출력이 논리적으로 일관적인가 | 반복이면 아이콘, 아니면 미노출 | ✅ | +| 2-4 | 데이터 타입 정의가 명확한가 | 기존 `Event.repeat.type` 유니온 타입 사용 | ✅ | + +- 비고: 서버 스키마 변경 없음. 표시 로직만 해당. + +## 3️⃣ 처리 로직 구체성 (Process Logic) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 3-1 | 처리 단계가 순서대로 정의되어 있는가 | 이벤트 필터링 → 컴포넌트 배지 렌더링 → 아이콘 표기 | ✅ | +| 3-2 | 단계가 테스트 가능하게 서술되어 있는가 | 각 뷰에서 쿼리(`getByLabelText`)로 검증 가능 | ✅ | +| 3-3 | 조건 분기(예외 흐름) 포함 | 알림 아이콘 병행 시 우선순위/순서 규칙 포함 | ✅ | + +- 비고: 생성·검증 로직은 범위 밖임을 명시하여 경계가 명확함. + +## 4️⃣ 에러 및 예외 처리 (Error Handling) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 4-1 | 주요 실패 케이스 정의 | 반복인데 아이콘 미노출, aria-label 누락 | ✅ | +| 4-2 | 에러 응답 형식 일관 | UI 레벨이므로 스낵바·네트워크 비해당 | ✅ | +| 4-3 | 성공/실패 응답 구분 | 테스트 통과/실패로 판단 가능 | ✅ | + +- 비고: 네트워크 의존성 없음(표시 전용). + +## 5️⃣ 테스트 기반성 (Testability) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 5-1 | 기능 단위가 작고 독립 | 표시 로직만 커버, 다른 기능과 결합 낮음 | ✅ | +| 5-2 | 기대 결과 명확 | 아이콘 존재/부재, DOM 순서 검증 | ✅ | +| 5-3 | 시나리오 도출 가능 | 주/월/리스트 각각 Given-When-Then 전개 | ✅ | + +- 비고: 접근성 쿼리 우선순위 준수 가능. + +## 6️⃣ 명세 문서 품질 (Documentation Quality) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 6-1 | 섹션 구조 일관 | 목적/시나리오/표시 규칙/접근성/테스트 케이스 | ✅ | +| 6-2 | 표 형식 정상 | 본 문서 및 스토리 문서 표기 정상 | ✅ | +| 6-3 | 중복·모순 없음 | 다른 스토리와 역할 경계 명확 | ✅ | +| 6-4 | 요약 명확 | 헤더와 목적에 기능 취지 명확 | ✅ | + +## 7️⃣ 프로젝트 맥락 및 영향 분석 (Context & Impact) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 7-1 | 도메인 용어 일관 | repeat, 알림, 주/월 뷰 용어 일치 | ✅ | +| 7-2 | 기존 기능과 충돌 없음 | 렌더 레이어만 변경, 저장/검증 무영향 | ✅ | +| 7-3 | 기능 경계 명확 | 표시 로직 vs 생성/수정/삭제 분리 | ✅ | +| 7-4 | 기존 동작 영향 없음 | 이벤트 목록/필터 흐름 유지 | ✅ | +| 7-5 | 영향 범위 명시 | `App.tsx` 렌더 섹션(주/월/리스트) | ✅ | +| 7-6 | 회귀 테스트 필요 식별 | 알림 아이콘 우선순위와의 조합 | ✅ | + +## 8️⃣ 에이전트 품질 점검 (Prompt Quality) +| 번호 | 항목 | 설명 | 점검 | +|------|------|------|------| +| 8-1 | 형식 요구 일관 | 체크리스트 기반 보고 구조 유지 | ✅ | +| 8-2 | 언어·포맷 지시 명확 | 한국어, 테이블/코드블록 규칙 준수 | ✅ | +| 8-3 | 일반 서술 제한 | 구체 규칙·검증 대상 위주 기술 | ✅ | +| 8-4 | 프로젝트 규칙 일치 | 접근성·테마·관심사분리 원칙 부합 | ✅ | + +--- + +## 최종 검증 결과 요약 +- 요구사항 적합: 적합(✅) +- 시스템 정합성: UI 레이어 한정, 기존 흐름과 정합(✅) +- 테스트 준비도: 주/월/리스트 단위·통합 테스트 도출 완료(✅) +- 영향도: 낮음(렌더 로직 추가) + +## 보완/메모 +- 실제 구현 시 `@mui/icons-material/Repeat` 도입 및 `aria-label` 부여 필수. +- DOM 순서 검증(알림 > 반복)을 테스트에 포함. + diff --git a/.cursor/outputs/orchestration-review.md b/.cursor/outputs/orchestration-review.md new file mode 100644 index 00000000..6632cabf --- /dev/null +++ b/.cursor/outputs/orchestration-review.md @@ -0,0 +1,48 @@ +# 🧵 오케스트레이션 결과 보고서 + +## 메타데이터 +- 에이전트: 오케스트레이션 +- 날짜: +- 관련 커밋/PR: +- 참고 문서: `.cursor/docs/tdd-document.md`, `.cursorrules` +- 사용 체크리스트: `.cursor/checklist/orchestration-agent-checklist.md` + +--- + +## 단계별 산출물 확인 +| 단계 | 산출물 | 경로 | 자체 점검 결과 | 확인 | +|------|--------|------|----------------|------| +| 테스트설계 | 결과 보고서 | `.cursor/outputs/test-design-review.md` | ✅/⚠️ | ☐ | +| 테스트코드 | 결과 보고서 | `.cursor/outputs/test-code-review.md` | ✅/⚠️ | ☐ | +| 코드작성 | 결과 보고서 | `.cursor/outputs/code-implementation-review.md` | ✅/⚠️ | ☐ | +| 리팩토링 | 결과 보고서 | `.cursor/outputs/refactoring-review.md` | ✅/⚠️ | ☐ | + +--- + +## 품질 게이트 실행 로그 +```bash +pnpm lint +``` + +```bash +pnpm test +``` + +```bash +pnpm build +``` + +--- + +## 리스크/의사결정 +| 리스크 | 영향 | 대응 | 상태 | +|--------|------|------|------| +| | | | | + +--- + +## 결론 +- 상태: 통과 / 보완 필요 +- 후속 조치: + + diff --git a/.cursor/outputs/refactoring-review.md b/.cursor/outputs/refactoring-review.md new file mode 100644 index 00000000..f14ee28f --- /dev/null +++ b/.cursor/outputs/refactoring-review.md @@ -0,0 +1,47 @@ +# 🔧 리팩토링 결과 보고서 (스토리 02/03/04/05 정리) + +## 메타데이터 +- 에이전트: 리팩토링 +- 날짜: 2025-10-30 +- 사용 체크리스트: `.cursor/checklist/refactoring-agent-checklist.md` + +--- + +## 요약 +- 대상 범위: 반복 아이콘(UI), 반복 수정/삭제 다이얼로그(UI), 저장/삭제 훅 분기(로직) +- 목적/효과: 중복 제거, inline 핸들러 분리, 매직 문자열 제거, 가독성/유지보수성 향상 +- 테스트 결과: 타깃 테스트 Green 유지 + +--- + +## 전/후 비교 +| 항목 | 변경 전 | 변경 후 | 기대 효과 | 근거 | +|------|--------|--------|----------|------| +| UI 중복 | 반복 아이콘/다이얼로그 inline 로직 | `RepeatIconIfNeeded`, 확인/삭제 핸들러 분리 | 중복 제거, 접근성 라벨 일관 | `src/App.tsx` | +| 매직 문자열 | 종료일 '2025-12-31' 하드코딩 | `MAX_REPEAT_END_DATE` 상수 | 변경 용이/오류 예방 | `repeatGeneration.ts` | +| 핸들러 구조 | inline onClick 다수 | `useCallback` 핸들러(`handleConfirm*`, `requestDelete`) | 재사용/성능/가독성 향상 | `src/App.tsx` | +| 훅 옵션 | 분기 인자 없음 | `saveEvent(event, { scope })`, `deleteEvent(id, { scope, repeatId })` | 단일/전체 분기 명확 | `useEventOperations.ts` | + +--- + +## 퍼블릭 API 안정성 +- 시그니처 변경 여부: 내부 훅 API에 선택적 옵션 추가(하위호환 유지) +- 대응 조치: 기존 호출 경로는 변경 없이 동작, 신규 분기를 테스트로 보장 + +--- + +## 실행 로그(요약) +```bash +pnpm test -t "반복 아이콘 표시" +pnpm test -t "clampToSystemMaxEndDate|getEffectiveEndDate" +pnpm test -t "반복 일정 수정 플로우|반복 일정 수정 분기" +pnpm test -t "삭제 분기|반복 일정 삭제 플로우" +``` + +--- + +## 결론 +- 상태: 통과(Green 유지) +- 후속 조치: 삭제 UI 추가 케이스(단일/전체 후 리스트 반영) 보강 검토 + + diff --git a/.cursor/outputs/test-code-review.md b/.cursor/outputs/test-code-review.md new file mode 100644 index 00000000..573c1f49 --- /dev/null +++ b/.cursor/outputs/test-code-review.md @@ -0,0 +1,56 @@ +# 🧪 테스트코드 결과 보고서 (스토리 02/03 완료, 스토리 04 진행 중) + +## 메타데이터 +- 에이전트: 테스트코드 +- 날짜: 2025-10-30 +- 관련 커밋/PR: 반복 아이콘 표시/종료일 기본·클램프/반복 수정 다이얼로그 노출(UI) +- 참고 문서: `.cursor/spec/stories/02-repeat-event-display.md`, `.cursor/spec/stories/03-repeat-end-condition.md` +- 사용 체크리스트: `.cursor/checklist/test-code-agent-checklist.md` + +--- + +## 요약 +- 통과(✅): 반복 아이콘 표시 2건, 종료일 기본/클램프 5건, 반복 수정 다이얼로그 노출 1건 (타깃 실행 기준) +- 실패(❌): 없음(스토리 04 분기/호출 케이스는 다음 단계에서 Red 예정) +- 커버리지: 단위 타깃 기준 적정, 전체 커버리지는 별도 실행 시 산출 + +--- + +## 시나리오 ↔ 테스트 매핑 +| 시나리오 | 테스트 파일 | 케이스명 | 상태 | 근거 | +|----------|-------------|---------|------|------| +| 반복 아이콘 표시 | `src/__tests__/medium.repeat-ui.spec.tsx` | 월/리스트에 반복 아이콘 노출 | ✅ | vitest -t "반복 아이콘 표시" | +| 종료일 기본값 | `src/__tests__/unit/easy.repeatEndDefault.spec.ts` | 미입력 → 2025-12-31 | ✅ | vitest -t getEffectiveEndDate | +| 종료일 클램프 | `src/__tests__/unit/easy.repeatEndClamp.spec.ts` | 2026-01-01 → 2025-12-31 | ✅ | vitest -t clampToSystemMaxEndDate | +| 반복 수정(UI) | `src/__tests__/medium.repeat-update-ui.spec.tsx` | 저장 시 단일/전체 다이얼로그 노출 | ✅ | vitest -t "반복 일정 수정 플로우" | + +--- + +## 커버리지 스냅샷 +```text +타깃 실행(부분)으로 커버리지 수집 생략. 전체 사이클 종료 후 수집 예정. +``` + +--- + +## 접근성/비기능 검증 +- 접근성 속성(id/aria/시맨틱) 검증 케이스: 반복 아이콘 `aria-label="반복 일정"` 확인 +- 결정성 확보(타임존/타이머/시드): `setupTests.ts`에서 TZ=UTC, 고정 타이머 적용 + +--- + +## 실행 로그(요약) +```bash +pnpm test -t "반복 아이콘 표시" +pnpm test -t "getEffectiveEndDate" +pnpm test -t "clampToSystemMaxEndDate" +pnpm test -t "반복 일정 수정 플로우" +``` + +--- + +## 결론 +- 상태: 통과(스토리 02/03), 스토리 04 일부 통과(UI 다이얼로그 노출) +- 후속 조치: 스토리 04 훅/분기 실패 테스트(단일/전체 호출 분기) 추가 후 구현 + + diff --git a/.cursor/outputs/test-design-review.md b/.cursor/outputs/test-design-review.md new file mode 100644 index 00000000..1e9c8291 --- /dev/null +++ b/.cursor/outputs/test-design-review.md @@ -0,0 +1,81 @@ +# 🧪 테스트설계 결과 보고서 (스토리 04 - 반복 일정 수정) + +## 메타데이터 +- 에이전트: 테스트설계 +- 날짜: 2025-10-30 +- 승인자: CEO(Riku) +- 참고 문서: `.cursor/spec/stories/04-repeat-event-update.md`, `.cursorrules` +- 사용 체크리스트: `.cursor/checklist/test-design-agent-checklist.md` + +--- + +## 요약 +- 목표: 단일/전체 수정 분기에 대한 훅/통합 테스트 설계 확정 +- 상태: 보완 필요(테스트 케이스 정의 완료, 아직 Red 단계 미작성) + +--- + +## 상세 체크 결과 (수신: Riku) +| 섹션 | 번호 | 항목 | 결과(✅/⚠️) | 근거 | 코멘트 | +|------|------|------|------------|------|--------| +| 요구사항 | 1-1 | 테스트 범위 정의 | ✅ | 스토리 04 문서 | 훅/UI 분리 설계 | +| I/O | 2-1 | 입력/타입 명확성 | ✅ | `src/types.ts` | `repeat.id` 포함 | +| 프로세스 | 3-1 | 분기 정의 | ✅ | 스토리 04 문서 | 단일 vs 전체 | +| 에러 | 4-1 | 실패 경로 | ⚠️ | (추가 예정) | 네트워크 실패 주입 | +| 접근성 | 5-1 | 다이얼로그/버튼 라벨 | ✅ | 스토리 04 문서 | `aria-label` 명시 | + +--- + +## 테스트 케이스(계획) +- 훅: 편집 모드에서 + - 단일 선택 → `PUT /api/events/:id`, 본문 `repeat.type='none'` + - 전체 선택 → `PUT /api/recurring-events/:repeatId` +- UI: 저장 시 다이얼로그 노출, 버튼 라벨, 아이콘 유지/제거 검증 + +--- + +## 결론 +- 상태: 보완 필요(다음 단계에서 Red 작성) +- 후속 조치: 훅/통합 실패 테스트 추가 후 보고 + +--- + +# 🧪 테스트설계 결과 보고서 (스토리 05 - 반복 일정 삭제) + +## 메타데이터 +- 에이전트: 테스트설계 +- 날짜: 2025-10-30 +- 승인자: CEO(Riku) +- 참고 문서: `.cursor/spec/stories/05-repeat-event-delete.md`, `.cursorrules` +- 사용 체크리스트: `.cursor/checklist/test-design-agent-checklist.md` + +--- + +## 요약 +- 목표: 단일/전체 삭제 분기에 대한 훅/통합 테스트 설계 확정 +- 상태: 보완 필요(테스트 케이스 정의 완료, Red 단계 미작성) + +--- + +## 상세 체크 결과 (수신: Riku) +| 섹션 | 번호 | 항목 | 결과(✅/⚠️) | 근거 | 코멘트 | +|------|------|------|------------|------|--------| +| 요구사항 | 1-1 | 테스트 범위 정의 | ✅ | 스토리 05 문서 | 훅/UI 분리 설계 | +| I/O | 2-1 | 입력/타입 명확성 | ✅ | `src/types.ts` | `repeat.id` 포함 | +| 프로세스 | 3-1 | 분기 정의 | ✅ | 스토리 05 문서 | 단일 vs 전체 | +| 에러 | 4-1 | 실패 경로 | ⚠️ | (추가 예정) | 네트워크 실패 주입 | +| 접근성 | 5-1 | 다이얼로그/버튼 라벨 | ✅ | 스토리 05 문서 | `aria-label` 명시 | + +--- + +## 테스트 케이스(계획) +- 훅: 삭제 분기 + - 단일 삭제 → `DELETE /api/events/:id` + - 전체 삭제 → `DELETE /api/recurring-events/:repeatId` +- UI: 삭제 버튼 클릭 시 다이얼로그 노출, `단일 삭제`/`전체 삭제` 선택에 따른 리스트 변화/호출 검증 + +--- + +## 결론 +- 상태: 보완 필요(다음 단계에서 Red 작성) +- 후속 조치: 훅/통합 실패 테스트 추가 후 보고 \ No newline at end of file diff --git a/.cursor/spec/requirements/repeat-event-feature.md b/.cursor/spec/requirements/repeat-event-feature.md new file mode 100644 index 00000000..33f9ead4 --- /dev/null +++ b/.cursor/spec/requirements/repeat-event-feature.md @@ -0,0 +1,78 @@ +--- +title: 반복 일정 기능 요구사항 +category: calendar +type: requirement +updated: 2025-10-26 +--- + +# 📅 반복 일정 기능 요구사항 + +## 🎯 목표 +기존 캘린더 시스템에 **반복 일정 기능**을 추가한다. +사용자는 일정 생성 또는 수정 시 반복 유형을 설정하고, 반복 종료일 및 수정·삭제 방식을 제어할 수 있어야 한다. + +--- + +## 🧩 주요 기능 + +### 1️⃣ 반복 유형 선택 +- 사용자는 일정 생성 또는 수정 시 **반복 유형**을 선택할 수 있다. +- 반복 유형 옵션: + - **매일 (daily)** + - **매주 (weekly)** + - **매월 (monthly)** + - **매년 (yearly)** +- **특수 조건** + - “매월 D일” 선택 시 → 해당 달에 D일이 존재할 때만 생성(보정 생성 금지) + - 예: 31일 → 31일이 있는 달에만 생성, 30일 → 2월은 제외, 29일 → 평년 2월은 제외 + - “윤년 2월 29일”에 **매년** 선택 시 → 윤년일 경우에만 생성 + - “평년 2월 28일”에 **매년** 선택 시 → 평년일 경우에만 생성(윤년은 제외) + - 반복 일정은 **일정 겹침(overlap)** 을 고려하지 않는다. + +--- + +### 2️⃣ 반복 일정 표시 +- 캘린더 뷰에서 반복 일정은 **반복 아이콘**으로 표시된다. +- 일반 일정과 구분되도록 UI 상에서 시각적 차별화 필요. + - 예: 🔁, ↻, 또는 별도 컬러 표시 + +--- + +### 3️⃣ 반복 종료 조건 +- 사용자는 **반복 종료 조건**을 설정할 수 있다. +- 종료 조건 옵션: + - 특정 날짜까지 +- 시스템 제약: + - 최대 반복 종료일은 **2025-12-31** 까지로 제한 + - 그 이후 날짜는 설정 불가 + +--- + +### 4️⃣ 반복 일정 수정 +- 사용자가 반복 일정을 수정하려 할 때, 다음과 같은 확인 메시지를 출력한다: +- ‘해당 일정만 수정하시겠어요?’ +- 사용자의 선택에 따라 동작이 다르다: +- **‘예’ 선택 → 단일 일정 수정** + - 해당 일정만 수정됨 + - 반복일정 아이콘 제거 +- **‘아니오’ 선택 → 전체 일정 수정** + - 모든 반복 일정에 수정사항 반영 + - 반복일정 아이콘 유지 + +--- + +### 5️⃣ 반복 일정 삭제 +- 사용자가 반복 일정을 삭제하려 할 때, 다음과 같은 확인 메시지를 출력한다: +- ‘해당 일정만 삭제하시겠어요?’ +- 선택에 따른 동작: +- **‘예’ 선택 → 단일 일정 삭제** + - 해당 일정만 삭제됨 +- **‘아니오’ 선택 → 전체 일정 삭제** + - 모든 반복 일정이 삭제됨 + +--- + +## ⚙️ 추가 제약사항 +- 반복일정 생성 시, 기존 일정과의 중복 여부는 **검증하지 않는다.** +- 반복 종료일(`repeatEndDate`)은 선택 사항이며, 미입력 시 기본 제한(2025-12-31)까지 반복. +- 매월, 매년 반복 시 **유효하지 않은 날짜(예: 2월 30일)** 는 생성하지 않음. diff --git a/.cursor/spec/stories/01-repeat-event-creation.md b/.cursor/spec/stories/01-repeat-event-creation.md new file mode 100644 index 00000000..d6ff261e --- /dev/null +++ b/.cursor/spec/stories/01-repeat-event-creation.md @@ -0,0 +1,342 @@ +# 반복 일정 생성 및 선택 기능 설계 + +## 📋 개요 +사용자가 일정 생성 또는 수정 시 반복 유형(매일, 매주, 매월, 매년)을 선택할 수 있는 기능을 설계합니다. + +## 🎯 목적 +- 일정 생성/수정 폼에서 반복 유형을 선택할 수 있는 UI 제공 +- 선택된 반복 유형에 따라 적절한 데이터 구조로 저장 +- 반복 종료 조건 설정 (2025-12-31까지) + +## 📥 입력 (Input) + +### 사용자 입력 +```typescript +interface RepeatFormInput { + isRepeating: boolean; // 반복 일정 여부 + repeatType: RepeatType; // 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' + repeatInterval: number; // 반복 간격 (기본값: 1) + repeatEndDate: string; // 반복 종료일 (YYYY-MM-DD, 최대: 2025-12-31) +} +``` + +### 제약 조건 +- `repeatType`: 매일, 매주, 매월, 매년 중 하나 +- `repeatInterval`: 1 이상의 정수 +- `repeatEndDate`: 현재 날짜 이후 ~ 2025-12-31 이내 +- **매월 D일 선택**: D일이 해당 달에 존재할 때만 생성(보정 생성 금지) + - 예: 31일 → 31일 있는 달만, 30일 → 2월 제외, 29일 → 평년 2월 제외 +- **윤년 2월 29일에 매년 선택**: 윤년 2월 29일에만 생성 +- **평년 2월 28일에 매년 선택**: 평년에만 생성(윤년은 제외) +- 반복 일정 생성 시 기존 일정과의 겹침(overlap) 검증을 수행하지 않음 + +## ⚙️ 처리 로직 (Process) + +### 1. UI 컴포넌트 수정 (App.tsx) + +```typescript +// 반복 일정 UI 활성화 (현재 주석 처리된 부분) +{isRepeating && ( + + + 반복 유형 + + + + + 반복 간격 + setRepeatInterval(Number(e.target.value))} + slotProps={{ htmlInput: { min: 1, max: 999 } }} + aria-label="반복 간격" + /> + + + + 반복 종료일 + setRepeatEndDate(e.target.value)} + slotProps={{ + htmlInput: { + min: date || new Date().toISOString().split('T')[0], + max: '2025-12-31' + } + }} + aria-label="반복 종료일" + /> + + +)} +``` + +### 2. 훅 수정 (useEventForm.ts) + +현재 `useEventForm` 훅은 반복 관련 상태를 이미 가지고 있으므로, `setRepeatType`, `setRepeatInterval`, `setRepeatEndDate`를 반환 객체에 추가합니다. + +```typescript +// 이미 구현되어 있지만, App.tsx에서 주석 처리되어 사용 안 함 +return { + // ... 기존 반환값 + setRepeatType, // 활성화 필요 + setRepeatInterval, // 활성화 필요 + setRepeatEndDate, // 활성화 필요 +}; +``` + +### 3. 유효성 검증 추가 + +새로운 유틸리티 함수 생성: `src/utils/repeatValidation.ts` + +```typescript +/** + * 반복 종료일이 유효한지 검증 + * @param startDate - 일정 시작일 + * @param endDate - 반복 종료일 + * @returns 에러 메시지 또는 null + */ +export function validateRepeatEndDate( + startDate: string, + endDate: string +): string | null { + if (!endDate) return null; + + const start = new Date(startDate); + const end = new Date(endDate); + const maxDate = new Date('2025-12-31'); + + if (end < start) { + return '반복 종료일은 시작일 이후여야 합니다.'; + } + + if (end > maxDate) { + return '반복 종료일은 2025-12-31 이전이어야 합니다.'; + } + + return null; +} + +/** + * 반복 간격이 유효한지 검증 + */ +export function validateRepeatInterval(interval: number): string | null { + if (interval < 1) { + return '반복 간격은 1 이상이어야 합니다.'; + } + if (interval > 999) { + return '반복 간격은 999 이하여야 합니다.'; + } + return null; +} +``` + +## 📤 출력 (Output) + +### 저장되는 데이터 구조 +```typescript +interface Event { + id: string; + title: string; + date: string; + startTime: string; + endTime: string; + description: string; + location: string; + category: string; + repeat: { + type: RepeatType; // 'daily' | 'weekly' | 'monthly' | 'yearly' + interval: number; // 1, 2, 3, ... + endDate?: string; // 'YYYY-MM-DD' 또는 undefined + id?: string; + }; + notificationTime: number; +} +``` + +### API 요청 형식 +```typescript +// 일반 일정 단건 생성 +// POST /api/events +{ + "title": "회의", + "date": "2025-01-01", + "startTime": "10:00", + "endTime": "11:00", + "description": "정기 회의", + "location": "회의실 A", + "category": "업무", + "repeat": { "type": "none", "interval": 1 }, + "notificationTime": 10 +} + +// 반복 일정 생성(저장 시 인스턴스 배열 생성 후 전송) +// POST /api/events-list +{ + "events": [ + { + "title": "주간 회의", + "date": "2025-01-06", + "startTime": "10:00", + "endTime": "11:00", + "description": "정기 주간 회의", + "location": "회의실 A", + "category": "업무", + "repeat": { "type": "weekly", "interval": 1, "endDate": "2025-12-31" }, + "notificationTime": 10 + } + // ... generateRepeatEvents로 생성된 다른 날짜들 + ] +} + +// 서버는 저장 시 각 이벤트에 id를 부여하고, +// 반복 일정인 경우 repeat.id(시리즈 식별자)를 공통으로 부여함 +``` + +## 📁 영향받는 파일 + +### 수정 필요 +1. ✅ `src/types.ts` - `RepeatInfo.id?` 필드 추가 +2. 🔧 `src/App.tsx` - 주석 해제 및 UI 활성화 +3. 🔧 `src/hooks/useEventForm.ts` - setter 함수 반환 활성화 +4. ✨ `src/utils/repeatValidation.ts` - 새로 생성 + +### 테스트 파일 추가 +5. ✨ `src/__tests__/unit/easy.repeatValidation.spec.ts` - 새로 생성 + +## 🚨 고려사항 + +### 특수 케이스 처리 + +#### 1. 매월 31일 선택 +```typescript +// ❌ 잘못된 처리: 매월 마지막 날 +// ✅ 올바른 처리: 31일이 있는 달에만 생성 (1, 3, 5, 7, 8, 10, 12월) + +// 예시: 2025-01-31에 매월 반복 설정 +// → 생성: 1/31, 3/31, 5/31, 7/31, 8/31, 10/31, 12/31 +// → 생략: 2월, 4월, 6월, 9월, 11월 +``` + +#### 2. 윤년 2월 29일 선택 +```typescript +// 예시: 2024-02-29에 매년 반복 설정 +// → 생성: 윤년에만 (2024-02-29) +// → 생략: 평년 (2025년은 생략) +``` + +#### 3. 반복 종료일 제한 +```typescript +// 최대 반복 종료일: 2025-12-31 +// 이후 날짜는 입력 불가 (HTML input max 속성 활용) +``` + +### UI/UX 고려사항 +- 반복 일정 체크박스를 해제하면 반복 관련 필드 숨김 +- 반복 간격 입력은 양의 정수만 허용 +- 반복 종료일은 필수가 아님 (선택 사항) +- 날짜 선택기의 min/max 속성으로 유효하지 않은 날짜 입력 방지 + +### 성능/저장 전략 +- 반복 일정은 저장 시점에 클라이언트에서 인스턴스 배열을 생성한 뒤 `/api/events-list`로 일괄 저장 +- 서버는 각 인스턴스에 `id`를, 반복 시리즈에는 공통 `repeat.id`를 부여 +- 조회 시에는 저장된 인스턴스를 그대로 사용(동적 계산 없음) +- 2025-12-31까지 최대 생성 가능한 일정 수: + - 매일: 최대 365개 + - 매주: 최대 52개 + - 매월: 최대 12개 + - 매년: 1개 + +## ✅ 검증 방법 + +### 단위 테스트 +```typescript +describe('반복 일정 유효성 검증', () => { + test('반복 종료일이 시작일보다 이전이면 에러', () => { + const error = validateRepeatEndDate('2025-12-31', '2025-01-01'); + expect(error).toBe('반복 종료일은 시작일 이후여야 합니다.'); + }); + + test('반복 종료일이 2025-12-31 이후면 에러', () => { + const error = validateRepeatEndDate('2025-01-01', '2026-01-01'); + expect(error).toBe('반복 종료일은 2025-12-31 이전이어야 합니다.'); + }); + + test('반복 간격이 1 미만이면 에러', () => { + const error = validateRepeatInterval(0); + expect(error).toBe('반복 간격은 1 이상이어야 합니다.'); + }); +}); +``` + +### 통합 테스트 +- 반복 일정 체크박스 선택 시 관련 필드 표시 확인 +- 각 반복 유형 선택 가능 확인 +- 유효하지 않은 값 입력 시 에러 메시지 표시 확인 + +## 🔗 관련 기능 +- [반복 일정 생성 로직](./05-repeat-event-generation.md) +- [반복 일정 표시](./02-repeat-event-display.md) +- [반복 일정 수정](./03-repeat-event-update.md) + + +## 🧭 개발 원칙과 TDD 사이클 지침 + +### 변경 최소화 원칙 +- **기존 코드 최대한 유지**: 기존 함수 시그니처, 파일 경로, 공용 타입은 가급적 변경하지 않습니다. +- **관심사 분리 준수**: 새 요구사항은 가능하면 새 파일로 추가합니다. + - 유틸: `src/utils/` + - 훅: `src/hooks/` + - 테스트: `src/__tests__/` +- **허용되는 변경의 예** + - 기존 훅의 반환 객체에 setter 등 필드 "추가" (삭제/이름변경 금지) + - UI 활성화를 위한 주석 해제 및 접근성 속성 추가 + - 타입의 선택 필드 "추가" (기존 필드 변경/삭제 금지) + +### TDD 사이클 +1) Red: 실패 테스트 먼저 작성 +- 새로운 요구사항을 드러내는 테스트를 선행합니다. +- 이 단계에서는 구현을 최소한으로 유지하며, 테스트가 실패하는지 확인합니다. + +2) Green: 최소 구현으로 통과 +- 기존 코드를 광범위하게 리팩터링하지 말고, "추가" 방식으로 통과시킵니다. +- 새 유틸/훅/타입을 추가하고, 필요한 지점에만 최소 연결을 합니다. + +3) Refactor: 중복 제거와 정리 +- 테스트가 모두 초록이 된 후, 코드 품질을 개선합니다. +- 이 단계에서 린트 예외를 제거하고, 주석/네이밍/성능을 손봅니다. + +### 실패 테스트 단계의 린트 예외 +- Red 단계(실패 테스트 작성)에서는 일시적으로 **ESLint 경고를 무시**할 수 있습니다. +- 테스트 파일 범위에서 필요한 규칙만 제한적으로 비활성화하세요. Green 단계에서 반드시 제거합니다. + +```ts +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */ +// Red 단계: 실패를 확인하기 위한 임시 린트 예외. Green/Refactor 단계에서 제거 필수. +``` + +### 실행 체크리스트 +- Red: 테스트 추가 → 실패 확인 +- Green: 최소 구현 → 테스트 통과 확인 +- Refactor: 린트 재활성화 → `pnpm lint` / `pnpm test` / `pnpm build` 모두 통과 + +### 로컬 확인 명령어 +- 애플리케이션 실행: `pnpm start` +- 접속 URL: `http://localhost:5173` diff --git a/.cursor/spec/stories/02-repeat-event-display.md b/.cursor/spec/stories/02-repeat-event-display.md new file mode 100644 index 00000000..33f69e89 --- /dev/null +++ b/.cursor/spec/stories/02-repeat-event-display.md @@ -0,0 +1,51 @@ +--- +title: 반복 일정 표시 스토리 +category: calendar +type: story +updated: 2025-10-30 +--- + +## 목적 +반복 일정은 캘린더 뷰와 일정 리스트에서 일반 일정과 시각적으로 구분되어야 한다. 접근성(ARIA) 속성을 포함하여 반복 여부를 명확히 표현한다. + +## 사용자 시나리오 +1. 사용자가 반복 일정(예: 매주)을 생성한다. +2. 캘린더 주/월 뷰의 해당 날짜 셀에 반복 아이콘이 노출된다. +3. 우측 일정 리스트에도 같은 반복 아이콘이 노출된다. +4. 알림이 트리거된 일정의 경우 알림 아이콘과 함께 표기되며, 아이콘 우선순위는 알림 > 반복이다. + +## 표시 규칙 +- 반복 여부: `event.repeat.type !== 'none'`이면 반복으로 간주 +- 아이콘: Material-UI `Repeat` 아이콘 사용 +- 접근성: 아이콘 요소에 `aria-label="반복 일정"` 부여 +- 위치 + - 주 뷰, 월 뷰: 각 일정 배지 내부의 텍스트 좌측 아이콘 Stack에 반복 아이콘 추가 + - 일정 리스트: 제목 좌측 아이콘 Stack에 반복 아이콘 추가 +- 아이콘 우선순위: 알림 아이콘(`Notifications`) 다음에 반복 아이콘(`Repeat`) 순서로 렌더링 + +## 예외/비고 +- 반복 여부는 표시만 하며, 인스턴스 생성 로직과 겹침 검증은 본 스토리 범위가 아님 +- 색상은 테마 기본을 사용하고 하드코딩 금지 + +## 접근성 +- `aria-label="반복 일정"` 필수 +- 시맨틱 마크업 유지, 아이콘만으로 의미가 전달되지 않도록 텍스트 정보 병행 표기(제목 등) + +## 테스트 케이스 (TDD) +1) 실패 테스트: 주/월 뷰에 반복 아이콘 노출 검증 + - 준비: 서버에서 이벤트 로드, 하나는 `repeat.type !== 'none'` + - 기대: 해당 날짜 셀 내에서 `getByLabelText('반복 일정')` 탐지됨 + +2) 실패 테스트: 일정 리스트에 반복 아이콘 노출 검증 + - 준비: 동일 데이터 + - 기대: 리스트 항목 내 `getAllByLabelText('반복 일정')` 중 최소 1개 존재 + +3) 우선순위 테스트: 알림 아이콘이 있을 때 반복 아이콘과 함께 노출되며, 순서가 알림 > 반복 + - 준비: `notifiedEvents`에 대상 이벤트 id 포함 + - 기대: DOM 순서상 Notifications 다음 Repeat가 배치됨 + +4) 접근성 테스트: 반복 아이콘에 `aria-label`이 정확히 부여됨 + +## Out of Scope +- 반복 인스턴스 생성/검증 규칙(요구사항 1, 3)은 별도 스토리/테스트에서 다룸 + diff --git a/.cursor/spec/stories/03-repeat-end-condition.md b/.cursor/spec/stories/03-repeat-end-condition.md new file mode 100644 index 00000000..068ba805 --- /dev/null +++ b/.cursor/spec/stories/03-repeat-end-condition.md @@ -0,0 +1,31 @@ +--- +title: 반복 종료 조건 스토리 +category: calendar +type: story +updated: 2025-10-30 +--- + +## 목적 +사용자가 반복 종료일을 설정할 수 있으며, 미입력 시 시스템이 기본 종료일(2025-12-31)을 적용한다. + +## 사용자 시나리오 +1. 사용자가 반복 일정을 생성하면서 종료일을 비워둔다. +2. 시스템은 기본 종료일 `2025-12-31`까지로 반복을 제한하여 인스턴스를 생성한다. +3. 종료일을 입력한 경우 해당 날짜(포함)까지만 생성한다. + +## 규칙 +- 종료일 입력이 없으면 기본값 `2025-12-31` 적용 +- 종료일이 있으면 해당 값 사용(별도 검증은 `repeatValidation`에서 수행) +- UI 제약: 종료일 입력의 `max` 속성은 `2025-12-31` + +## 접근성 +- 폼 입력에 `id`/레이블 제공(기존 규칙 유지) + +## 테스트 케이스 (TDD) +1) 실패 테스트: 유틸 `getEffectiveEndDate`가 종료일 미입력 시 `2025-12-31`을 반환 +2) 실패 테스트: 종료일 입력 시 해당 값을 그대로 반환 +3) 통합: `generateRepeatEvents`가 기본 종료일을 적용해 무한 생성이 발생하지 않음(월/년 반복) + +## Out of Scope +- 종료일 형식·범위 검증은 `repeatValidation`에서 커버 + diff --git a/.cursor/spec/stories/03-repeat-event-update.md b/.cursor/spec/stories/03-repeat-event-update.md new file mode 100644 index 00000000..ce0b8067 --- /dev/null +++ b/.cursor/spec/stories/03-repeat-event-update.md @@ -0,0 +1,401 @@ +# 반복 일정 수정 기능 설계 + +## 📋 개요 +반복 일정을 수정할 때 "해당 일정만 수정" 또는 "전체 수정"을 선택할 수 있는 기능을 설계합니다. + +## 🎯 목적 +- 사용자가 반복 일정의 한 인스턴스를 수정할 때 선택권 제공 +- 단일 수정: 해당 일정만 변경, 반복 일정에서 분리 +- 전체 수정: 모든 반복 일정에 변경사항 적용 + +## 📥 입력 (Input) + +### 사용자 입력 +```typescript +interface UpdateRepeatEventInput { + eventId: string; // 수정할 이벤트 ID + updateType: 'single' | 'all'; // 수정 범위 + updatedData: Partial; // 변경할 데이터 +} +``` + +### 수정 시나리오 +1. **단일 수정 ('예' 선택)** + - 해당 일정만 수정 + - 반복 일정에서 분리 (repeat.type을 'none'으로 변경) + - 반복 아이콘 사라짐 + - 원본 반복 일정은 유지 + +2. **전체 수정 ('아니오' 선택)** + - 모든 반복 일정에 변경사항 적용 + - 반복 설정 유지 + - 반복 아이콘 유지 + +## ⚙️ 처리 로직 (Process) + +### 1. 타입 정의 확장 (types.ts) + +```typescript +// 반복 시리즈 식별자는 서버에서 repeat.id로 부여됨 +export interface RepeatInfo { + type: RepeatType; + interval: number; + endDate?: string; + id?: string; // 선택 필드, 시리즈 전체 수정/삭제에 사용 +} + +export type RepeatUpdateType = 'single' | 'all'; +``` + +### 2. 확인 다이얼로그 추가 (App.tsx) + +```typescript +// 상태 추가 +const [isRepeatUpdateDialogOpen, setIsRepeatUpdateDialogOpen] = useState(false); +const [pendingUpdate, setPendingUpdate] = useState<{ + event: Event; + updateData: Partial; +} | null>(null); + +// 수정 버튼 클릭 핸들러 +const handleEditClick = (event: Event) => { + // 반복 일정인지 확인 + if (event.repeat.type !== 'none') { + // 폼에 데이터 채우기 + editEvent(event); + + // 확인 다이얼로그 표시 (수정 완료 버튼 클릭 시) + // 이 로직은 addOrUpdateEvent 함수 내부로 이동 + } else { + // 일반 일정은 바로 수정 모드 + editEvent(event); + } +}; + +// 일정 저장 로직 수정 +const addOrUpdateEvent = async () => { + // ... 유효성 검증 + + const eventData: Event | EventForm = { + // ... 데이터 구성 + }; + + // 반복 일정 수정인 경우 + if (editingEvent && editingEvent.repeat.type !== 'none') { + setPendingUpdate({ event: editingEvent, updateData: eventData }); + setIsRepeatUpdateDialogOpen(true); + return; + } + + // 일반 저장 로직 + const overlapping = findOverlappingEvents(eventData, events); + if (overlapping.length > 0) { + setOverlappingEvents(overlapping); + setIsOverlapDialogOpen(true); + } else { + await saveEvent(eventData); + resetForm(); + } +}; + +// 반복 일정 수정 확인 다이얼로그 + setIsRepeatUpdateDialogOpen(false)} +> + 반복 일정 수정 + + + 해당 일정만 수정하시겠어요? + + + + + + + +``` + +### 3. 수정 처리 함수 + +```typescript +const handleRepeatUpdate = async (updateType: RepeatUpdateType) => { + if (!pendingUpdate) return; + + const { event, updateData } = pendingUpdate; + + if (updateType === 'single') { + // 단일 수정: 반복 정보 제거 + const updatedEvent = { + ...updateData, + id: event.id, + repeat: { + type: 'none' as RepeatType, + interval: 1, + }, + // 시리즈에서 분리: repeat.id는 서버 저장 후 제거됨(선택) + }; + + await saveEvent(updatedEvent); + } else { + // 전체 수정: 반복 정보 유지 + const updatedEvent = { + ...updateData, + id: event.id, + repeat: event.repeat, // 기존 반복 정보 유지 + }; + + await updateRepeatSeries(event.repeat.id!, updatedEvent); + } + + setIsRepeatUpdateDialogOpen(false); + setPendingUpdate(null); + resetForm(); +}; +``` + +### 4. 반복 시리즈 전체 수정 (useEventOperations.ts) + +```typescript +/** + * 반복 시리즈의 모든 일정 업데이트 + */ +const updateRepeatSeries = async (repeatId: string, updateData: Partial) => { + try { + // 서버에 전체 수정 요청 (기존 엔드포인트 사용) + const response = await fetch(`/api/recurring-events/${repeatId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updateData), + }); + + if (!response.ok) { + throw new Error('Failed to update repeat group'); + } + + await fetchEvents(); + enqueueSnackbar('반복 일정이 모두 수정되었습니다.', { variant: 'success' }); + } catch (error) { + console.error('Error updating repeat series:', error); + enqueueSnackbar('반복 일정 수정 실패', { variant: 'error' }); + } +}; + +// 반환 객체에 추가 +return { + events, + fetchEvents, + saveEvent, + deleteEvent, + updateRepeatSeries // 새로 추가 +}; +``` + +### 5. 서버 API 엔드포인트 + +기존 서버의 반복 시리즈 엔드포인트를 사용합니다. + +```http +PUT /api/recurring-events/:repeatId // 시리즈 전체 수정 +``` + +## 📤 출력 (Output) + +### 단일 수정 결과 +```typescript +// Before (반복 일정) +{ + id: "event-1-20250106", + title: "주간 회의", + date: "2025-01-06", + repeat: { + type: "weekly", + interval: 1, + endDate: "2025-12-31", + id: "series-1" + } +} + +// After (단일 일정으로 변경) +{ + id: "event-1-20250106", + title: "임시 회의", // 제목 변경 + date: "2025-01-06", + repeat: { + type: "none", // 반복 제거 + interval: 1 + } +} + +// 다른 반복 일정들은 유지 +// event-1-20250113, event-1-20250120, ... 계속 존재 +``` + +### 전체 수정 결과 +```typescript +// Before +{ + id: "event-1-20250106", + title: "주간 회의", + startTime: "10:00", + repeat: { type: "weekly", interval: 1, id: "series-1" } +} + +// After (그룹의 모든 이벤트) +{ + id: "event-1-20250106", + title: "팀 스탠드업", // 제목 변경 + startTime: "09:00", // 시간 변경 + repeat: { type: "weekly", interval: 1, id: "series-1" } // 반복 유지 +} +// event-1-20250113, event-1-20250120도 모두 동일하게 변경 +``` + +## 📁 영향받는 파일 + +### 수정 필요 +1. 🔧 `src/types.ts` - `RepeatInfo.id?` 필드 추가(완료) +2. 🔧 `src/App.tsx` - 확인 다이얼로그 및 수정 로직 추가 +3. 🔧 `src/hooks/useEventOperations.ts` - `updateRepeatSeries` 함수 추가 +4. 🔧 서버 엔드포인트 변경 없음(기존 `/api/recurring-events/:repeatId` 사용) + +### 테스트 파일 추가 +5. 🔧 `src/__tests__/hooks/medium.useEventOperations.spec.ts` - 테스트 추가 +6. ✨ `src/__tests__/integration/repeat-update.spec.tsx` - 통합 테스트 + +## 🚨 고려사항 + +### 데이터 무결성 + +#### 반복 시리즈 식별자 관리 +```typescript +// 반복 일정 저장 시 서버가 공통 시리즈 식별자를 repeat.id로 부여 +// 클라이언트는 repeat.id를 사용해 전체 수정/삭제 요청을 수행 +``` + +#### 단일 수정 후 원본 추적 +```typescript +// originalDate를 사용하여 원본 반복 일정 추적 가능 +// 필요 시 "원래 반복 일정으로 되돌리기" 기능 구현 가능 +``` + +### UI/UX 고려사항 + +#### 다이얼로그 문구 +``` +제목: "반복 일정 수정" +내용: "해당 일정만 수정하시겠어요?" +버튼: + - "예 (해당 일정만)" → 단일 수정 + - "아니오 (전체 수정)" → 전체 수정 + - "취소" (X 버튼) +``` + +#### 수정 후 피드백 +- 단일 수정: "일정이 수정되었습니다." +- 전체 수정: "반복 일정 N개가 모두 수정되었습니다." + +#### 수정 제한사항 +```typescript +// 전체 수정 시 반복 설정 자체는 수정 가능 +// 하지만 반복 유형 변경은 주의 필요 (예: 매주 → 매월) + +// 안전한 방법: 전체 수정 시 반복 설정은 변경 불가 +// 변경하려면 삭제 후 재생성 권장 +``` + +### 성능 고려사항 + +#### 전체 수정 시 트랜잭션 +```text +// 서버는 /api/recurring-events/:repeatId 엔드포인트에서 일괄 업데이트를 처리 +// 일부 실패 시 적절한 에러 응답을 반환(서버 구현 범위) +``` + +#### 대량 업데이트 최적화 +```typescript +// 반복 일정이 많을 경우 (100개 이상) +// 서버 측에서 배치 업데이트 최적화 필요 +// 클라이언트는 로딩 상태 표시 +``` + +### 예외 상황 처리 + +#### 1. 반복 일정의 일부가 이미 단일 수정된 경우 +```typescript +// 전체 수정은 동일한 repeat.id를 가진 일정에만 적용 +// 이미 단일 수정되어 repeat.type === 'none'인 일정은 영향 없음 +``` + +#### 2. 수정 중 네트워크 오류 +```typescript +// 재시도 로직 또는 사용자에게 명확한 오류 메시지 +enqueueSnackbar('네트워크 오류로 수정에 실패했습니다. 다시 시도해주세요.', { + variant: 'error' +}); +``` + +## ✅ 검증 방법 + +### 단위 테스트 +```typescript +describe('반복 일정 수정', () => { + test('단일 수정 시 repeat.type이 none으로 변경', async () => { + const event: Event = { + id: '1', + repeat: { type: 'weekly', interval: 1, id: 'series-1' }, + // ... + }; + + const result = await updateSingleEvent(event, { title: 'New Title' }); + expect(result.repeat.type).toBe('none'); + expect(result.repeat.type).toBe('none'); + }); + + test('전체 수정 시 모든 시리즈 이벤트가 업데이트', async () => { + const repeatId = 'series-1'; + const updateData = { title: 'Updated Title' }; + + await updateRepeatSeries(repeatId, updateData); + + const seriesEvents = events.filter(e => e.repeat.id === repeatId); + seriesEvents.forEach(event => { + expect(event.title).toBe('Updated Title'); + expect(event.repeat.type).not.toBe('none'); + }); + }); +}); +``` + +### 통합 테스트 +```typescript +test('반복 일정 수정 플로우', async () => { + // 1. 반복 일정 생성 + // 2. 수정 버튼 클릭 + // 3. 확인 다이얼로그 표시 확인 + // 4. "예" 클릭 → 단일 수정 + // 5. 반복 아이콘 사라짐 확인 + + // 6. 다른 반복 일정 수정 + // 7. "아니오" 클릭 → 전체 수정 + // 8. 모든 반복 일정 변경 확인 + // 9. 반복 아이콘 유지 확인 +}); +``` + +## 🔗 관련 기능 +- [반복 일정 생성](./01-repeat-event-creation.md) +- [반복 일정 삭제](./04-repeat-event-delete.md) +- [반복 일정 생성 로직](./05-repeat-event-generation.md) + diff --git a/.cursor/spec/stories/04-repeat-event-delete.md b/.cursor/spec/stories/04-repeat-event-delete.md new file mode 100644 index 00000000..79ea81a8 --- /dev/null +++ b/.cursor/spec/stories/04-repeat-event-delete.md @@ -0,0 +1,489 @@ +# 반복 일정 삭제 기능 설계 + +## 📋 개요 +반복 일정을 삭제할 때 "해당 일정만 삭제" 또는 "전체 삭제"를 선택할 수 있는 기능을 설계합니다. + +## 🎯 목적 +- 사용자가 반복 일정의 한 인스턴스를 삭제할 때 선택권 제공 +- 단일 삭제: 해당 일정만 삭제 +- 전체 삭제: 모든 반복 일정 삭제 + +## 📥 입력 (Input) + +### 사용자 입력 +```typescript +interface DeleteRepeatEventInput { + eventId: string; // 삭제할 이벤트 ID + deleteType: 'single' | 'all'; // 삭제 범위 + repeatId?: string; // 반복 시리즈 ID (전체 삭제 시 필요, repeat.id) +} +``` + +### 삭제 시나리오 +1. **단일 삭제 ('예' 선택)** + - 해당 일정만 삭제 + - 다른 반복 일정은 유지 + +2. **전체 삭제 ('아니오' 선택)** + - 같은 반복 시리즈의 모든 일정 삭제 + - 동일한 `repeat.id`를 가진 모든 이벤트 제거 + +## ⚙️ 처리 로직 (Process) + +### 1. 확인 다이얼로그 추가 (App.tsx) + +```typescript +// 상태 추가 +const [isRepeatDeleteDialogOpen, setIsRepeatDeleteDialogOpen] = useState(false); +const [pendingDelete, setPendingDelete] = useState(null); + +// 삭제 버튼 클릭 핸들러 수정 +const handleDeleteClick = (event: Event) => { + // 반복 일정인지 확인 + if (event.repeat.type !== 'none') { + setPendingDelete(event); + setIsRepeatDeleteDialogOpen(true); + } else { + // 일반 일정은 바로 삭제 + deleteEvent(event.id); + } +}; + +// 기존 삭제 버튼 onClick 수정 + handleDeleteClick(event)} // 수정 +> + + + +// 반복 일정 삭제 확인 다이얼로그 + setIsRepeatDeleteDialogOpen(false)} +> + 반복 일정 삭제 + + + 해당 일정만 삭제하시겠어요? + + + + + + + + +``` + +### 2. 삭제 처리 함수 (App.tsx) + +```typescript +/** + * 반복 일정 삭제 처리 + */ +const handleRepeatDelete = async (deleteType: 'single' | 'all') => { + if (!pendingDelete) return; + + if (deleteType === 'single') { + // 단일 삭제: 해당 일정만 제거 + await deleteEvent(pendingDelete.id); + } else { + // 전체 삭제: 반복 시리즈 전체 제거 + await deleteRepeatSeries(pendingDelete.repeat.id); + } + + setIsRepeatDeleteDialogOpen(false); + setPendingDelete(null); +}; +``` + +### 3. 반복 시리즈 전체 삭제 (useEventOperations.ts) + +```typescript +/** + * 반복 시리즈의 모든 일정 삭제 + */ +const deleteRepeatSeries = async (repeatId: string) => { + try { + const response = await fetch(`/api/recurring-events/${repeatId}`, { method: 'DELETE' }); + + if (!response.ok) { + throw new Error('Failed to delete repeat group'); + } + + await fetchEvents(); + enqueueSnackbar('반복 일정이 모두 삭제되었습니다.', { variant: 'info' }); + } catch (error) { + console.error('Error deleting repeat series:', error); + enqueueSnackbar('반복 일정 삭제 실패', { variant: 'error' }); + } +}; + +// 반환 객체에 추가 +return { + events, + fetchEvents, + saveEvent, + deleteEvent, + deleteRepeatSeries // 새로 추가 +}; +``` + +### 4. 서버 API 엔드포인트 (변경 없음) + +기존 서버의 반복 시리즈 엔드포인트를 사용합니다. + +```http +DELETE /api/recurring-events/:repeatId // 시리즈 전체 삭제 +``` + +### 5. Hook 확장 (useEventOperations.ts) + +```typescript +export const useEventOperations = (editing: boolean, onSave?: () => void) => { + const [events, setEvents] = useState([]); + const { enqueueSnackbar } = useSnackbar(); + + // ... 기존 함수들 + + /** + * 반복 그룹 전체 삭제 + */ + const deleteRepeatSeries = async (repeatId?: string) => { + if (!repeatId) { + enqueueSnackbar('반복 그룹 ID가 없습니다.', { variant: 'error' }); + return; + } + + try { + const response = await fetch(`/api/recurring-events/${repeatId}`, { method: 'DELETE' }); + + if (!response.ok) { + throw new Error('Failed to delete repeat group'); + } + + await fetchEvents(); + enqueueSnackbar('반복 일정이 삭제되었습니다.', { variant: 'info' }); + } catch (error) { + console.error('Error deleting repeat series:', error); + enqueueSnackbar('반복 일정 삭제 실패', { variant: 'error' }); + } + }; + + return { + events, + fetchEvents, + saveEvent, + deleteEvent, + deleteRepeatSeries + }; +}; +``` + +## 📤 출력 (Output) + +### 단일 삭제 결과 +```typescript +// Before: 3개의 반복 일정 +[ + { id: "1", date: "2025-01-06", repeat: { type: "weekly", interval: 1, id: "series-1" } }, + { id: "2", date: "2025-01-13", repeat: { type: "weekly", interval: 1, id: "series-1" } }, + { id: "3", date: "2025-01-20", repeat: { type: "weekly", interval: 1, id: "series-1" } }, +] + +// After: id="2"만 삭제 +[ + { id: "1", date: "2025-01-06", repeat: { type: "weekly", interval: 1, id: "series-1" } }, // 유지 + // id="2" 삭제됨 + { id: "3", date: "2025-01-20", repeat: { type: "weekly", interval: 1, id: "series-1" } }, // 유지 +] +``` + +### 전체 삭제 결과 +```typescript +// Before: 여러 반복 일정과 일반 일정 +[ + { id: "1", date: "2025-01-06", repeat: { type: "weekly", interval: 1, id: "series-1" } }, + { id: "2", date: "2025-01-13", repeat: { type: "weekly", interval: 1, id: "series-1" } }, + { id: "3", date: "2025-01-20", repeat: { type: "weekly", interval: 1, id: "series-1" } }, + { id: "4", date: "2025-01-15", repeat: { type: "none", interval: 1 } }, // 일반 일정 +] + +// After: repeat.id="series-1" 모두 삭제 +[ + { id: "4", date: "2025-01-15", repeat: { type: "none" } }, // 유지 +] +``` + +### API 응답 +```typescript +// 단일 삭제 +{ + "message": "Event deleted" +} + +// 전체 삭제 +{ + "message": "Repeat group deleted", + "deletedCount": 10 +} +``` + +### 사용자 피드백 +```typescript +// 단일 삭제 +"일정이 삭제되었습니다." (info) + +// 전체 삭제 +"반복 일정 10개가 삭제되었습니다." (info) + +// 오류 +"반복 일정 삭제 실패" (error) +``` + +## 📁 영향받는 파일 + +### 수정 필요 +1. 🔧 `src/App.tsx` - 확인 다이얼로그 및 삭제 로직 추가 +2. 🔧 `src/hooks/useEventOperations.ts` - `deleteRepeatSeries` 함수 추가 +3. 🔧 서버 엔드포인트 변경 없음(기존 `/api/recurring-events/:repeatId` 사용) + +### 테스트 파일 추가 +4. 🔧 `src/__tests__/hooks/medium.useEventOperations.spec.ts` - 테스트 추가 +5. ✨ `src/__tests__/integration/repeat-delete.spec.tsx` - 통합 테스트 + +## 🚨 고려사항 + +### 안전성 + +#### 실수 방지 +```typescript +// 다이얼로그 문구를 명확하게 +// "예" = 해당 일정만, "아니오" = 전체 +// 위험한 작업(전체 삭제)은 빨간색으로 강조 + + + +``` + +#### 되돌리기 불가능 경고 +```typescript +// 전체 삭제 시 추가 경고 (선택사항) + + 해당 일정만 삭제하시겠어요? + + ※ 전체 삭제 시 모든 반복 일정이 영구적으로 삭제되며 복구할 수 없습니다. + + +``` + +### UI/UX 고려사항 + +#### 다이얼로그 디자인 +``` +┌────────────────────────────────┐ +│ 반복 일정 삭제 [X]│ +├────────────────────────────────┤ +│ 해당 일정만 삭제하시겠어요? │ +│ │ +│ ※ 전체 삭제 시 N개의 반복 │ +│ 일정이 모두 삭제됩니다. │ +├────────────────────────────────┤ +│ [취소] [아니오-전체] [예-단일]│ +└────────────────────────────────┘ +``` + +#### 삭제된 개수 표시 +```typescript +// 전체 삭제 후 피드백에 삭제된 개수 포함 +enqueueSnackbar( + `반복 일정 ${deletedCount}개가 삭제되었습니다.`, + { variant: 'info' } +); +``` + +#### 로딩 상태 +```typescript +// 대량 삭제 시 로딩 표시 +const [isDeleting, setIsDeleting] = useState(false); + +const deleteRepeatSeries = async (repeatId) => { + setIsDeleting(true); + try { + // ... 삭제 로직 + } finally { + setIsDeleting(false); + } +}; +``` + +### 성능 고려사항 + +#### 대량 삭제 최적화 +```javascript +// 서버에서 배치 삭제 +app.delete('/api/recurring-events/:repeatId', (req, res) => { + const { repeatId } = req.params; + + // filter를 한 번만 실행 + const newEvents = events.filter(e => e.repeat?.id !== repeatId); + const deletedCount = events.length - newEvents.length; + events = newEvents; + + res.json({ deletedCount }); +}); +``` + +#### 클라이언트 캐시 업데이트 +```typescript +// 삭제 후 전체 목록 재조회 vs. 로컬 상태 업데이트 +// 현재: fetchEvents() 호출 (안전하지만 느림) +// 최적화: 로컬 상태에서 직접 제거 (빠르지만 동기화 주의) + +// 최적화 예시 +const deleteRepeatSeries = async (repeatId) => { + // 서버 요청 + await fetch(`/api/recurring-events/${repeatId}`, { method: 'DELETE' }); + + // 로컬 상태 업데이트 (재조회 없이) + setEvents(prev => prev.filter(e => e.repeat?.id !== repeatId)); +}; +``` + +### 예외 상황 처리 + +#### 1. repeat.id가 없는 경우 +```typescript +if (!pendingDelete.repeat?.id) { + enqueueSnackbar('반복 그룹 정보가 없습니다.', { variant: 'error' }); + return; +} +``` + +#### 2. 이미 삭제된 일정 +```typescript +// 서버에서 404 처리 +if (groupEvents.length === 0) { + return res.status(404).json({ error: 'Repeat group not found' }); +} + +// 클라이언트에서 처리 +if (!response.ok) { + if (response.status === 404) { + enqueueSnackbar('이미 삭제된 일정입니다.', { variant: 'warning' }); + } +} +``` + +#### 3. 네트워크 오류 +```typescript +try { + await deleteRepeatSeries(repeatId); +} catch (error) { + enqueueSnackbar( + '네트워크 오류로 삭제에 실패했습니다. 다시 시도해주세요.', + { variant: 'error' } + ); +} +``` + +### 알림 삭제 처리 + +```typescript +// 반복 일정 삭제 시 관련 알림도 제거 +// useNotifications 훅에서 처리 필요 +useEffect(() => { + // 존재하지 않는 이벤트의 알림 제거 + setNotifications(prev => + prev.filter(notif => + events.some(event => event.id === notif.id) + ) + ); +}, [events]); +``` + +## ✅ 검증 방법 + +### 단위 테스트 +```typescript +describe('반복 일정 삭제', () => { + test('단일 삭제 시 해당 일정만 제거', async () => { + const events = [ + { id: '1', repeat: { id: 'series-1' } }, + { id: '2', repeat: { id: 'series-1' } }, + { id: '3', repeat: { id: 'series-1' } }, + ]; + + await deleteEvent('2'); + + const remaining = getEvents(); + expect(remaining).toHaveLength(2); + expect(remaining.find(e => e.id === '2')).toBeUndefined(); + }); + + test('전체 삭제 시 그룹의 모든 일정 제거', async () => { + const events = [ + { id: '1', repeat: { id: 'series-1' } }, + { id: '2', repeat: { id: 'series-1' } }, + { id: '3', repeat: { id: 'series-2' } }, + ]; + + const result = await deleteRepeatSeries('series-1'); + + expect(result.deletedCount).toBe(2); + const remaining = getEvents(); + expect(remaining).toHaveLength(1); + expect(remaining[0].id).toBe('3'); + }); +}); +``` + +### 통합 테스트 +```typescript +test('반복 일정 삭제 플로우', async () => { + // 1. 반복 일정 생성 + // 2. 삭제 버튼 클릭 + // 3. 확인 다이얼로그 표시 + // 4. "예" 클릭 → 단일 삭제 + // 5. 해당 일정만 삭제 확인 + + // 6. 다른 반복 일정 삭제 버튼 클릭 + // 7. "아니오" 클릭 → 전체 삭제 + // 8. 모든 반복 일정 삭제 확인 + // 9. 피드백 메시지 확인 +}); +``` + +### 수동 테스트 체크리스트 +- [ ] 일반 일정 삭제 시 다이얼로그 표시 안 함 +- [ ] 반복 일정 삭제 시 다이얼로그 표시 +- [ ] "예" 선택 시 해당 일정만 삭제 +- [ ] "아니오" 선택 시 전체 삭제 +- [ ] "취소" 선택 시 삭제 안 함 +- [ ] 삭제 후 적절한 피드백 메시지 표시 +- [ ] 전체 삭제 시 삭제된 개수 표시 +- [ ] 캘린더와 목록에서 삭제된 일정 사라짐 확인 + +## 🔗 관련 기능 +- [반복 일정 생성](./01-repeat-event-creation.md) +- [반복 일정 수정](./03-repeat-event-update.md) +- [반복 일정 생성 로직](./05-repeat-event-generation.md) + diff --git a/.cursor/spec/stories/04-repeat-event-update.md b/.cursor/spec/stories/04-repeat-event-update.md new file mode 100644 index 00000000..d4a5bcb6 --- /dev/null +++ b/.cursor/spec/stories/04-repeat-event-update.md @@ -0,0 +1,56 @@ +--- +title: 반복 일정 수정 스토리 +category: calendar +type: story +updated: 2025-10-30 +--- + +## 목적 +반복 일정을 수정할 때 사용자에게 단일 수정과 전체 수정을 선택하게 하고, 선택에 따라 적절히 저장한다. + +## 사용자 시나리오 +1. 사용자가 반복 일정(반복 아이콘 표시)을 편집한다. +2. 저장 시 확인 다이얼로그가 뜬다: “해당 일정만 수정하시겠어요?” +3. 사용자가 선택한다: + - 예(단일): 해당 인스턴스만 수정되고 반복에서 분리된다(`repeat.type = 'none'`). + - 아니오(전체): 동일 반복 그룹의 모든 인스턴스가 수정된다(`repeat.id`로 식별). + +## 동작 규칙 +- 단일 수정 시 + - 현재 편집 중 이벤트만 `PUT /api/events/:id` + - 본문에서 `repeat.type = 'none'`로 지정하여 반복에서 분리 + - UI에서 해당 이벤트의 반복 아이콘 제거 +- 전체 수정 시 + - `PUT /api/recurring-events/:repeatId`로 일괄 수정 + - UI에서 반복 아이콘 유지 + +## 데이터/타입 +- `RepeatInfo`: `{ type: 'none'|'daily'|'weekly'|'monthly'|'yearly', interval: number, endDate?: string, id?: string }` +- 반복 그룹 식별: `repeat.id`(repeatGroupId) + +## API +- 단일 수정: `PUT /api/events/:id` (본문: `Event`) +- 전체 수정: `PUT /api/recurring-events/:repeatId` (본문: `EventForm` 또는 변경 필드) +- 테스트에서는 `msw`로 해당 엔드포인트를 `server.use`로 오버라이드해 검증 + +## UI +- 저장 버튼 클릭 시 확인 다이얼로그 노출 +- 예/아니오 버튼에 `aria-label` 제공: `aria-label="단일 수정"`, `aria-label="전체 수정"` +- 결과에 따라 반복 아이콘 표시 여부 변경(단일: 제거, 전체: 유지) + +## 접근성 +- 다이얼로그에 역할/라벨 제공 +- 버튼에 명확한 `aria-label` + +## 테스트 케이스 (TDD) +1) 훅 테스트(실패 → 구현) + - 편집 모드에서 단일 선택 시 `PUT /api/events/:id` 호출, 본문에 `repeat.type='none'` + - 편집 모드에서 전체 선택 시 `PUT /api/recurring-events/:repeatId` 호출 +2) UI 테스트(실패 → 구현) + - 저장 시 다이얼로그 노출 및 두 버튼 존재 + - 단일 선택 시 리스트의 해당 항목에서 반복 아이콘 제거 + - 전체 선택 시 반복 아이콘 유지 + +## Out of Scope +- 반복 생성/삭제는 별도 스토리에서 다룸(01/05) + diff --git a/.cursor/spec/stories/05-repeat-event-delete.md b/.cursor/spec/stories/05-repeat-event-delete.md new file mode 100644 index 00000000..7908e31f --- /dev/null +++ b/.cursor/spec/stories/05-repeat-event-delete.md @@ -0,0 +1,36 @@ +--- +title: 반복 일정 삭제 스토리 +category: calendar +type: story +updated: 2025-10-30 +--- + +## 목적 +반복 일정을 삭제할 때 사용자에게 단일 삭제와 전체 삭제를 선택하게 하고, 선택에 따라 적절히 삭제한다. + +## 사용자 시나리오 +1. 사용자가 반복 일정(반복 아이콘 표시)을 삭제하려고 한다. +2. 삭제 시 확인 다이얼로그가 뜬다: “해당 일정만 삭제하시겠어요?” +3. 사용자가 선택한다: + - 예(단일): 해당 인스턴스만 삭제된다. + - 아니오(전체): 동일 반복 그룹의 모든 인스턴스가 삭제된다(`repeat.id`). + +## 동작 규칙 +- 단일 삭제: `DELETE /api/events/:id` +- 전체 삭제: `DELETE /api/recurring-events/:repeatId` +- UI: 다이얼로그 버튼 `aria-label="단일 삭제"`, `aria-label="전체 삭제"` + +## 접근성 +- 다이얼로그에 제목/본문 제공, 버튼 라벨 명확화 + +## 테스트 케이스 (TDD) +1) 훅 테스트(실패 → 구현) + - 단일 삭제 선택 시 `/api/events/:id` 호출, 전체 삭제 호출 없음 + - 전체 삭제 선택 시 `/api/recurring-events/:repeatId` 호출, 단일 삭제 호출 없음 +2) UI 테스트(실패 → 구현) + - 삭제 버튼 클릭 시 확인 다이얼로그 노출 + - 단일 삭제 선택 시 리스트에서 해당 일정 제거, 전체 삭제 선택 시 동일 그룹 모두 제거(간단히 네트워크 카운트로 검증) + +## Out of Scope +- 영구 삭제 취소/undo, 휴지통 등 추가 UX는 범위 외 + diff --git a/.cursor/spec/stories/05-repeat-event-generation.md b/.cursor/spec/stories/05-repeat-event-generation.md new file mode 100644 index 00000000..eed2e17e --- /dev/null +++ b/.cursor/spec/stories/05-repeat-event-generation.md @@ -0,0 +1,335 @@ +# 반복 일정 생성 로직 설계 + +## 📋 개요 +반복 설정에 따라 실제 이벤트 인스턴스를 생성하는 핵심 로직을 설계합니다. + +## 🎯 목적 +- 반복 설정(매일/매주/매월/매년)에 따라 이벤트 생성 +- 특수 케이스 처리 (31일, 윤년 2월 29일) +- 2025-12-31까지 생성 제한 +- 반복 일정 겹침은 고려하지 않음 + +## 📥 입력 (Input) + +```typescript +interface RepeatEventGenerationInput { + baseEvent: EventForm; // 기본 이벤트 데이터 + repeat: { + type: RepeatType; // 'daily' | 'weekly' | 'monthly' | 'yearly' + interval: number; // 반복 간격 + endDate?: string; // 종료일 (최대 2025-12-31) + }; +} +``` + +## ⚙️ 처리 로직 (Process) + +### 1. 유틸리티 함수 생성 (`src/utils/repeatUtils.ts`) + +```typescript +import { EventForm, RepeatType } from '../types'; + +const MAX_END_DATE = new Date('2025-12-31'); + +/** + * 반복 일정 생성 + */ +export function generateRepeatEvents( + baseEvent: EventForm +): EventForm[] { + if (baseEvent.repeat.type === 'none') { + return []; + } + + const events: EventForm[] = []; + const startDate = new Date(baseEvent.date); + const endDate = baseEvent.repeat.endDate + ? new Date(baseEvent.repeat.endDate) + : MAX_END_DATE; + + let currentDate = new Date(startDate); + let count = 0; + const MAX_ITERATIONS = 1000; // 무한 루프 방지 + + while (currentDate <= endDate && count < MAX_ITERATIONS) { + const eventDate = formatDate(currentDate); + + events.push({ + ...baseEvent, + date: eventDate, + }); + + currentDate = getNextRepeatDate( + currentDate, + baseEvent.repeat.type, + baseEvent.repeat.interval, + startDate + ); + + count++; + } + + return events; +} + +/** + * 다음 반복 날짜 계산 + */ +function getNextRepeatDate( + current: Date, + type: RepeatType, + interval: number, + originalDate: Date +): Date { + const next = new Date(current); + + switch (type) { + case 'daily': + next.setDate(next.getDate() + interval); + break; + + case 'weekly': + next.setDate(next.getDate() + (7 * interval)); + break; + + case 'monthly': + return getNextMonthlyDate(current, interval, originalDate); + + case 'yearly': + return getNextYearlyDate(current, interval, originalDate); + + default: + break; + } + + return next; +} + +/** + * 매월 반복 날짜 계산 + * 31일 규칙: 31일이 있는 달에만 생성 + */ +function getNextMonthlyDate( + current: Date, + interval: number, + original: Date +): Date { + const originalDay = original.getDate(); + let next = new Date(current); + let attempts = 0; + const MAX_ATTEMPTS = 24; // 최대 2년 + + while (attempts < MAX_ATTEMPTS) { + next.setMonth(next.getMonth() + interval); + + // 해당 월의 마지막 날 확인 + const daysInMonth = new Date( + next.getFullYear(), + next.getMonth() + 1, + 0 + ).getDate(); + + // 원본 날짜가 해당 월에 존재하는 경우 + if (originalDay <= daysInMonth) { + next.setDate(originalDay); + break; + } + + attempts++; + } + + return next; +} + +/** + * 매년 반복 날짜 계산 + * 윤년 2/29 규칙: 윤년에만 생성 + */ +function getNextYearlyDate( + current: Date, + interval: number, + original: Date +): Date { + const originalMonth = original.getMonth(); + const originalDay = original.getDate(); + let next = new Date(current); + let attempts = 0; + const MAX_ATTEMPTS = 10; + + // 윤년 2월 29일 체크 + const isLeapDayOriginal = originalMonth === 1 && originalDay === 29; + + while (attempts < MAX_ATTEMPTS) { + next.setFullYear(next.getFullYear() + interval); + + if (isLeapDayOriginal) { + // 윤년인지 확인 + if (isLeapYear(next.getFullYear())) { + next.setMonth(1); + next.setDate(29); + break; + } + } else { + next.setMonth(originalMonth); + next.setDate(originalDay); + break; + } + + attempts++; + } + + return next; +} + +/** + * 윤년 판별 + */ +function isLeapYear(year: number): boolean { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +} + +/** + * 날짜 포맷 (YYYY-MM-DD) + */ +function formatDate(date: Date): string { + return date.toISOString().split('T')[0]; +} +``` + +### 2. API 통합 (클라이언트 → 서버) + +```typescript +// 저장 시 클라이언트에서 인스턴스 생성 후 일괄 저장 +const instances = generateRepeatEvents(baseEvent); +await fetch('/api/events-list', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ events: instances }), +}); + +// 서버는 각 이벤트에 id를, 반복 시리즈에는 공통 repeat.id를 부여하여 응답 +``` + +## 📤 출력 (Output) + +### 예시: 매주 반복 +```typescript +// Input +{ + title: "주간 회의", + date: "2025-01-06", + repeat: { type: "weekly", interval: 1, endDate: "2025-01-31" } +} + +// Output: 4개 생성(서버 저장 후 각 이벤트에 id와 repeat.id 부여) +[ + { id: "...", date: "2025-01-06", repeat: { type: 'weekly', interval: 1, id: "series-1" }, ... }, + { id: "...", date: "2025-01-13", repeat: { type: 'weekly', interval: 1, id: "series-1" }, ... }, + { id: "...", date: "2025-01-20", repeat: { type: 'weekly', interval: 1, id: "series-1" }, ... }, + { id: "...", date: "2025-01-27", repeat: { type: 'weekly', interval: 1, id: "series-1" }, ... }, +] +``` + +### 예시: 매월 31일 (특수 케이스) +```typescript +// Input: 2025-01-31부터 매월 +// Output: 31일이 있는 달만 +[ + { date: "2025-01-31" }, + { date: "2025-03-31" }, + { date: "2025-05-31" }, + { date: "2025-07-31" }, + { date: "2025-08-31" }, + { date: "2025-10-31" }, + { date: "2025-12-31" }, +] +// 2월, 4월, 6월, 9월, 11월은 생략 +``` + +### 예시: 윤년 2월 29일 (특수 케이스) +```typescript +// Input: 2024-02-29부터 매년 +// Output: 윤년만 +[ + { date: "2024-02-29" }, + // 2025년 생략 (평년) +] +``` + +## 📁 영향받는 파일 + +### 새로 생성 +1. ✨ `src/utils/repeatUtils.ts` - 반복 로직 유틸리티 +2. ✨ `src/__tests__/unit/easy.repeatUtils.spec.ts` - 단위 테스트 + +### 수정 필요 +3. 🔧 `server.js` - 반복 일정 생성 통합 +4. 🔧 `src/hooks/useEventOperations.ts` - 클라이언트 생성 로직 (선택) + +## 🚨 고려사항 + +### 생성 위치 +**서버 측 생성 (권장)** +- 장점: 일관성, 클라이언트 부담 감소 +- 단점: 서버 부하 + +**클라이언트 측 생성 (대안)** +- 장점: 서버 부하 감소 +- 단점: 일관성 유지 어려움 + +### 성능 +- 최대 생성 개수: 365개 (매일, 1년) +- 무한 루프 방지: MAX_ITERATIONS 제한 +- 효율적인 날짜 계산 알고리즘 + +### 반복 일정 조회 최적화 +```typescript +// 뷰 렌더링 시 필터링 +const visibleEvents = events.filter(event => { + const eventDate = new Date(event.date); + return eventDate >= viewStartDate && eventDate <= viewEndDate; +}); +``` + +## ✅ 검증 방법 + +### 단위 테스트 +```typescript +describe('반복 일정 생성', () => { + test('매일 반복', () => { + const events = generateRepeatEvents({ + date: '2025-01-01', + repeat: { type: 'daily', interval: 1, endDate: '2025-01-07' }, + // ... + }); + + expect(events).toHaveLength(7); + }); + + test('매월 31일 - 31일 있는 달만', () => { + const events = generateRepeatEvents({ + date: '2025-01-31', + repeat: { type: 'monthly', interval: 1, endDate: '2025-12-31' }, + // ... + }); + + expect(events).toHaveLength(7); // 1,3,5,7,8,10,12월 + expect(events.find(e => e.date === '2025-02-28')).toBeUndefined(); + }); + + test('윤년 2월 29일 - 윤년만', () => { + const events = generateRepeatEvents({ + date: '2024-02-29', + repeat: { type: 'yearly', interval: 1, endDate: '2025-12-31' }, + // ... + }); + + expect(events).toHaveLength(1); // 2024년만 + }); +}); +``` + +## 🔗 관련 기능 +- [반복 일정 생성](./01-repeat-event-creation.md) +- [반복 일정 표시](./02-repeat-event-display.md) + diff --git a/.cursor/spec/stories/README.md b/.cursor/spec/stories/README.md new file mode 100644 index 00000000..5e7888b0 --- /dev/null +++ b/.cursor/spec/stories/README.md @@ -0,0 +1,238 @@ +# 반복 일정 기능 설계 문서 + +## 📚 문서 개요 +이 디렉토리는 캘린더 애플리케이션의 **반복 일정 기능**에 대한 상세 설계 문서를 포함합니다. + +## 🎯 기능 목표 +사용자가 일정을 생성할 때 반복 설정(매일/매주/매월/매년)을 선택하고, 이를 캘린더에 표시하며, 개별 또는 전체 수정/삭제가 가능하도록 합니다. + +## 📋 설계 문서 목록 + +### 1. [반복 일정 생성 및 선택](./01-repeat-event-creation.md) +**목적**: 일정 폼에서 반복 유형 선택 UI 및 데이터 저장 + +**주요 내용**: +- 반복 유형 선택 (매일/매주/매월/매년) +- 반복 간격 설정 +- 반복 종료일 설정 (최대 2025-12-31) +- 유효성 검증 + +**영향받는 파일**: +- `src/App.tsx` - UI 활성화 +- `src/hooks/useEventForm.ts` - 상태 관리 +- `src/utils/repeatValidation.ts` - 검증 로직 (신규) + +--- + +### 2. [반복 일정 표시](./02-repeat-event-display.md) +**목적**: 캘린더와 목록에서 반복 일정을 시각적으로 구분 + +**주요 내용**: +- 반복 아이콘 표시 (Material-UI Repeat 아이콘) +- 월간/주간 뷰에서 아이콘 표시 +- 일정 목록에서 반복 정보 표시 +- 접근성 고려 (aria-label) + +**영향받는 파일**: +- `src/App.tsx` - 렌더링 로직 수정 +- `src/utils/eventUtils.ts` - 유틸리티 함수 추가 + +--- + +### 3. [반복 일정 수정](./03-repeat-event-update.md) +**목적**: 반복 일정 수정 시 단일/전체 선택 기능 + +**주요 내용**: +- 수정 확인 다이얼로그 ("해당 일정만 수정하시겠어요?") +- 단일 수정: 반복에서 분리, 아이콘 제거 +- 전체 수정: 모든 반복 일정 업데이트 +- repeat.id로 그룹 관리 + +**영향받는 파일**: +- `src/types.ts` - `RepeatInfo.id?` 필드 추가 +- `src/App.tsx` - 확인 다이얼로그 +- `src/hooks/useEventOperations.ts` - 전체 수정 로직 +- `server.js` - API 엔드포인트 추가 + +--- + +### 4. [반복 일정 삭제](./04-repeat-event-delete.md) +**목적**: 반복 일정 삭제 시 단일/전체 선택 기능 + +**주요 내용**: +- 삭제 확인 다이얼로그 ("해당 일정만 삭제하시겠어요?") +- 단일 삭제: 해당 일정만 제거 +- 전체 삭제: 모든 반복 일정 제거 +- 안전성 고려 (실수 방지) + +**영향받는 파일**: +- `src/App.tsx` - 확인 다이얼로그 +- `src/hooks/useEventOperations.ts` - 전체 삭제 로직 +- `server.js` - API 엔드포인트 추가 + +--- + +### 5. [반복 일정 생성 로직](./05-repeat-event-generation.md) +**목적**: 반복 설정에 따라 실제 이벤트 인스턴스 생성 + +**주요 내용**: +- 매일/매주/매월/매년 반복 계산 +- **특수 케이스**: 매월 31일 → 31일 있는 달만 +- **특수 케이스**: 윤년 2/29 → 윤년만 +- 2025-12-31까지 제한 +- 반복 일정 겹침 무시 + +**영향받는 파일**: +- `src/utils/repeatUtils.ts` - 반복 생성 로직 (신규) +- `server.js` - API 통합 + +--- + +## 🏗️ 아키텍처 개요 + +``` +┌─────────────────────────────────────────────────────┐ +│ 사용자 입력 │ +│ (반복 유형, 간격, 종료일) │ +└───────────────────┬─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ useEventForm 훅 │ +│ (상태 관리 + 유효성 검증) │ +└───────────────────┬─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ useEventOperations 훅 │ +│ (API 호출 + 이벤트 CRUD) │ +└───────────────────┬─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Server API │ +│ (반복 일정 생성 + 그룹 관리) │ +└───────────────────┬─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 반복 일정 생성 로직 │ +│ (repeatUtils.ts - 날짜 계산) │ +└───────────────────┬─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 캘린더 표시 │ +│ (App.tsx - 반복 아이콘 표시) │ +└─────────────────────────────────────────────────────┘ +``` + +## 🔑 핵심 개념 + +### repeat.id (시리즈 식별자) +- 같은 반복 그룹의 일정들을 식별하는 고유 ID +- 전체 수정/삭제 시 사용 +- 형식: `"repeat-{timestamp}-{random}"` + +### 단일 vs 전체 수정/삭제 +| 작업 | 단일 | 전체 | +|------|------|------| +| **수정** | 반복에서 분리 (`repeat.type = 'none'`) | 그룹 전체 업데이트 | +| **삭제** | 해당 일정만 제거 | 그룹 전체 제거 | +| **아이콘** | 사라짐 | 유지 | +| **API** | `PUT /api/events/:id` | `PUT /api/recurring-events/:repeatId` | + +### 특수 케이스 처리 +1. **매월 31일**: 31일이 있는 달에만 생성 (1,3,5,7,8,10,12월) +2. **윤년 2월 29일**: 윤년에만 생성 +3. **반복 종료일**: 최대 2025-12-31까지 +4. **겹침**: 반복 일정은 겹침 체크 안 함 + +## 📁 파일 구조 + +``` +src/ +├── types.ts # RepeatInfo.id 추가 +├── App.tsx # UI 수정 (아이콘, 다이얼로그) +├── hooks/ +│ ├── useEventForm.ts # 반복 상태 관리 +│ └── useEventOperations.ts # 그룹 수정/삭제 추가 +├── utils/ +│ ├── repeatValidation.ts # 유효성 검증 (신규) +│ ├── repeatUtils.ts # 반복 생성 로직 (신규) +│ └── eventUtils.ts # 반복 관련 유틸 추가 +└── __tests__/ + ├── unit/ + │ ├── easy.repeatValidation.spec.ts (신규) + │ ├── easy.repeatUtils.spec.ts (신규) + │ └── easy.eventUtils.spec.ts (수정) + └── integration/ + ├── repeat-update.spec.tsx (신규) + └── repeat-delete.spec.tsx (신규) + +server.js # 반복 그룹 API 추가 +``` + +## 🔄 작업 순서 (권장) + +### Phase 1: 기본 구조 +1. ✅ 타입 정의 확장 (`RepeatInfo.id` 추가) +2. ✅ 반복 생성 로직 구현 (`repeatUtils.ts`) +3. ✅ 유효성 검증 구현 (`repeatValidation.ts`) +4. ✅ 단위 테스트 작성 + +### Phase 2: UI 구현 +5. ✅ 반복 설정 UI 활성화 (App.tsx) +6. ✅ 반복 아이콘 표시 (eventUtils.ts, App.tsx) +7. ✅ 확인 다이얼로그 추가 (수정/삭제) + +### Phase 3: API 통합 +8. ✅ 서버 API 구현 (반복 생성, 그룹 수정, 그룹 삭제) +9. ✅ 훅 업데이트 (useEventOperations) +10. ✅ 통합 테스트 + +### Phase 4: 테스트 및 최적화 +11. ✅ 특수 케이스 테스트 (31일, 윤년) +12. ✅ 성능 최적화 +13. ✅ 접근성 검증 + +## ⚠️ 주의사항 + +### 필수 구현 사항 +- ✅ 매월 31일 → 31일 있는 달만 (마지막 날 X) +- ✅ 윤년 2월 29일 → 윤년만 +- ✅ 2025-12-31까지 제한 +- ✅ 반복 일정 겹침 무시 + +### 사용자 경험 +- 확인 다이얼로그 문구 명확히 ("해당 일정만 ~") +- 삭제된 개수 피드백 ("N개 삭제됨") +- 로딩 상태 표시 (대량 작업 시) +- 에러 처리 및 사용자 피드백 + +### 성능 고려 +- 최대 365개 일정 생성 (매일, 1년) +- 무한 루프 방지 (MAX_ITERATIONS) +- 서버 측 배치 처리 +- 클라이언트 캐시 최적화 + +## 📝 테스트 체크리스트 + +### 단위 테스트 +- [ ] 반복 날짜 계산 (매일/매주/매월/매년) +- [ ] 매월 31일 특수 케이스 +- [ ] 윤년 2월 29일 특수 케이스 +- [ ] 유효성 검증 (종료일, 간격) +- [ ] 반복 아이콘 표시 로직 + +### 통합 테스트 +- [ ] 반복 일정 생성 플로우 +- [ ] 단일/전체 수정 플로우 +- [ ] 단일/전체 삭제 플로우 +- [ ] 캘린더 표시 (아이콘 포함) + + +## ▶️ 실행 방법 +- 개발 서버 실행: `pnpm start` +- 접속 URL: `http://localhost:5173` + diff --git a/.cursor/templates/ceo-approval.tmpl.md b/.cursor/templates/ceo-approval.tmpl.md new file mode 100644 index 00000000..5be0fe05 --- /dev/null +++ b/.cursor/templates/ceo-approval.tmpl.md @@ -0,0 +1,17 @@ +# ✅ CEO 승인 보고서 + +## 메타데이터 +- 역할: CEO 승인 +- 날짜: {{DATE}} +- Feature: {{FEATURE}} + +--- + +## 승인 내용 +- 결론: 승인(✅) / 보완(⚠️) +- 코멘트: {{SUMMARY}} + +--- + +## 커밋 로그 +- (단계 통과 시 아래에 한 줄씩 추가) diff --git a/.cursor/templates/code-implementation-review.tmpl.md b/.cursor/templates/code-implementation-review.tmpl.md new file mode 100644 index 00000000..24b59c0b --- /dev/null +++ b/.cursor/templates/code-implementation-review.tmpl.md @@ -0,0 +1,21 @@ +# 👨‍💻 구현 결과 보고서 + +## 메타데이터 +- 에이전트: 구현 +- 날짜: {{DATE}} +- Feature: {{FEATURE}} + +--- + +## 변경 요약 +- 요점: {{SUMMARY}} + +--- + +## 실행 로그 +- lint/test/build 상태: {{RUN_STATUS}} + +--- + +## 커밋 로그 +- (단계 통과 시 아래에 한 줄씩 추가) diff --git a/.cursor/templates/feature-verification-review.tmpl.md b/.cursor/templates/feature-verification-review.tmpl.md new file mode 100644 index 00000000..fce55b4b --- /dev/null +++ b/.cursor/templates/feature-verification-review.tmpl.md @@ -0,0 +1,21 @@ +# ✅ 기능 검증 결과 보고서 + +## 메타데이터 +- 에이전트: 기능 검증 +- 날짜: {{DATE}} +- Feature: {{FEATURE}} + +--- + +## 검증 요약 +- 시나리오/결과: {{SUMMARY}} + +--- + +## 실행 로그 +- lint/test/build 상태: {{RUN_STATUS}} + +--- + +## 커밋 로그 +- (단계 통과 시 아래에 한 줄씩 추가) diff --git a/.cursor/templates/test-code-review.tmpl.md b/.cursor/templates/test-code-review.tmpl.md new file mode 100644 index 00000000..5e71135d --- /dev/null +++ b/.cursor/templates/test-code-review.tmpl.md @@ -0,0 +1,23 @@ +# ✅ 테스트코드 결과 보고서 + +## 메타데이터 +- 에이전트: 테스트코드 +- 날짜: {{DATE}} +- Feature: {{FEATURE}} +- 사용 체크리스트: `.cursor/checklist/test-code-agent-checklist.md` + +--- + +## 변경 요약 +- 커버리지/주요 케이스: {{SUMMARY}} + +--- + +## 실행 로그 +- lint/test/build 상태: {{RUN_STATUS}} + +--- + +## 커밋 로그 +- (단계 통과 시 아래에 한 줄씩 추가) + diff --git a/.cursor/templates/test-design-review.tmpl.md b/.cursor/templates/test-design-review.tmpl.md new file mode 100644 index 00000000..d883b196 --- /dev/null +++ b/.cursor/templates/test-design-review.tmpl.md @@ -0,0 +1,26 @@ +# 🧪 테스트설계 결과 보고서 + +## 메타데이터 +- 에이전트: 테스트설계 +- 날짜: {{DATE}} +- 승인자: CEO(Riku) +- 관련 커밋/PR: +- 참고 문서: `.cursor/docs/tdd-document.md`, `.cursorrules` +- 사용 체크리스트: `.cursor/checklist/test-design-agent-checklist.md` + +--- + +## 요약 +- Feature: {{FEATURE}} +- 통과(✅)/주의(⚠️): {{PASS}}/{{WARN}} + +--- + +## 상세 체크 결과 +- 근거 파일: {{EVIDENCE}} + +--- + +## 커밋 로그 +- (단계 통과 시 아래에 한 줄씩 추가) + diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 00000000..5cc29930 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,655 @@ +# 📅 캘린더 애플리케이션 개발 규칙 + +## 🎯 프로젝트 개요 + +React TypeScript 기반의 **캘린더 일정 관리 시스템**입니다. + +### 핵심 아키텍처 원칙 +1. **관심사 분리**: Hooks(로직) / Utils(순수 함수) / Components(UI) +2. **테스트 주도 개발**: 한글 설명의 포괄적인 테스트 커버리지 +3. **타입 안전성**: 엄격한 TypeScript, 명시적 타입 정의 +4. **접근성 우선**: ARIA 레이블 및 시맨틱 HTML +5. **성능 최적화**: React 최적화 패턴 적절히 사용 + +### 주요 명령어 +```bash +pnpm dev # 개발 서버 시작 (서버 + 클라이언트) +pnpm test # 테스트 실행 +pnpm lint # 코드 품질 검사 +pnpm build # 프로덕션 빌드 +``` + +--- + +## 📂 프로젝트 구조 + +### 필수 디렉토리 구조 +``` +src/ +├── App.tsx # 메인 애플리케이션 컴포넌트 +├── types.ts # TypeScript 타입 정의 +├── main.tsx # 진입점 +├── hooks/ # 커스텀 React 훅만 위치 +│ ├── useEventForm.ts +│ ├── useCalendarView.ts +│ ├── useEventOperations.ts +│ ├── useNotifications.ts +│ └── useSearch.ts +├── utils/ # 순수 함수만 위치 +│ ├── dateUtils.ts +│ ├── eventOverlap.ts +│ ├── eventUtils.ts +│ ├── notificationUtils.ts +│ └── timeValidation.ts +├── apis/ # API 관련 함수만 위치 +│ └── fetchHolidays.ts +├── __tests__/ # 모든 테스트 파일 +│ ├── hooks/ # 훅 테스트 +│ ├── unit/ # 단위 테스트 +│ └── medium.integration.spec.tsx +└── __mocks__/ # 목 데이터 및 핸들러 +``` + +### 절대 금지 사항 +- ❌ 비즈니스 로직을 컴포넌트에 직접 작성 +- ❌ 유틸리티 함수와 React 훅 혼합 +- ❌ 부모 디렉토리에서 두 번 이상 import (`../../` 금지) +- ❌ 정해진 디렉토리 구조 외부에 파일 생성 + +--- + +## 🔒 TypeScript 규칙 + +### 필수 사항 +- **반드시** 모든 데이터 구조에 인터페이스 정의 +- **반드시** 제한된 값 집합에는 유니온 타입 사용 +- **반드시** 복잡한 함수에 JSDoc 주석 추가 +- **반드시** 함수의 명시적 반환 타입 지정 +- **반드시** 함수 파라미터 타입 지정 +- **반드시** 불변 배열에 `as const` 사용 + +### 절대 금지 +- ❌ `any` 타입 사용 (필요시 `unknown` 사용) +- ❌ TypeScript 에러 무시 +- ❌ 설명 없이 `@ts-ignore` 사용 +- ❌ 과도하게 복잡한 제네릭 타입 생성 + +### 올바른 타입 정의 예시 +```typescript +/** + * 캘린더 이벤트를 나타내는 인터페이스 + */ +export interface Event { + id: string; + title: string; + date: string; // YYYY-MM-DD 형식 + startTime: string; // HH:MM 형식 + endTime: string; // HH:MM 형식 + description: string; + location: string; + category: string; + repeat: RepeatInfo; + notificationTime: number; +} + +export type RepeatType = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly'; + +type TimeErrorRecord = Record<'startTimeError' | 'endTimeError', string | null>; +``` + +--- + +## ⚛️ React & Hooks 규칙 + +### 커스텀 훅 구조 (필수 패턴) +```typescript +export const useCustomHook = (initialValue?: Type) => { + // 1. 상태 선언 + const [state, setState] = useState(initialValue || defaultValue); + + // 2. 파생 상태 + const [errorState, setErrorState] = useState({}); + + // 3. 이벤트 핸들러 + const handleEvent = (e: ChangeEvent) => { + // 핸들러 로직 + }; + + // 4. 유틸리티 함수 + const resetFunction = () => { + // 리셋 로직 + }; + + // 5. 객체로 반환 (배열 금지) + return { + state, + setState, + errorState, + handleEvent, + resetFunction, + }; +}; +``` + +### 훅 규칙 (절대 위반 금지) +- **반드시** 커스텀 훅은 객체를 반환 (배열 반환 금지) +- **반드시** 커스텀 훅 이름은 `use`로 시작 +- **반드시** 로딩 및 에러 상태 처리 +- **절대** 조건부로 훅 호출 금지 +- **절대** 상태 직접 변경 금지 + +### 상태 업데이트 패턴 +```typescript +// ✅ 올바른 방법 +const updateState = (newValue: string) => { + setState(prev => ({ ...prev, property: newValue })); +}; + +// ❌ 잘못된 방법 +const updateState = (newValue: string) => { + state.property = newValue; // 직접 변경 금지! +}; +``` + +### 컴포넌트 구조 패턴 +```typescript +function ComponentName() { + // 1. 커스텀 훅 먼저 + const { data, setData } = useCustomHook(); + + // 2. 로컬 상태 + const [localState, setLocalState] = useState(); + + // 3. 이벤트 핸들러 + const handleEvent = useCallback(() => { + // 로직 + }, [dependencies]); + + // 4. 렌더 함수 + const renderSection = () => { + return
{/* JSX */}
; + }; + + // 5. 메인 JSX 반환 + return
{/* 컴포넌트 */}
; +} +``` + +### Import 순서 (절대 준수) +```typescript +// 1. 외부 라이브러리 (Material-UI, React 등) +import { Box, Button } from '@mui/material'; +import { useState, useCallback } from 'react'; + +// 2. 내부 모듈 (hooks, utils, types) +import { useEventForm } from './hooks/useEventForm.ts'; +import { Event } from './types'; +``` + +--- + +## 🎨 Material-UI & 스타일링 + +### 컴포넌트 사용 (필수) +```typescript +// ✅ 올바른 Material-UI 사용 + + + 제목 + setValue(e.target.value)} + /> + + +``` + +### 접근성 규칙 (절대 생략 금지) +- **반드시** 폼 입력에 `id` 속성 제공 +- **반드시** 아이콘 버튼에 `aria-label` 사용 +- **반드시** 시맨틱 HTML 요소 사용 +- **반드시** 적절한 폼 레이블 제공 + +### 폼 컴포넌트 패턴 +```typescript + + 필드 레이블 + setValue(e.target.value)} + error={!!error} + helperText={error} + /> + +``` + +### 아이콘 버튼 패턴 +```typescript + + + +``` + +### 절대 금지 +- ❌ `sx` prop 대신 인라인 스타일 사용 +- ❌ 접근성 속성 누락 +- ❌ 하드코딩된 색상 (테마 색상 사용) +- ❌ Material-UI에 있는데 커스텀 컴포넌트 생성 + +--- + +## 📅 캘린더 도메인 규칙 + +### 날짜 처리 (필수) +- **반드시** 날짜는 ISO 형식 사용 (YYYY-MM-DD) +- **반드시** 시간은 24시간 형식 사용 (HH:MM) +- **반드시** 테스트에서 타임존은 UTC로 처리 +- **반드시** 날짜 범위 및 윤년 검증 + +### 이벤트 검증 규칙 +- **반드시** 시작 시간이 종료 시간보다 앞인지 검증 +- **반드시** 이벤트 겹침 확인 +- **반드시** 필수 필드 검증 (title, date, startTime, endTime) +- **절대** 유효하지 않은 날짜/시간 조합 허용 금지 + +### 이벤트 겹침 감지 +```typescript +const findOverlappingEvents = (newEvent: EventForm, existingEvents: Event[]): Event[] => { + return existingEvents.filter(event => { + // 같은 날짜인지 확인 + if (event.date !== newEvent.date) return false; + + // 시간 겹침 로직 + const newStart = newEvent.startTime; + const newEnd = newEvent.endTime; + const existingStart = event.startTime; + const existingEnd = event.endTime; + + return (newStart < existingEnd && newEnd > existingStart); + }); +}; +``` + +### 주(Week) 계산 규칙 +- **반드시** 주는 일요일부터 시작 (한국 캘린더 표준) +- **반드시** 월 경계를 올바르게 처리 +- **반드시** 2월의 윤년 고려 +- **반드시** ISO 주에는 목요일 기반 주 번호 사용 + +### 절대 금지 +- ❌ 계산에 로컬 타임존 사용 +- ❌ 윤년 엣지 케이스 무시 +- ❌ 여러 날에 걸친 이벤트 허용 +- ❌ 겹침 검증 생략 + +--- + +## 🌐 API & 데이터 관리 + +### API 함수 구조 (필수) +```typescript +export const fetchData = async (): Promise => { + try { + const response = await fetch('/api/endpoint'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('Failed to fetch data:', error); + throw error; + } +}; +``` + +### 에러 처리 규칙 +- **반드시** HTTP 에러를 적절히 처리 +- **반드시** 에러 발생 시 사용자 피드백 제공 +- **반드시** 디버깅을 위한 에러 로깅 +- **절대** API 에러를 조용히 무시 금지 + +### 훅 통합 패턴 +```typescript +export const useDataOperations = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const saveData = async (newData: DataType) => { + setLoading(true); + setError(null); + try { + const saved = await apiCall(newData); + setData(prev => [...prev, saved]); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + return { data, loading, error, saveData }; +}; +``` + +### 절대 금지 +- ❌ API 응답에 `any` 타입 사용 +- ❌ 로딩 상태 무시 +- ❌ 에러 처리 없이 API 호출 +- ❌ API 엔드포인트 하드코딩 + +--- + +## ✅ 코드 품질 & 모범 사례 + +### 코드 조직 (필수) +- **반드시** 의미 있는 변수 및 함수 이름 사용 +- **반드시** 한 가지 일만 잘하는 함수 작성 +- **반드시** 상속보다 조합 선호 +- **반드시** 불변 값에 `const` 사용 + +### 성능 규칙 +- **반드시** 자식 컴포넌트에 전달하는 이벤트 핸들러에 `useCallback` 사용 +- **반드시** 비용이 큰 계산에 `useMemo` 사용 +- **반드시** 렌더 중 객체/함수 생성 피하기 +- **절대** 불필요한 리렌더 발생시키지 않기 + +### 상수 패턴 +```typescript +// ✅ 올바른 방법 - 명명된 상수 +const CATEGORIES = ['업무', '개인', '가족', '기타'] as const; +const WEEK_DAYS = ['일', '월', '화', '수', '목', '금', '토'] as const; +const NOTIFICATION_OPTIONS = [ + { value: 1, label: '1분 전' }, + { value: 10, label: '10분 전' }, + { value: 60, label: '1시간 전' }, + { value: 120, label: '2시간 전' }, + { value: 1440, label: '1일 전' }, +] as const; + +// ❌ 잘못된 방법 - 매직 넘버 +if (value === 1) { /* 1이 무엇을 의미하는가? */ } +``` + +### 함수 문서화 +```typescript +/** + * 주어진 년도와 월의 일수를 계산합니다 + * @param year - 년도 (예: 2025) + * @param month - 월 (1-12) + * @returns 해당 월의 일수 + */ +export function getDaysInMonth(year: number, month: number): number { + return new Date(year, month, 0).getDate(); +} +``` + +### 절대 금지 (코드 스멜) +- ❌ 50줄 이상의 함수 작성 +- ❌ 매직 넘버 사용 (명명된 상수 사용) +- ❌ 코드 중복 (유틸리티로 추출) +- ❌ ESLint 경고 무시 + +--- + +## 🧪 테스팅 표준 + +### 테스트 구조 (필수) +```typescript +describe('함수명', () => { + it('유효한 입력이 주어지면 예상된 결과를 반환해야 함', () => { + // Arrange (준비) + const input = 'test-input'; + + // Act (실행) + const result = functionToTest(input); + + // Assert (검증) + expect(result).toBe('expected-output'); + }); + + it('엣지 케이스를 올바르게 처리해야 함', () => { + // 엣지 케이스 테스트 + }); +}); +``` + +### 테스트 작성 규칙 +- **반드시** 비즈니스 로직 설명은 한글로 작성 +- **반드시** 엣지 케이스 테스트 (윤년, 월 경계 등) +- **반드시** 시나리오를 설명하는 서술적인 테스트 이름 +- **반드시** AAA 패턴 (Arrange, Act, Assert) 따르기 + +### 테스트 데이터 패턴 +```typescript +const mockEvents: Event[] = [ + { + id: '1', + title: '테스트 이벤트', + date: '2025-07-01', + startTime: '09:00', + endTime: '10:00', + description: '', + location: '', + category: '업무', + repeat: { type: 'none', interval: 1 }, + notificationTime: 10, + }, +]; +``` + +### 훅 테스트 패턴 +```typescript +describe('useCustomHook', () => { + it('올바른 기본값으로 초기화되어야 함', () => { + const { result } = renderHook(() => useCustomHook()); + + expect(result.current.state).toBe('default-value'); + }); + + it('액션 호출 시 상태가 업데이트되어야 함', () => { + const { result } = renderHook(() => useCustomHook()); + + act(() => { + result.current.updateState('new-value'); + }); + + expect(result.current.state).toBe('new-value'); + }); +}); +``` + +### 절대 금지 +- ❌ 단언(assertion) 없는 테스트 작성 +- ❌ 구현 세부사항 테스트 +- ❌ 에러 조건 테스트 생략 +- ❌ 테스트 데이터에 `any` 타입 사용 + +--- + +## 🛠️ 개발 워크플로우 + +### 개발 명령어 (필수 사용) +```bash +pnpm dev # 개발 서버 시작 (서버 + 클라이언트) +pnpm test # 테스트 실행 +pnpm test:coverage # 테스트 커버리지 확인 +pnpm lint # 코드 품질 검사 +pnpm build # 프로덕션 빌드 검증 +``` + +### Git 워크플로우 규칙 +- **반드시** 논리적이고 원자적인 단위로 커밋 +- **반드시** 서술적인 커밋 메시지 작성 +- **반드시** 커밋 전 테스트 실행 +- **절대** 깨진 코드 커밋 금지 + +### 커밋 전 체크리스트 +```bash +# 모든 커밋 전에 실행 +pnpm lint # 코드 품질 확인 +pnpm test # 모든 테스트 실행 +pnpm build # 빌드 작동 확인 +``` + +### 코드 리뷰 체크리스트 +- [ ] 모든 테스트 통과 +- [ ] TypeScript 에러 없음 +- [ ] ESLint 경고 해결 +- [ ] 접근성 속성 존재 +- [ ] 에러 처리 구현 +- [ ] 성능 고려사항 처리 + +### 절대 금지 +- ❌ 린팅 없이 커밋 +- ❌ 새 기능 추가 시 테스트 생략 +- ❌ 대용량 파일이나 빌드 결과물 커밋 +- ❌ TypeScript 에러 무시 + +--- + +## 📋 반복 일정 기능 특별 규칙 + +### 반복 일정 생성 +- **반드시** 반복 종료일은 2025-12-31까지로 제한 +- **반드시** 매월 31일 선택 시 31일이 있는 달에만 생성 (마지막 날 X) +- **반드시** 윤년 2월 29일 선택 시 윤년에만 생성 +- **반드시** `repeatGroupId`로 반복 그룹 관리 + +### 반복 일정 수정/삭제 +- **반드시** 사용자에게 "단일" vs "전체" 선택 옵션 제공 +- **반드시** 단일 수정 시 반복에서 분리 (`repeat.type = 'none'`) +- **반드시** 전체 수정 시 `repeatGroupId`로 그룹 전체 업데이트 +- **반드시** 삭제 시 안전 확인 다이얼로그 표시 + +### 반복 일정 표시 +- **반드시** 반복 아이콘으로 시각적 구분 +- **반드시** Material-UI `Repeat` 아이콘 사용 +- **반드시** 아이콘에 `aria-label="반복 일정"` 추가 +- **반드시** 우선순위: 알림 아이콘 > 반복 아이콘 + +--- + +## 🚫 절대 금지 사항 요약 + +### 타입 & 코드 +- ❌ `any` 타입 사용 +- ❌ TypeScript 에러 무시 +- ❌ 매직 넘버 사용 +- ❌ 50줄 이상 함수 +- ❌ 코드 중복 + +### React & 훅 +- ❌ 훅을 조건부로 호출 +- ❌ 상태 직접 변경 +- ❌ useEffect에서 의존성 배열 생략 +- ❌ 커스텀 훅이 배열 반환 +- ❌ 컴포넌트에 비즈니스 로직 + +### 스타일 & UI +- ❌ 접근성 속성 누락 +- ❌ 인라인 스타일 사용 +- ❌ 하드코딩된 색상 +- ❌ 시맨틱 HTML 미사용 + +### API & 데이터 +- ❌ 에러 처리 없이 API 호출 +- ❌ 로딩 상태 무시 +- ❌ API 에러 조용히 무시 +- ❌ 엔드포인트 하드코딩 + +### 테스팅 +- ❌ 단언 없는 테스트 +- ❌ 에러 조건 테스트 생략 +- ❌ 엣지 케이스 무시 +- ❌ 구현 세부사항 테스트 + +### 워크플로우 +- ❌ 린팅 없이 커밋 +- ❌ 테스트 없이 커밋 +- ❌ 깨진 코드 커밋 +- ❌ ESLint 경고 무시 + +--- + +## ✅ 항상 해야 할 사항 요약 + +### 필수 패턴 +- ✅ 명시적 반환 타입 지정 +- ✅ 로딩/에러 상태 처리 +- ✅ 엣지 케이스 테스트 +- ✅ 의미 있는 변수명 사용 +- ✅ 접근성 속성 제공 +- ✅ JSDoc 주석 작성 +- ✅ AAA 패턴 테스트 +- ✅ 불변성 유지 +- ✅ 순수 함수 작성 +- ✅ 관심사 분리 + +### 커밋 전 필수 +- ✅ `pnpm lint` 실행 +- ✅ `pnpm test` 실행 +- ✅ `pnpm build` 실행 +- ✅ TypeScript 에러 확인 +- ✅ 접근성 검증 + +--- + +## 📚 참고 파일 위치 + +### 핵심 파일 +- 타입 정의: `src/types.ts` +- 메인 컴포넌트: `src/App.tsx` +- 진입점: `src/main.tsx` + +### 유틸리티 +- 날짜 계산: `src/utils/dateUtils.ts` +- 이벤트 겹침: `src/utils/eventOverlap.ts` +- 이벤트 유틸: `src/utils/eventUtils.ts` +- 시간 검증: `src/utils/timeValidation.ts` +- 알림 유틸: `src/utils/notificationUtils.ts` + +### 훅 +- 이벤트 폼: `src/hooks/useEventForm.ts` +- 캘린더 뷰: `src/hooks/useCalendarView.ts` +- 이벤트 작업: `src/hooks/useEventOperations.ts` +- 알림: `src/hooks/useNotifications.ts` +- 검색: `src/hooks/useSearch.ts` + +### 설정 +- TypeScript: `tsconfig.json` +- ESLint: `eslint.config.js` +- Vite: `vite.config.ts` +- 패키지: `package.json` + +### 테스트 +- 테스트 설정: `src/setupTests.ts` +- 목 핸들러: `src/__mocks__/handlers.ts` +- 통합 테스트: `src/__tests__/medium.integration.spec.tsx` + +--- + +## 💡 마지막 체크리스트 + +코드를 작성할 때마다 다음을 확인하세요: + +1. [ ] 타입이 명시적으로 정의되어 있는가? +2. [ ] 에러 처리가 구현되어 있는가? +3. [ ] 접근성 속성이 있는가? +4. [ ] 테스트가 작성되어 있는가? +5. [ ] 함수가 50줄 이하인가? +6. [ ] 코드가 중복되지 않았는가? +7. [ ] 변수명이 의미 있는가? +8. [ ] 불변성이 유지되는가? +9. [ ] 성능이 고려되었는가? +10. [ ] 린팅 경고가 없는가? + +**이 규칙들을 준수하면 고품질의 유지보수 가능한 코드를 작성할 수 있습니다!** 🎉 + diff --git a/scripts/commit-stage.sh b/scripts/commit-stage.sh new file mode 100755 index 00000000..cf3d3ec1 --- /dev/null +++ b/scripts/commit-stage.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +FEATURE="$1" # 예: repeat-event +STAGE="$2" # Architect | QA-RED | Dev-GREEN | Dev-REFACTOR | QA-VERIFY | CEO +ROLE="$3" # Architect | QA | Dev | CEO +DETAIL="${4:-}" # 선택 상세 +RUNLINE="${5:-}" # 선택 실행 로그 (예: "pnpm test, pnpm lint, pnpm build") + +case "$STAGE" in + Architect) TYPE="docs"; PREFIX="기능 명세"; OUT_FILE=".cursor/outputs/test-design-review.md" ;; + QA-RED) TYPE="test"; PREFIX="테스트 설계"; OUT_FILE=".cursor/outputs/test-design-review.md" ;; + Dev-GREEN) TYPE="feat"; PREFIX="기능 구현"; OUT_FILE=".cursor/outputs/code-implementation-review.md" ;; + Dev-REFACTOR) TYPE="refactor";PREFIX="코드 리팩토링"; OUT_FILE=".cursor/outputs/code-implementation-review.md" ;; + QA-VERIFY) TYPE="test"; PREFIX="테스트 통과"; OUT_FILE=".cursor/outputs/feature-verification-review.md" ;; + CEO) TYPE="chore"; PREFIX="승인 완료"; OUT_FILE=".cursor/outputs/ceo-approval.md" ;; + *) echo "[commit-stage] unknown STAGE: $STAGE"; exit 1 ;; + esac + +MSG="$TYPE: $PREFIX [by $ROLE] ($FEATURE)" +if [ -n "$DETAIL" ]; then + MSG="$TYPE: $PREFIX - $DETAIL [by $ROLE] ($FEATURE)" +fi + +git add . +git commit -m "$MSG" || true +HASH=$(git rev-parse --short HEAD) +TS=$(date "+%F %T") + +if [ -n "$RUNLINE" ]; then + echo "commit: $HASH | $MSG | $TS | ran: $RUNLINE" >> "$OUT_FILE" +else + echo "commit: $HASH | $MSG | $TS" >> "$OUT_FILE" +fi + +echo "[commit-stage] committed: $HASH -> $MSG (logged to $OUT_FILE)" diff --git a/scripts/new-cycle.sh b/scripts/new-cycle.sh new file mode 100755 index 00000000..58151346 --- /dev/null +++ b/scripts/new-cycle.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +FEATURE="$1" # 예: repeat-event +DATE="${2:-$(date +%F)}" # 선택: 날짜 오버라이드 + +mkdir -p .cursor/outputs + +copy_tmpl() { + local TMPL="$1" + local OUT="$2" + sed -e "s/{{FEATURE}}/${FEATURE}/g" \ + -e "s/{{DATE}}/${DATE}/g" \ + -e "s/{{PASS}}/0/g" \ + -e "s/{{WARN}}/0/g" \ + -e "s/{{EVIDENCE}}/TBD/g" \ + -e "s/{{SUMMARY}}/TBD/g" \ + -e "s/{{RUN_STATUS}}/TBD/g" \ + ".cursor/templates/${TMPL}" > "${OUT}" +} + +copy_tmpl "test-design-review.tmpl.md" ".cursor/outputs/test-design-review.md" +copy_tmpl "test-code-review.tmpl.md" ".cursor/outputs/test-code-review.md" +copy_tmpl "code-implementation-review.tmpl.md" ".cursor/outputs/code-implementation-review.md" +copy_tmpl "feature-verification-review.tmpl.md" ".cursor/outputs/feature-verification-review.md" +copy_tmpl "ceo-approval.tmpl.md" ".cursor/outputs/ceo-approval.md" + +echo "[new-cycle] initialized documents for feature: ${FEATURE} on ${DATE}" diff --git a/src/App.tsx b/src/App.tsx index 195c5b05..01209610 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,12 @@ -import { Notifications, ChevronLeft, ChevronRight, Delete, Edit, Close } from '@mui/icons-material'; +import { + Notifications, + ChevronLeft, + ChevronRight, + Delete, + Edit, + Close, + Repeat, +} from '@mui/icons-material'; import { Alert, AlertTitle, @@ -28,15 +36,14 @@ import { Typography, } from '@mui/material'; import { useSnackbar } from 'notistack'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useCalendarView } from './hooks/useCalendarView.ts'; import { useEventForm } from './hooks/useEventForm.ts'; import { useEventOperations } from './hooks/useEventOperations.ts'; import { useNotifications } from './hooks/useNotifications.ts'; import { useSearch } from './hooks/useSearch.ts'; -// import { Event, EventForm, RepeatType } from './types'; -import { Event, EventForm } from './types'; +import { Event, EventForm, RepeatType } from './types'; import { formatDate, formatMonth, @@ -60,6 +67,14 @@ const notificationOptions = [ { value: 1440, label: '1일 전' }, ]; +const RepeatIconIfNeeded = ({ + repeatType, + size = 'inherit', +}: { + repeatType: RepeatType; + size?: 'small' | 'inherit'; +}) => (repeatType !== 'none' ? : null); + function App() { const { title, @@ -77,11 +92,11 @@ function App() { isRepeating, setIsRepeating, repeatType, - // setRepeatType, + setRepeatType, repeatInterval, - // setRepeatInterval, + setRepeatInterval, repeatEndDate, - // setRepeatEndDate, + setRepeatEndDate, notificationTime, setNotificationTime, startTimeError, @@ -104,6 +119,41 @@ function App() { const [isOverlapDialogOpen, setIsOverlapDialogOpen] = useState(false); const [overlappingEvents, setOverlappingEvents] = useState([]); + const [isRepeatEditDialogOpen, setIsRepeatEditDialogOpen] = useState(false); + const [pendingEventData, setPendingEventData] = useState(null); + const [isRepeatDeleteDialogOpen, setIsRepeatDeleteDialogOpen] = useState(false); + const [pendingDeleteEvent, setPendingDeleteEvent] = useState(null); + + const requestDelete = useCallback( + (event: Event) => { + if (event.repeat.type !== 'none') { + setPendingDeleteEvent(event); + setIsRepeatDeleteDialogOpen(true); + } else { + void deleteEvent(event.id); + } + }, + [deleteEvent] + ); + + const handleConfirmSingleDelete = useCallback(async () => { + setIsRepeatDeleteDialogOpen(false); + if (pendingDeleteEvent) { + await deleteEvent(pendingDeleteEvent.id, { scope: 'single' }); + setPendingDeleteEvent(null); + } + }, [pendingDeleteEvent, deleteEvent]); + + const handleConfirmAllDelete = useCallback(async () => { + setIsRepeatDeleteDialogOpen(false); + if (pendingDeleteEvent) { + await deleteEvent(pendingDeleteEvent.id, { + scope: 'all', + repeatId: pendingDeleteEvent.repeat.id, + }); + setPendingDeleteEvent(null); + } + }, [pendingDeleteEvent, deleteEvent]); const { enqueueSnackbar } = useSnackbar(); @@ -135,6 +185,13 @@ function App() { notificationTime, }; + // 편집 중인 반복 일정 저장 시: 단일/전체 선택 다이얼로그 노출 + if (editingEvent && editingEvent.repeat.type !== 'none') { + setPendingEventData(eventData); + setIsRepeatEditDialogOpen(true); + return; + } + const overlapping = findOverlappingEvents(eventData, events); if (overlapping.length > 0) { setOverlappingEvents(overlapping); @@ -145,6 +202,22 @@ function App() { } }; + const handleConfirmSingleEdit = useCallback(async () => { + setIsRepeatEditDialogOpen(false); + if (pendingEventData) { + await saveEvent(pendingEventData as Event, { scope: 'single' }); + setPendingEventData(null); + } + }, [pendingEventData, saveEvent]); + + const handleConfirmAllEdit = useCallback(async () => { + setIsRepeatEditDialogOpen(false); + if (pendingEventData) { + await saveEvent(pendingEventData as Event, { scope: 'all' }); + setPendingEventData(null); + } + }, [pendingEventData, saveEvent]); + const renderWeekView = () => { const weekDates = getWeekDates(currentDate); return ( @@ -201,6 +274,7 @@ function App() { > {isNotified && } + {isNotified && } + - {/* ! 반복은 8주차 과제에 포함됩니다. 구현하고 싶어도 참아주세요~ */} - {/* {isRepeating && ( + {isRepeating && ( - 반복 유형 + + 반복 유형 +