Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Vercel Postgres 연결 문자열
POSTGRES_URL=
3 changes: 3 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# .env.test (테스트용)
DATABASE_URL=postgresql://localhost:5432/tika_test
NODE_ENV=test
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Dependencies
node_modules/

# Build
.next/
out/

# Environment
.env
.env.local
.env.*.local

# IDE
.idea/
.vscode/
*.swp

# OS
.DS_Store

# Testing
coverage/

# Drizzle
drizzle/meta/
97 changes: 97 additions & 0 deletions CLAUDE-SDD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# CLAUDE.md - Tika Project

## 프로젝트 개요
Tika는 티켓 기반 칸반 보드 TODO 앱이다.
Next.js App Router 기반으로, 프론트엔드와 백엔드를 디렉토리 수준에서 분리한다.
src/shared/에서 타입과 검증 스키마를 공유한다.

## 프로젝트 구조
- app/api/ : 백엔드 진입점 (Route Handlers, 요청 파싱 + 응답만)
- src/server/ : 백엔드 로직 (services, db, middleware)
- src/client/ : 프론트엔드 로직 (components, hooks, api 호출)
- src/shared/ : 공유 타입, Zod 스키마, 상수
- docs/ : 프로젝트 명세 문서

## 기술 스택
- Framework: Next.js 15 (App Router)
- Language: TypeScript (strict mode)
- Frontend: React 19
- Styling: Tailwind CSS 4
- Drag & Drop: @dnd-kit/core + @dnd-kit/sortable
- ORM: Drizzle ORM
- DB: PostgreSQL (로컬 개발), Vercel Postgres (배포)
- Validation: Zod
- Testing: Jest + React Testing Library
- Deployment: Vercel

## 프로젝트 문서 (반드시 참조)
- 제품 요구사항: /docs/PRD.md
- 기술 요구사항: /docs/TRD.md
- 상세 요구사항: /docs/REQUIREMENTS.md
- API 명세: /docs/API_SPEC.md
- 데이터 모델: /docs/DATA_MODEL.md
- 컴포넌트 명세: /docs/COMPONENT_SPEC.md
- 테스트 케이스: /docs/TEST_CASES.md

## 코딩 컨벤션

### TypeScript (공통)
- strict 모드 사용
- any 사용 금지, unknown 사용 후 타입 가드
- 인터페이스는 I 접두사 없이 명사로 (예: Ticket, BoardData)
- enum 대신 const 객체 + typeof 패턴 사용
- 공유 타입은 반드시 @/shared/types에서 import

### 백엔드 (app/api/ + src/server/)
- Route Handler는 얇게: 요청 파싱 → 서비스 호출 → 응답 반환
- 비즈니스 로직은 src/server/services/에 작성
- Zod로 요청 검증 (shared/validations에서 import)
- 에러 응답 형식 통일: { error: { code, message } }
- HTTP 상태 코드: 200, 201, 204, 400, 404, 500
- DB 쿼리는 Drizzle ORM으로만 작성 (raw SQL 금지)

### 프론트엔드 (src/client/)
- 함수 컴포넌트 + 화살표 함수
- Props 타입은 컴포넌트 파일 내 정의
- API 호출은 src/client/api/ticketApi.ts를 통해서만
- 파일명: PascalCase (예: TicketCard.tsx)

## 개발 규칙

### 반드시 지켜야 할 것
- 구현 전 반드시 관련 명세 문서 확인 (API_SPEC.md, COMPONENT_SPEC.md 등)
- API 구현 시 API_SPEC.md의 요청/응답 형식 정확히 따르기
- 에러 코드와 메시지는 명세에 정의된 것만 사용
- 컴포넌트 구현 시 COMPONENT_SPEC.md의 Props와 동작 준수
- 타입 변경 시 src/shared/types 먼저 수정

### 하지 말아야 할 것
- 명세에 없는 기능 임의 추가 금지
- any 타입 사용 금지
- console.log 커밋 금지 (디버깅 후 제거)
- src/client/에서 직접 DB 접근 금지
- src/server/에서 React 관련 코드 작성 금지

### 경계 규칙
- 백엔드 작업 시(app/api/, src/server/) 프론트엔드(src/client/) 코드 수정 금지
- 프론트엔드 작업 시(src/client/) 백엔드(app/api/, src/server/) 코드 수정 금지
- 양쪽에 영향을 주는 변경은 src/shared/ 먼저 수정 후 각각 반영

