ν°μΌ κΈ°λ° μΉΈλ° λ³΄λ ν μΌ κ΄λ¦¬ μ ν리μΌμ΄μ (MVP) https://ticket-kanban.vercel.app
ν΄λ‘λ μ½λ λ§μ€ν° μ± μμ λ‘ 6μ₯μ ν΄λΉ νλ μμ μ¬μ΄νΈ μ΄λ€.
λ¨μΌ μ¬μ©μ νκ²½μμ ν°μΌμ μμ±νκ³ , 4κ° μΉΌλΌ(Backlog, TODO, In Progress, Done) κ° λλκ·Έμ€λλ‘μΌλ‘ μνλ₯Ό κ΄λ¦¬νλ μΉΈλ° λ³΄λ μ±μ΄λ€.
| ID | κΈ°λ₯ | μ€λͺ |
|---|---|---|
| FR-001 | ν°μΌ μμ± | μ λͺ©, μ€λͺ , μ°μ μμ, μμμμ μΌ, μ’ λ£μμ μΌ μ λ ₯. Backlog 맨 μμ λ°°μΉ |
| FR-002 | 보λ μ‘°ν | 4κ° μΉΌλΌλ³ κ·Έλ£Ήν, position μ€λ¦μ°¨μ μ λ ¬ |
| FR-003 | ν°μΌ μμΈ | μΉ΄λ ν΄λ¦ μ λͺ¨λ¬λ‘ μ 체 μ 보 νμ |
| FR-004 | ν°μΌ μμ | μ λͺ©, μ€λͺ , μ°μ μμ, μμμμ μΌ, μ’ λ£μμ μΌ μμ |
| FR-005 | ν°μΌ μλ£ | Done μ΄λ μ completedAt μλ μ€μ , 24μκ° κ²½κ³Ό μ 보λμμ μ μΈ |
| FR-006 | ν°μΌ μμ | νμΈ λ€μ΄μΌλ‘κ·Έ ν μꡬ μμ |
| FR-007 | λλκ·Έμ€λλ‘ | μΉΌλΌ κ°/λ΄ μ΄λ, startedAt/completedAt μλ κ΄λ¦¬ |
| FR-008 | μ€λ²λ νμ | μ’ λ£μμ μΌ μ΄κ³Ό + λ―Έμλ£ ν°μΌμ μκ°μ κ²½κ³ |
| κΈ°μ | λ²μ | μ©λ |
|---|---|---|
| Next.js | 15.x | νμ€ν νλ μμν¬ (App Router) |
| React | 19.x | UI λ λλ§ |
| TypeScript | 5.x | νμ μμ μ± (strict mode) |
| Tailwind CSS | 4.x | μ νΈλ¦¬ν° κΈ°λ° μ€νμΌλ§ |
| @dnd-kit | core 6.x / sortable 8.x | λλκ·Έμ€λλ‘ |
| κΈ°μ | λ²μ | μ©λ |
|---|---|---|
| Drizzle ORM | 0.38.x | DB 쿼리, μ€ν€λ§ κ΄λ¦¬ |
| @vercel/postgres | latest | Vercel Postgres μ°κ²° λλΌμ΄λ² |
| Zod | 3.x | μμ²/νΌ λ°μ΄ν° κ²μ¦ (νλ‘ νΈ/λ°±μλ 곡μ ) |
| κΈ°μ | μ©λ |
|---|---|
| Jest + React Testing Library | TDD ν μ€νΈ |
| ESLint | λ¦°νΈ |
| Prettier | μ½λ ν¬λ§·ν |
| Drizzle Kit | DB λ§μ΄κ·Έλ μ΄μ |
| μλν¬μΈνΈ | ν μ€νΈ | μν |
|---|---|---|
| POST /api/tickets | 11 passed | β |
| GET /api/tickets | 8 passed | β |
| GET /api/tickets/:id | 3 passed | β |
| PATCH /api/tickets/:id | 8 passed | β |
| PATCH /api/tickets/:id/complete | 5 passed | β |
| DELETE /api/tickets/:id | 2 passed | β |
| PATCH /api/tickets/reorder | 10 passed | β |
| Phase | μ»΄ν¬λνΈ | ν μ€νΈ | μν |
|---|---|---|---|
| 1 | Button, Badge, Modal, ConfirmDialog | 22 passed | β |
| 2 | TicketCard, ColumnHeader, Column, Board | 25 passed | β |
| 3 | TicketDetailView, TicketForm, TicketModal | 17 passed | β |
| 4 | ticketApi, useTickets | 21 passed | β |
| 5 | BoardHeader, FilterBar, BoardContainer, page.tsx | 16 passed | β |
μ 체: 26 test suites, 169 tests passed
tika/
βββ app/ # Next.js App Router
β βββ api/ # λ°±μλ: API Route Handlers
β β βββ tickets/
β β βββ route.ts # GET /api/tickets, POST /api/tickets
β β βββ [id]/
β β β βββ route.ts # GET, PATCH, DELETE /api/tickets/:id
β β β βββ complete/
β β β βββ route.ts # PATCH /api/tickets/:id/complete
β β βββ reorder/
β β βββ route.ts # PATCH /api/tickets/reorder
β β
β βββ page.tsx # λ©μΈ νμ΄μ§ (μλ² μ»΄ν¬λνΈ)
β βββ layout.tsx # λ£¨νΈ λ μ΄μμ
β βββ globals.css # λμμΈ μμ€ν
+ CSS λ³μ
β
βββ src/
β βββ server/ # λ°±μλ λ‘μ§ (μλ²μμλ§ μ€ν)
β β βββ services/
β β β βββ ticketService.ts # λΉμ¦λμ€ λ‘μ§
β β β βββ index.ts # μλΉμ€ export
β β βββ db/
β β βββ index.ts # Drizzle ν΄λΌμ΄μΈνΈ μ΄κΈ°ν
β β βββ schema.ts # DB μ€ν€λ§ μ μ
β β βββ seed.ts # μλ λ°μ΄ν°
β β
β βββ client/ # νλ°νΈμλ λ‘μ§ (λΈλΌμ°μ μμ μ€ν)
β β βββ components/
β β β βββ board/ # BoardContainer, Board, Column, ColumnHeader
β β β β βββ BoardContainer.tsx # λ©μΈ 컨ν
μ΄λ (νν°, λͺ¨λ¬, CRUD)
β β β β βββ BoardHeader.tsx # μλ¨ ν€λ (νμ΄ν, μ μ
무 λ²νΌ)
β β β β βββ FilterBar.tsx # νν° λ° (μ΄λ²μ£Ό μ
무, μΌμ μ΄κ³Ό)
β β β β βββ Board.tsx # 4μΉΌλΌ λ³΄λ λ μ΄μμ
β β β β βββ Column.tsx # λ¨μΌ μΉΌλΌ (DnD λλ‘ μμ)
β β β β βββ ColumnHeader.tsx # μΉΌλΌ ν€λ (μ΄λ¦ + μΉ΄λ μ)
β β β βββ ticket/ # TicketCard, TicketModal, TicketForm
β β β β βββ TicketCard.tsx # κ°λ³ μΉ΄λ (DnD λλκ·Έ μμ€)
β β β β βββ TicketModal.tsx # μμΈ/μμ λͺ¨λ¬
β β β β βββ TicketForm.tsx # μμ±/μμ νΌ
β β β β βββ TicketDetailView.tsx # μ½κΈ° μ μ© νλ
β β β βββ ui/ # κ³΅ν΅ UI μ»΄ν¬λνΈ
β β β βββ Button.tsx # variant, size, isLoading
β β β βββ Modal.tsx # ESC λ«κΈ°, λ°κΉ₯ ν΄λ¦, μ€ν¬λ‘€ μ κΈ
β β β βββ Badge.tsx # PriorityBadge, DueDateBadge
β β β βββ ConfirmDialog.tsx # μμ νμΈ λ€μ΄μΌλ‘κ·Έ
β β βββ hooks/
β β β βββ useTickets.ts # ν°μΌ CRUD + 보λ μν κ΄λ¦¬
β β βββ api/
β β βββ ticketApi.ts # API νΈμΆ ν¨μ (fetch λνΌ)
β β
β βββ shared/ # νλ‘ νΈ/λ°±μλ 곡μ μ½λ
β βββ types/
β β βββ index.ts # Ticket, BoardData, API νμ
β βββ validations/
β β βββ ticket.ts # Zod μ€ν€λ§ (νΌ + API κ²μ¦)
β βββ errors/
β βββ index.ts # TicketNotFoundError λ±
β
βββ __tests__/ # ν
μ€νΈ μ½λ (169 tests)
β βββ api/ # API Route + ticketApi ν
μ€νΈ
β βββ services/ # μλΉμ€ ν΅ν© ν
μ€νΈ (@jest-environment node)
β βββ components/ # μ»΄ν¬λνΈ ν
μ€νΈ (jsdom)
β βββ hooks/ # Hook ν
μ€νΈ
β
βββ docs/ # νλ‘μ νΈ λͺ
μΈ λ¬Έμ
βββ drizzle/ # Drizzle λ§μ΄κ·Έλ μ΄μ
βββ CLAUDE.md # Claude Code νλ‘μ νΈ μ€μ
βββ CHANGELOG.md # κ°λ° νμ€ν 리
βββ jest.config.ts # Jest μ€μ (jsdom + runInBand)
βββ package.json
| κ·μΉ | μ€λͺ |
|---|---|
src/server/ β src/client/ import κΈμ§ |
λ°±μλμμ νλ°νΈμλ μ½λ μ°Έμ‘° λΆκ° |
src/client/ β src/server/ import κΈμ§ |
νλ°νΈμλμμ λ°±μλ μ½λ μ§μ μ°Έμ‘° λΆκ° |
src/shared/λ§ μμͺ½μμ μ°Έμ‘° κ°λ₯ |
νμ , Zod μ€ν€λ§, μμλ§ κ³΅μ |
app/page.tsxλ§ src/server/ μ§μ μ°Έμ‘° |
μλ² μ»΄ν¬λνΈμμ initialData μ λ¬ |
| # | λ©μλ | κ²½λ‘ | μ€λͺ |
|---|---|---|---|
| 1 | POST | /api/tickets | ν°μΌ μμ± |
| 2 | GET | /api/tickets | μ 체 보λ μ‘°ν |
| 3 | GET | /api/tickets/:id | ν°μΌ μμΈ μ‘°ν |
| 4 | PATCH | /api/tickets/:id | ν°μΌ μμ |
| 5 | PATCH | /api/tickets/:id/complete | ν°μΌ μλ£ |
| 6 | DELETE | /api/tickets/:id | ν°μΌ μμ |
| 7 | PATCH | /api/tickets/reorder | μν/μμ λ³κ²½ (DnD) |
DoneμΌλ‘μ μ΄λμ
/complete, κ·Έ μΈ λͺ¨λ μ΄λμ/reorderλ₯Ό μ¬μ©νλ€.
npm installcp .env.example .env.local.env.local νμΌμ DATABASE_URL κ°μ μ€μ νλ€.
λ‘컬 PostgreSQL μ¬μ© μ:
DATABASE_URL=postgresql://username:password@localhost:5432/tika
Vercel Postgres μ¬μ© μ:
vercel env pull .env.localnpm run db:generate # λ§μ΄κ·Έλ μ΄μ
νμΌ μμ±
npm run db:migrate # λ§μ΄κ·Έλ μ΄μ
μ μ©npm run db:seednpm run devhttp://localhost:3000 μμ νμΈ
| λͺ λ Ήμ΄ | μ€λͺ |
|---|---|
npm run dev |
κ°λ° μλ² μ€ν |
npm run build |
νλ‘λμ λΉλ |
npm run test |
μ 체 ν μ€νΈ μ€ν (169 tests, --runInBand) |
npm run test:components |
μ»΄ν¬λνΈ ν μ€νΈλ§ (80 tests) |
npm run test:watch |
ν μ€νΈ κ°μ λͺ¨λ |
npx tsc --noEmit |
νμ μ²΄ν¬ |
npm run db:generate |
DB λ§μ΄κ·Έλ μ΄μ μμ± |
npm run db:migrate |
DB λ§μ΄κ·Έλ μ΄μ μ μ© |
npm run db:studio |
Drizzle Studio (DB GUI) |
npm run db:seed |
μλ λ°μ΄ν° μ½μ |
npm run lint |
ESLint κ²μ¬ |
| λ¬Έμ | μ€λͺ |
|---|---|
| PRD.md | μ ν μꡬμ¬ν (μλ리μ€, μμ΄μ΄νλ μ, FR/NFR) |
| TRD.md | κΈ°μ μꡬμ¬ν (μν€ν μ², κΈ°μ μ€ν, λλ ν 리 ꡬ쑰) |
| REQUIREMENTS.md | μμΈ μꡬμ¬ν (FR, NFR, US, μΆμ λ§€νΈλ¦μ€) |
| API_SPEC.md | 7κ° REST API μλν¬μΈνΈ λͺ μΈ |
| DATA_MODEL.md | DB μ€ν€λ§, Drizzle μ μ, λΉμ¦λμ€ κ·μΉ |
| COMPONENT_SPEC.md | μ»΄ν¬λνΈ κ³μΈ΅, Props, μ΄λ²€νΈ νλ¦, Hook λͺ μΈ |
| TEST_CASES.md | TDDμ© ν μ€νΈ μΌμ΄μ€ (API + μ»΄ν¬λνΈ + ν΅ν©) |
| FRONTEND_TASKS.md | νλ°νΈμλ ꡬν μμ + TDD 체ν¬λ¦¬μ€νΈ |
__tests__/
βββ api/ # API ν
μ€νΈ
β βββ tickets.test.ts # POST /api/tickets Route (@jest-environment node)
β βββ tickets-detail.test.ts # GET/PATCH/DELETE Route (@jest-environment node)
β βββ tickets-reorder.test.ts # PATCH /api/tickets/reorder (@jest-environment node)
β βββ ticketApi.test.ts # fetch λνΌ ν
μ€νΈ (jsdom)
βββ services/ # μλΉμ€ ν΅ν© ν
μ€νΈ (@jest-environment node)
β βββ ticketService.test.ts # μμ± (TC-API-001)
β βββ ticketService.board.test.ts # 보λ μ‘°ν (TC-API-002)
β βββ ... # getById, update, complete, delete, reorder, overdue
βββ components/ # μ»΄ν¬λνΈ ν
μ€νΈ (jsdom)
β βββ Button.test.tsx # Phase 1
β βββ TicketCard.test.tsx # Phase 2
β βββ TicketForm.test.tsx # Phase 3
β βββ BoardContainer.test.tsx # Phase 5
β βββ ...
βββ hooks/
βββ useTickets.test.ts # Phase 4
- μλΉμ€/API Route ν
μ€νΈ: μ€μ DB μ°κ²° νμ β
@jest-environment node+--runInBand - μ»΄ν¬λνΈ/Hook ν
μ€νΈ: mock κΈ°λ° β
jsdomνκ²½ (κΈ°λ³Έκ°)
μ΄ νλ‘μ νΈλ Claude Codeμ ν¨κ» SDD(Specification-Driven Development) + TDD λ°©μμΌλ‘ κ°λ°λμλ€.
# Claude Code μ€ν
claude
# CLAUDE.mdλ₯Ό μ°Έκ³ νλλ‘ μμ²
> CLAUDE.mdλ₯Ό μ½κ³ νλ‘μ νΈ μ»¨ν
μ€νΈλ₯Ό νμ
ν΄μ€κ°λ° μμ:
- λͺ μΈ μμ±: PRD β TRD β REQUIREMENTS β API_SPEC β DATA_MODEL β COMPONENT_SPEC β TEST_CASES
- λ°±μλ TDD: TEST_CASES.mdμ TC-API κΈ°λ°μΌλ‘ Red β Green β Refactor
- νλ°νΈμλ TDD: FRONTEND_TASKS.md μμλλ‘ Bottom-up ꡬν (UI β Board β Ticket β Data β Container)
MIT
