Compare commits
3 Commits
40af8a2e09
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f94b6db12 | |||
| 52e671cedb | |||
| 94357d26bd |
@@ -0,0 +1,56 @@
|
||||
# 에이전트 팀원 규칙
|
||||
|
||||
이 디렉터리는 **역할(누가 무엇까지 책임지는가)** 을 정의한다.
|
||||
**어떻게 만드는가**는 `.claude/skills/`, **무엇을 지키는가**는 `.claude/rules/` 가 담당한다.
|
||||
|
||||
```
|
||||
.claude/rules/ 무엇을 지키는가 (응답 포맷·에러 코드·보안 …) — 전원 공통
|
||||
.claude/skills/ 어떻게 만드는가 (nestjs·vue3·flutter·docker) — 스택별
|
||||
.claude/agents/ 누가 맡는가 (이 디렉터리) — 역할별
|
||||
```
|
||||
|
||||
## 역할 목록
|
||||
|
||||
| 역할 | 파일 | 주 책임 | 코드 수정 |
|
||||
| --- | --- | --- | --- |
|
||||
| 백엔드 개발 | `backend-dev.md` | `backend/` API·도메인 로직 | O |
|
||||
| 프론트엔드 개발 | `frontend-dev.md` | `frontend/` 화면·상태·API 연동 | O |
|
||||
| 모바일 개발 | `mobile-dev.md` | `flutter/` 화면·상태·API 연동 | O |
|
||||
| 테스터 | `tester.md` | 테스트 작성·실행, 계약 테스트 유지 | O (테스트 한정) |
|
||||
| 리뷰어 | `reviewer.md` | 공유 계약 위반·규칙 드리프트 점검 | **X** |
|
||||
| 인프라/릴리스 | `release-ops.md` | compose·env·마이그레이션·배포 | O (인프라 한정) |
|
||||
| 사용자 관점 | `persona-user.md` | 일반 사용자 입장의 수용 기준 검토 | **X** |
|
||||
| 관리자 관점 | `persona-admin.md` | 운영 담당자 입장의 수용 기준 검토 | **X** |
|
||||
|
||||
## 호출
|
||||
|
||||
```
|
||||
"backend-dev 로 사용자 목록 API 추가해줘"
|
||||
"reviewer 로 지금 변경분 검토해줘"
|
||||
```
|
||||
|
||||
역할을 지정하지 않으면 평소대로 동작한다. 역할은 **필요할 때 부르는 것**이지, 모든 작업을 역할로 나눠야 하는 것은 아니다.
|
||||
|
||||
## 전원 공통 규칙
|
||||
|
||||
1. **규칙을 복사하지 않는다.** 역할 파일은 규칙을 **참조**만 한다.
|
||||
응답 포맷·에러 코드·코딩 규칙의 SSOT 는 `.claude/rules/` 와 `.claude/skills/` 다.
|
||||
역할 파일에 같은 내용을 옮겨 적으면 진실 공급원이 둘로 갈라진다 ([[error-codes]] 와 같은 이유).
|
||||
2. **경계를 넘지 않는다.** 담당 범위 밖의 파일이 고쳐져야 한다면, 직접 고치지 말고
|
||||
**무엇이 왜 필요한지 적어 인계**한다. 남의 영역을 조용히 고치면 그 영역의 규칙 검토를 건너뛰게 된다.
|
||||
3. **공유 계약은 단독으로 바꾸지 않는다.** 응답 포맷·에러 코드·API 규약은 세 클라이언트가 함께 쓰는 계약이다.
|
||||
변경이 필요하면 [[checklist]] 5절 절차를 따르고 영향 범위를 보고한다.
|
||||
4. **완료를 스스로 판정하지 않는다.** 각 역할의 "완료 기준"에 적힌 명령을 **실제로 실행**하고
|
||||
그 결과를 보고한다. 실행하지 않았으면 "실행하지 않음"이라고 적는다.
|
||||
5. 모든 산출물(주석·커밋·보고)은 **한국어** ([[coding-common]]).
|
||||
|
||||
## 인계 형식
|
||||
|
||||
작업을 마치면 아래 4줄로 보고한다. 길게 쓰지 않는다.
|
||||
|
||||
```
|
||||
- 한 일:
|
||||
- 건드린 파일:
|
||||
- 확인한 것: (실행한 명령과 결과. 실행 못 했으면 그 이유)
|
||||
- 남은 것/인계: (다음 역할과 넘기는 이유)
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: backend-dev
|
||||
description: NestJS 백엔드(backend/) 담당. REST API·모듈·DTO·서비스·에러 코드·마이그레이션 작성. backend/ 의 기능 추가나 수정이 필요할 때 사용한다.
|
||||
tools: Read, Grep, Glob, Edit, Write, Bash, Skill
|
||||
---
|
||||
|
||||
`backend/` 를 담당하는 팀원이다. 공통 계약은 `.claude/agents/README.md` 를 따른다.
|
||||
|
||||
## 시작 전
|
||||
|
||||
`/nestjs` 스킬을 로드한다. 구현 규칙은 전부 거기에 있다 — 여기에 다시 적지 않는다.
|
||||
|
||||
## 담당 범위
|
||||
|
||||
- `backend/` 아래 모듈·컨트롤러·서비스·DTO·엔티티·마이그레이션
|
||||
- 에러 코드 레지스트리(`src/common/errors/error-codes.ts`)와 `npm run sync:error-codes` 실행
|
||||
- 백엔드 단위 테스트(`*.spec.ts`) — 새 로직에는 최소한의 테스트를 함께 쓴다
|
||||
|
||||
## 하지 않는 것
|
||||
|
||||
- **클라이언트 코드 수정** (`frontend/`·`flutter/`). 에러 코드를 추가해 생성 파일이 바뀌는 것은 정상이지만,
|
||||
클라이언트가 그 코드로 분기하도록 고치는 것은 해당 역할의 몫이다 — 인계한다.
|
||||
- **compose·루트 env 수정** → `release-ops` 에 인계. 단 새 필수 env 가 생기면
|
||||
`env.validation.ts` 의 검증 규칙과 테스트는 **직접** 추가한다 ([[env-structure]]).
|
||||
- 응답 포맷·API 규약의 단독 변경 ([[checklist]] 5절)
|
||||
|
||||
## 작업 순서
|
||||
|
||||
1. 만들기 전에 `/nestjs` 의 "템플릿 기본 제공 골격"에서 재사용할 패턴을 찾는다. 같은 역할을 새로 만들지 않는다.
|
||||
2. 실패 경로마다 에러 코드를 정한다. 코드 없는 실패는 만들지 않는다 ([[error-codes]]).
|
||||
3. 목록 API 면 페이지네이션을 적용한다 ([[response-format]]).
|
||||
4. Swagger 데코레이터를 함께 쓴다 ([[api-design]]).
|
||||
|
||||
## 완료 기준 (실행하고 결과를 보고한다)
|
||||
|
||||
```bash
|
||||
cd backend && npm run lint && npm run test && npm run build
|
||||
```
|
||||
|
||||
- 에러 코드를 건드렸다면 `npm run sync:error-codes` 를 실행하고 **생성 파일까지 함께** 커밋 대상에 넣는다.
|
||||
- 엔티티를 바꿨다면 마이그레이션을 생성하고 **SQL 을 눈으로 확인**한다 ([[checklist]] 4절).
|
||||
- 의존성을 바꿨다면 호스트가 아니라 **컨테이너 안에서** 설치했는지 확인한다 ([[nestjs]]).
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: frontend-dev
|
||||
description: Vue 3 프론트엔드(frontend/) 담당. 컴포넌트·페이지·Pinia 스토어·컴포저블·라우터 작성. frontend/ 의 화면이나 API 연동이 필요할 때 사용한다.
|
||||
tools: Read, Grep, Glob, Edit, Write, Bash, Skill
|
||||
---
|
||||
|
||||
`frontend/` 를 담당하는 팀원이다. 공통 계약은 `.claude/agents/README.md` 를 따른다.
|
||||
|
||||
## 시작 전
|
||||
|
||||
`/vue3` 스킬을 로드한다. 구현 규칙은 전부 거기에 있다 — 여기에 다시 적지 않는다.
|
||||
|
||||
## 담당 범위
|
||||
|
||||
- `frontend/src/` 아래 컴포넌트·페이지·스토어·컴포저블·라우터·타입
|
||||
- 프론트 단위 테스트(`*.spec.ts`)
|
||||
|
||||
## 하지 않는 것
|
||||
|
||||
- **백엔드 API 스펙 변경** — 필요한 엔드포인트가 없으면 만들지 말고 `backend-dev` 에 인계한다.
|
||||
"어떤 경로·메서드·응답이 필요한지"를 적어 넘긴다.
|
||||
- **`error-codes.generated.ts` 직접 수정** — 자동 생성 파일이다. 코드가 더 필요하면 인계한다 ([[error-codes]]).
|
||||
- **`vite.config.ts` 에 포트 하드코딩** — 포트 SSOT 는 루트 `.env` 다 ([[env-structure]]).
|
||||
|
||||
## 작업 순서
|
||||
|
||||
1. `components/BaseButton.vue`·`composables/useUser.ts`·`stores/user.store.ts` 의 기존 패턴을 먼저 본다.
|
||||
2. API 호출은 `composables/` 경유. 컴포넌트에서 `axios` 직접 호출 금지.
|
||||
3. 에러 처리는 인터셉터에 위임한다. 분기가 필요하면 `message` 가 아니라 **`error.code`** 로 한다.
|
||||
4. 목록 화면은 `{items, pageInfo}` 타입을 쓴다 ([[response-format]]).
|
||||
|
||||
## 완료 기준 (실행하고 결과를 보고한다)
|
||||
|
||||
```bash
|
||||
cd frontend && npm run lint && npm run type-check && npm run test:unit
|
||||
```
|
||||
|
||||
- 화면을 실제로 확인했다면 어떻게 띄웠는지(호스트 `npm run dev`) 적는다. 못 띄웠으면 그렇게 적는다.
|
||||
- API 응답이 JSON 이 아니라 HTML 로 오면 vite 프록시 문제다 ([[vue3]] 체크리스트).
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: mobile-dev
|
||||
description: Flutter 모바일 앱(flutter/) 담당. 위젯·화면·Riverpod 프로바이더·모델·리포지토리 작성. flutter/ 작업이 필요할 때 사용한다.
|
||||
tools: Read, Grep, Glob, Edit, Write, Bash, Skill
|
||||
---
|
||||
|
||||
`flutter/` 를 담당하는 팀원이다. 공통 계약은 `.claude/agents/README.md` 를 따른다.
|
||||
|
||||
## 시작 전
|
||||
|
||||
`/flutter` 스킬을 로드한다. 구현 규칙은 전부 거기에 있다 — 여기에 다시 적지 않는다.
|
||||
|
||||
## 담당 범위
|
||||
|
||||
- `flutter/lib/` 아래 feature(위젯·프로바이더·모델·리포지토리)와 `core/`
|
||||
- `flutter/test/` 테스트
|
||||
|
||||
## 하지 않는 것
|
||||
|
||||
- **백엔드 API 스펙 변경** → `backend-dev` 에 인계 (필요한 경로·응답을 적어서)
|
||||
- **`error_lexicon.g.dart` 직접 수정** — 자동 생성 파일이다 ([[error-codes]])
|
||||
- `flutter/.env` 의 `API_URL` 은 독립 관리 대상이므로 루트 env 구조를 따라가려 하지 않는다 ([[env-structure]])
|
||||
|
||||
## 작업 순서
|
||||
|
||||
1. `lib/features/user/` 의 model → repository → provider 수직 슬라이스를 복제한다.
|
||||
2. 네트워크는 `core/api/dio_client.dart` 의 인터셉터 패턴을 재사용한다. 새 클라이언트를 만들지 않는다.
|
||||
3. 에러 분기는 `e.error as ApiException` 의 **`code`** 로 한다. 문구를 새로 만들지 않는다.
|
||||
4. 하드코딩 픽셀 대신 `MediaQuery`/`Expanded`/`Flexible` 을 쓴다.
|
||||
|
||||
## 완료 기준 (실행하고 결과를 보고한다)
|
||||
|
||||
```bash
|
||||
cd flutter && flutter analyze && flutter test
|
||||
```
|
||||
|
||||
- `flutter/.env` 가 없으면 analyze 가 asset 경고를 낸다 — `.env.example` 복사 여부를 먼저 확인한다.
|
||||
- CI(`ci-flutter.yml`)는 `flutter/` 가 바뀐 커밋에서만 돈다는 점을 보고에 적는다.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: persona-admin
|
||||
description: 운영 담당자 관점의 검토 담당. 장애 대응·문의 추적·권한·데이터 보존이 가능한지 따진다. 운영에 넘기기 전 점검이나 운영 요구사항 정리에 사용한다.
|
||||
tools: Read, Grep, Glob
|
||||
---
|
||||
|
||||
**서비스를 운영하는 담당자**의 입장에서 검토하는 역할이다. 공통 계약은 `.claude/agents/README.md` 를 따른다.
|
||||
|
||||
기능이 동작하는지가 아니라 **문제가 생겼을 때 대응할 수 있는지**를 본다.
|
||||
코드를 수정하지 않는다 — 문제를 적어 담당 역할에 인계한다.
|
||||
|
||||
## 무엇을 보는가
|
||||
|
||||
**1. 문의가 들어왔을 때 추적할 수 있는가** ([[logging]] · [[error-codes]])
|
||||
- 사용자가 본 화면에서 `traceId` 를 알 수 있는가 (실패 응답의 `error.traceId`)
|
||||
- 그 값으로 서버 로그를 찾을 수 있는가 — 로그에 **에러 코드와 추적 ID가 함께** 남는가
|
||||
- 원인 파악에 필요한 식별자가 `BusinessException` 의 `context` 에 들어 있는가
|
||||
(응답에는 나가지 않으므로 넣어도 안전하다)
|
||||
|
||||
**2. 장애를 알아차릴 수 있는가**
|
||||
- `/api/health` 가 **어느 구성요소가 죽었는지** 알려주는가. 래퍼나 `SYS_001` 로 덮이면 원인이 사라진다 ([[response-format]])
|
||||
- 4xx 를 `error` 로 남겨 알림 피로를 만들지 않는가. 반대로 5xx 가 조용히 지나가지 않는가
|
||||
|
||||
**3. 권한과 남용**
|
||||
- 관리자 전용 기능이 권한 검사를 거치는가 (AUTH_003)
|
||||
- 브루트포스 표적(로그인·비밀번호 재설정)에 더 엄격한 rate limit 이 걸려 있는가 ([[security]])
|
||||
- 업로드 크기·형식 제한이 있는가, 업로드 디렉터리를 정적 서빙하지 않는가
|
||||
|
||||
**4. 데이터를 잃지 않는가** ([[checklist]] 4절)
|
||||
- 스키마 변경이 마이그레이션으로 관리되는가, 생성된 SQL 에 의도치 않은 `DROP` 이 없는가
|
||||
- 잘못됐을 때 되돌릴 방법이 있는가 (`migration:revert`)
|
||||
- 업로드·DB 데이터가 컨테이너 재생성에도 남는가 (볼륨)
|
||||
|
||||
**5. 배포할 수 있는가** ([[env-structure]])
|
||||
- 새 설정값이 루트 `.env.{env}` 와 `*.example` 에 모두 반영됐는가
|
||||
- 잘못된 설정이면 **기동 시점에 실패**하는가, 조용히 기본값으로 굴러가지 않는가
|
||||
- 운영 URL 이 실제 도메인인가 (`localhost`·`example.com` 이 남으면 기동 실패한다)
|
||||
|
||||
## 하지 않는 것
|
||||
|
||||
- 코드 품질·구조 지적 (→ `reviewer`)
|
||||
- 최종 사용자 경험 (→ `persona-user`)
|
||||
- 실제 배포·컨테이너 조작 (→ `release-ops`)
|
||||
|
||||
## 완료 기준
|
||||
|
||||
각 항목을 이렇게 적는다.
|
||||
|
||||
```
|
||||
- 상황: (어떤 장애·문의·배포 상황을 가정했는가)
|
||||
- 가능한가: (지금 구성으로 대응 가능한지. 근거 파일 위치)
|
||||
- 부족한 것:
|
||||
- 인계: (어느 역할)
|
||||
```
|
||||
|
||||
문제가 없으면 "없음"이라고 적는다. 가정만으로 판단한 항목은 **확인하지 않았다**고 밝힌다.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: persona-user
|
||||
description: 일반 사용자 관점의 검토 담당. 화면 흐름·문구·실패 상황이 사용자에게 어떻게 보이는지 따진다. 기능을 만들기 전 수용 기준을 정하거나, 만든 뒤 사용자 관점으로 점검할 때 사용한다.
|
||||
tools: Read, Grep, Glob
|
||||
---
|
||||
|
||||
**서비스를 쓰는 일반 사용자**의 입장에서 검토하는 역할이다. 공통 계약은 `.claude/agents/README.md` 를 따른다.
|
||||
|
||||
개발자가 아니다. 코드가 올바른지가 아니라 **쓰는 사람이 막히지 않는지**를 본다.
|
||||
코드를 수정하지 않는다 — 문제를 적어 담당 역할에 인계한다.
|
||||
|
||||
## 무엇을 보는가
|
||||
|
||||
**1. 실패했을 때 무엇을 보게 되는가** ([[error-codes]])
|
||||
- 사용자에게 보이는 문구는 `backend/src/common/errors/error-codes.ts` 의 메시지가 전부다. 그 문구를 읽고 판단한다.
|
||||
- 무엇이 잘못됐는지 알 수 있는가
|
||||
- **다음에 무엇을 하면 되는지** 알 수 있는가
|
||||
- 내부 용어(엔티티명·상태 코드·스택)가 새어 나오지 않는가
|
||||
- 새로 추가된 실패 경로에 코드가 없다면 그 자체가 문제다 — 사용자는 원인 모를 실패를 보게 된다.
|
||||
- 같은 알림이 연달아 뜨지 않는가 (인터셉터가 억제하게 되어 있다)
|
||||
|
||||
**2. 기다리는 동안과 비어 있을 때**
|
||||
- 로딩 상태가 보이는가, 아무 반응 없이 멈춘 것처럼 보이지 않는가
|
||||
- 목록이 비었을 때 "없음"이 안내되는가, 빈 화면만 남지 않는가
|
||||
|
||||
**3. 흐름이 끊기지 않는가**
|
||||
- 세션이 만료되면(AUTH_002) 사용자가 무엇을 해야 하는지 알 수 있는가
|
||||
- 로그인이 필요한 화면에 바로 들어갔을 때 어디로 가는가
|
||||
- 입력이 틀렸을 때 **어느 칸이 틀렸는지** 알 수 있는가
|
||||
|
||||
**4. 접근성** ([[vue3]])
|
||||
- 버튼·입력에 이름이 붙어 있는가 (`aria-label`·`role`), 키보드만으로 쓸 수 있는가
|
||||
|
||||
## 하지 않는 것
|
||||
|
||||
- 코드 품질·구조·성능 지적 (→ `reviewer`)
|
||||
- 운영·모니터링 관점 (→ `persona-admin`)
|
||||
- 구현 방법 제안. **무엇이 불편한지**까지만 말하고 해결 방법은 담당 역할에 맡긴다.
|
||||
|
||||
## 완료 기준
|
||||
|
||||
각 항목을 이렇게 적는다.
|
||||
|
||||
```
|
||||
- 상황: (사용자가 무엇을 하다가)
|
||||
- 보게 되는 것: (실제 문구·화면. 근거 파일 위치)
|
||||
- 문제: (왜 막히는가)
|
||||
- 인계: (어느 역할)
|
||||
```
|
||||
|
||||
문제가 없으면 "없음"이라고 적는다. 확인하지 못한 화면이 있으면 **확인하지 못했다**고 적는다.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: release-ops
|
||||
description: 인프라·릴리스 담당. docker compose, 루트 env, DB 마이그레이션 적용, 배포 전 설정 점검. 컨테이너 구성이나 배포 준비가 필요할 때 사용한다.
|
||||
tools: Read, Grep, Glob, Edit, Write, Bash, Skill
|
||||
---
|
||||
|
||||
인프라와 릴리스를 담당하는 팀원이다. 공통 계약은 `.claude/agents/README.md` 를 따른다.
|
||||
|
||||
## 시작 전
|
||||
|
||||
`/docker` 스킬을 로드한다. 실행 명령과 옵션 규칙은 전부 거기에 있다.
|
||||
|
||||
## 담당 범위
|
||||
|
||||
- `docker-compose.yml` · `docker-compose.dev.yml` · 각 `Dockerfile` · `nginx.conf`
|
||||
- 루트 `.env.{env}` 구조와 `*.example` ([[env-structure]])
|
||||
- 마이그레이션 **적용**(`migration:run`)과 배포 전 점검
|
||||
- `.github/workflows/` 와 `ci/` 러너
|
||||
|
||||
## 하지 않는 것
|
||||
|
||||
- **애플리케이션 로직 수정** → 해당 개발 역할에 인계
|
||||
- **마이그레이션 파일 작성** — 엔티티를 아는 `backend-dev` 가 생성한다. 이 역할은 적용과 SQL 검토를 맡는다.
|
||||
- 이미 커밋된 마이그레이션 파일 수정 (잘못됐으면 새 마이그레이션으로 고친다)
|
||||
|
||||
## 점검 기준
|
||||
|
||||
- **빌드 최소화**: 의존성 변경이 없으면 `--build` 없이 기동한다. 의존성을 바꿨다면 `--renew-anon-volumes` 를 함께 준다.
|
||||
- **포트 노출**: DB·Redis 호스트 포트는 dev 에서만, 반드시 `127.0.0.1` 로 한정 ([[security]]).
|
||||
- **비-root 구동**: dev 오버라이드를 포함해 모든 컨테이너가 비-root 여야 한다.
|
||||
- **env 파일 존재**: 루트 `.env.{APP_ENV}` 와 `backend/.env.{APP_ENV}` 가 둘 다 있어야 기동한다.
|
||||
- **운영 배포 전**: `BACKEND_API_URL`·`CORS_ORIGIN` 이 실제 도메인인지 확인한다.
|
||||
`localhost`·`example.com` 이 남아 있으면 백엔드가 기동에 실패한다 ([[env-structure]]).
|
||||
- **프론트엔드**: 개발에서는 컨테이너로 띄우지 않는다(profile `frontend` 뒤에 있다). 운영만 nginx 정적 호스팅이다.
|
||||
|
||||
## 완료 기준 (실행하고 결과를 보고한다)
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development config # 파싱·변수 치환 확인
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development ps # 상태 확인
|
||||
```
|
||||
|
||||
- 컨테이너를 기동·종료했다면 **무엇을 띄웠고 무엇을 정리했는지** 적는다.
|
||||
- `down -v` 는 데이터 볼륨까지 지운다. 실행 전 반드시 확인을 받는다.
|
||||
- 정상 종료 시 backend 컨테이너 종료 코드 143 은 오류가 아니라 SIGTERM 에 의한 정상 종료다.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: reviewer
|
||||
description: 코드 리뷰 담당. 이 프로젝트 고유의 공유 계약 위반과 규칙 드리프트를 점검한다. 커밋·PR 전 변경분 검토가 필요할 때 사용한다.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
---
|
||||
|
||||
리뷰를 담당하는 팀원이다. 공통 계약은 `.claude/agents/README.md` 를 따른다.
|
||||
|
||||
> **일반적인 버그 사냥은 내장 `/code-review` 의 몫이다.** 이 역할은 그것이 잡지 못하는
|
||||
> **이 저장소 고유의 계약**을 본다. 범위를 넓히지 말고 아래 목록에 집중한다.
|
||||
|
||||
## 코드를 고치지 않는다
|
||||
|
||||
수정 권한 없이 **지적만** 한다. 파일·줄 위치와 근거 규칙을 함께 적어 담당 역할에 인계한다.
|
||||
고쳐야 할 것이 명백해도 직접 고치지 않는다 — 그 영역의 완료 기준(lint·test)을 건너뛰게 된다.
|
||||
|
||||
## 점검 목록
|
||||
|
||||
**1. 응답 계약** ([[response-format]])
|
||||
- 컨트롤러가 `{success,data}` 를 직접 래핑하지 않는가 (인터셉터 담당)
|
||||
- 목록 응답이 배열이 아니라 `{items, pageInfo}` 인가
|
||||
- 스트림·204·헬스체크에 `@SkipTransform()` 이 붙어 있는가
|
||||
|
||||
**2. 에러 코드 SSOT 드리프트** ([[error-codes]])
|
||||
- `error-codes.ts` 를 고쳤는데 `npm run sync:error-codes` 결과가 빠지지 않았는가
|
||||
(CI 의 `error-codes-sync` 잡이 같은 검사를 한다 — 여기서 먼저 잡는다)
|
||||
- 생성 파일(`*.generated.ts`·`*.g.dart`)을 손으로 고치지 않았는가
|
||||
- 코드 없는 실패(`BusinessException` 없이 던진 실패)가 없는가
|
||||
- 클라이언트가 `message` 로 분기하지 않는가 (`code` 로 해야 한다)
|
||||
|
||||
**3. 경계 위반**
|
||||
- 한 역할의 변경이 남의 영역 파일까지 조용히 고치지 않았는가
|
||||
- 자동 생성 파일·계약 테스트가 삭제되거나 skip 되지 않았는가
|
||||
|
||||
**4. 설정·보안** ([[security]] · [[env-structure]])
|
||||
- `.env*` 실값이 커밋에 섞이지 않았는가, `*.example` 값이 placeholder 인가
|
||||
- 새 필수 env 에 `env.validation.ts` 검증과 테스트가 함께 왔는가
|
||||
- 로그에 토큰·비밀번호·요청 본문 전체가 남지 않는가 ([[logging]])
|
||||
|
||||
**5. 커밋 위생** ([[git-convention]])
|
||||
- 메시지가 `<type>(<scope>): <한국어 제목>` 형식인가, 한 커밋에 한 변경인가
|
||||
|
||||
## 작업 순서
|
||||
|
||||
```bash
|
||||
git status --short && git diff # 무엇이 바뀌었는지부터 본다
|
||||
```
|
||||
|
||||
1. 변경 파일 목록에서 **어느 역할의 영역이 섞였는지** 먼저 확인한다.
|
||||
2. 위 목록 순서대로 본다. 목록 밖의 취향 문제는 지적하지 않는다.
|
||||
3. 지적은 **근거 규칙과 함께** 적는다. 근거를 댈 수 없으면 지적하지 않는다.
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- 지적 사항마다 `파일:줄` · 무엇이 · 어느 규칙 위반인지 · 누구에게 넘기는지를 적는다.
|
||||
- 지적할 것이 없으면 "없음"이라고 명확히 적는다. 억지로 만들지 않는다.
|
||||
- [[checklist]] 2절(커밋 전 공통 점검) 통과 여부를 함께 보고한다.
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: tester
|
||||
description: 테스트 담당. 변경 영역의 단위·계약 테스트 작성과 실행, 세 스택 테스트 수트 유지. 테스트가 빠졌거나 계약 테스트가 깨졌을 때 사용한다.
|
||||
tools: Read, Grep, Glob, Edit, Write, Bash, Skill
|
||||
---
|
||||
|
||||
테스트를 담당하는 팀원이다. 공통 계약은 `.claude/agents/README.md` 를 따른다.
|
||||
|
||||
## 시작 전
|
||||
|
||||
[[testing]] 규칙을 기준으로 삼는다. 스택별 실행 방법은 해당 스킬을 로드해 확인한다.
|
||||
|
||||
## 담당 범위
|
||||
|
||||
- 변경된 로직에 대한 단위 테스트 (backend `*.spec.ts` / frontend `*.spec.ts` / flutter `*_test.dart`)
|
||||
- **계약 테스트 유지** — 세 스택이 공유하는 계약은 어긋나도 컴파일 에러가 나지 않는다. 이 테스트가 유일한 안전망이다.
|
||||
|
||||
| 대상 | 위치 |
|
||||
| --- | --- |
|
||||
| 에러 코드 레지스트리 | `backend/src/common/errors/error-codes.spec.ts` |
|
||||
| 에러 응답 포맷 | `backend/src/common/filters/http-exception.filter.spec.ts` |
|
||||
| 프론트 에러 사전 | `frontend/src/composables/useApi.spec.ts` |
|
||||
| 앱 에러 사전 | `flutter/test/error_lexicon_test.dart` |
|
||||
| 응답 계약 (e2e) | `backend/test/app.e2e-spec.ts` |
|
||||
|
||||
## 하지 않는 것
|
||||
|
||||
- **계약 테스트를 삭제하거나 skip 하지 않는다.** 깨졌다면 테스트가 아니라 **구현이 계약을 벗어난 것**이다.
|
||||
구현을 고칠 권한이 없으면 무엇이 어떻게 어긋났는지 적어 해당 개발 역할에 인계한다.
|
||||
- **테스트를 통과시키기 위한 구현 수정** — 테스트 외 파일을 고쳐야 한다면 인계한다.
|
||||
- 프레임워크 기본 동작(라우팅·DI·ORM 쿼리 빌더)이나 스타일 값 테스트 ([[testing]])
|
||||
|
||||
## 작업 순서
|
||||
|
||||
1. 무엇이 바뀌었는지 먼저 본다 (`git diff`). 바뀐 로직에만 테스트를 붙인다.
|
||||
2. 순수 함수 → 서비스/비즈니스 로직 → 에러 코드 분기 순으로 우선한다.
|
||||
3. 외부 네트워크·시간·랜덤은 모킹해 **결정적**으로 만든다.
|
||||
4. 테스트 제목은 한국어로 "무엇을 검증하는지" 적는다.
|
||||
|
||||
## 완료 기준 (실행하고 결과를 보고한다)
|
||||
|
||||
```bash
|
||||
cd backend && npm run test
|
||||
cd frontend && npm run test:unit
|
||||
cd flutter && flutter test
|
||||
```
|
||||
|
||||
- e2e 는 DB 가 필요하다. 로컬이면 개발 스택을 띄운 뒤 컨테이너 안에서 실행한다 ([[testing]]).
|
||||
- 실행하지 못한 수트가 있으면 **어느 것을 왜 못 돌렸는지** 반드시 적는다. 통과했다고 뭉뚱그리지 않는다.
|
||||
@@ -8,13 +8,16 @@
|
||||
## 1. 신규 프로젝트 셋업 (클론 직후 1회)
|
||||
|
||||
- [ ] 루트 `.env.{env}.example` → `.env.{env}` 복사 후 **placeholder 값 교체** (`change-me-*` 가 남아 있으면 안 된다, [[env-structure]])
|
||||
운영은 `BACKEND_API_URL`·`CORS_ORIGIN` 을 **실제 도메인**으로 — `localhost`/`example.com` 이 남으면 기동 실패한다
|
||||
- [ ] `backend/.env.{env}.example` → `backend/.env.{env}` 복사 후 `JWT_SECRET` 등 고유 시크릿 작성 (compose `env_file` 로 로드 — 파일이 없으면 기동 실패)
|
||||
- [ ] `flutter/.env.example` → `flutter/.env` 복사 후 `API_URL` 확인 (Android 에뮬레이터: `10.0.2.2`)
|
||||
— **이 파일이 없으면 `flutter analyze` 가 asset 경고를 낸다**
|
||||
- [ ] 프로젝트 식별 값 교체 (`PROJECT_NAME` 등 루트 env 키)
|
||||
- [ ] 의존성 설치: `cd backend && npm ci`, `cd frontend && npm ci`, `cd flutter && flutter pub get`
|
||||
- [ ] Docker 개발 환경 기동으로 동작 확인 ([[docker]])
|
||||
(Redis 는 기본 비활성이다 — 필요하면 루트 `.env` 에 `COMPOSE_PROFILES=redis`)
|
||||
- [ ] Docker 개발 환경 기동으로 동작 확인 — backend + db ([[docker]])
|
||||
(Redis 와 프론트엔드 컨테이너는 기본 비활성이다 — 필요하면 루트 `.env` 에 `COMPOSE_PROFILES=redis,frontend`)
|
||||
- [ ] 프론트엔드 dev 서버는 **호스트에서** 기동 — `cd frontend && npm run dev` (→ <http://localhost:5173>)
|
||||
컨테이너 vite 는 바인드 마운트 때문에 첫 서빙까지 수 분이 걸려 profile 뒤로 숨겨 두었다 ([[docker]])
|
||||
|
||||
## 2. 커밋 전 공통 점검 (매 작업)
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{
|
||||
├─▶ backend : env_file + environment 로 DB/Redis/CORS/포트 주입
|
||||
│ (backend/.env.{APP_ENV} 는 "존재"만 하면 됨, 내용 최소)
|
||||
├─▶ frontend : BACKEND_API_URL → build arg(VITE_API_BASE_URL) 로 주입
|
||||
│ (호스트에서 `npm run dev` 로 띄울 때는 compose 를 거치지 않으므로
|
||||
│ vite.config.ts 가 이 파일을 `loadEnv` 로 직접 읽어 FRONTEND_PORT/BACKEND_PORT 사용)
|
||||
├─▶ db : DB_USER/DB_PASSWORD/DB_NAME, DB_DATA_DIR 볼륨
|
||||
└─▶ redis : REDIS_PASSWORD, REDIS_DATA_DIR 볼륨
|
||||
```
|
||||
@@ -26,6 +28,10 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{
|
||||
숫자 설정값은 `shared/utils/env.ts` 의 `parsePositiveInt()` 처럼 빈 문자열까지 방어하는 파서를 거친다.
|
||||
(기존 `.env` 를 쓰는 사람이 새 키를 추가하지 않아도 안전해야 한다)
|
||||
- `docker-compose.yml` 의 `env_file` 지시자 때문에 `backend/.env.{APP_ENV}` 파일은 비어 있어도 **존재해야 한다.**
|
||||
- **프론트엔드 dev 서버는 compose 를 거치지 않는다** — 호스트에서 직접 띄우므로([[docker]])
|
||||
`vite.config.ts` 가 루트 `.env.{mode}` 를 `loadEnv` 로 읽어 포트·프록시 대상을 정한다.
|
||||
포트를 `frontend/.env` 나 `vite.config.ts` 에 중복 작성하지 말 것 — 루트가 SSOT 다.
|
||||
(읽은 값은 dev 서버 설정에만 쓰고 클라이언트 번들에는 넣지 않는다 — `VITE_` 없는 값의 번들 노출 금지, [[security]])
|
||||
- **Flutter 는 예외** — docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립적으로 관리한다.
|
||||
- 모든 env 파일은 커밋 금지, `*.example` 만 커밋한다. (`.gitignore` 의 `!*.example` 재포함 규칙)
|
||||
- **설정이 잘못되면 서버가 뜨지 않아야 한다(fail-fast).** 조용히 기본값으로 굴러가다 운영에서 터지는 것이 더 위험하다.
|
||||
@@ -33,6 +39,10 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{
|
||||
- 숫자 항목은 형식 검사 (모든 환경)
|
||||
- 운영 환경: `DB_HOST`/`DB_USER`/`DB_PASSWORD`/`DB_NAME` 필수
|
||||
- 운영 환경: `change-me`·`password` 같은 **예시 값이 그대로 배포되는 것을 차단**
|
||||
- 운영 환경: `CORS_ORIGIN`·`BACKEND_API_URL` 에 **로컬 주소(`localhost`·`127.0.0.1` 등)나
|
||||
예시 도메인(`example.com`)이 남아 있으면 차단** — 기동은 되는데 브라우저만 조용히 실패하는 조합을 막는다.
|
||||
로컬에서 운영 이미지를 검증하느라 일부러 localhost 를 쓸 때만 `ALLOW_LOCALHOST_URL=true` 로 건너뛴다
|
||||
(`BACKEND_API_URL` 은 앱이 쓰지 않지만 이 검사를 위해 compose 가 backend 에 주입한다)
|
||||
- 문제는 하나씩이 아니라 **한 번에 모아서** 보여준다 (재기동 반복 방지)
|
||||
- 새 필수 설정을 추가하면 이 파일의 목록과 테스트도 함께 갱신한다
|
||||
|
||||
|
||||
@@ -12,8 +12,12 @@ description: Docker Compose 기반 로컬·운영 오케스트레이션 실행/
|
||||
|
||||
## 개발 환경
|
||||
|
||||
> **개발 환경에서 프론트엔드는 컨테이너로 뜨지 않는다.** compose profile `frontend` 뒤에 숨겨져 있다.
|
||||
> vite 를 컨테이너에서 돌리면 소스가 바인드 마운트(`./frontend:/app`) 너머라 첫 서빙까지 수 분이 걸린다.
|
||||
> 프론트엔드는 호스트에서 `cd frontend && npm run dev` 로 띄운다 (아래 "프론트엔드는 호스트에서" 참조).
|
||||
|
||||
```bash
|
||||
# 코드만 수정했을 때 (의존성 변경 없음)
|
||||
# 코드만 수정했을 때 (의존성 변경 없음) — backend + db 가 뜬다
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d
|
||||
|
||||
# 의존성 변경 시에만 --build
|
||||
@@ -25,6 +29,10 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.d
|
||||
|
||||
## 운영 환경
|
||||
|
||||
> ⚠️ `.env.production` 의 `BACKEND_API_URL`·`CORS_ORIGIN` 을 **실제 도메인으로 교체**해야 한다.
|
||||
> `localhost` 나 예시 도메인(`example.com`)이 남아 있으면 백엔드가 기동에 실패한다 ([[env-structure]]).
|
||||
> 로컬에서 운영 이미지를 검증하느라 일부러 localhost 를 쓰는 경우에만 `ALLOW_LOCALHOST_URL=true` 를 함께 준다.
|
||||
|
||||
```bash
|
||||
# 운영 환경 배포
|
||||
sudo docker compose --env-file .env.production up -d --build
|
||||
@@ -35,6 +43,44 @@ sudo docker compose --env-file .env.production logs -f backend
|
||||
|
||||
---
|
||||
|
||||
## 프론트엔드는 호스트에서 (개발 환경)
|
||||
|
||||
```bash
|
||||
cd frontend && npm install && npm run dev # → http://localhost:5173
|
||||
```
|
||||
|
||||
- **포트는 루트 `.env.{APP_ENV}` 가 SSOT 다** ([[env-structure]]). `vite.config.ts` 가 루트 env 를 직접 읽어
|
||||
`FRONTEND_PORT` 로 열고, `/api` 프록시 대상은 `http://localhost:${BACKEND_PORT}`(백엔드 컨테이너가
|
||||
호스트에 노출한 포트)로 잡는다. 포트를 바꿀 때 루트 env 한 줄만 고치면 된다.
|
||||
백엔드를 아예 다른 주소에 띄웠다면 `VITE_DEV_API_PROXY` 로 대상을 덮어쓴다.
|
||||
- `strictPort: true` 라 포트가 이미 사용 중이면 **다음 포트로 옮기지 않고 실패**한다.
|
||||
`CORS_ORIGIN` 과 compose 포트 매핑이 그 포트에 묶여 있어, 조용히 5174 로 옮겨가면
|
||||
API 요청이 원인 모르게 막힌다 (fail-fast).
|
||||
- 폴링 감시(`watch.usePolling`)는 **컨테이너에서만** 켜진다 (`CHOKIDAR_USEPOLLING` 로 판별).
|
||||
호스트에서는 OS 파일 감시 이벤트로 충분하며, 폴링을 켜면 기동이 느려지고 CPU 만 쓴다.
|
||||
- 의존성 설치는 호스트에서 해도 된다 — 프론트엔드 dev 는 컨테이너를 쓰지 않으므로
|
||||
lock 파일이 깨지는 문제([[nestjs]] 의 Windows 주의 항목)는 **backend 에만** 해당한다.
|
||||
단 `frontend` 프로필이나 운영 이미지를 빌드할 계획이라면 같은 주의가 적용된다.
|
||||
|
||||
### 컨테이너로 띄워야 할 때 (선택)
|
||||
|
||||
호스트에 Node 를 둘 수 없는 환경 등에서만 쓴다. 느린 것은 정상이다.
|
||||
|
||||
```bash
|
||||
# 일회성
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development --profile frontend up -d
|
||||
|
||||
# 상시 — 루트 .env.development 에 (redis 와 함께 켤 때는 frontend,redis)
|
||||
COMPOSE_PROFILES=frontend
|
||||
```
|
||||
|
||||
- 호스트 vite 와 컨테이너 vite 는 같은 호스트 포트(`FRONTEND_PORT`)를 쓴다 — **동시에 띄우지 말 것.**
|
||||
- 컨테이너 **내부** 포트는 5173 고정이다 (compose 의 `--port`·healthcheck·포트 매핑이 함께 묶여 있다).
|
||||
루트 env 는 컨테이너 안에 없으므로 `vite.config.ts` 는 기본값으로 떨어지고, compose 의 `--port` 가 이긴다.
|
||||
- 운영(`docker-compose.yml` 단독)에는 profile 이 없으므로 nginx 정적 호스팅이 그대로 동작한다.
|
||||
|
||||
---
|
||||
|
||||
## 자주 쓰는 보조 명령
|
||||
|
||||
> 개발 prefix가 길어 아래는 다음 별칭으로 줄여 표기한다.
|
||||
@@ -103,6 +149,7 @@ docker compose -f docker-compose.runner.yml --env-file .env up -d
|
||||
```bash
|
||||
# 켜기 — 루트 .env.{APP_ENV} 한 줄 수정 (이후 평소 명령 그대로)
|
||||
COMPOSE_PROFILES=redis
|
||||
# (프론트엔드 컨테이너도 함께 켤 때는 COMPOSE_PROFILES=redis,frontend)
|
||||
|
||||
# 또는 일회성
|
||||
dc --profile redis up -d
|
||||
@@ -136,6 +183,7 @@ dc --profile redis up -d
|
||||
- 정상 종료 시 컨테이너 **종료 코드는 143**(=128+SIGTERM)이다. 오류가 아니라 시그널에 의한 정상 종료다.
|
||||
- ⚠️ dev 오버라이드는 PID 1 이 `npm` 이라 SIGTERM 이 앱까지 전달되지 않는다(개발 편의상 허용).
|
||||
graceful shutdown 동작 확인은 **운영 이미지**(`--target production`)로 해야 한다.
|
||||
이때 로컬 주소를 쓰게 되므로 `ALLOW_LOCALHOST_URL=true` 로 공개 주소 검사를 건너뛴다 ([[env-structure]]).
|
||||
- **컨테이너 비-root 구동**: 모든 서비스 컨테이너는 비-root 로 실행한다 — **dev 오버라이드도 포함** ([[security]] 참조).
|
||||
- 프론트엔드 운영 컨테이너는 `nginx-unprivileged` 라 **8080** 포트를 쓴다 (`expose`·healthcheck·nginx.conf 가 모두 8080).
|
||||
- 프론트엔드 dev 컨테이너는 vite 를 **5173** 으로 띄운다 (비-root 는 80 포트 바인드 불가).
|
||||
|
||||
@@ -28,6 +28,12 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API
|
||||
| 라우터 | `src/router/index.ts` | lazy import + `auth.store` 연동 `requiresAuth` 가드 |
|
||||
| env 타입 선언 | `frontend/env.d.ts` | `VITE_API_BASE_URL` |
|
||||
| dev API 프록시 | `vite.config.ts` 의 `server.proxy` | `/api` → 백엔드. 없으면 vite 가 SPA 폴백으로 **HTML 을 200 으로** 돌려줘 조용히 실패한다 |
|
||||
| dev 서버 포트 | `vite.config.ts` 의 `loadEnv` | 루트 `.env.{mode}` 의 `FRONTEND_PORT`/`BACKEND_PORT` 를 읽는다. `strictPort` 로 포트 충돌 시 fail-fast ([[env-structure]]) |
|
||||
|
||||
> **개발 서버는 호스트에서 띄운다** — `cd frontend && npm run dev` (→ `http://localhost:${FRONTEND_PORT}`).
|
||||
> 컨테이너 vite 는 바인드 마운트 때문에 첫 서빙에 수 분이 걸려 compose profile `frontend` 뒤로 숨겼다 ([[docker]]).
|
||||
> 백엔드·DB 는 평소대로 컨테이너로 띄우고, `/api` 는 vite 프록시가 `localhost:${BACKEND_PORT}` 로 넘긴다.
|
||||
> 두 포트 모두 `vite.config.ts` 가 **루트 `.env.{mode}` 에서 직접 읽는다** — 프론트 쪽에 포트를 중복 작성하지 않는다.
|
||||
|
||||
> **확장 시 따르는 규칙:**
|
||||
> - **로그인 연동**: 실제 `/auth/login` 엔드포인트 연결 → 성공 시 `useAuth` 가 `authStore.markAuthenticated()` 호출
|
||||
@@ -217,5 +223,8 @@ router.beforeEach((to) => {
|
||||
- [ ] **lint/test**: 커밋 전 `npm run lint` (`--max-warnings 0`) + `npm run type-check` + `npm run test:unit` 통과. 자동 수정은 `npm run lint:fix`.
|
||||
- [ ] **린트로 강제되는 항목**: Options API·인라인 스타일·`console`·`any` 는 빌드가 실패한다.
|
||||
- [ ] **env**: `VITE_API_BASE_URL` 은 빌드 시 루트 `BACKEND_API_URL` 에서 주입된다 — `frontend/.env` 에 별도 작성 불필요.
|
||||
- [ ] **dev 서버**: 프론트엔드는 호스트에서 `npm run dev` 로 띄운다 (컨테이너는 `--profile frontend` 선택, [[docker]]).
|
||||
- [ ] **포트**: `vite.config.ts` 에 포트를 새로 하드코딩하지 않는다 — 루트 `.env.{mode}` 의 `FRONTEND_PORT`/`BACKEND_PORT` 가 SSOT ([[env-structure]]).
|
||||
- [ ] **dev 에서 API 가 안 될 때**: 응답이 JSON 이 아니라 HTML 이면 vite 프록시(`server.proxy`) 문제다.
|
||||
`baseURL` 이 `/api` 상대경로라 프록시가 없으면 index.html 이 200 으로 돌아온다.
|
||||
백엔드 컨테이너가 떠 있는지(`docker compose ... ps`), 프록시 대상 포트가 맞는지 확인한다.
|
||||
|
||||
@@ -9,6 +9,10 @@ BACKEND_DIR=backend
|
||||
|
||||
# ==========================================
|
||||
# 포트 설정
|
||||
#
|
||||
# 컨테이너 포트 매핑뿐 아니라 **호스트에서 띄우는 vite dev 서버**도 이 값을 따른다.
|
||||
# (frontend/vite.config.ts 가 이 파일을 직접 읽는다 — FRONTEND_PORT 로 열고
|
||||
# /api 프록시는 BACKEND_PORT 로 보낸다. 포트를 바꾸면 CORS_ORIGIN 도 함께 고칠 것)
|
||||
# ==========================================
|
||||
FRONTEND_PORT=5173
|
||||
BACKEND_PORT=3000
|
||||
@@ -57,10 +61,16 @@ DB_SYNCHRONIZE=false
|
||||
# ==========================================
|
||||
# 선택적 서비스 (Compose profiles)
|
||||
#
|
||||
# 기본값(비어 있음) = redis 를 기동하지 않는다.
|
||||
# 캐시·세션 공유가 필요해지면 아래를 redis 로 바꾸면 컨테이너가 함께 뜬다.
|
||||
# COMPOSE_PROFILES=redis
|
||||
# 기본값(비어 있음) = redis 와 frontend 컨테이너를 기동하지 않는다.
|
||||
#
|
||||
# redis 캐시·세션 공유가 필요해질 때 → COMPOSE_PROFILES=redis
|
||||
# frontend vite 를 컨테이너로 띄워야 할 때 → COMPOSE_PROFILES=frontend
|
||||
# 둘 다 → COMPOSE_PROFILES=redis,frontend
|
||||
#
|
||||
# 일회성으로 켜려면: docker compose --profile redis ... up -d
|
||||
#
|
||||
# ⚠️ 개발 환경의 프론트엔드는 호스트에서 띄우는 것이 기본이다 (`cd frontend && npm run dev`).
|
||||
# 컨테이너 vite 는 바인드 마운트 때문에 첫 서빙까지 수 분이 걸린다.
|
||||
# ==========================================
|
||||
COMPOSE_PROFILES=
|
||||
|
||||
|
||||
+13
-2
@@ -34,9 +34,20 @@ REDIS_DATA_DIR=./data/redis
|
||||
|
||||
# ==========================================
|
||||
# API 경로 설정
|
||||
#
|
||||
# ⚠️ 운영에서는 **반드시 실제 공개 도메인**으로 교체한다.
|
||||
# - BACKEND_API_URL : 브라우저가 호출할 API 주소 (프론트엔드 번들에 그대로 박힌다)
|
||||
# - CORS_ORIGIN : 백엔드가 허용할 오리진 (scheme+host[+port], 여러 개면 콤마 구분)
|
||||
#
|
||||
# 아래 example.com 이나 localhost 가 남아 있으면 **백엔드가 기동에 실패한다**
|
||||
# (backend/src/shared/config/env.validation.ts — fail-fast).
|
||||
# 기동은 되는데 브라우저만 조용히 실패하는 상황을 막기 위한 검사다.
|
||||
#
|
||||
# 로컬에서 운영 이미지를 검증하느라 일부러 localhost 를 쓰는 경우에만
|
||||
# ALLOW_LOCALHOST_URL=true 로 이 검사를 건너뛴다. 실제 배포에는 설정하지 않는다.
|
||||
# ==========================================
|
||||
BACKEND_API_URL=http://localhost:3000/api
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
BACKEND_API_URL=https://api.example.com/api
|
||||
CORS_ORIGIN=https://app.example.com
|
||||
|
||||
# ==========================================
|
||||
# Rate Limiting (API 남용 방지)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
> - `.claude/rules/` — 세 플랫폼 공통 규칙 (코딩·응답 포맷·에러 코드·API 설계·로깅·커밋·보안·환경변수·테스트)
|
||||
> 새 규칙 파일을 추가하면 **반드시 아래 `@` import 목록에도 등록**한다(등록하지 않으면 로드되지 않는다).
|
||||
> - `.claude/skills/` — 도메인별 개발 규칙 (스킬). 슬래시 명령(`/nestjs` 등) 또는 해당 디렉터리 작업 시 자동 적용
|
||||
> - `.claude/agents/` — 역할별 규칙 (에이전트 팀원). 역할을 지정해 호출할 때 적용
|
||||
> **`@` import 하지 않는다** — 규칙이 아니라 호출 시점에 로드되는 역할 정의다 (아래 목록 참조)
|
||||
> - `.claude/settings.json` — Claude Code 권한·환경 설정
|
||||
|
||||
---
|
||||
@@ -40,3 +42,29 @@
|
||||
- 프론트엔드(Vue 3): @.claude/skills/vue3/SKILL.md
|
||||
- 모바일(Flutter): @.claude/skills/flutter/SKILL.md
|
||||
- 실행/배포(Docker): @.claude/skills/docker/SKILL.md
|
||||
|
||||
---
|
||||
|
||||
## 역할별 규칙 (에이전트 팀원)
|
||||
|
||||
세 층의 역할 분담은 다음과 같다. **규칙은 한 곳에만 적는다** — 역할 파일은 규칙을 참조만 한다.
|
||||
|
||||
```
|
||||
.claude/rules/ 무엇을 지키는가 — 전원 공통 (위 @import 목록)
|
||||
.claude/skills/ 어떻게 만드는가 — 스택별
|
||||
.claude/agents/ 누가 맡는가 — 역할별
|
||||
```
|
||||
|
||||
| 역할 | 파일 | 코드 수정 |
|
||||
| --- | --- | --- |
|
||||
| 백엔드 개발 | `.claude/agents/backend-dev.md` | O |
|
||||
| 프론트엔드 개발 | `.claude/agents/frontend-dev.md` | O |
|
||||
| 모바일 개발 | `.claude/agents/mobile-dev.md` | O |
|
||||
| 테스터 | `.claude/agents/tester.md` | O (테스트 한정) |
|
||||
| 리뷰어 | `.claude/agents/reviewer.md` | X |
|
||||
| 인프라/릴리스 | `.claude/agents/release-ops.md` | O (인프라 한정) |
|
||||
| 사용자 관점 | `.claude/agents/persona-user.md` | X |
|
||||
| 관리자 관점 | `.claude/agents/persona-admin.md` | X |
|
||||
|
||||
공통 계약(경계·인계 형식·완료 기준)은 [`.claude/agents/README.md`](.claude/agents/README.md) 참조.
|
||||
역할을 지정하지 않으면 평소대로 동작한다 — 모든 작업을 역할로 나눌 필요는 없다.
|
||||
|
||||
@@ -28,11 +28,14 @@ cp .env.development.example .env.development # ← 여기
|
||||
cp backend/.env.development.example backend/.env.development # (존재 필요: JWT 등 고유 시크릿)
|
||||
# frontend 는 compose 가 빌드 시 값을 주입하므로 자체 env 복사 불필요
|
||||
|
||||
# 2) 개발 환경 기동 (코드만 수정 시)
|
||||
# 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
|
||||
```
|
||||
|
||||
기동 후:
|
||||
@@ -41,8 +44,31 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.d
|
||||
- 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`](./.claude/skills/docker/SKILL.md) 참조.
|
||||
|
||||
## 모바일 앱 실행 (Flutter)
|
||||
@@ -112,7 +138,7 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.d
|
||||
# 2) backend — DB_HOST 기본값이 localhost 라 그대로 붙는다
|
||||
cd backend && npm ci && npm run start:dev
|
||||
|
||||
# 3) frontend — vite dev 서버가 /api 를 localhost:3000 으로 프록시한다
|
||||
# 3) frontend — vite dev 서버가 /api 를 localhost:3000 으로 프록시한다 (개발 기본 방식)
|
||||
cd frontend && npm ci && npm run dev
|
||||
|
||||
# 4) flutter
|
||||
@@ -152,6 +178,7 @@ lint 는 `--max-warnings 0` 이라 경고 하나만 있어도 실패합니다.
|
||||
| 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) |
|
||||
|
||||
## 디렉터리 구조
|
||||
|
||||
@@ -170,5 +197,6 @@ lint 는 `--max-warnings 0` 이라 경고 하나만 있어도 실패합니다.
|
||||
└── .claude/
|
||||
├── settings.json # Claude Code 권한·환경 설정
|
||||
├── rules/ # 공통 규칙 (코딩·응답 포맷·에러 코드·API 설계·로깅·보안·환경변수·테스트)
|
||||
└── skills/ # 도메인별 개발 규칙 (nestjs·vue3·flutter·docker)
|
||||
├── skills/ # 도메인별 개발 규칙 (nestjs·vue3·flutter·docker)
|
||||
└── agents/ # 역할별 규칙 (개발 3종·테스터·리뷰어·인프라·사용자/관리자 관점)
|
||||
```
|
||||
|
||||
@@ -61,6 +61,55 @@ describe('validateEnv', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('공개 주소에 로컬 주소가 남아 있으면 실패한다', () => {
|
||||
expect(() =>
|
||||
validateEnv(prod({ CORS_ORIGIN: 'http://localhost:5173' })),
|
||||
).toThrow(/CORS_ORIGIN 가 운영에서 쓸 수 없는 주소입니다/);
|
||||
|
||||
expect(() =>
|
||||
validateEnv(prod({ BACKEND_API_URL: 'http://127.0.0.1:3000/api' })),
|
||||
).toThrow(/BACKEND_API_URL 가 운영에서 쓸 수 없는 주소입니다/);
|
||||
});
|
||||
|
||||
it('예시 파일의 example.com 이 그대로면 실패한다', () => {
|
||||
expect(() =>
|
||||
validateEnv(prod({ CORS_ORIGIN: 'https://app.example.com' })),
|
||||
).toThrow(/CORS_ORIGIN 가 운영에서 쓸 수 없는 주소입니다/);
|
||||
});
|
||||
|
||||
it('실제 도메인이면 통과한다', () => {
|
||||
expect(() =>
|
||||
validateEnv(
|
||||
prod({
|
||||
CORS_ORIGIN: 'https://app.myservice.io',
|
||||
BACKEND_API_URL: 'https://api.myservice.io/api',
|
||||
}),
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('ALLOW_LOCALHOST_URL=true 면 공개 주소 검사를 건너뛴다 (운영 이미지 로컬 검증용)', () => {
|
||||
expect(() =>
|
||||
validateEnv(
|
||||
prod({
|
||||
CORS_ORIGIN: 'http://localhost:5173',
|
||||
BACKEND_API_URL: 'http://localhost:3000/api',
|
||||
ALLOW_LOCALHOST_URL: 'true',
|
||||
}),
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('공개 주소 검사는 운영 환경에서만 동작한다', () => {
|
||||
expect(() =>
|
||||
validateEnv({
|
||||
NODE_ENV: 'development',
|
||||
CORS_ORIGIN: 'http://localhost:5173',
|
||||
BACKEND_API_URL: 'http://localhost:3000/api',
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('문제가 여러 개면 한 번에 모두 보여준다', () => {
|
||||
const partial = omit(prod(), 'DB_HOST', 'DB_NAME');
|
||||
|
||||
|
||||
@@ -33,6 +33,29 @@ const NO_PLACEHOLDER_IN_PRODUCTION = [
|
||||
'JWT_SECRET',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 운영에서 **공개 주소**여야 하는 키.
|
||||
* 로컬 주소가 남은 채 배포되면 서버는 정상 기동하지만
|
||||
* 브라우저가 API 에 닿지 못하거나 CORS 화이트리스트가 실제 도메인을 막아 **조용히** 실패한다.
|
||||
*/
|
||||
const PUBLIC_URL_IN_PRODUCTION = ['CORS_ORIGIN', 'BACKEND_API_URL'] as const;
|
||||
|
||||
/** 공개 주소로 쓸 수 없는 값 — 로컬 주소와 예시 파일의 예시 도메인 */
|
||||
const NON_PUBLIC_URL_PATTERNS = [
|
||||
/localhost/i,
|
||||
/127\.0\.0\.1/,
|
||||
/\[::1\]/,
|
||||
/0\.0\.0\.0/,
|
||||
/example\.com/i,
|
||||
];
|
||||
|
||||
/**
|
||||
* 위 공개 주소 검사를 건너뛰는 탈출구.
|
||||
* 로컬에서 **운영 이미지를 검증**할 때만 쓴다 (예: graceful shutdown 확인 — .claude/skills/docker/SKILL.md).
|
||||
* 실제 배포에서는 절대 설정하지 않는다.
|
||||
*/
|
||||
const ALLOW_LOCALHOST_URL_KEY = 'ALLOW_LOCALHOST_URL';
|
||||
|
||||
function readString(
|
||||
config: Record<string, unknown>,
|
||||
key: string,
|
||||
@@ -47,6 +70,10 @@ function looksLikePlaceholder(value: string): boolean {
|
||||
return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value));
|
||||
}
|
||||
|
||||
function looksNonPublic(value: string): boolean {
|
||||
return NON_PUBLIC_URL_PATTERNS.some((pattern) => pattern.test(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 환경변수를 검증한다. 문제가 있으면 **모든 문제를 모아** 예외를 던진다.
|
||||
* (하나씩 고쳐가며 재기동하는 낭비를 막기 위해 한 번에 보여준다)
|
||||
@@ -85,6 +112,22 @@ export function validateEnv(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 공개 주소가 로컬 주소·예시 도메인으로 남은 채 배포되는 것을 막는다
|
||||
const allowLocalhostUrl =
|
||||
readString(config, ALLOW_LOCALHOST_URL_KEY) === 'true';
|
||||
|
||||
if (!allowLocalhostUrl) {
|
||||
for (const key of PUBLIC_URL_IN_PRODUCTION) {
|
||||
const value = readString(config, key);
|
||||
if (value !== undefined && looksNonPublic(value)) {
|
||||
errors.push(
|
||||
`${key} 가 운영에서 쓸 수 없는 주소입니다. 실제 도메인으로 교체해 주세요. (현재 값: "${value}")` +
|
||||
` — 로컬에서 운영 이미지를 검증하는 중이라면 ${ALLOW_LOCALHOST_URL_KEY}=true`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
|
||||
+18
-1
@@ -1,5 +1,20 @@
|
||||
services:
|
||||
# ----------------------------------------
|
||||
# frontend — dev 에서는 기본 기동하지 않는다 (profile 로 숨김)
|
||||
#
|
||||
# vite 를 컨테이너에서 돌리면 소스가 Windows 바인드 마운트(./frontend:/app) 너머에 있어
|
||||
# 수천 개 파일을 매 요청마다 가상 파일시스템으로 읽는다. 여기에 폴링 감시까지 겹쳐
|
||||
# 첫 서빙까지 수 분이 걸린다. 호스트에서 직접 띄우면 같은 작업이 1~2초다.
|
||||
#
|
||||
# cd frontend && npm install && npm run dev # → http://localhost:5173
|
||||
#
|
||||
# vite dev 프록시가 /api 를 localhost:${BACKEND_PORT} 로 넘기므로 백엔드는 컨테이너 그대로 쓴다.
|
||||
# 그래도 컨테이너로 띄워야 한다면 (호스트에 Node 를 깔 수 없는 환경 등):
|
||||
# docker compose ... --profile frontend up -d
|
||||
# 또는 루트 .env.development 에 COMPOSE_PROFILES=frontend
|
||||
# ----------------------------------------
|
||||
frontend:
|
||||
profiles: ["frontend"]
|
||||
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (수동 up 때만 실행)
|
||||
restart: "no"
|
||||
build:
|
||||
@@ -12,6 +27,8 @@ services:
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5173/ || exit 1"]
|
||||
environment:
|
||||
# 바인드 마운트 너머의 변경은 inotify 가 잡지 못하므로 컨테이너에서만 폴링을 켠다
|
||||
# (vite.config.ts 가 이 값을 보고 watch.usePolling 을 결정한다 — 호스트 실행 시엔 꺼진다)
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
- WATCHPACK_POLLING=true
|
||||
# vite dev 서버가 /api 요청을 백엔드 컨테이너로 프록시한다 (vite.config.ts)
|
||||
@@ -60,4 +77,4 @@ services:
|
||||
|
||||
# base의 proxy 외부 네트워크 의존을 dev에서 제거
|
||||
networks:
|
||||
proxy: !reset null
|
||||
proxy: !reset null
|
||||
|
||||
@@ -55,6 +55,11 @@ services:
|
||||
- PORT=${BACKEND_PORT}
|
||||
- WEBSOCKET_PORT=${WEBSOCKET_PORT}
|
||||
- CORS_ORIGIN=${CORS_ORIGIN}
|
||||
# 앱이 직접 쓰지는 않는다. 로컬 주소가 남은 채 운영에 배포되는 것을
|
||||
# 기동 시점에 잡기 위해 검증용으로 주입한다 (shared/config/env.validation.ts)
|
||||
- BACKEND_API_URL=${BACKEND_API_URL}
|
||||
# 위 공개 주소 검사를 건너뛰는 탈출구 — 로컬에서 운영 이미지를 검증할 때만 true
|
||||
- ALLOW_LOCALHOST_URL=${ALLOW_LOCALHOST_URL:-}
|
||||
- THROTTLE_TTL_MS=${THROTTLE_TTL_MS:-60000}
|
||||
- THROTTLE_LIMIT=${THROTTLE_LIMIT:-100}
|
||||
- DB_HOST=db
|
||||
|
||||
+69
-28
@@ -1,39 +1,80 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
/** 루트 .env 에 값이 없을 때만 쓰는 기본 포트 (.env.*.example 과 같은 값) */
|
||||
const DEFAULT_FRONTEND_PORT = 5173
|
||||
const DEFAULT_BACKEND_PORT = 3000
|
||||
|
||||
/**
|
||||
* 환경변수를 양의 정수로 파싱한다.
|
||||
* 값이 없거나(undefined·빈 문자열) 숫자가 아니거나 0 이하면 기본값을 반환한다.
|
||||
* (compose 는 정의되지 않은 변수를 빈 문자열로 주입한다 — `Number('') === 0` 방어.
|
||||
* backend 의 `shared/utils/env.ts` 와 같은 규칙)
|
||||
*/
|
||||
function parsePositiveInt(value: string | undefined, fallback: number): number {
|
||||
if (value === undefined || value.trim() === '') return fallback
|
||||
|
||||
const parsed = Number(value)
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) return fallback
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 포트·주소의 SSOT 는 루트 `.env.{mode}` 다 (.claude/rules/env-structure.md).
|
||||
// 프론트엔드 디렉터리 밖이라 vite 가 자동으로 읽지 않으므로 여기서 직접 로드한다.
|
||||
// 접두어 ''(전체)로 읽지만, 읽은 값은 **dev 서버 설정에만** 쓰고 클라이언트 번들에는 넣지 않는다
|
||||
// (VITE_ 접두어가 없는 값을 번들에 넣으면 시크릿이 노출된다 — .claude/rules/security.md).
|
||||
// 파일이 없으면(클론 직후 등) process.env 만 반영되고 아래 기본값으로 떨어진다.
|
||||
const rootEnv = loadEnv(mode, fileURLToPath(new URL('..', import.meta.url)), '')
|
||||
|
||||
const frontendPort = parsePositiveInt(rootEnv.FRONTEND_PORT, DEFAULT_FRONTEND_PORT)
|
||||
const backendPort = parsePositiveInt(rootEnv.BACKEND_PORT, DEFAULT_BACKEND_PORT)
|
||||
|
||||
// 컨테이너(바인드 마운트) 환경에서만 true — docker-compose.dev.yml 이 주입한다.
|
||||
// 호스트에서 직접 `npm run dev` 로 띄울 때는 OS 파일 감시 이벤트가 정상 동작하므로
|
||||
// 폴링을 켜면 CPU 만 쓰고 기동이 느려진다.
|
||||
const usePolling = rootEnv.CHOKIDAR_USEPOLLING === 'true'
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// ngrok 등 외부 도메인 접근 허용 (DNS Rebinding 보호 비활성화)
|
||||
allowedHosts: true,
|
||||
// 도커/윈도우 환경 파일 핫 리로딩 강제 활성화
|
||||
watch: {
|
||||
usePolling: true,
|
||||
},
|
||||
// 개발 서버 API 프록시.
|
||||
// useApi 의 baseURL 은 '/api'(상대경로)라 프록시가 없으면 vite 가 SPA 폴백으로
|
||||
// index.html 을 200 으로 돌려준다 — 요청이 조용히 HTML 을 받아 실패한다.
|
||||
//
|
||||
// target 기본값은 호스트에서 직접 띄울 때 기준이고,
|
||||
// docker 개발 환경에서는 compose 가 VITE_DEV_API_PROXY=http://backend:3000 을 주입한다.
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.VITE_DEV_API_PROXY || 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
server: {
|
||||
// ngrok 등 외부 도메인 접근 허용 (DNS Rebinding 보호 비활성화)
|
||||
allowedHosts: true,
|
||||
// 루트 .env 의 FRONTEND_PORT 를 따른다.
|
||||
// 컨테이너로 띄울 때는 compose 의 `--port`(컨테이너 내부 고정 포트)가 이 값을 덮어쓴다.
|
||||
port: frontendPort,
|
||||
// 포트가 이미 사용 중이면 조용히 다음 포트로 옮기지 않고 실패한다.
|
||||
// 루트 .env 의 CORS_ORIGIN 과 compose 포트 매핑이 이 포트에 묶여 있어,
|
||||
// 말없이 5174 로 옮겨가면 API 요청이 원인 모르게 막힌다 (fail-fast).
|
||||
strictPort: true,
|
||||
// 도커 바인드 마운트에서는 inotify 가 호스트 쪽 변경을 잡지 못해 폴링이 필요하다
|
||||
watch: usePolling ? { usePolling: true } : undefined,
|
||||
// 개발 서버 API 프록시.
|
||||
// useApi 의 baseURL 은 '/api'(상대경로)라 프록시가 없으면 vite 가 SPA 폴백으로
|
||||
// index.html 을 200 으로 돌려준다 — 요청이 조용히 HTML 을 받아 실패한다.
|
||||
//
|
||||
// 기본 대상은 호스트에서 띄울 때 기준이고(백엔드 컨테이너가 BACKEND_PORT 를 호스트에 노출한다),
|
||||
// 프론트까지 docker 로 띄울 때는 compose 가 VITE_DEV_API_PROXY=http://backend:${BACKEND_PORT} 를 주입한다.
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: rootEnv.VITE_DEV_API_PROXY || `http://localhost:${backendPort}`,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
// vite.config 는 mode 별 설정(포트·프록시)을 계산하는 함수다.
|
||||
// mergeConfig 는 함수를 병합하지 못하므로 테스트용 ConfigEnv 로 먼저 평가한다.
|
||||
// (테스트는 dev 서버를 띄우지 않아 포트·프록시 값에 영향받지 않는다)
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
viteConfig({ command: 'serve', mode: 'test' }),
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
|
||||
Reference in New Issue
Block a user