### Git 커밋 규칙
- 커밋 메시지에 Co-Authored-By 포함하지 않음

## SDD 워크플로우 규칙

### 구현 전 명세 확인
1. API 구현 전: API_SPEC.md에서 요청/응답 형식, 에러 메시지 확인
2. 컴포넌트 구현 전: COMPONENT_SPEC.md에서 Props, 이벤트 흐름 확인
3. DB 작업 전: DATA_MODEL.md에서 스키마, 비즈니스 규칙 확인

### 명세 우선 원칙
- 명세에 정의된 에러 메시지를 그대로 사용할 것
- 명세에 없는 필드나 동작을 임의로 추가하지 말 것
- 명세와 구현이 다를 경우, 명세를 먼저 수정한 후 구현 변경

### 검증 단계
- 구현 완료 후 TEST_CASES.md의 시나리오로 검증
- 테스트 실패 시 구현을 수정 (명세 오류인 경우 명세 먼저 수정)
27 changes: 20 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ src/shared/에서 타입과 검증 스키마를 공유한다.
- Styling: Tailwind CSS 4
- Drag & Drop: @dnd-kit/core + @dnd-kit/sortable
- ORM: Drizzle ORM
- DB: Vercel Postgres (Neon)
- DB: PostgreSQL (로컬 개발), Vercel Postgres (Neon)
- Validation: Zod
- Testing: Jest + React Testing Library
- Deployment: Vercel
Expand Down Expand Up @@ -77,9 +77,22 @@ src/shared/에서 타입과 검증 스키마를 공유한다.
- 프론트엔드 작업 시(src/client/) 백엔드(app/api/, src/server/) 코드 수정 금지
- 양쪽에 영향을 주는 변경은 src/shared/ 먼저 수정 후 각각 반영

### TDD 사이클 규칙
- Red 단계: 테스트 코드만 작성, 구현 코드 생성 금지
- Green 단계: 테스트를 통과하는 최소한의 코드만 작성, 테스트 코드 수정 금지
- Refactor 단계: 코드 개선만, 새 기능 추가 금지, 테스트는 반드시 통과 유지
- 테스트와 구현을 한 번에 작성하지 말 것 — 반드시 단계별로 진행
- 테스트 실패 시 구현을 수정할 것, 테스트를 수정하지 말 것 (명세 오류인 경우 명세 먼저 수정)
### Git 커밋 규칙
- 커밋 메시지에 Co-Authored-By 포함하지 않음


## SDD 워크플로우 규칙

### 구현 전 명세 확인
1. API 구현 전: API_SPEC.md에서 요청/응답 형식, 에러 메시지 확인
2. 컴포넌트 구현 전: COMPONENT_SPEC.md에서 Props, 이벤트 흐름 확인
3. DB 작업 전: DATA_MODEL.md에서 스키마, 비즈니스 규칙 확인

### 명세 우선 원칙
- 명세에 정의된 에러 메시지를 그대로 사용할 것
- 명세에 없는 필드나 동작을 임의로 추가하지 말 것
- 명세와 구현이 다를 경우, 명세를 먼저 수정한 후 구현 변경

### 검증 단계
- 구현 완료 후 TEST_CASES.md의 시나리오로 검증
- 테스트 실패 시 구현을 수정 (명세 오류인 경우 명세 먼저 수정)
Empty file added __tests__/api/.gitkeep
Empty file.
151 changes: 151 additions & 0 deletions __tests__/api/tickets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @jest-environment node
*
* TC-API-001: POST /api/tickets — 티켓 생성
* TDD Red 단계: 테스트만 작성, 구현 없음
*/

import { POST } from '@/app/api/tickets/route';
import { NextRequest } from 'next/server';

