From 700a100a387f28f30dfc96fa2aa60f9c6e15b387 Mon Sep 17 00:00:00 2001 From: ttipo Date: Thu, 17 Sep 2026 14:32:23 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EA=B7=9C=EC=B9=99=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=8B=A4=EC=A0=9C=20=EA=B5=AC=ED=98=84=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 규칙과 구현이 어긋나 있던 부분을 바로잡고, 작업하며 발견한 함정을 기록한다. - 스킬 문서의 "기본 제공 골격" 표를 실제 파일 목록과 일치시킴 - 커밋 전 체크리스트에 테스트 실행·에러 코드 동기화 절차 추가 - 커밋 규칙에 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) --- .claude/rules/checklist.md | 60 +++++++++----- .claude/rules/coding-common.md | 10 ++- .claude/rules/env-structure.md | 12 ++- .claude/rules/git-convention.md | 42 ++++++++-- .claude/rules/testing.md | 36 ++++++++- .claude/skills/flutter/SKILL.md | 43 ++++++---- .claude/skills/nestjs/SKILL.md | 135 ++++++++++++++++++++++++-------- .claude/skills/vue3/SKILL.md | 45 +++++++---- CLAUDE.md | 5 +- README.md | 66 ++++++++++++++-- 10 files changed, 352 insertions(+), 102 deletions(-) diff --git a/.claude/rules/checklist.md b/.claude/rules/checklist.md index a1ca0d2..b03755c 100644 --- a/.claude/rules/checklist.md +++ b/.claude/rules/checklist.md @@ -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]]) -- [ ] 커밋 메시지 `: <한국어 제목>` 형식, 한 커밋 한 변경 ([[git-convention]]) +- [ ] 민감정보(`.env`, 시크릿) 미커밋 — `*.example` 만 추적하며 그 값은 placeholder ([[security]]) +- [ ] 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음 ([[logging]]) +- [ ] 커밋 메시지 `(): <한국어 제목>` 형식, 한 커밋 한 변경 ([[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 구조) diff --git a/.claude/rules/coding-common.md b/.claude/rules/coding-common.md index 4d8ff3d..15648e5 100644 --- a/.claude/rules/coding-common.md +++ b/.claude/rules/coding-common.md @@ -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]] diff --git a/.claude/rules/env-structure.md b/.claude/rules/env-structure.md index 169012f..3558b5f 100644 --- a/.claude/rules/env-structure.md +++ b/.claude/rules/env-structure.md @@ -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]] 참조. diff --git a/.claude/rules/git-convention.md b/.claude/rules/git-convention.md index 6fc5e77..5b693e7 100644 --- a/.claude/rules/git-convention.md +++ b/.claude/rules/git-convention.md @@ -1,12 +1,38 @@ ---- -name: git-convention -description: Git 커밋 메시지 컨벤션 — : <제목> 형식, 한국어 명령형, 한 커밋 한 논리 변경. ---- +# Git 컨벤션 -# Git 커밋 컨벤션 +## 커밋 메시지 -`: <제목>` 형식 — type: **feat / fix / refactor / docs / chore / perf / test / style** +`(): <제목>` 형식 — 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 가 말해준다). + +## 브랜치 + +`/<간단한-설명>` — 기본 브랜치는 `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]]) diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index 7d1ff49..ee7d97f 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -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]]). diff --git a/.claude/skills/flutter/SKILL.md b/.claude/skills/flutter/SKILL.md index 179423e..c84e8cb 100644 --- a/.claude/skills/flutter/SKILL.md +++ b/.claude/skills/flutter/SKILL.md @@ -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` 가 경고**한다. diff --git a/.claude/skills/nestjs/SKILL.md b/.claude/skills/nestjs/SKILL.md index ec731be..848129f 100644 --- a/.claude/skills/nestjs/SKILL.md +++ b/.claude/skills/nestjs/SKILL.md @@ -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 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(); +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` 로 로드). diff --git a/.claude/skills/vue3/SKILL.md b/.claude/skills/vue3/SKILL.md index efef90b..e474c8a 100644 --- a/.claude/skills/vue3/SKILL.md +++ b/.claude/skills/vue3/SKILL.md @@ -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` 타입 사용. - [ ] **컴포넌트**: `