docs: 규칙 문서를 실제 구현에 맞춰 갱신
CI / Frontend (lint · type-check · test) (push) Has been cancelled
CI / Flutter (analyze · test) (push) Has been cancelled
CI / Backend (lint · test · e2e · build) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled

규칙과 구현이 어긋나 있던 부분을 바로잡고, 작업하며 발견한 함정을 기록한다.

- 스킬 문서의 "기본 제공 골격" 표를 실제 파일 목록과 일치시킴
- 커밋 전 체크리스트에 테스트 실행·에러 코드 동기화 절차 추가
- 커밋 규칙에 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:
2026-09-17 14:32:23 +09:00
parent 258e0e37cc
commit 700a100a38
10 changed files with 352 additions and 102 deletions
+40 -20
View File
@@ -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 구조)
+9 -1
View File
@@ -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]]
+11 -1
View File
@@ -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]] 참조.
+34 -8
View File
@@ -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]])
+34 -2
View File
@@ -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]]).