기준선(설계 §9.1)에서 자동 채점 실패의 3분의 2가 식당이 식사 시간대 밖에 놓인 것이었다. 식사 시간대는 약한 위반이라 고치기에 걸리지 않아서, 강한 위반을 고친 뒤 식당을 식사 시간대로 옮기는 단계를 더했다(설계 §3.6 2a). 작은 모델이 시간을 셈하다 코스를 짧게 끝내거나 끼니를 빠뜨려서, 장소 수와 끼니를 코드가 계산해 메시지에 적고(course-v2), 조건 해석의 시간 표현 · 동네 이름 규칙을 보탰다(conditions-v2). 프롬프트 예시가 평가셋 문장과 겹치면 그 사례의 점수가 외운 답이 되므로 예시를 바꾸고, 평가 실행마다 겹치는 구절을 찾아 요약에 적는다. 같은 v2 로 두 모델을 재니 자동 채점이 Sonnet 88%, Haiku 69~75%였고 경계 사례는 6/6 과 2/6 이었다. Haiku 는 반려 동반 · 영업시간 표현 규칙을 실행마다 다르게 지켜, 코스 초안 기본 모델을 Sonnet 5.5(effort low)로 정했다(사용자 결정, 설계 §8 · §9.2). 조건 해석은 Haiku 5.5 그대로다. 코스를 못 만든 사례를 되짚을 수 있게 평가 결과에 AI 호출마다 고른 곳과 위반 목록을 남긴다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
풀스택 모노레포 기본 템플릿
NestJS(백엔드) · Vue 3(웹) · Flutter(모바일)를 하나의 저장소에서 운영하는 풀스택 템플릿입니다. 세 클라이언트는 동일한 표준 응답 포맷과 공통 에러 코드 맵을 공유하며, Docker Compose로 한 번에 구동됩니다.
구성
| 디렉터리 | 스택 | 역할 | 포트(기본) |
|---|---|---|---|
backend/ |
NestJS + TypeORM | REST API 서버 | 3000 |
frontend/ |
Vue 3 + Vite | 웹 프론트엔드 | 5173 |
flutter/ |
Flutter + Riverpod | 모바일 앱 | - |
db |
PostgreSQL | 데이터베이스 | 5432 |
redis |
Redis | 캐시 / 세션 (기본 비활성) | 6379 |
사전 요구사항
- Docker / Docker Compose — backend·frontend·db·redis 구동
- Flutter SDK
^3.11— 모바일 앱 실행용 (compose 대상 아님)
빠른 시작 (Docker)
# 1) 환경변수 파일 생성 (예제 복사)
# 실제 설정값은 루트 .env.development 한 곳에서 관리한다 (SSOT).
# backend/frontend 는 docker-compose 가 값을 주입하므로 예제만 복사하면 된다.
cp .env.development.example .env.development # ← 여기에만 값 입력
cp backend/.env.development.example backend/.env.development # (존재 필요: JWT 등 고유 시크릿)
# frontend 는 compose 가 빌드 시 값을 주입하므로 자체 env 복사 불필요
# 2) 백엔드 스택 기동 (backend + db)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d
# 3) 의존성 변경 시에만 --build
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d --build
# 4) 프론트엔드는 호스트에서 직접 띄운다 (컨테이너보다 수십 배 빠르다)
cd frontend && npm install && npm run dev
기동 후:
- 웹: http://localhost:5173
- API: http://localhost:3000/api
- Swagger 문서: http://localhost:3000/api-docs
개발 환경에서 프론트엔드는 컨테이너로 뜨지 않습니다. vite 를 컨테이너에서 돌리면 소스가 Windows 바인드 마운트 너머에 있어 첫 서빙까지 수 분이 걸립니다. 호스트에서
npm run dev로 띄우면 1~2초이고,/api요청은 vite 프록시가 백엔드 컨테이너로 넘깁니다. 컨테이너로 띄워야 한다면--profile frontend를 붙이세요 (아래 "프론트엔드 실행 방식" 참조). 운영(docker-compose.yml단독)은 그대로 nginx 정적 호스팅으로 동작합니다.
Redis 는 기본적으로 뜨지 않습니다. 필요해지면 루트
.env.{env}에COMPOSE_PROFILES=redis를 설정하세요.
프론트엔드 실행 방식
| 방식 | 명령 | 첫 서빙 | 용도 |
|---|---|---|---|
| 호스트 vite (개발 기본) | cd frontend && npm run dev |
1~2초 (최초 의존성 사전번들 10초 내외) | 일상 개발 |
| 컨테이너 vite (선택) | docker compose ... --profile frontend up -d |
수 분 (Windows 바인드 마운트) | 호스트에 Node 를 둘 수 없을 때 |
| nginx 정적 (운영) | docker compose --env-file .env.production up -d --build |
- | 배포 |
컨테이너 방식을 상시 사용하려면 루트
.env.development에COMPOSE_PROFILES=frontend를 넣습니다. (redis 와 함께 켤 때는COMPOSE_PROFILES=frontend,redis) 호스트 vite 와 컨테이너 vite 는 같은 호스트 포트를 쓰므로 동시에 띄우지 마세요.
포트는 루트
.env.{env}를 따릅니다. 호스트에서 띄우는 vite 도FRONTEND_PORT로 열리고,/api프록시 대상은BACKEND_PORT에서 나옵니다 —vite.config.ts가 루트 env 를 직접 읽습니다. 포트가 이미 사용 중이면 조용히 다른 포트로 옮기지 않고 실패합니다 (CORS_ORIGIN과 compose 포트 매핑이 그 값에 묶여 있어, 말없이 옮겨가면 원인 모르게 어긋납니다).
전체 실행/배포 명령어는 .claude/skills/docker/SKILL.md 참조.
모바일 앱 실행 (Flutter)
Flutter 는 Docker Compose 대상이 아니므로 로컬에서 직접 실행합니다.
cd flutter && cp .env.example .env && flutter pub get && flutter run
flutter/.env의API_URL을 백엔드 주소로 설정합니다. (Android 에뮬레이터는http://10.0.2.2:3000/api)
환경변수
.env* 는 커밋하지 않습니다. 각 디렉터리의 *.example 파일을 복사해 사용하세요.
루트 .env.{env} 가 단일 진실 공급원(SSOT) 입니다. backend·frontend 는 docker-compose 가
루트 값을 주입하므로 자체 env 에 값을 중복 작성하지 않습니다. (자세한 위임 구조는 CLAUDE.md 의 "환경변수 구조" 참조)
| 파일 | 용도 | 값 작성 위치 |
|---|---|---|
.env.{env}.example |
루트 — 모든 설정의 SSOT (compose 주입) | ✅ 여기에 작성 |
backend/.env.{env}.example |
백엔드 — 고유 시크릿(JWT 등)만 | 최소 (존재 필수) |
frontend/.env.{env}.example |
프론트 — 빌드 시 루트 BACKEND_API_URL 주입 |
작성 불필요 |
flutter/.env.example |
모바일 — API_URL (compose 비대상) |
✅ 독립 관리 |
{env} = development | production
표준 응답 포맷
// 성공
{ "success": true, "data": { /* ... */ } }
// 실패 — 사용자에게 보이는 모든 실패는 에러 코드를 가진다
{ "success": false, "error": { "code": "VAL_001", "message": "...", "traceId": "3f1c0a2e-..." } }
// 목록 — data 안의 형태가 고정되어 있다
{ "success": true, "data": { "items": [], "pageInfo": { "hasNext": false, "nextCursor": null, "total": 0 } } }
traceId 는 요청마다 발급되는 추적 ID(x-request-id 헤더와 동일)로, 서버 로그에 같은 값이 남습니다.
사용자 문의가 들어오면 이 값으로 로그를 검색하면 됩니다.
/api/health는 예외입니다. 모니터링 도구가 읽는 엔드포인트라 래퍼 없이 원본 형식을 반환합니다.
에러 코드는 한 곳에서 관리됩니다
backend/src/common/errors/error-codes.ts ← 여기만 수정
│ cd backend && npm run sync:error-codes
├─▶ frontend/src/shared/constants/error-codes.generated.ts (자동 생성)
└─▶ flutter/lib/core/api/error_lexicon.g.dart (자동 생성)
생성 파일은 직접 수정하지 마세요. CI 가 동기화 여부를 검사합니다.
자세한 규칙은 .claude/rules/error-codes.md 참조.
로컬 개발 (Docker 없이)
DB 는 컨테이너로 띄우고 앱만 호스트에서 실행하는 방식이 가장 간단합니다.
# 1) DB 만 컨테이너로 기동 (127.0.0.1:5432 로 노출된다)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d db
# 2) backend — DB_HOST 기본값이 localhost 라 그대로 붙는다
cd backend && npm ci && npm run start:dev
# 3) frontend — vite dev 서버가 /api 를 localhost:3000 으로 프록시한다 (개발 기본 방식)
cd frontend && npm ci && npm run dev
# 4) flutter
cd flutter && flutter pub get && flutter run
프론트엔드의 API 요청은
/api상대경로로 나가고 vite dev 프록시가 백엔드로 넘깁니다. 백엔드를 다른 주소에서 띄웠다면VITE_DEV_API_PROXY로 대상을 바꾸세요.
커밋 전 검사
cd backend && npm run lint && npm run test && npm run build
cd frontend && npm run lint && npm run type-check && npm run test:unit
cd flutter && flutter analyze && flutter test
lint 는 --max-warnings 0 이라 경고 하나만 있어도 실패합니다. 자동 수정은 npm run lint:fix.
같은 검사를 CI(.github/workflows/ci.yml)가 PR 마다 수행합니다.
⚠️ 의존성을 추가·제거할 때는 컨테이너 안에서 실행하세요. Windows 에서
npm install하면 Linux 전용 패키지가 lock 에서 잘려 Docker 빌드가 깨집니다.docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development exec backend npm install <패키지>
개발 규칙
| 영역 | 문서 |
|---|---|
| 공통/보안 | CLAUDE.md · .claude/rules/ |
| 응답·에러 | response-format.md · error-codes.md |
| API 설계 | api-design.md |
| 로깅 | logging.md |
| 체크리스트 | checklist.md |
| NestJS | .claude/skills/nestjs/SKILL.md |
| Vue 3 | .claude/skills/vue3/SKILL.md |
| Flutter | .claude/skills/flutter/SKILL.md |
| Docker | .claude/skills/docker/SKILL.md |
| 역할 분담 | .claude/agents/README.md |
디렉터리 구조
.
├── backend/ # NestJS API (Controller / Service / DTO / 공통 필터·인터셉터)
├── frontend/ # Vue 3 (pages / components / composables / stores / router)
├── flutter/ # Flutter (core / features / shared)
├── docker-compose.yml # 베이스 정의
├── docker-compose.dev.yml # 개발 오버라이드 (핫리로드, 포트 노출)
├── CLAUDE.md # 프로젝트 개요 + 공통 규칙 @import
├── .github/
│ ├── workflows/ci.yml # lint·test·build + 에러 코드 동기화 검사
│ └── pull_request_template.md
├── ci/ # CI 러너 (Gitea Actions) — 앱 스택과 분리
└── .claude/
├── settings.json # Claude Code 권한·환경 설정
├── rules/ # 공통 규칙 (코딩·응답 포맷·에러 코드·API 설계·로깅·보안·환경변수·테스트)
├── skills/ # 도메인별 개발 규칙 (nestjs·vue3·flutter·docker)
└── agents/ # 역할별 규칙 (개발 3종·테스터·리뷰어·인프라·사용자/관리자 관점)