3f703b10f4
빌드(tsconfig.build.json)는 spec 을 제외하고 ts-jest 는 isolatedModules 로 타입을 보지 않아, 테스트 파일의 타입 오류가 어떤 검사에도 걸리지 않았다. 타입이 틀린 목·픽스처로도 테스트가 통과한다. - backend 에 type-check 스크립트(tsc --noEmit) 추가 — src·test·scripts 를 모두 본다 - CI backend 잡에 Type check 단계 추가, 커밋 전 점검·역할 문서와 PR 템플릿에 반영 - 이 검사로 드러난 기존 계약 테스트(error-codes.spec.ts)의 타입 오류 수정 — 테스트 의미는 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
277 lines
15 KiB
Markdown
277 lines
15 KiB
Markdown
# 풀스택 모노레포 기본 템플릿
|
|
|
|
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 구동 (컨테이너는 Node 24 LTS 이미지를 쓴다)
|
|
- Node.js 24 LTS 권장 (최소 22.12) — 호스트에서 띄우는 vite dev 서버와 lint·test 용
|
|
- Flutter SDK `^3.11` — 모바일 앱 실행용 (compose 대상 아님)
|
|
|
|
## 빠른 시작 (Docker)
|
|
|
|
```bash
|
|
# 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:5173/api> — 브라우저가 쓰는 경로 (vite 프록시가 백엔드로 넘긴다, 운영과 같은 구조)
|
|
· 백엔드 직접: <http://localhost:3000/api>
|
|
- Swagger 문서: <http://localhost:3000/api-docs> — 운영에서는 기본으로 꺼진다 (`SWAGGER_ENABLED`)
|
|
|
|
> **개발 환경에서 프론트엔드는 컨테이너로 뜨지 않습니다.**
|
|
> 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 를 직접 읽습니다.
|
|
> 포트가 이미 사용 중이면 조용히 다른 포트로 옮기지 않고 **실패**합니다
|
|
> (대개 다른 vite 가 이미 그 포트에 떠 있다는 뜻이라, 말없이 옮겨가면 브라우저가 엉뚱한 서버를 보게 됩니다).
|
|
|
|
전체 실행/배포 명령어는 [`.claude/skills/docker/SKILL.md`](./.claude/skills/docker/SKILL.md) 참조.
|
|
|
|
## 운영 배포 — 프론트·API 를 같은 도메인으로 (Nginx Proxy Manager)
|
|
|
|
브라우저는 **도메인 하나만** 봅니다. 경로로 프론트와 백엔드를 나눕니다.
|
|
|
|
```
|
|
https://app.your-domain.com/ → <PROJECT_NAME>-frontend:8080 nginx 정적 호스팅 (Proxy Host 기본 대상)
|
|
https://app.your-domain.com/api/... → <PROJECT_NAME>-backend:3000 NestJS (Custom location /api)
|
|
```
|
|
|
|
- 프론트 번들은 API 를 `/api` **상대경로**로 호출합니다 — 도메인이 바뀌어도 다시 빌드할 필요가 없습니다.
|
|
- 같은 출처 요청이라 **CORS 설정이 필요 없고**, CSP `connect-src 'self'` 가 그대로 동작하며, 인증 쿠키도 같은 사이트로 다룹니다.
|
|
- 개발 환경도 같은 구조입니다 — vite dev 서버가 `/api` 를 백엔드로 프록시합니다.
|
|
|
|
### 1) 공유 네트워크 (서버당 1회)
|
|
|
|
NPM 과 앱 컨테이너가 외부 네트워크 `proxy` 를 공유해야 NPM 이 컨테이너 이름으로 찾아갈 수 있습니다.
|
|
이 네트워크가 없으면 compose 가 기동하지 않습니다.
|
|
|
|
```bash
|
|
docker network create proxy # 이미 있으면 생략
|
|
docker network connect proxy <NPM 컨테이너 이름> # NPM 이 아직 proxy 네트워크에 없다면
|
|
```
|
|
|
|
> NPM 은 `<PROJECT_NAME>-frontend` 같은 **컨테이너 이름**으로 접근하므로, 같은 서버에 여러 프로젝트를 올린다면 `PROJECT_NAME` 이 겹치지 않게 합니다.
|
|
|
|
### 2) 앱 기동
|
|
|
|
```bash
|
|
cp .env.production.example .env.production # placeholder 교체 — BACKEND_API_URL=/api 는 그대로 둔다
|
|
cp backend/.env.production.example backend/.env.production
|
|
|
|
# 업로드 디렉터리 소유자를 앱 사용자(node, uid 1000)로 — Docker 가 root 소유로 만들면 백엔드가 기동에 실패한다
|
|
mkdir -p data/uploads && sudo chown 1000:1000 data/uploads
|
|
|
|
sudo docker compose --env-file .env.production up -d --build
|
|
```
|
|
|
|
### 3) NPM 설정 — Proxy Host 하나 + Custom location 하나
|
|
|
|
| 탭 | 항목 | 값 |
|
|
| --- | --- | --- |
|
|
| Details | Domain Names | `app.your-domain.com` |
|
|
| | Scheme · Forward Hostname · Port | `http` · `<PROJECT_NAME>-frontend` · `8080` |
|
|
| | Cache Assets | **끔** (아래 주의 참조) |
|
|
| | Block Common Exploits | 켬 (선택) |
|
|
| Custom locations | location | `/api` |
|
|
| | Scheme · Forward Hostname · Port | `http` · `<PROJECT_NAME>-backend` · `3000` (`BACKEND_PORT`) |
|
|
| SSL | | 인증서 발급 + Force SSL |
|
|
|
|
> ⚠️ **Cache Assets 는 끕니다.** 켜면 NPM 이 `.png`·`.js`·`.css` 등으로 끝나는 요청을 custom location 안에서도 가로채
|
|
> **Proxy Host 의 기본 대상(frontend)** 으로 보냅니다. `/api/files/abc.png` 같은 API 가 프론트로 가 버립니다.
|
|
> 프론트 정적 자원의 캐시 헤더는 frontend nginx 가 이미 붙입니다.
|
|
|
|
> - location 은 `/api` (끝 슬래시 없음) — 접두어 매칭이라 Swagger(`/api-docs`)까지 백엔드로 갑니다. 개발의 vite 프록시와 같은 규칙입니다.
|
|
> (Swagger 는 운영에서 기본으로 꺼져 있습니다. 켜려면 `SWAGGER_ENABLED=true` + NPM Access List 로 접근을 제한하세요)
|
|
> - Forward Hostname 에 경로를 붙이지 않습니다. 요청 경로가 그대로 넘어가야 백엔드의 `/api` prefix 와 맞습니다.
|
|
> - **`/api` 로 시작하는 경로는 프론트 라우트로 쓰지 않습니다** (전부 백엔드로 갑니다).
|
|
|
|
### 4) 확인
|
|
|
|
```bash
|
|
curl -i https://app.your-domain.com/api/health # 200 {"status":"ok",...} ← 백엔드가 응답
|
|
curl -I https://app.your-domain.com/ # 200 text/html ← 프론트가 응답
|
|
```
|
|
|
|
`/api/health` 가 **404** 면 custom location 이 빠진 것입니다 —
|
|
frontend nginx 는 자기에게 떨어진 `/api` 요청을 404 로 막아 둡니다 (index.html 로 폴백해 조용히 실패하지 않도록).
|
|
|
|
> API 를 별도 도메인(`api.your-domain.com`)에 두는 구성이 필요하다면 `.env.production.example` 의 안내대로
|
|
> `BACKEND_API_URL`·`CORS_ORIGIN` 과 `frontend/nginx.conf` 의 CSP `connect-src` 를 함께 바꿉니다.
|
|
|
|
## 모바일 앱 실행 (Flutter)
|
|
|
|
Flutter 는 Docker Compose 대상이 아니므로 로컬에서 직접 실행합니다.
|
|
|
|
```bash
|
|
cd flutter && cp .env.example .env && flutter pub get && flutter run
|
|
```
|
|
|
|
> `flutter/.env` 의 `API_URL` 을 백엔드 주소로 설정합니다. (Android 에뮬레이터는 `http://10.0.2.2:3000/api`,
|
|
> 운영은 웹과 같은 도메인의 `https://app.your-domain.com/api`)
|
|
|
|
## 환경변수
|
|
|
|
`.env*` 는 커밋하지 않습니다. 각 디렉터리의 `*.example` 파일을 복사해 사용하세요.
|
|
|
|
**루트 `.env.{env}` 가 단일 진실 공급원(SSOT)** 입니다. backend·frontend 는 docker-compose 가
|
|
루트 값을 주입하므로 자체 env 에 값을 중복 작성하지 않습니다. (자세한 위임 구조는 [`CLAUDE.md`](./CLAUDE.md) 의 "환경변수 구조" 참조)
|
|
|
|
| 파일 | 용도 | 값 작성 위치 |
|
|
| ----------------------------- | ---------------------------------------- | --------------------- |
|
|
| `.env.{env}.example` | 루트 — 모든 설정의 SSOT (compose 주입) | ✅ 여기에 작성 |
|
|
| `backend/.env.{env}.example` | 백엔드 — 고유 시크릿(JWT 등)만 | 최소 (존재 필수) |
|
|
| `frontend/.env.{env}.example` | 프론트 — 빌드 시 루트 `BACKEND_API_URL`(기본 `/api`) 주입 | 작성 불필요 |
|
|
| `flutter/.env.example` | 모바일 — `API_URL` (compose 비대상) | ✅ 독립 관리 |
|
|
|
|
`{env}` = `development` | `production`
|
|
|
|
## 표준 응답 포맷
|
|
|
|
```jsonc
|
|
// 성공
|
|
{ "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`](./.claude/rules/error-codes.md) 참조.
|
|
|
|
## 로컬 개발 (Docker 없이)
|
|
|
|
DB 는 컨테이너로 띄우고 앱만 호스트에서 실행하는 방식이 가장 간단합니다.
|
|
|
|
```bash
|
|
# 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` 로 대상을 바꾸세요.
|
|
|
|
### 커밋 전 검사
|
|
|
|
```bash
|
|
cd backend && npm run lint && npm run type-check && 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`](./.github/workflows/ci.yml))가 PR 마다 수행합니다.
|
|
|
|
> ⚠️ **의존성을 추가·제거할 때는 컨테이너 안에서** 실행하세요.
|
|
> Windows 에서 `npm install` 하면 Linux 전용 패키지가 lock 에서 잘려 Docker 빌드가 깨집니다.
|
|
> ```bash
|
|
> docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development exec backend npm install <패키지>
|
|
> ```
|
|
|
|
## 개발 규칙
|
|
|
|
| 영역 | 문서 |
|
|
| --------- | ------------------------------------------------- |
|
|
| 공통/보안 | [`CLAUDE.md`](./CLAUDE.md) · [`.claude/rules/`](./.claude/rules/) |
|
|
| 응답·에러 | [`response-format.md`](./.claude/rules/response-format.md) · [`error-codes.md`](./.claude/rules/error-codes.md) |
|
|
| API 설계 | [`api-design.md`](./.claude/rules/api-design.md) |
|
|
| 로깅 | [`logging.md`](./.claude/rules/logging.md) |
|
|
| 체크리스트 | [`checklist.md`](./.claude/rules/checklist.md) |
|
|
| NestJS | [`.claude/skills/nestjs/SKILL.md`](./.claude/skills/nestjs/SKILL.md) |
|
|
| Vue 3 | [`.claude/skills/vue3/SKILL.md`](./.claude/skills/vue3/SKILL.md) |
|
|
| Flutter | [`.claude/skills/flutter/SKILL.md`](./.claude/skills/flutter/SKILL.md) |
|
|
| Docker | [`.claude/skills/docker/SKILL.md`](./.claude/skills/docker/SKILL.md) |
|
|
| 역할 분담 | [`.claude/agents/README.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·type-check·test·build + 에러 코드 동기화 검사
|
|
│ └── pull_request_template.md
|
|
├── ci/ # CI 러너 (Gitea Actions) — 앱 스택과 분리
|
|
└── .claude/
|
|
├── settings.json # Claude Code 권한·환경 설정
|
|
├── rules/ # 공통 규칙 (코딩·응답 포맷·에러 코드·API 설계·로깅·보안·환경변수·테스트)
|
|
├── skills/ # 도메인별 개발 규칙 (nestjs·vue3·flutter·docker)
|
|
└── agents/ # 역할별 규칙 (개발 3종·테스터·리뷰어·인프라·사용자/관리자 관점)
|
|
```
|