function createRequest(body: object) {
return new NextRequest('http://localhost:3000/api/tickets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}

describe('POST /api/tickets', () => {
describe('정상 생성', () => {
it('모든 필드를 포함한 티켓을 생성한다', async () => {
// Given
const request = createRequest({
title: 'API 설계 문서 작성',
description: 'REST API 엔드포인트와 요청/응답 형식을 정의한다',
priority: 'HIGH',
plannedStartDate: '2026-02-10',
dueDate: '2026-02-15',
});

// When
const response = await POST(request);
const data = await response.json();

// Then
expect(response.status).toBe(201);
expect(data).toMatchObject({
title: 'API 설계 문서 작성',
description: 'REST API 엔드포인트와 요청/응답 형식을 정의한다',
status: 'BACKLOG',
priority: 'HIGH',
plannedStartDate: '2026-02-10',
dueDate: '2026-02-15',
startedAt: null,
completedAt: null,
});
expect(data.id).toBeDefined();
expect(data.position).toBeDefined();
expect(data.createdAt).toBeDefined();
expect(data.updatedAt).toBeDefined();
});

it('제목만으로 티켓을 생성하면 기본값이 적용된다', async () => {
// Given
const request = createRequest({
title: '테스트 할일',
});

// When
const response = await POST(request);
const data = await response.json();

// Then
expect(response.status).toBe(201);
expect(data).toMatchObject({
title: '테스트 할일',
description: null,
status: 'BACKLOG',
priority: 'MEDIUM',
plannedStartDate: null,
dueDate: null,
startedAt: null,
completedAt: null,
});
});
});

describe('검증 에러', () => {
it('제목이 없으면 400 에러를 반환한다', async () => {
// Given
const request = createRequest({});

// When
const response = await POST(request);
const data = await response.json();

// Then
expect(response.status).toBe(400);
expect(data.error).toEqual({
code: 'VALIDATION_ERROR',
message: '제목을 입력해주세요',
});
});

it('제목이 200자를 초과하면 400 에러를 반환한다', async () => {
// Given
const request = createRequest({
title: 'a'.repeat(201),
});

// When
const response = await POST(request);
const data = await response.json();

// Then
expect(response.status).toBe(400);
expect(data.error).toEqual({
code: 'VALIDATION_ERROR',
message: '제목은 200자 이내로 입력해주세요',
});
});

it('과거 마감일이면 400 에러를 반환한다', async () => {
// Given
const request = createRequest({
title: '테스트',
dueDate: '2020-01-01',
});

// When
const response = await POST(request);
const data = await response.json();

// Then
expect(response.status).toBe(400);
expect(data.error).toEqual({
code: 'VALIDATION_ERROR',
message: '종료예정일은 오늘 이후 날짜를 선택해주세요',
});
});

it('잘못된 우선순위면 400 에러를 반환한다', async () => {
// Given
const request = createRequest({
title: '테스트',
priority: 'URGENT',
});

// When
const response = await POST(request);
const data = await response.json();

// Then
expect(response.status).toBe(400);
expect(data.error).toEqual({
code: 'VALIDATION_ERROR',
message: '우선순위는 LOW, MEDIUM, HIGH 중 선택해주세요',
});
});
});
});
Empty file added __tests__/components/.gitkeep
Empty file.
Empty file added __tests__/hooks/.gitkeep
Empty file.
Empty file added __tests__/services/.gitkeep
Empty file.
Empty file added app/api/tickets/.gitkeep
Empty file.
25 changes: 25 additions & 0 deletions app/api/tickets/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from 'next/server';
import { createTicketSchema } from '@/shared/validations/ticket';
import * as ticketService from '@/server/services/ticketService';

export async function POST(request: NextRequest) {
const body = await request.json();
const validationResult = createTicketSchema.safeParse(body);

if (!validationResult.success) {
const firstError = validationResult.error.errors[0];
return NextResponse.json(
{
error: {
code: 'VALIDATION_ERROR',
message: firstError.message,
},
},
{ status: 400 }
);
}

const ticket = await ticketService.create(validationResult.data);

return NextResponse.json(ticket, { status: 201 });
}
1 change: 1 addition & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import 'tailwindcss';
19 changes: 19 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
title: 'Tika',
description: '티켓 기반 칸반 보드 TODO 앱',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
}
7 changes: 7 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function HomePage() {
return (
<main>
<h1>Tika</h1>
</main>
);
}
2 changes: 1 addition & 1 deletion docs/TEST_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
| TC-COMP-005 | — | US-007 | TicketModal 컴포넌트 |
| TC-COMP-006 | — | US-008 | ConfirmDialog 컴포넌트 |
| TC-INT-001 | FR-005, FR-007 | US-005, US-006 | 드래그앤드롭 통합 |
| TC-INT-002 | FR-002, FR-005, FR-006 | US-006, US-008 | 완료 → 삭제 흐름 |
| TC-INT-002 | FR-002, FR-005, FR-006 | US-006, US-008 | completed_at+24시간 이후인 건 Done 영역에 노출안됨 |

---

Expand Down
10 changes: 10 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
schema: './src/server/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Empty file added drizzle/.gitkeep
Empty file.
Loading