docs: 규칙 문서를 실제 구현에 맞춰 갱신
규칙과 구현이 어긋나 있던 부분을 바로잡고, 작업하며 발견한 함정을 기록한다.
- 스킬 문서의 "기본 제공 골격" 표를 실제 파일 목록과 일치시킴
- 커밋 전 체크리스트에 테스트 실행·에러 코드 동기화 절차 추가
- 커밋 규칙에 scope·브랜치 전략·PR 규칙 보강
- 새 규칙 파일(api-design, logging)을 CLAUDE.md import 목록에 등록하고,
규칙 추가 시 등록이 필수라는 메타 규칙 명시
- README 를 현재 구조에 맞게 갱신 (traceId·페이지네이션·에러 코드 SSOT·로컬 개발 명령)
작업 중 실제로 겪은 함정들을 규칙으로 남긴다:
- Windows 에서 npm install 하면 Linux 전용 optional 의존성이 lock 에서 잘려
Docker 의 npm ci 가 실패한다 → 의존성 변경은 컨테이너 안에서 수행
- --build 만으로는 node_modules 익명 볼륨이 갱신되지 않아
새 패키지가 컨테이너에 반영되지 않는다 → --renew-anon-volumes 필요
- compose 는 미정의 변수를 빈 문자열로 주입해 Number('')===0 함정이 생긴다
- dev 는 PID 1 이 npm 이라 graceful shutdown 확인은 운영 이미지로 해야 한다
- Redis 는 컨테이너만 떠 있고 사용처가 없다 — 확장 지점임을 명시
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+40
-20
@@ -1,8 +1,3 @@
|
||||
---
|
||||
name: checklist
|
||||
description: 신규 프로젝트 셋업 절차 + 매 작업 공통 점검(에러코드 SSOT 동기화 등). 특정 시점의 구현 상태가 아니라 언제나 유효한 공용 규칙만 담는다.
|
||||
---
|
||||
|
||||
# 작업 점검 & 신규 프로젝트 셋업
|
||||
|
||||
> 이 저장소는 **재사용 템플릿**이다. 클론해 어떤 프로젝트든 시작할 수 있도록, 여기에는 **언제나 유효한 공용 규칙**만 둔다(특정 시점의 구현 상태는 두지 않는다).
|
||||
@@ -12,41 +7,66 @@ description: 신규 프로젝트 셋업 절차 + 매 작업 공통 점검(에러
|
||||
|
||||
## 1. 신규 프로젝트 셋업 (클론 직후 1회)
|
||||
|
||||
- [ ] 루트 `.env.{env}.example` → `.env.{env}` 복사 (루트가 설정 SSOT — 대표 키가 채워져 있음, [[env-structure]])
|
||||
- [ ] 루트 `.env.{env}.example` → `.env.{env}` 복사 후 **placeholder 값 교체** (`change-me-*` 가 남아 있으면 안 된다, [[env-structure]])
|
||||
- [ ] `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 키)
|
||||
- [ ] 백엔드 고유 시크릿(`JWT_SECRET` 등)을 `backend/.env.{env}` 에 작성 (compose `env_file` 로 로드)
|
||||
- [ ] 의존성 설치: `cd backend && npm ci`, `cd frontend && npm ci`, `cd flutter && flutter pub get`
|
||||
- [ ] Docker 개발 환경 기동으로 동작 확인 ([[docker]])
|
||||
|
||||
## 2. 커밋 전 공통 점검 (매 작업)
|
||||
|
||||
> 아래 항목은 `.github/workflows/ci.yml` 이 PR 마다 자동 검사한다. 로컬에서 먼저 통과시키면 CI 도 통과한다.
|
||||
|
||||
- [ ] 주석은 한국어, 매직 넘버·하드코딩 문자열 없음 ([[coding-common]])
|
||||
- [ ] 린트/분석 통과 — backend `npm run lint`, frontend `npm run lint && npm run type-check`, flutter `flutter analyze` (Warning 0)
|
||||
- [ ] 린트/분석 통과 — backend `npm run lint`, frontend `npm run lint && npm run type-check`, flutter `flutter analyze`
|
||||
(lint 스크립트에 `--max-warnings 0` 이 걸려 있어 **Warning 하나만 있어도 실패**한다. 자동 수정은 `npm run lint:fix`)
|
||||
- [ ] 변경 영역 테스트 통과 — backend `npm run test`, frontend `npm run test:unit`, flutter `flutter test` ([[testing]])
|
||||
- [ ] 절대경로 alias 사용 (TS `@/`, Dart `package:`), 깊은 상대경로 없음
|
||||
- [ ] 민감정보(`.env`, 시크릿) 미커밋 — `*.example` 만 추적 ([[security]])
|
||||
- [ ] 커밋 메시지 `<type>: <한국어 제목>` 형식, 한 커밋 한 변경 ([[git-convention]])
|
||||
- [ ] 민감정보(`.env`, 시크릿) 미커밋 — `*.example` 만 추적하며 그 값은 placeholder ([[security]])
|
||||
- [ ] 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음 ([[logging]])
|
||||
- [ ] 커밋 메시지 `<type>(<scope>): <한국어 제목>` 형식, 한 커밋 한 변경 ([[git-convention]])
|
||||
- [ ] **의존성을 바꿨다면** lock 파일이 Linux 빌드와 호환되는지 확인 — Windows 에서 `npm install` 하면
|
||||
Linux 전용 optional 의존성이 lock 에서 잘려 Docker 의 `npm ci` 가 실패한다. 컨테이너 안에서 설치할 것 ([[nestjs]])
|
||||
- [ ] **의존성을 바꿨다면** dev 컨테이너를 `--renew-anon-volumes` 로 재생성 — `--build` 만으로는
|
||||
`node_modules` 익명 볼륨이 갱신되지 않아 새 패키지가 컨테이너에 반영되지 않는다 ([[docker]])
|
||||
|
||||
## 3. 에러 코드 SSOT 동기화 (메시지/코드 변경 시 반드시 3곳 동시 수정)
|
||||
## 3. 에러 코드 변경 시 (SSOT 절차)
|
||||
|
||||
- [ ] backend `src/common/filters/http-exception.filter.ts`
|
||||
- [ ] frontend `src/composables/useApi.ts` (`ErrorCodeLexicon`)
|
||||
- [ ] flutter `lib/core/api/error_lexicon.dart` (`ErrorLexicon`)
|
||||
에러 코드/메시지는 **백엔드 레지스트리 한 파일**만 고친다. 세 파일을 손으로 맞추지 않는다.
|
||||
|
||||
- [ ] `backend/src/common/errors/error-codes.ts` 수정 (코드는 `DOMAIN_NNN`, 번호 재사용 금지)
|
||||
- [ ] `cd backend && npm run sync:error-codes` 실행 → 클라이언트 사전 자동 생성
|
||||
(`frontend/src/shared/constants/error-codes.generated.ts`, `flutter/lib/core/api/error_lexicon.g.dart` — **직접 수정 금지**)
|
||||
- [ ] 세 스택의 코드 목록 테스트에 새 코드 반영 후 통과 확인
|
||||
(`error-codes.spec.ts` · `useApi.spec.ts` · `error_lexicon_test.dart`)
|
||||
- [ ] 생성 파일까지 함께 커밋
|
||||
(CI 의 `error-codes-sync` 잡이 `sync` 재실행 후 `git diff` 로 드리프트를 검사하므로, 빠뜨리면 PR 이 실패한다)
|
||||
|
||||
> `AUTH_002`(세션 만료)는 **클라이언트 전용** 코드로 서버 응답에는 나타나지 않는다. 자세한 내용은 [[error-codes]].
|
||||
|
||||
## 4. 공유 계약을 바꿀 때 (응답 포맷 · 페이지네이션 · API 규약)
|
||||
|
||||
- [ ] 백엔드 구현 수정 ([[response-format]] · [[api-design]])
|
||||
- [ ] 세 클라이언트의 인터셉터/타입 동기화 (`useApi.ts` · `dio_client.dart` · 공유 타입 파일)
|
||||
- [ ] Swagger 데코레이터(`ApiStandardResponse` / `ApiErrorResponse`) 갱신 — 문서와 실응답 불일치 방지
|
||||
- [ ] PR 본문에 영향 범위 명시 ([[git-convention]])
|
||||
|
||||
---
|
||||
|
||||
## 4. 기능 확장 시 따르는 순서
|
||||
## 5. 기능 확장 시 따르는 순서
|
||||
|
||||
새 기능은 만들기 전에, 해당 스킬의 **"템플릿 기본 제공 골격"** 에서 재사용할 패턴을 먼저 찾는다(같은 역할을 새로 만들지 말 것).
|
||||
|
||||
1. 해당 디렉터리 스킬 규칙 확인 ([[nestjs]] / [[vue3]] / [[flutter]])
|
||||
2. 기본 제공 골격(인터셉터·필터·dio client·error lexicon·예제 모듈)에서 패턴 재사용
|
||||
3. 표준 응답/에러 흐름 준수 ([[response-format]] · [[error-codes]])
|
||||
4. env 신규 값은 루트 `.env` 부터 ([[env-structure]])
|
||||
5. 커밋 전 위 2·3절 점검 통과
|
||||
2. 기본 제공 골격(인터셉터·필터·에러 레지스트리·dio client·예제 모듈)에서 패턴 재사용
|
||||
3. 표준 응답/에러 흐름 준수 ([[response-format]] · [[error-codes]] · [[api-design]])
|
||||
4. 실패 경로마다 에러 코드 지정 — `BusinessException('CODE')` 로 던진다
|
||||
5. env 신규 값은 루트 `.env` 부터 ([[env-structure]])
|
||||
6. 커밋 전 위 2·3·4절 점검 통과
|
||||
|
||||
> 대표 확장 지점(각 스킬의 "확장 시 따르는 규칙" 참조):
|
||||
> - 인증/권한 → [[nestjs]](guards·JWT) · [[vue3]](auth store + 라우터 가드)
|
||||
> - 데이터 영속화 → [[nestjs]](TypeORM 엔티티/리포지토리)
|
||||
> - 데이터 영속화 → [[nestjs]](TypeORM 엔티티/리포지토리 · 마이그레이션)
|
||||
> - 상태·화면 계층 → [[flutter]](Riverpod 프로바이더 · features 구조)
|
||||
|
||||
@@ -8,9 +8,16 @@ description: 세 클라이언트(backend/frontend/flutter) 공통 코딩 규칙
|
||||
- 모든 코드 주석은 **한국어**로 작성
|
||||
- 웹(`frontend`)·서버(`backend`)는 100% TypeScript, 모바일(`flutter`)은 Dart Sound Null Safety 준수
|
||||
- 컴파일러/린터 **Warning 방치 금지** — 커밋 전 각 프로젝트 `lint` / `analyze` 통과
|
||||
(lint 스크립트에 `--max-warnings 0` 이 걸려 있다. 자동 수정은 `npm run lint:fix`)
|
||||
- 아래 규칙은 **린트로 강제**되므로 위반 시 빌드가 실패한다:
|
||||
`any`/`dynamic` 사용, `console`/`print` 출력, Vue 인라인 스타일·Options API,
|
||||
Dart `const` 누락·await 누락, backend 에서 `HttpException` 직접 생성(→ `BusinessException` 사용)
|
||||
- 유틸 함수는 순수 함수로 분리 — TS는 `shared/utils/`, Dart는 `core/utils/`
|
||||
- 매직 넘버·하드코딩 문자열 지양 → 상수 또는 환경변수로 분리
|
||||
(반복 사용되는 키 문자열도 상수로: 예 `kJwtTokenKey`, `REQUEST_ID_HEADER`)
|
||||
- 절대경로 alias 사용 (TS `@/`, Dart `package:`), 깊은 상대경로(`../../`) 지양
|
||||
— backend/frontend 모두 `@/` → `src/` 로 설정되어 있다 (backend 는 `tsconfig.json` paths + jest `moduleNameMapper`)
|
||||
- **자동 생성 파일은 직접 수정하지 않는다** — `*.generated.ts`, `*.g.dart` 는 생성 스크립트를 통해서만 바꾼다 ([[error-codes]])
|
||||
|
||||
## 네이밍 규약
|
||||
|
||||
@@ -23,5 +30,6 @@ description: 세 클라이언트(backend/frontend/flutter) 공통 코딩 규칙
|
||||
| 변수/함수 | camelCase (Dart 포함) | `getUser`, `isLoggedIn` |
|
||||
| 상수 | TS `UPPER_SNAKE` 또는 모듈 상수, Dart `lowerCamel` | `MAX_RETRY`, `defaultTimeout` |
|
||||
| 에러 코드 | `DOMAIN_NNN` | `AUTH_001`, `VAL_001` ([[error-codes]]) |
|
||||
| 자동 생성 파일 | TS `*.generated.ts`, Dart `*.g.dart` | `error-codes.generated.ts`, `error_lexicon.g.dart` |
|
||||
|
||||
> 도메인별 세부 규칙: [[nestjs]] · [[vue3]] · [[flutter]] · 테스트: [[testing]]
|
||||
> 도메인별 세부 규칙: [[nestjs]] · [[vue3]] · [[flutter]] · 테스트: [[testing]] · 로깅: [[logging]] · API 설계: [[api-design]]
|
||||
|
||||
@@ -22,9 +22,19 @@ description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{
|
||||
|
||||
- **새 설정값은 루트 `.env.{APP_ENV}` 에 먼저 추가**하고, 필요한 서비스에 `docker-compose.yml` 의 `environment` / `args` 로 전달한다.
|
||||
- 서비스 자체 env 파일에는 **루트에서 내려주지 않는 고유 값만** 작성한다. (예: 백엔드 `JWT_SECRET`)
|
||||
- **compose 는 정의되지 않은 변수를 빈 문자열로 주입한다.** 코드에서 `??` 기본값만 쓰면 `Number('')===0` 같은 함정에 빠진다 —
|
||||
숫자 설정값은 `shared/utils/env.ts` 의 `parsePositiveInt()` 처럼 빈 문자열까지 방어하는 파서를 거친다.
|
||||
(기존 `.env` 를 쓰는 사람이 새 키를 추가하지 않아도 안전해야 한다)
|
||||
- `docker-compose.yml` 의 `env_file` 지시자 때문에 `backend/.env.{APP_ENV}` 파일은 비어 있어도 **존재해야 한다.**
|
||||
- **Flutter 는 예외** — docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립적으로 관리한다.
|
||||
- 모든 env 파일은 커밋 금지, `*.example` 만 커밋한다. (`.gitignore` 의 `!*.example` 재포함 규칙)
|
||||
- **설정이 잘못되면 서버가 뜨지 않아야 한다(fail-fast).** 조용히 기본값으로 굴러가다 운영에서 터지는 것이 더 위험하다.
|
||||
검증은 `backend/src/shared/config/env.validation.ts` 가 담당하며 `ConfigModule.forRoot({ validate })` 에 연결되어 있다.
|
||||
- 숫자 항목은 형식 검사 (모든 환경)
|
||||
- 운영 환경: `DB_HOST`/`DB_USER`/`DB_PASSWORD`/`DB_NAME` 필수
|
||||
- 운영 환경: `change-me`·`password` 같은 **예시 값이 그대로 배포되는 것을 차단**
|
||||
- 문제는 하나씩이 아니라 **한 번에 모아서** 보여준다 (재기동 반복 방지)
|
||||
- 새 필수 설정을 추가하면 이 파일의 목록과 테스트도 함께 갱신한다
|
||||
|
||||
> 루트 env의 대표 키: `PROJECT_NAME`, `APP_ENV`, `BUILD_MODE`, 각종 `*_PORT`, `BACKEND_API_URL`, `CORS_ORIGIN`, `DB_*`/`DB_DATA_DIR`, `REDIS_*`/`REDIS_DATA_DIR`. 실제 목록은 `.env.development.example` 참조.
|
||||
> 루트 env의 대표 키: `PROJECT_NAME`, `APP_ENV`, `BUILD_MODE`, 각종 `*_PORT`, `BACKEND_API_URL`, `CORS_ORIGIN`, `DB_*`/`DB_DATA_DIR`, `REDIS_*`/`REDIS_DATA_DIR`, `THROTTLE_TTL_MS`/`THROTTLE_LIMIT`. 실제 목록은 `.env.development.example` 참조.
|
||||
> 실행/배포 명령은 [[docker]], 보안 규칙은 [[security]] 참조.
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
---
|
||||
name: git-convention
|
||||
description: Git 커밋 메시지 컨벤션 — <type>: <제목> 형식, 한국어 명령형, 한 커밋 한 논리 변경.
|
||||
---
|
||||
# Git 컨벤션
|
||||
|
||||
# Git 커밋 컨벤션
|
||||
## 커밋 메시지
|
||||
|
||||
`<type>: <제목>` 형식 — type: **feat / fix / refactor / docs / chore / perf / test / style**
|
||||
`<type>(<scope>): <제목>` 형식 — scope 는 선택.
|
||||
|
||||
- 예: `feat: 사용자 목록 페이지네이션 추가`
|
||||
- 제목은 한국어, 명령형 현재 시제로 작성
|
||||
- type: **feat / fix / refactor / docs / chore / perf / test / style**
|
||||
- scope: 변경이 한 스택에 한정되면 명시 — `backend` / `frontend` / `flutter` / `docker` / `rules`
|
||||
- 제목은 한국어, 명령형 현재 시제, 마침표 없음
|
||||
- 한 커밋은 하나의 논리적 변경만 포함
|
||||
|
||||
```
|
||||
feat(backend): 사용자 목록 커서 페이지네이션 추가
|
||||
fix(frontend): 세션 만료 시 중복 알림 노출 수정
|
||||
docs(rules): 에러 코드 SSOT 동기화 절차 정리
|
||||
```
|
||||
|
||||
본문이 필요하면 제목 다음 줄을 비우고 **왜** 바꿨는지 적는다(무엇을 바꿨는지는 diff 가 말해준다).
|
||||
|
||||
## 브랜치
|
||||
|
||||
`<type>/<간단한-설명>` — 기본 브랜치는 `main`.
|
||||
|
||||
```
|
||||
feat/user-pagination
|
||||
fix/session-expired-alert
|
||||
```
|
||||
|
||||
- 기본 브랜치에 직접 커밋하지 않는다. 작업은 브랜치에서 하고 PR 로 합친다.
|
||||
- 브랜치는 머지 후 삭제한다. 오래 유지되는 브랜치를 만들지 않는다.
|
||||
|
||||
## PR
|
||||
|
||||
- 제목은 커밋 규칙과 동일한 형식
|
||||
- 본문에 **무엇을 왜 바꿨는지**와 **확인 방법**을 적는다
|
||||
- `.github/pull_request_template.md` 의 체크리스트를 채운다
|
||||
- 머지 전 CI(`.github/workflows/ci.yml`)와 [[checklist]] 의 "커밋 전 공통 점검"을 통과해야 한다
|
||||
- 에러 코드·응답 포맷 등 **공유 계약을 바꾸는 PR** 은 세 스택 영향 범위를 본문에 명시한다 ([[error-codes]] · [[response-format]])
|
||||
|
||||
@@ -11,8 +11,10 @@ description: 세 스택 공통 테스트 규칙 — 테스트 위치/네이밍,
|
||||
## 공통 원칙
|
||||
|
||||
- **무엇을 테스트하나**: 순수 함수(utils), 서비스/비즈니스 로직, 에러 코드 매핑 분기를 우선한다. 프레임워크 기본 동작은 테스트하지 않는다.
|
||||
- **공유 계약은 반드시 테스트한다** — 아래 "계약 테스트" 절 참조.
|
||||
- **테스트는 결정적(deterministic)** 으로 — 외부 네트워크/시간/랜덤에 의존하지 않도록 모킹한다.
|
||||
- **커밋 전 실행** — 변경 영역의 테스트가 통과해야 한다. ([[checklist]])
|
||||
- **CI 가 강제한다** — `.github/workflows/ci.yml` 이 세 스택의 lint·test·build 와 에러 코드 동기화를 PR 마다 검사한다.
|
||||
- 테스트 설명(it/test 제목)은 한국어로 "무엇을 검증하는지" 명확히 작성한다.
|
||||
|
||||
## 스택별 위치·실행
|
||||
@@ -20,6 +22,7 @@ description: 세 스택 공통 테스트 규칙 — 테스트 위치/네이밍,
|
||||
| 스택 | 러너 | 위치·네이밍 | 실행 |
|
||||
| --- | --- | --- | --- |
|
||||
| backend | Jest | 단위 `*.spec.ts` (소스 옆), e2e `test/*.e2e-spec.ts` | `npm run test`, `npm run test:e2e` |
|
||||
|
||||
| frontend | Vitest | `*.spec.ts` / `*.test.ts` (대상 옆 또는 `__tests__/`) | `npm run test:unit` |
|
||||
| flutter | flutter_test | `test/` 하위, `*_test.dart` | `flutter test` |
|
||||
|
||||
@@ -36,5 +39,34 @@ describe('formatSomething', () => {
|
||||
});
|
||||
```
|
||||
|
||||
> API 응답 래퍼/에러 코드는 세 플랫폼 공유 계약이므로([[response-format]] · [[error-codes]]),
|
||||
> 매핑 로직을 변경하면 해당 분기 테스트를 추가/갱신한다.
|
||||
---
|
||||
|
||||
## 계약 테스트 (삭제·비활성화 금지)
|
||||
|
||||
세 플랫폼이 공유하는 계약은 어긋나도 컴파일 에러가 나지 않는다. 아래 테스트가 그 안전망이다.
|
||||
|
||||
| 대상 | 위치 | 검증 내용 |
|
||||
| --- | --- | --- |
|
||||
| 에러 코드 레지스트리 | `backend/src/common/errors/error-codes.spec.ts` | 네이밍 규약, 상태 역매핑, 클라이언트 전용 코드가 서버에서 선택되지 않음 |
|
||||
| 에러 응답 포맷 | `backend/src/common/filters/http-exception.filter.spec.ts` | 코드/상태/traceId, **내부 정보 미노출**, 검증 상세 사유 미노출 |
|
||||
| 프론트 에러 사전 | `frontend/src/composables/useApi.spec.ts` | 코드 목록이 백엔드와 일치, 폴백 동작 |
|
||||
| 앱 에러 사전 | `flutter/test/error_lexicon_test.dart` | 코드 목록이 백엔드와 일치, 폴백 동작 |
|
||||
| 응답 계약 (e2e) | `backend/test/app.e2e-spec.ts` | 실제 HTTP 로 래퍼·에러코드·traceId·페이지네이션·헬스체크 형식 검증 |
|
||||
|
||||
> **e2e 는 DB 가 필요하다** (AppModule 이 TypeORM 을 초기화한다).
|
||||
> 로컬에서는 개발 스택을 띄운 뒤 컨테이너 안에서 실행하는 것이 가장 간단하다.
|
||||
> ```bash
|
||||
> docker exec <프로젝트>-backend npx jest --config ./test/jest-e2e.json
|
||||
> ```
|
||||
> CI 는 postgres 서비스 컨테이너를 띄워 자동으로 실행한다.
|
||||
|
||||
- 에러 코드를 추가/삭제하면 **세 곳의 기대 목록을 함께 갱신**한다 ([[checklist]] 3절).
|
||||
- 매핑 로직(응답 래퍼·상태 역매핑)을 바꾸면 해당 분기 테스트를 추가한다.
|
||||
|
||||
## 테스트하지 않아도 되는 것
|
||||
|
||||
- 프레임워크가 보장하는 동작 (라우팅, DI 주입 자체, ORM 쿼리 빌더)
|
||||
- 단순 getter/setter, 위임만 하는 컨트롤러 메서드
|
||||
- 화면 스타일·레이아웃 값
|
||||
|
||||
> API 응답 래퍼/에러 코드는 세 플랫폼 공유 계약이다 ([[response-format]] · [[error-codes]]).
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null S
|
||||
|
||||
> 이 스킬은 `.claude/skills/flutter/SKILL.md`에 위치한다.
|
||||
> 사용법: `/flutter [요청 내용]` 또는 flutter/ 작업 시 자동 적용.
|
||||
> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]]
|
||||
> 공통 규칙: [[response-format]] · [[error-codes]] · [[api-design]] · [[security]] · [[coding-common]]
|
||||
> Flutter 는 docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립 관리한다 ([[env-structure]] 참조).
|
||||
|
||||
---
|
||||
@@ -18,11 +18,13 @@ description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null S
|
||||
| --- | --- | --- |
|
||||
| Dio 클라이언트 | `lib/core/api/dio_client.dart` | 싱글톤, baseUrl=dotenv `API_URL`(fallback 로컬), 타임아웃 |
|
||||
| Auth/Error 인터셉터 | `dio_client.dart` 의 `InterceptorsWrapper` | onRequest(토큰 첨부)·onResponse(언래핑)·onError(코드 매핑) — 인라인 |
|
||||
| ErrorLexicon | `lib/core/api/error_lexicon.dart` | 에러 코드 → 한국어 메시지, fallback `SYS_001` ([[error-codes]]) |
|
||||
| 토큰 저장 | `flutter_secure_storage`, key `jwt_token` | `dio_client` 에서 사용 |
|
||||
| **ApiException** | `lib/core/api/api_exception.dart` | `{code, message, traceId, statusCode}` 정규화 예외. 인터셉터가 `DioException.error` 에 실어준다 |
|
||||
| ErrorLexicon | `lib/core/api/error_lexicon.dart` | 사전 접근 API. 실제 문구는 `error_lexicon.g.dart`(**자동 생성, 수정 금지**) ([[error-codes]]) |
|
||||
| 세션 만료 처리 | `dio_client.dart` 의 `_toApiException()` | 토큰 보유 상태에서 401 → `AUTH_002` 승격 + 토큰 삭제 |
|
||||
| 토큰 저장 | `flutter_secure_storage`, key 상수 `kJwtTokenKey` | `dio_client` 에서 사용 |
|
||||
| 상태관리 초기화 | `main.dart` 의 `ProviderScope` | Riverpod 사용 준비됨 |
|
||||
| Dio Provider | `lib/core/api/dio_provider.dart` | 인터셉터 적용된 Dio 를 DI 로 주입 |
|
||||
| 예제 feature(참조용) | `lib/features/user/` | `data/models`·`data/repositories`·`presentation/providers` 수직 슬라이스 패턴 |
|
||||
| 예제 feature(참조용) | `lib/features/user/` | `data/models`(UserModel)·`data/repositories`(단건+커서 목록)·`presentation/providers` 수직 슬라이스 |
|
||||
|
||||
> **확장 시 따르는 규칙** (아래 "프로젝트 구조"는 목표 구조 — 기능 추가 시 점진적으로 채운다):
|
||||
> - **상태관리**: `features/user` 의 model→repository→provider 패턴을 복제. 위젯에서 `ref.watch(userProvider(id))`
|
||||
@@ -64,7 +66,7 @@ lib/
|
||||
│ ├── widgets/ ← 재사용 위젯
|
||||
│ └── providers/ ← 상태 관리
|
||||
└── shared/
|
||||
└── widgets/ ← 앱 공통 위젯
|
||||
└── widgets/ ← 앱 공통 위젯 (생성 완료)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -176,17 +178,18 @@ class DioClient {
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// jwt_token 읽어 Authorization 헤더 첨부
|
||||
final token = await _storage.read(key: 'jwt_token');
|
||||
final token = await _storage.read(key: kJwtTokenKey);
|
||||
if (token != null) options.headers['Authorization'] = 'Bearer $token';
|
||||
handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
// success==true → data 언래핑, false → 에러 코드로 reject
|
||||
// success==true → data 언래핑, false → ApiException 으로 reject
|
||||
handler.next(response);
|
||||
},
|
||||
onError: (err, handler) {
|
||||
// 상태코드/서버 error.code → ErrorLexicon 한국어 메시지 ([[error-codes]] 참조)
|
||||
handler.next(err);
|
||||
onError: (err, handler) async {
|
||||
// 상태코드/서버 error.code → ApiException{code,message,traceId} ([[error-codes]] 참조)
|
||||
// 토큰 보유 상태의 401 은 AUTH_002(세션 만료)로 승격하고 토큰을 삭제한다.
|
||||
handler.next(err.copyWith(error: await _toApiException(err)));
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -203,17 +206,18 @@ class DioClient {
|
||||
// flutter_secure_storage 사용
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
// 저장 (키 이름은 dio_client 인터셉터와 통일: jwt_token)
|
||||
await storage.write(key: 'jwt_token', value: token);
|
||||
// 저장 (키 이름은 dio_client 의 상수로 통일: kJwtTokenKey == 'jwt_token')
|
||||
await storage.write(key: kJwtTokenKey, value: token);
|
||||
|
||||
// 읽기
|
||||
final token = await storage.read(key: 'jwt_token');
|
||||
final token = await storage.read(key: kJwtTokenKey);
|
||||
|
||||
// 삭제 (로그아웃)
|
||||
await storage.deleteAll();
|
||||
```
|
||||
|
||||
- JWT, 민감 정보는 반드시 `flutter_secure_storage` 사용
|
||||
- 키 문자열을 여기저기 적지 말고 `kJwtTokenKey` 상수를 쓴다 ([[coding-common]])
|
||||
- `SharedPreferences`에 토큰 저장 **금지**
|
||||
- 추가 보안 규칙은 [[security]] 참조.
|
||||
|
||||
@@ -237,10 +241,15 @@ await storage.deleteAll();
|
||||
- [ ] **Null Safety**: `dynamic` 금지, `?`/`!`/`late` 정확히 사용.
|
||||
- [ ] **위젯**: depth 깊어지면 private 위젯 분리, `const` 적극, `build()` 내 비즈니스 로직 금지.
|
||||
- [ ] **반응형**: 하드코딩 픽셀 금지 → `MediaQuery`/`Expanded`/`Flexible`.
|
||||
- [ ] **API**: `dio_client.dart` 의 인터셉터 패턴 재사용. 에러 메시지는 `error_lexicon.dart` 경유.
|
||||
- [ ] **토큰**: `flutter_secure_storage` + key `jwt_token` 통일. `SharedPreferences` 저장 금지.
|
||||
- [ ] **API**: `dio_client.dart` 의 인터셉터 패턴 재사용. 리포지토리에서 try/catch 로 메시지를 만들지 않는다.
|
||||
- [ ] **에러 분기**: `e.error as ApiException` 의 **`code` 로 분기**한다. 문구는 사전에서만 온다.
|
||||
- [ ] **에러 사전**: `error_lexicon.g.dart` 직접 수정 금지 — 백엔드 레지스트리 수정 후 `npm run sync:error-codes`.
|
||||
- [ ] **목록**: 응답은 `{items, pageInfo}` — `UserRepository.getUsers()` 패턴 재사용 ([[response-format]]).
|
||||
- [ ] **토큰**: `flutter_secure_storage` + 키 상수 `kJwtTokenKey` 통일. `SharedPreferences` 저장 금지.
|
||||
- [ ] **상태관리**: 새 기능은 Riverpod 프로바이더/노티파이어로 작성 (`ProviderScope` 는 기본 제공).
|
||||
- [ ] **구조 보완 시**: `app.dart` 분리, `core/{constants,theme,storage}` 생성, 공통 위젯 `lib/widgets/` → `shared/widgets/` 이동 고려.
|
||||
- [ ] **구조 보완 시**: `app.dart` 분리, `core/{constants,theme,storage}` 생성. 공통 위젯은 `lib/shared/widgets/` 에 둔다(이동 완료).
|
||||
- [ ] **권장 패키지**: 모델(`freezed`/`json_serializable`)·라우팅(`go_router`) 도입 시 `pubspec.yaml` 추가 후 `flutter pub get`.
|
||||
- [ ] **analyze**: 커밋 전 `flutter analyze` 통과.
|
||||
- [ ] **analyze/test**: 커밋 전 `flutter analyze` (Warning 0) + `flutter test` 통과.
|
||||
- [ ] **린트로 강제되는 항목**: `dynamic` 호출·`print`·`const` 누락·await 누락·작은따옴표·import 정렬 (`analysis_options.yaml`).
|
||||
- [ ] **env**: `flutter/.env.example` → `.env` 복사 후 `API_URL` 설정 (Android 에뮬레이터는 `10.0.2.2`).
|
||||
`.env` 는 `pubspec.yaml` 의 asset 이라 **없으면 `flutter analyze` 가 경고**한다.
|
||||
|
||||
+103
-32
@@ -5,7 +5,7 @@ description: NestJS 백엔드(backend/) 개발 규칙 — 컨트롤러/서비스
|
||||
|
||||
> 이 스킬은 `.claude/skills/nestjs/SKILL.md`에 위치한다.
|
||||
> 사용법: `/nestjs [요청 내용]` 또는 backend/ 작업 시 자동 적용.
|
||||
> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]]
|
||||
> 공통 규칙: [[response-format]] · [[error-codes]] · [[api-design]] · [[logging]] · [[security]] · [[coding-common]]
|
||||
|
||||
---
|
||||
|
||||
@@ -15,20 +15,30 @@ description: NestJS 백엔드(backend/) 개발 규칙 — 컨트롤러/서비스
|
||||
|
||||
| 제공 항목 | 위치 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| 표준 응답 인터셉터 | `src/common/interceptors/transform.interceptor.ts` | main.ts 전역 등록 — 컨트롤러는 raw 데이터만 반환 |
|
||||
| 전역 예외 필터 | `src/common/filters/http-exception.filter.ts` | `HttpException` → `{success:false,error:{code,message}}` ([[error-codes]]) |
|
||||
| **에러 코드 레지스트리** | `src/common/errors/error-codes.ts` | 코드↔메시지↔HTTP 상태 **SSOT**. `npm run sync:error-codes` 로 클라이언트 사전 생성 |
|
||||
| **BusinessException** | `src/common/errors/business.exception.ts` | 에러 코드가 **생성자 필수 인자** — 코드 없는 실패를 만들 수 없다 |
|
||||
| 표준 응답 인터셉터 | `src/common/interceptors/transform.interceptor.ts` | main.ts 전역 등록 — 컨트롤러는 raw 데이터만 반환. `@SkipTransform()` 으로 제외 |
|
||||
| 전역 예외 필터 | `src/common/filters/http-exception.filter.ts` | 레지스트리 조회로 `{success:false,error:{code,message,traceId}}` 생성 ([[error-codes]]) |
|
||||
| 요청 추적 미들웨어 | `src/common/middleware/request-id.middleware.ts` | `x-request-id` 발급 → 로그·응답 `traceId` ([[logging]]) |
|
||||
| Swagger 래퍼 데코레이터 | `src/common/decorators/api-standard-response.decorator.ts` | `ApiStandardResponse` / `ApiErrorResponse` — 문서를 실응답과 일치시킨다 |
|
||||
| 페이지네이션 DTO | `src/common/dto/pagination.dto.ts` | 커서/오프셋 쿼리 + `PageInfoDto` + `toPaginatedResult()` |
|
||||
| 전역 ValidationPipe | `main.ts` | `whitelist/forbidNonWhitelisted/transform` |
|
||||
| Swagger | `main.ts` → `/api-docs` | 모든 엔드포인트 데코레이터 |
|
||||
| 보안 미들웨어 | `main.ts` | helmet · CORS 화이트리스트 · Rate Limiting(`express-rate-limit`) |
|
||||
| 로깅 | `shared/logger/winston.config.ts` | `console.log` 대신 사용 |
|
||||
| 예제 모듈(참조용) | `modules/user`(DTO+Swagger), `modules/health`(`/health`) | 새 모듈 패턴 참고. user 예제는 in-memory 데모 저장소 |
|
||||
| 보안 미들웨어 | `main.ts` | helmet · CORS 화이트리스트 |
|
||||
| Rate Limiting | `app.module.ts` 의 `ThrottlerGuard`(APP_GUARD) | 초과 시 429 → `REQ_001`. 루트 `.env` 의 `THROTTLE_*` 로 조정 |
|
||||
| 로깅 | `shared/logger/winston.config.ts` | `console.log` 대신 사용 ([[logging]]) |
|
||||
| env 검증 | `shared/config/env.validation.ts` | 잘못된 설정이면 **기동 실패**(fail-fast) ([[env-structure]]) |
|
||||
| Graceful shutdown | `main.ts` 의 `enableShutdownHooks()` + `shared/logger/shutdown.logger.ts` | SIGTERM 시 요청/커넥션 정리 후 종료 |
|
||||
| 예제 모듈(참조용) | `modules/user`(DTO+Swagger+페이지네이션) | 새 모듈 패턴 참고. in-memory 데모 저장소 |
|
||||
| 헬스체크 | `modules/health` | `@SkipThrottle`+`@SkipTransform` — 기계가 읽는 엔드포인트 패턴 ([[response-format]]) |
|
||||
| 순수 유틸 | `shared/utils/` | 순수 함수 분리 위치 |
|
||||
|
||||
> **확장 시 따르는 규칙** (기능이 필요해지면 규칙대로 구현):
|
||||
> - **인증(JWT)**: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/AUTH_003 흐름 검증
|
||||
> - **DB 영속화**: TypeORM 엔티티/리포지토리 추가 (예제의 in-memory 저장 대체)
|
||||
> - **페이지네이션**: 목록 API 에 커서/오프셋 적용
|
||||
> - `common/{guards,decorators,pipes}/` 는 해당 관심사 등장 시 생성 (아래 "모듈 구조"는 목표 구조)
|
||||
> - **DB 영속화**: TypeORM 엔티티/리포지토리 추가 (예제의 in-memory 저장 대체).
|
||||
> `app.module.ts` 는 이미 `TypeOrmModule.forRootAsync` 로 연결되어 있으며 **운영에서는 `synchronize:false` + 마이그레이션**이 강제된다 — 이 설정을 바꾸지 말 것
|
||||
> - **도메인 에러 코드**: `error-codes.ts` 의 `DOMAIN_ERROR_CODES` 에 등록 → `npm run sync:error-codes`
|
||||
> - `common/{guards,pipes}/` 는 해당 관심사 등장 시 생성 (아래 "모듈 구조"는 목표 구조)
|
||||
|
||||
---
|
||||
|
||||
@@ -119,23 +129,30 @@ export class TransformInterceptor<T> implements NestInterceptor {
|
||||
|
||||
---
|
||||
|
||||
## 전역 Exception Filter
|
||||
## 에러 처리 — 코드 없는 실패는 만들지 않는다
|
||||
|
||||
사용자에게 노출되는 모든 실패는 `BusinessException` 으로 던진다. 에러 코드가 **생성자 필수 인자**이므로 코드 누락이 구조적으로 불가능하다.
|
||||
|
||||
```typescript
|
||||
// common/filters/http-exception.filter.ts
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
import { BusinessException } from '@/common/errors/business.exception';
|
||||
|
||||
// HTTP 상태 → 에러 코드 매핑 후 { success: false, error: { code, message } } 반환
|
||||
// SYS_001 / AUTH_001 / VAL_001 등 에러 맵은 [[error-codes]] 참조
|
||||
}
|
||||
}
|
||||
// 레지스트리의 상태 코드·메시지를 그대로 사용
|
||||
throw new BusinessException('RES_001', { context: { userId: id } });
|
||||
|
||||
// 동적 값이 필요할 때만 메시지 재정의 (코드·HTTP 상태는 레지스트리 값 유지)
|
||||
throw new BusinessException('USER_001', {
|
||||
message: `이미 가입된 이메일입니다. (${email})`,
|
||||
context: { email },
|
||||
});
|
||||
```
|
||||
|
||||
이 필터는 세 플랫폼 공유 에러 코드 맵의 **단일 진실 공급원(SSOT)** 이다. 메시지 수정 시 [[error-codes]] 의 3개 파일을 함께 갱신한다.
|
||||
- `context` 는 **로그 전용** — 응답 본문에 포함되지 않으므로 식별자를 자유롭게 넣어도 된다 ([[logging]]).
|
||||
- 새 코드는 `error-codes.ts` 의 `DOMAIN_ERROR_CODES` 에 등록하고 `npm run sync:error-codes` 를 실행한다.
|
||||
- `HttpExceptionFilter` 는 레지스트리를 **조회만** 한다. 필터에 문구를 하드코딩하지 말 것.
|
||||
- Nest 내장 예외(`NotFoundException` 등)도 필터가 상태 코드로 역매핑해 주지만, 새 코드에서는 `BusinessException` 을 쓴다.
|
||||
- 미정의 예외는 항상 `SYS_001` 로 수렴하며 내부 정보(스택·호스트명)는 응답에 노출되지 않는다.
|
||||
|
||||
전체 규칙과 도메인 코드 추가 절차는 [[error-codes]] 참조.
|
||||
|
||||
---
|
||||
|
||||
@@ -155,8 +172,15 @@ SwaggerModule.setup('api-docs', app, document);
|
||||
```
|
||||
|
||||
- 모든 Controller에 `@ApiTags()` 적용
|
||||
- 모든 엔드포인트에 `@ApiOperation()`, `@ApiResponse()` 작성
|
||||
- 모든 DTO에 `@ApiProperty()` 작성
|
||||
- 모든 엔드포인트에 `@ApiOperation()` 작성
|
||||
- **응답은 `@ApiResponse` 대신 래퍼 데코레이터를 쓴다** — 컨트롤러는 raw 를 반환하지만 실응답은 `{success,data}` 라 문서가 어긋난다.
|
||||
|
||||
```typescript
|
||||
@ApiStandardResponse(UserResponseDto, { description: '사용자 조회 성공' })
|
||||
@ApiErrorResponse('RES_001', '사용자를 찾을 수 없음') // 상태·메시지를 레지스트리에서 가져온다
|
||||
```
|
||||
|
||||
- 모든 DTO에 `@ApiProperty()` + 한국어 `description` 작성
|
||||
|
||||
---
|
||||
|
||||
@@ -174,24 +198,71 @@ SwaggerModule.setup('api-docs', app, document);
|
||||
|
||||
## 성능 & 최신 트렌드 추가 규칙
|
||||
|
||||
- **캐싱**: 자주 조회되는 데이터는 `@nestjs/cache-manager` (Redis) 활용 고려
|
||||
- **Rate Limiting**: 템플릿은 `main.ts` 에서 `express-rate-limit`(초과 시 `SYS_001`)로 API 남용을 방지한다. 라우트 단위 데코레이터 제어가 필요하면 `@nestjs/throttler` 로 전환 고려
|
||||
- **Pagination**: 목록 API는 반드시 커서 기반 또는 오프셋 페이지네이션 적용
|
||||
- **Logging**: `winston` 또는 `@nestjs/common Logger` 사용, `console.log` 지양
|
||||
- **Health Check**: `@nestjs/terminus`로 `/health` 엔드포인트 구성 권장
|
||||
- **캐싱**: `@nestjs/cache-manager` 가 전역 등록되어 있으나 **현재 저장소는 in-memory** 다.
|
||||
> 📐 **Redis 는 확장 지점이다.** compose 에 redis 컨테이너가 뜨고 `REDIS_HOST`/`REDIS_PORT`/`REDIS_PASSWORD` 가
|
||||
> 백엔드로 주입되지만, **아직 이를 사용하는 코드는 없다.** 단일 인스턴스에서는 in-memory 로 충분하다.
|
||||
>
|
||||
> Redis 가 실제로 필요해지는 시점(다중 인스턴스 운영, 세션·캐시 공유, 분산 락)에 연결한다:
|
||||
> 1. Redis store 패키지 설치 (**컨테이너 안에서** — 위 의존성 주의 항목 참조)
|
||||
> 2. `app.module.ts` 의 `CacheModule.register` 를 `registerAsync` + Redis store 로 교체
|
||||
> 3. `modules/health` 에 Redis 인디케이터 추가 (지금은 DB 만 검사한다)
|
||||
> 4. 연결 실패 시 앱이 죽지 않도록 폴백 전략을 정한다
|
||||
>
|
||||
> 끝까지 쓰지 않을 프로젝트라면 compose 에서 redis 서비스를 빼는 편이 낫다 — 쓰지 않는 컨테이너는 혼란만 준다.
|
||||
- **Rate Limiting**: `@nestjs/throttler` 로 일원화 (`app.module.ts` 의 `APP_GUARD`). 초과 시 429 → `REQ_001`.
|
||||
라우트 단위 조정은 `@Throttle({ default: { ttl, limit } })`, 제외는 `@SkipThrottle()` (헬스체크에 적용되어 있다).
|
||||
**rate limiter 를 중복으로 추가하지 말 것** ([[security]])
|
||||
- **Pagination**: 목록 API는 반드시 페이지네이션 적용 — `common/dto/pagination.dto.ts` 의 쿼리 DTO + `toPaginatedResult()` 사용. 응답은 `{items, pageInfo}` 고정 ([[response-format]])
|
||||
- **Logging**: `winston` 또는 `@nestjs/common Logger` 사용, `console.log` 금지. 레벨 기준·민감정보 마스킹은 [[logging]]
|
||||
- **Graceful shutdown**: `app.enableShutdownHooks()` 가 적용되어 있다. 종료 시 정리가 필요한 리소스(큐 소비자, 워커, 외부 연결)는
|
||||
`OnApplicationShutdown` 훅으로 정리한다. **`process.on(signal)` 로 직접 잡지 말 것** — Nest 의 종료 흐름에 간섭해 종료 코드가 달라진다
|
||||
(`shutdown.logger.ts` 가 참고 패턴).
|
||||
- **Health Check**: `@nestjs/terminus` 로 구성. 주의점 두 가지 —
|
||||
`pingCheck` 기본 타임아웃 1초는 짧아 유휴 커넥션 재연결 시 오판하므로 늘려서 쓰고,
|
||||
표준 응답 래퍼/에러 코드에서 제외해 원인이 보이게 한다 ([[response-format]])
|
||||
- **Versioning**: API 버전 관리 (`/v1/`, `/v2/`) 적용 고려
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 의존성 추가/제거 (Windows 개발 환경 주의)
|
||||
|
||||
**Windows 에서 `npm install` / `npm uninstall` 을 실행하면 lock 파일에서 Linux 전용 optional 의존성이 잘려나간다.**
|
||||
로컬에서는 멀쩡하지만 Docker(Linux) 빌드의 `npm ci` 가 다음처럼 실패한다.
|
||||
|
||||
```
|
||||
npm error `npm ci` can only install packages when your package.json and package-lock.json are in sync.
|
||||
npm error Missing: @emnapi/core@1.11.3 from lock file
|
||||
```
|
||||
|
||||
그래서 의존성 변경은 **컨테이너 안에서** 수행한다 (lock 이 Linux 기준으로 생성된다).
|
||||
|
||||
```bash
|
||||
# 개발 스택이 떠 있는 상태에서
|
||||
dc exec backend npm install <패키지>
|
||||
dc exec backend npm uninstall <패키지>
|
||||
```
|
||||
|
||||
- 변경 후 호스트의 `package.json`·`package-lock.json` 이 갱신됐는지 확인하고 **함께 커밋**한다 (bind mount 라 호스트에 반영된다).
|
||||
- 호스트에서 실행해버렸다면 `git checkout backend/package-lock.json` 으로 되돌린 뒤 위 방법으로 다시 한다.
|
||||
- 커밋 전 `docker compose ... build backend` 로 `npm ci` 가 통과하는지 확인한다. CI 도 같은 검사를 한다.
|
||||
|
||||
---
|
||||
|
||||
## 작업 점검 체크리스트
|
||||
|
||||
코드 작성/수정 후 아래를 점검한다. (전체 확장 지점·점검 목록은 [[checklist]])
|
||||
|
||||
- [ ] **응답 포맷**: 컨트롤러는 raw 데이터만 반환 — `{success,data}` 래핑은 인터셉터가 담당. 직접 래핑 금지.
|
||||
- [ ] **에러**: 사용자 노출 실패는 `HttpException` 으로 던져 필터가 코드 매핑하게 한다. 새 코드 추가 시 [[error-codes]] 의 3개 파일 동시 갱신.
|
||||
- [ ] **응답 포맷**: 컨트롤러는 raw 데이터만 반환 — `{success,data}` 래핑은 인터셉터가 담당. 직접 래핑 금지. 스트림/204 는 `@SkipTransform()`.
|
||||
- [ ] **에러**: 사용자 노출 실패는 `BusinessException('CODE')` 로 던진다. 코드 없는 실패 금지.
|
||||
- [ ] **에러 코드 추가 시**: `error-codes.ts` 만 수정 → `npm run sync:error-codes` 실행 → 세 스택 목록 테스트 갱신 ([[error-codes]]).
|
||||
- [ ] **목록 API**: 페이지네이션 적용, 응답은 `{items, pageInfo}`.
|
||||
- [ ] **DTO**: 모든 입력 DTO 에 `class-validator` + 한국어 메시지 + `@ApiProperty()`.
|
||||
- [ ] **Swagger**: 새 엔드포인트에 `@ApiTags`/`@ApiOperation`/`@ApiResponse`.
|
||||
- [ ] **lint**: 커밋 전 `npm run lint` 통과 (Warning 0).
|
||||
- [ ] **Swagger**: 새 엔드포인트에 `@ApiTags`/`@ApiOperation` + `@ApiStandardResponse`/`@ApiErrorResponse`.
|
||||
- [ ] **경로 alias**: `@/` 사용 (`tsconfig.json` paths + jest `moduleNameMapper` 설정 완료). 깊은 상대경로 금지.
|
||||
- [ ] **lint/test**: 커밋 전 `npm run lint` (`--max-warnings 0`) + `npm run test` 통과. 자동 수정은 `npm run lint:fix`.
|
||||
- [ ] **의존성 변경 시**: 호스트가 아니라 **컨테이너 안에서** `npm install/uninstall` (위 주의 항목 참조). 변경 후 Docker 빌드 확인.
|
||||
- [ ] **에러 던지기**: `HttpException` 직접 생성은 린트로 차단된다 — `BusinessException(code)` 사용.
|
||||
- [ ] **새 필수 env 추가 시**: `shared/config/env.validation.ts` 에 검증 규칙과 테스트를 함께 추가 ([[env-structure]]).
|
||||
- [ ] **DB 전환 시**: in-memory Map → TypeORM 엔티티/리포지토리로 교체.
|
||||
- [ ] **인증 추가 시**: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/003 흐름 확인.
|
||||
- [ ] **env**: DB/Redis/CORS/포트는 루트 `.env` 에서 주입된다. 백엔드 고유 시크릿(`JWT_SECRET` 등)만 `backend/.env.{env}` 에 작성(compose `env_file` 로 로드).
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API
|
||||
|
||||
> 이 스킬은 `.claude/skills/vue3/SKILL.md`에 위치한다.
|
||||
> 사용법: `/vue3 [요청 내용]` 또는 frontend/ 작업 시 자동 적용.
|
||||
> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]]
|
||||
> 공통 규칙: [[response-format]] · [[error-codes]] · [[api-design]] · [[security]] · [[coding-common]]
|
||||
|
||||
---
|
||||
|
||||
@@ -16,8 +16,10 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API
|
||||
| 제공 항목 | 위치 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| 중앙 Axios 인스턴스 | `src/composables/useApi.ts` | baseURL=`VITE_API_BASE_URL`, `withCredentials` |
|
||||
| 응답 언래핑 인터셉터 | `useApi.ts` | `{success,data}→data`, 에러 코드 처리 |
|
||||
| ErrorCodeLexicon | `useApi.ts` 내부 | 에러 코드 → 한국어 메시지 ([[error-codes]]) |
|
||||
| 응답 언래핑 인터셉터 | `useApi.ts` | `{success,data}→data`, 에러 코드 정규화(`ApiError`), 중복 알림 억제 |
|
||||
| 에러 코드 사전 | `src/shared/constants/error-codes.generated.ts` | **자동 생성 파일 — 직접 수정 금지** ([[error-codes]]) |
|
||||
| 세션 만료 처리 | `useApi.ts` 의 `toAuthCode()` | 로그인 상태에서 401 → `AUTH_002` 승격 + `authStore.clearAuth()` |
|
||||
| 페이지네이션 타입 | `src/shared/types/pagination.ts` | 목록 응답 공유 계약 ([[response-format]]) |
|
||||
| 도메인 Composable 예제 | `src/composables/useUser.ts` | 새 도메인 API 패턴 참고 |
|
||||
| 인증 Composable 예제 | `src/composables/useAuth.ts` | 로그인/로그아웃 흐름(쿠키 기반) 시작점 |
|
||||
| Pinia 스토어 예제 | `src/stores/user.store.ts` | 새 스토어 패턴 참고 |
|
||||
@@ -112,23 +114,33 @@ export const useUserStore = defineStore('user', () => {
|
||||
## API 호출 (Composables + Axios)
|
||||
|
||||
```typescript
|
||||
// composables/useApi.ts — 중앙 Axios 인스턴스
|
||||
// composables/useApi.ts — 중앙 Axios 인스턴스 (구조 요약)
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // env.d.ts 에 타입 선언
|
||||
withCredentials: true, // HttpOnly 쿠키 전송
|
||||
})
|
||||
|
||||
// 응답 인터셉터 — 표준 응답 언래핑 + 에러 코드 전역 처리
|
||||
api.interceptors.response.use(
|
||||
(response) => response.data.data, // { success, data } → data 언래핑
|
||||
(error) => {
|
||||
const code = error.response?.data?.error?.code
|
||||
// 에러 코드별 Toast 알림 처리 ([[error-codes]] 참조)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
// 실패 시 ApiError { code, message, traceId, status } 로 정규화해 reject 한다.
|
||||
// 문구는 생성된 사전에서만 오고, 같은 코드의 알림은 중복 억제된다.
|
||||
```
|
||||
|
||||
에러 처리 규칙:
|
||||
|
||||
- **개별 `try/catch` 에서 메시지를 만들지 않는다.** 알림은 인터셉터가 이미 처리했다.
|
||||
- 호출부에서 분기가 필요하면 `message` 가 아니라 **`code` 로 분기**한다.
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await api.post('/users', payload)
|
||||
} catch (e) {
|
||||
const error = e as ApiError
|
||||
if (error.code === 'USER_001') focusEmailField()
|
||||
}
|
||||
```
|
||||
|
||||
- Toast 라이브러리를 도입하면 `useApi.ts` 의 `showErrorUI()` **한 함수만** 교체한다.
|
||||
|
||||
```typescript
|
||||
// composables/useUser.ts — 도메인별 API Composable
|
||||
export function useUser() {
|
||||
@@ -194,10 +206,13 @@ router.beforeEach((to) => {
|
||||
(전체 확장 지점·점검 목록은 [[checklist]])
|
||||
|
||||
- [ ] **API 호출**: 컴포넌트에서 `axios` 직접 호출 금지 → `composables/` 경유.
|
||||
- [ ] **에러 처리**: 개별 try/catch 로 메시지를 만들지 말고 `useApi` 인터셉터 + [[error-codes]] 에 위임.
|
||||
- [ ] **에러 처리**: 개별 try/catch 로 메시지를 만들지 말고 `useApi` 인터셉터 + [[error-codes]] 에 위임. 분기는 `error.code` 로.
|
||||
- [ ] **에러 사전**: `error-codes.generated.ts` 직접 수정 금지 — 백엔드 레지스트리 수정 후 `npm run sync:error-codes`.
|
||||
- [ ] **목록 화면**: 응답은 `{items, pageInfo}` — `shared/types/pagination.ts` 타입 사용.
|
||||
- [ ] **컴포넌트**: `<script setup lang="ts">` + `<style scoped>` + rem 단위. 인라인 스타일·Options API 금지.
|
||||
- [ ] **타입**: Props/Emits 는 인터페이스로 정의.
|
||||
- [ ] **라우트**: 페이지는 lazy import, 보호 라우트는 `meta.requiresAuth`.
|
||||
- [ ] **auth 가드(인증 도입 시)**: `router/index.ts` 의 `TODO` — `auth.store.ts` 작성 후 `isLoggedIn` 검사 연결.
|
||||
- [ ] **lint**: 커밋 전 `npm run lint` + `npm run type-check` 통과.
|
||||
- [ ] **auth 가드**: `router/index.ts` 가드는 `auth.store.ts` 의 `isLoggedIn` 과 이미 연결되어 있다. 로그인 페이지 추가 시 리다이렉트 대상을 `{ name: 'login' }` 으로 바꾼다.
|
||||
- [ ] **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` 에 별도 작성 불필요.
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
세 클라이언트는 동일한 **표준 응답 포맷**과 **에러 코드 맵**을 공유한다 (아래 공통 규칙 참조).
|
||||
|
||||
> 규칙은 `.claude/` 아래로 모듈화되어 있다.
|
||||
> - `.claude/rules/` — 세 플랫폼 공통 규칙 (코딩·응답 포맷·에러 코드·커밋·보안·환경변수)
|
||||
> - `.claude/rules/` — 세 플랫폼 공통 규칙 (코딩·응답 포맷·에러 코드·API 설계·로깅·커밋·보안·환경변수·테스트)
|
||||
> 새 규칙 파일을 추가하면 **반드시 아래 `@` import 목록에도 등록**한다(등록하지 않으면 로드되지 않는다).
|
||||
> - `.claude/skills/` — 도메인별 개발 규칙 (스킬). 슬래시 명령(`/nestjs` 등) 또는 해당 디렉터리 작업 시 자동 적용
|
||||
> - `.claude/settings.json` — Claude Code 권한·환경 설정
|
||||
|
||||
@@ -23,6 +24,8 @@
|
||||
@.claude/rules/coding-common.md
|
||||
@.claude/rules/response-format.md
|
||||
@.claude/rules/error-codes.md
|
||||
@.claude/rules/api-design.md
|
||||
@.claude/rules/logging.md
|
||||
@.claude/rules/git-convention.md
|
||||
@.claude/rules/security.md
|
||||
@.claude/rules/env-structure.md
|
||||
|
||||
@@ -74,17 +74,70 @@ cd flutter && cp .env.example .env && flutter pub get && flutter run
|
||||
```jsonc
|
||||
// 성공
|
||||
{ "success": true, "data": { /* ... */ } }
|
||||
// 실패
|
||||
{ "success": false, "error": { "code": "VAL_001", "message": "..." } }
|
||||
|
||||
// 실패 — 사용자에게 보이는 모든 실패는 에러 코드를 가진다
|
||||
{ "success": false, "error": { "code": "VAL_001", "message": "...", "traceId": "3f1c0a2e-..." } }
|
||||
|
||||
// 목록 — data 안의 형태가 고정되어 있다
|
||||
{ "success": true, "data": { "items": [], "pageInfo": { "hasNext": false, "nextCursor": null, "total": 0 } } }
|
||||
```
|
||||
|
||||
에러 코드 맵과 코딩 규칙 전문은 [`CLAUDE.md`](./CLAUDE.md) 및 `.claude/rules/`·`.claude/skills/` 의 규칙 문서를 참고하세요.
|
||||
`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 없이)
|
||||
|
||||
```bash
|
||||
# backend
|
||||
cd backend && npm ci && npm run start:dev
|
||||
|
||||
# frontend
|
||||
cd frontend && npm ci && npm run dev
|
||||
|
||||
# flutter
|
||||
cd flutter && flutter pub get && flutter run
|
||||
```
|
||||
|
||||
### 커밋 전 검사
|
||||
|
||||
```bash
|
||||
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`](./.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) |
|
||||
@@ -96,12 +149,15 @@ cd flutter && cp .env.example .env && flutter pub get && flutter run
|
||||
.
|
||||
├── backend/ # NestJS API (Controller / Service / DTO / 공통 필터·인터셉터)
|
||||
├── frontend/ # Vue 3 (pages / components / composables / stores / router)
|
||||
├── flutter/ # Flutter (core / features / widgets)
|
||||
├── 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
|
||||
└── .claude/
|
||||
├── settings.json # Claude Code 권한·환경 설정
|
||||
├── rules/ # 공통 규칙 (코딩·응답 포맷·에러 코드·커밋·보안·환경변수)
|
||||
├── rules/ # 공통 규칙 (코딩·응답 포맷·에러 코드·API 설계·로깅·보안·환경변수·테스트)
|
||||
└── skills/ # 도메인별 개발 규칙 (nestjs·vue3·flutter·docker)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user