diff --git a/.claude/commands/flutter.md b/.claude/commands/flutter.md deleted file mode 100644 index 3fb0e79..0000000 --- a/.claude/commands/flutter.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -description: Flutter 모바일 앱 개발 규칙 ---- - -> 이 파일은 `.claude/commands/flutter.md`에 위치한다. -> 사용법: `/flutter [요청 내용]` - ---- - -## 언어 & 기본 원칙 - -- **Dart Sound Null Safety** 완벽 준수 (`?`, `!`, `late` 올바르게 사용) -- `dynamic` 타입 사용 **지양** → 명시적 타입 선언 -- 모든 주석은 **한국어**로 작성 - ---- - -## 프로젝트 구조 - -``` -lib/ -├── main.dart -├── app.dart ← MaterialApp / 라우터 설정 -├── core/ -│ ├── constants/ ← 상수 (색상, 크기, 텍스트 등) -│ ├── theme/ ← 앱 테마 정의 -│ ├── api/ ← Dio 인스턴스(dio_client), 에러 사전(error_lexicon), 인터셉터 -│ ├── storage/ ← flutter_secure_storage 래퍼 (선택) -│ └── utils/ ← 순수 유틸 함수 -├── features/ -│ └── user/ -│ ├── data/ -│ │ ├── models/ ← JSON 직렬화 모델 -│ │ └── repositories/ ← API 호출 구현체 -│ ├── domain/ -│ │ └── entities/ ← 순수 비즈니스 엔티티 -│ └── presentation/ -│ ├── pages/ ← 화면 단위 위젯 -│ ├── widgets/ ← 재사용 위젯 -│ └── providers/ ← 상태 관리 -└── shared/ - └── widgets/ ← 앱 공통 위젯 -``` - ---- - -## 위젯 작성 원칙 - -```dart -// ✅ 좋은 예 — 작은 위젯으로 분리 -class UserProfilePage extends StatelessWidget { - const UserProfilePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: const _AppBar(), - body: const _ProfileBody(), - ); - } -} - -class _ProfileBody extends StatelessWidget { - const _ProfileBody(); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const _AvatarSection(), - const _InfoSection(), - ], - ); - } -} -``` - -- 위젯 트리 depth가 깊어지면 **커스텀 위젯으로 분리** -- `build()` 메서드 내 비즈니스 로직 작성 **금지** -- `const` 생성자 적극 활용 (불필요한 리빌드 방지) - ---- - -## 반응형 UI - -```dart -// ❌ 하드코딩 금지 -Container(width: 375, height: 200) - -// ✅ 반응형 처리 -Container( - width: MediaQuery.of(context).size.width * 0.9, - child: ..., -) - -// ✅ 유연한 레이아웃 -Expanded(flex: 2, child: LeftPanel()), -Expanded(flex: 1, child: RightPanel()), -``` - -- `Expanded`, `Flexible`, `MediaQuery`, `LayoutBuilder` 적극 활용 -- 하드코딩 픽셀값 사용 **금지** -- 폰트 크기는 `Theme.of(context).textTheme` 활용 - ---- - -## 상태 관리 - -```dart -// Riverpod 예시 (권장) -@riverpod -Future fetchUser(FetchUserRef ref, int id) async { - final repo = ref.watch(userRepositoryProvider); - return repo.getUser(id); -} - -// 위젯에서 사용 -class UserWidget extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final userAsync = ref.watch(fetchUserProvider(userId)); - return userAsync.when( - data: (user) => Text(user.name), - loading: () => const CircularProgressIndicator(), - error: (e, _) => const ErrorWidget(), - ); - } -} -``` - -- **Riverpod** 권장 (Provider는 레거시로 간주) -- `StatefulWidget`은 애니메이션, 폼 등 로컬 UI 상태에만 제한적 사용 - ---- - -## API 통신 (Dio + 인터셉터) - -```dart -// core/api/dio_client.dart -class DioClient { - late final Dio _dio; - - DioClient() { - // flutter_dotenv 로 .env 로드, 미설정 시 로컬 기본값 - final baseUrl = dotenv.maybeGet('API_URL') ?? 'http://localhost:3000/api'; - _dio = Dio(BaseOptions(baseUrl: baseUrl)); - _dio.interceptors.add(AuthInterceptor()); // 토큰 자동 첨부 - _dio.interceptors.add(ErrorInterceptor()); // 에러 코드 처리 (error_lexicon 참조) - } -} - -// 에러 인터셉터 — CLAUDE.md 에러 맵 기준 한국어 메시지 처리 -class ErrorInterceptor extends Interceptor { - @override - void onError(DioException err, ErrorInterceptorHandler handler) { - // 에러 코드 파싱 후 SnackBar/Dialog 표시 - } -} -``` - ---- - -## 보안 (토큰 저장) - -```dart -// flutter_secure_storage 사용 -const storage = FlutterSecureStorage(); - -// 저장 (키 이름은 dio_client 인터셉터와 통일: jwt_token) -await storage.write(key: 'jwt_token', value: token); - -// 읽기 -final token = await storage.read(key: 'jwt_token'); - -// 삭제 (로그아웃) -await storage.deleteAll(); -``` - -- JWT, 민감 정보는 반드시 `flutter_secure_storage` 사용 -- `SharedPreferences`에 토큰 저장 **금지** - ---- - -## 성능 & 최신 트렌드 추가 규칙 - -- **모델 직렬화**: `json_serializable` + `freezed` 사용 권장 (불변 모델) -- **이미지**: `cached_network_image`로 네트워크 이미지 캐싱 -- **목록**: 긴 목록은 `ListView.builder` 사용 (전체 렌더링 금지) -- **에러 핸들링**: `Either` 패턴 고려 (`fpdart` 패키지) -- **라우팅**: `go_router` 사용 권장 (딥링크, 중첩 라우팅 지원) -- **환경 분리**: `--dart-define` 또는 `flutter_dotenv`로 dev/prod 분리 diff --git a/.claude/rules/checklist.md b/.claude/rules/checklist.md new file mode 100644 index 0000000..a1ca0d2 --- /dev/null +++ b/.claude/rules/checklist.md @@ -0,0 +1,52 @@ +--- +name: checklist +description: 신규 프로젝트 셋업 절차 + 매 작업 공통 점검(에러코드 SSOT 동기화 등). 특정 시점의 구현 상태가 아니라 언제나 유효한 공용 규칙만 담는다. +--- + +# 작업 점검 & 신규 프로젝트 셋업 + +> 이 저장소는 **재사용 템플릿**이다. 클론해 어떤 프로젝트든 시작할 수 있도록, 여기에는 **언제나 유효한 공용 규칙**만 둔다(특정 시점의 구현 상태는 두지 않는다). +> 각 스택이 기본 제공하는 골격과 확장 규칙은 [[nestjs]] · [[vue3]] · [[flutter]] · [[docker]] 스킬 참조. + +--- + +## 1. 신규 프로젝트 셋업 (클론 직후 1회) + +- [ ] 루트 `.env.{env}.example` → `.env.{env}` 복사 (루트가 설정 SSOT — 대표 키가 채워져 있음, [[env-structure]]) +- [ ] `flutter/.env.example` → `flutter/.env` 복사 후 `API_URL` 확인 (Android 에뮬레이터: `10.0.2.2`) +- [ ] 프로젝트 식별 값 교체 (`PROJECT_NAME` 등 루트 env 키) +- [ ] 백엔드 고유 시크릿(`JWT_SECRET` 등)을 `backend/.env.{env}` 에 작성 (compose `env_file` 로 로드) +- [ ] Docker 개발 환경 기동으로 동작 확인 ([[docker]]) + +## 2. 커밋 전 공통 점검 (매 작업) + +- [ ] 주석은 한국어, 매직 넘버·하드코딩 문자열 없음 ([[coding-common]]) +- [ ] 린트/분석 통과 — backend `npm run lint`, frontend `npm run lint && npm run type-check`, flutter `flutter analyze` (Warning 0) +- [ ] 절대경로 alias 사용 (TS `@/`, Dart `package:`), 깊은 상대경로 없음 +- [ ] 민감정보(`.env`, 시크릿) 미커밋 — `*.example` 만 추적 ([[security]]) +- [ ] 커밋 메시지 `: <한국어 제목>` 형식, 한 커밋 한 변경 ([[git-convention]]) + +## 3. 에러 코드 SSOT 동기화 (메시지/코드 변경 시 반드시 3곳 동시 수정) + +- [ ] backend `src/common/filters/http-exception.filter.ts` +- [ ] frontend `src/composables/useApi.ts` (`ErrorCodeLexicon`) +- [ ] flutter `lib/core/api/error_lexicon.dart` (`ErrorLexicon`) + +> `AUTH_002`(세션 만료)는 **클라이언트 전용** 코드로 서버 응답에는 나타나지 않는다. 자세한 내용은 [[error-codes]]. + +--- + +## 4. 기능 확장 시 따르는 순서 + +새 기능은 만들기 전에, 해당 스킬의 **"템플릿 기본 제공 골격"** 에서 재사용할 패턴을 먼저 찾는다(같은 역할을 새로 만들지 말 것). + +1. 해당 디렉터리 스킬 규칙 확인 ([[nestjs]] / [[vue3]] / [[flutter]]) +2. 기본 제공 골격(인터셉터·필터·dio client·error lexicon·예제 모듈)에서 패턴 재사용 +3. 표준 응답/에러 흐름 준수 ([[response-format]] · [[error-codes]]) +4. env 신규 값은 루트 `.env` 부터 ([[env-structure]]) +5. 커밋 전 위 2·3절 점검 통과 + +> 대표 확장 지점(각 스킬의 "확장 시 따르는 규칙" 참조): +> - 인증/권한 → [[nestjs]](guards·JWT) · [[vue3]](auth store + 라우터 가드) +> - 데이터 영속화 → [[nestjs]](TypeORM 엔티티/리포지토리) +> - 상태·화면 계층 → [[flutter]](Riverpod 프로바이더 · features 구조) diff --git a/.claude/rules/coding-common.md b/.claude/rules/coding-common.md new file mode 100644 index 0000000..4d8ff3d --- /dev/null +++ b/.claude/rules/coding-common.md @@ -0,0 +1,27 @@ +--- +name: coding-common +description: 세 클라이언트(backend/frontend/flutter) 공통 코딩 규칙 — 주석 언어, 타입 정책, 린트, 순수 유틸 분리, 절대경로 alias. +--- + +# 공통 코딩 규칙 + +- 모든 코드 주석은 **한국어**로 작성 +- 웹(`frontend`)·서버(`backend`)는 100% TypeScript, 모바일(`flutter`)은 Dart Sound Null Safety 준수 +- 컴파일러/린터 **Warning 방치 금지** — 커밋 전 각 프로젝트 `lint` / `analyze` 통과 +- 유틸 함수는 순수 함수로 분리 — TS는 `shared/utils/`, Dart는 `core/utils/` +- 매직 넘버·하드코딩 문자열 지양 → 상수 또는 환경변수로 분리 +- 절대경로 alias 사용 (TS `@/`, Dart `package:`), 깊은 상대경로(`../../`) 지양 + +## 네이밍 규약 + +| 대상 | 규칙 | 예 | +| --- | --- | --- | +| TS 파일(일반) | kebab-case | `user.store.ts`, `use-api.ts` / 기존 composable 은 `useApi.ts` 형태 유지 | +| Vue 컴포넌트 파일 | PascalCase | `BaseButton.vue`, `UserCard.vue` | +| Dart 파일·디렉터리 | snake_case | `dio_client.dart`, `user_repository.dart` | +| 클래스/타입 | PascalCase | `UserModel`, `TransformInterceptor` | +| 변수/함수 | camelCase (Dart 포함) | `getUser`, `isLoggedIn` | +| 상수 | TS `UPPER_SNAKE` 또는 모듈 상수, Dart `lowerCamel` | `MAX_RETRY`, `defaultTimeout` | +| 에러 코드 | `DOMAIN_NNN` | `AUTH_001`, `VAL_001` ([[error-codes]]) | + +> 도메인별 세부 규칙: [[nestjs]] · [[vue3]] · [[flutter]] · 테스트: [[testing]] diff --git a/.claude/rules/env-structure.md b/.claude/rules/env-structure.md new file mode 100644 index 0000000..169012f --- /dev/null +++ b/.claude/rules/env-structure.md @@ -0,0 +1,30 @@ +--- +name: env-structure +description: 환경변수 단일 진실 공급원(SSOT) 구조 — 루트 .env.{APP_ENV} 가 모든 설정의 SSOT, docker-compose 가 서비스에 주입. 새 설정값 추가/서비스 env 작성 규칙. +--- + +# 환경변수 구조 (단일 진실 공급원: 루트 `.env`) + +이 템플릿은 **루트 `.env.{APP_ENV}` 가 모든 설정의 단일 진실 공급원(SSOT)** 이다. +값을 한 곳에서만 관리하기 위해, 서비스(backend/frontend)는 자체 env에 값을 중복 작성하지 않고 **루트 → docker-compose 가 주입한 값을 그대로 사용**한다. + +``` +루트 .env.{APP_ENV} + │ (docker compose --env-file 로 로드) + ├─▶ backend : env_file + environment 로 DB/Redis/CORS/포트 주입 + │ (backend/.env.{APP_ENV} 는 "존재"만 하면 됨, 내용 최소) + ├─▶ frontend : BACKEND_API_URL → build arg(VITE_API_BASE_URL) 로 주입 + ├─▶ db : DB_USER/DB_PASSWORD/DB_NAME, DB_DATA_DIR 볼륨 + └─▶ redis : REDIS_PASSWORD, REDIS_DATA_DIR 볼륨 +``` + +규칙: + +- **새 설정값은 루트 `.env.{APP_ENV}` 에 먼저 추가**하고, 필요한 서비스에 `docker-compose.yml` 의 `environment` / `args` 로 전달한다. +- 서비스 자체 env 파일에는 **루트에서 내려주지 않는 고유 값만** 작성한다. (예: 백엔드 `JWT_SECRET`) +- `docker-compose.yml` 의 `env_file` 지시자 때문에 `backend/.env.{APP_ENV}` 파일은 비어 있어도 **존재해야 한다.** +- **Flutter 는 예외** — docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립적으로 관리한다. +- 모든 env 파일은 커밋 금지, `*.example` 만 커밋한다. (`.gitignore` 의 `!*.example` 재포함 규칙) + +> 루트 env의 대표 키: `PROJECT_NAME`, `APP_ENV`, `BUILD_MODE`, 각종 `*_PORT`, `BACKEND_API_URL`, `CORS_ORIGIN`, `DB_*`/`DB_DATA_DIR`, `REDIS_*`/`REDIS_DATA_DIR`. 실제 목록은 `.env.development.example` 참조. +> 실행/배포 명령은 [[docker]], 보안 규칙은 [[security]] 참조. diff --git a/.claude/rules/error-codes.md b/.claude/rules/error-codes.md new file mode 100644 index 0000000..d6ef5d0 --- /dev/null +++ b/.claude/rules/error-codes.md @@ -0,0 +1,29 @@ +--- +name: error-codes +description: 세 플랫폼이 공유하는 에러 코드 ↔ 한국어 메시지 매핑(SSOT). 메시지 수정 시 backend/frontend/flutter 3개 파일을 함께 갱신한다. +--- + +# 에러 코드 맵핑 + +세 플랫폼이 공유하는 **단일 진실 공급원(SSOT)**. 메시지를 수정할 때는 아래 세 파일을 함께 갱신한다. + +- backend: `src/common/filters/http-exception.filter.ts` +- frontend: `src/composables/useApi.ts` (`ErrorCodeLexicon`) +- flutter: `lib/core/api/error_lexicon.dart` (`ErrorLexicon`) + +| 코드 | HTTP / 상황 | 메시지 | +| -------- | -------------------- | ------------------------------------------------------- | +| SYS_001 | 500 / 미정의 예외 | "일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." | +| AUTH_001 | 401 | "로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요." | +| AUTH_002 | 세션 만료 (클라이언트) | "안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요." | +| AUTH_003 | 403 | "해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다." | +| VAL_001 | 400 / 422 (Validation) | "입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)" | +| RES_001 | 404 | "요청하신 정보나 페이지를 찾을 수 없습니다." | +| BIZ_001 | 200(비즈니스 실패) / 기타 | "요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요." | + +> 비즈니스 예외는 `HttpException` 응답의 `message` 로 사용자 노출 문구를 재정의할 수 있다. +> 응답 래퍼 구조는 [[response-format]] 참조. + +> **구현 주의:** 백엔드 필터(`http-exception.filter.ts`)는 **6개 코드**(SYS_001 / AUTH_001 / AUTH_003 / VAL_001 / RES_001 / BIZ_001)만 방출한다. +> `AUTH_002`(세션 만료)는 **클라이언트(frontend/flutter)에서만** 생성하는 코드로, 서버 응답에는 나타나지 않는다. +> 코드/메시지 변경 시 위 3개 파일을 함께 갱신해야 하며, 점검 항목은 [[checklist]] 참조. diff --git a/.claude/rules/git-convention.md b/.claude/rules/git-convention.md new file mode 100644 index 0000000..6fc5e77 --- /dev/null +++ b/.claude/rules/git-convention.md @@ -0,0 +1,12 @@ +--- +name: git-convention +description: Git 커밋 메시지 컨벤션 — : <제목> 형식, 한국어 명령형, 한 커밋 한 논리 변경. +--- + +# Git 커밋 컨벤션 + +`: <제목>` 형식 — type: **feat / fix / refactor / docs / chore / perf / test / style** + +- 예: `feat: 사용자 목록 페이지네이션 추가` +- 제목은 한국어, 명령형 현재 시제로 작성 +- 한 커밋은 하나의 논리적 변경만 포함 diff --git a/.claude/rules/response-format.md b/.claude/rules/response-format.md new file mode 100644 index 0000000..e3014f3 --- /dev/null +++ b/.claude/rules/response-format.md @@ -0,0 +1,21 @@ +--- +name: response-format +description: 모든 API 응답이 따르는 표준 응답 래퍼(success/data/error) 규칙. SSOT는 backend TransformInterceptor + HttpExceptionFilter. +--- + +# 표준 응답 포맷 (Global Response Wrapper) + +모든 API 응답은 아래 구조로 통일한다. +(SSOT: backend `TransformInterceptor` + `HttpExceptionFilter` — [[nestjs]] 참조) + +```jsonc +// 성공 +{ "success": true, "data": { /* ... */ } } + +// 실패 +{ "success": false, "error": { "code": "VAL_001", "message": "..." } } +``` + +- 프론트/플러터 인터셉터는 `success: true` 면 `data` 만 언래핑하여 반환한다. +- `success: false` 이거나 HTTP 4xx/5xx 면 `error.code` 로 메시지를 매핑한다. +- 에러 코드 ↔ 메시지 매핑은 [[error-codes]] 참조. diff --git a/.claude/rules/security.md b/.claude/rules/security.md new file mode 100644 index 0000000..2e4fe47 --- /dev/null +++ b/.claude/rules/security.md @@ -0,0 +1,15 @@ +--- +name: security +description: 프로젝트 공통 보안 규칙 — 민감정보 커밋 금지, JWT 쿠키/secure storage, SQL Injection 차단, DTO 검증, 보안 헤더/CORS/Rate Limiting, 컨테이너 비-root 구동. +--- + +# 보안 + +- `.env*` 등 민감정보 Git 커밋 금지 (`.gitignore` 확인) — 예시는 `*.example`(예: `.env.development.example`, `.env.example`) 로만 공유 +- JWT는 HttpOnly·Secure 쿠키 사용 (웹 `localStorage` 금지) / 모바일은 `flutter_secure_storage` 사용 +- TypeORM ORM 쿼리 사용으로 SQL Injection 차단 (Raw 쿼리 시 파라미터 바인딩 필수) +- 입력값은 DTO(`class-validator`) 검증을 거친 뒤 서비스 계층에 전달 +- 보안 헤더(`helmet`)·CORS·Rate Limiting(`@nestjs/throttler`) 적용 +- Docker 컨테이너 내부 root 실행 금지 (비-root 사용자로 구동) + +> 환경변수 관리 규칙은 [[env-structure]], 도메인별 적용은 [[nestjs]] · [[flutter]] 참조. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..7d1ff49 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,40 @@ +--- +name: testing +description: 세 스택 공통 테스트 규칙 — 테스트 위치/네이밍, 무엇을 테스트할지, 커밋 전 실행. backend Jest, frontend Vitest, flutter flutter_test 기준. +--- + +# 테스트 규칙 + +> 템플릿은 각 스택에 테스트 러너가 준비되어 있다 — backend `jest`, frontend `vitest`, flutter `flutter_test`. +> 새 로직을 추가하면 최소한의 테스트를 함께 작성한다. + +## 공통 원칙 + +- **무엇을 테스트하나**: 순수 함수(utils), 서비스/비즈니스 로직, 에러 코드 매핑 분기를 우선한다. 프레임워크 기본 동작은 테스트하지 않는다. +- **테스트는 결정적(deterministic)** 으로 — 외부 네트워크/시간/랜덤에 의존하지 않도록 모킹한다. +- **커밋 전 실행** — 변경 영역의 테스트가 통과해야 한다. ([[checklist]]) +- 테스트 설명(it/test 제목)은 한국어로 "무엇을 검증하는지" 명확히 작성한다. + +## 스택별 위치·실행 + +| 스택 | 러너 | 위치·네이밍 | 실행 | +| --- | --- | --- | --- | +| 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` | + +## 예시 (backend 순수 유틸) + +```typescript +// shared/utils/format.spec.ts +import { formatSomething } from './format'; + +describe('formatSomething', () => { + it('빈 입력이면 빈 문자열을 반환한다', () => { + expect(formatSomething('')).toBe(''); + }); +}); +``` + +> API 응답 래퍼/에러 코드는 세 플랫폼 공유 계약이므로([[response-format]] · [[error-codes]]), +> 매핑 로직을 변경하면 해당 분기 테스트를 추가/갱신한다. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..78aa667 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Read(//**)", + "Bash(npm run lint:*)", + "Bash(npm run build:*)", + "Bash(npm run test:*)", + "Bash(npm run start:*)", + "Bash(npm ci)", + "Bash(npm install)", + "Bash(pnpm lint:*)", + "Bash(pnpm build:*)", + "Bash(flutter analyze:*)", + "Bash(flutter test:*)", + "Bash(flutter pub get)", + "Bash(dart format:*)", + "Bash(docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development logs:*)", + "Bash(docker compose --env-file .env.production logs:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git add:*)" + ], + "ask": [ + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(docker compose:*)" + ], + "deny": [ + "Read(./.env)", + "Read(./.env.development)", + "Read(./.env.production)", + "Read(./backend/.env)", + "Read(./backend/.env.development)", + "Read(./backend/.env.production)", + "Read(./frontend/.env)", + "Read(./frontend/.env.development)", + "Read(./frontend/.env.production)", + "Read(./flutter/.env)" + ] + }, + "env": { + "APP_ENV": "development" + } +} diff --git a/.claude/skills/docker/SKILL.md b/.claude/skills/docker/SKILL.md new file mode 100644 index 0000000..2bb7128 --- /dev/null +++ b/.claude/skills/docker/SKILL.md @@ -0,0 +1,84 @@ +--- +name: docker +description: Docker Compose 기반 로컬·운영 오케스트레이션 실행/배포 규칙 — 개발/운영 환경 기동, 빌드 여부 판단(--build), 로그 확인, 루트 .env SSOT 주입. 컨테이너를 띄우거나 배포·로그 확인·docker-compose 작업을 할 때 사용한다. +--- + +> 이 스킬은 `.claude/skills/docker/SKILL.md`에 위치한다. +> 환경변수 SSOT 구조는 [[env-structure]] 참조. +> `.env.development` / `.env.production` 는 커밋된 `.env.*.example` 를 복사해 생성한다. +> (`.gitignore` 는 `.env.*` 를 무시하되 `.env.*.example` 만 추적한다.) + +--- + +## 개발 환경 + +```bash +# 코드만 수정했을 때 (의존성 변경 없음) +docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d + +# 의존성 변경 시에만 --build +docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development up -d --build + +# 로그 확인 (예: backend) +docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development logs -f backend +``` + +## 운영 환경 + +```bash +# 운영 환경 배포 +sudo docker compose --env-file .env.production up -d --build + +# 로그 확인 +sudo docker compose --env-file .env.production logs -f backend +``` + +--- + +## 자주 쓰는 보조 명령 + +> 개발 prefix가 길어 아래는 다음 별칭으로 줄여 표기한다. +> 운영은 `docker compose --env-file .env.production` (필요 시 `sudo`) 으로 동일하게 사용한다. +> +> ```bash +> alias dc='docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.development' +> ``` + +```bash +# 상태 확인 +dc ps + +# 종료 — 컨테이너 중지·삭제 (DB/Redis 데이터 볼륨은 보존) +dc down + +# ⚠️ 데이터까지 삭제 — DB/Redis 볼륨 제거 (복구 불가, 신중히) +dc down -v + +# 특정 서비스만 재빌드·기동 (예: 의존성 바뀐 backend) +dc up -d --build backend + +# 특정 서비스 재시작 (코드 변경은 dev 핫리로드라 보통 불필요) +dc restart backend + +# 전체 / 특정 서비스 로그 +dc logs -f +dc logs -f backend + +# 컨테이너 쉘 접속 +dc exec backend sh + +# PostgreSQL 접속 (· 은 루트 .env 값으로 치환) +dc exec db psql -U -d + +# Redis 접속 ( 은 루트 .env 값) +dc exec redis redis-cli -a +``` + +--- + +## 규칙 + +- **빌드 최소화**: 의존성(package.json / pubspec 등) 변경이 없으면 `--build` 없이 기동한다. +- **env 파일**: 실행 전 `.env.{APP_ENV}` 존재를 확인한다. 없으면 `.env.{APP_ENV}.example` 을 복사해 생성. +- **컨테이너 비-root 구동**: 모든 서비스 컨테이너는 비-root 사용자로 실행한다 ([[security]] 참조). +- **개발/운영 분리**: 개발은 `docker-compose.yml + docker-compose.dev.yml`, 운영은 `docker-compose.yml` 단독 + `.env.production`. diff --git a/.claude/skills/flutter/SKILL.md b/.claude/skills/flutter/SKILL.md new file mode 100644 index 0000000..179423e --- /dev/null +++ b/.claude/skills/flutter/SKILL.md @@ -0,0 +1,246 @@ +--- +name: flutter +description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null Safety, feature-first 디렉터리 구조, 작은 위젯 분리, 반응형 UI, Riverpod 상태관리, Dio 인터셉터, flutter_secure_storage 토큰 저장. flutter/ 에서 작업하거나 위젯·프로바이더·모델·API 클라이언트를 만들 때 사용한다. +--- + +> 이 스킬은 `.claude/skills/flutter/SKILL.md`에 위치한다. +> 사용법: `/flutter [요청 내용]` 또는 flutter/ 작업 시 자동 적용. +> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]] +> Flutter 는 docker-compose 대상이 아니므로 `flutter/.env` 에서 `API_URL` 을 독립 관리한다 ([[env-structure]] 참조). + +--- + +## 템플릿 기본 제공 골격 + +> 이 템플릿은 네트워크 계층을 **기본 제공**한다. 새 코드는 이 파일들의 패턴을 재사용한다(같은 역할을 새로 만들지 말 것). + +| 제공 항목 | 위치 | 비고 | +| --- | --- | --- | +| 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` 에서 사용 | +| 상태관리 초기화 | `main.dart` 의 `ProviderScope` | Riverpod 사용 준비됨 | +| Dio Provider | `lib/core/api/dio_provider.dart` | 인터셉터 적용된 Dio 를 DI 로 주입 | +| 예제 feature(참조용) | `lib/features/user/` | `data/models`·`data/repositories`·`presentation/providers` 수직 슬라이스 패턴 | + +> **확장 시 따르는 규칙** (아래 "프로젝트 구조"는 목표 구조 — 기능 추가 시 점진적으로 채운다): +> - **상태관리**: `features/user` 의 model→repository→provider 패턴을 복제. 위젯에서 `ref.watch(userProvider(id))` +> - **앱 구조**: 라우터 도입 시 `MyApp` 을 `app.dart` 로 분리, `core/{constants,theme,storage}` 채움, 공통 위젯은 `shared/widgets/` +> - **토큰 저장 추상화(선택)**: 필요 시 `core/storage/` 래퍼로 분리 +> - **권장 패키지(선택)**: `go_router`·`freezed`·`json_serializable`·`cached_network_image`·`fpdart` 도입 시 `pubspec.yaml` 추가 + +--- + +## 언어 & 기본 원칙 + +- **Dart Sound Null Safety** 완벽 준수 (`?`, `!`, `late` 올바르게 사용) +- `dynamic` 타입 사용 **지양** → 명시적 타입 선언 +- 모든 주석은 **한국어**로 작성 + +--- + +## 프로젝트 구조 + +``` +lib/ +├── main.dart +├── app.dart ← MaterialApp / 라우터 설정 +├── core/ +│ ├── constants/ ← 상수 (색상, 크기, 텍스트 등) +│ ├── theme/ ← 앱 테마 정의 +│ ├── api/ ← Dio 인스턴스(dio_client), 에러 사전(error_lexicon), 인터셉터 +│ ├── storage/ ← flutter_secure_storage 래퍼 (선택) +│ └── utils/ ← 순수 유틸 함수 +├── features/ +│ └── user/ +│ ├── data/ +│ │ ├── models/ ← JSON 직렬화 모델 +│ │ └── repositories/ ← API 호출 구현체 +│ ├── domain/ +│ │ └── entities/ ← 순수 비즈니스 엔티티 +│ └── presentation/ +│ ├── pages/ ← 화면 단위 위젯 +│ ├── widgets/ ← 재사용 위젯 +│ └── providers/ ← 상태 관리 +└── shared/ + └── widgets/ ← 앱 공통 위젯 +``` + +--- + +## 위젯 작성 원칙 + +```dart +// ✅ 좋은 예 — 작은 위젯으로 분리 +class UserProfilePage extends StatelessWidget { + const UserProfilePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const _AppBar(), + body: const _ProfileBody(), + ); + } +} + +class _ProfileBody extends StatelessWidget { + const _ProfileBody(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const _AvatarSection(), + const _InfoSection(), + ], + ); + } +} +``` + +- 위젯 트리 depth가 깊어지면 **커스텀 위젯으로 분리** +- `build()` 메서드 내 비즈니스 로직 작성 **금지** +- `const` 생성자 적극 활용 (불필요한 리빌드 방지) + +--- + +## 반응형 UI + +```dart +// ❌ 하드코딩 금지 +Container(width: 375, height: 200) + +// ✅ 반응형 처리 +Container( + width: MediaQuery.of(context).size.width * 0.9, + child: ..., +) + +// ✅ 유연한 레이아웃 +Expanded(flex: 2, child: LeftPanel()), +Expanded(flex: 1, child: RightPanel()), +``` + +- `Expanded`, `Flexible`, `MediaQuery`, `LayoutBuilder` 적극 활용 +- 하드코딩 픽셀값 사용 **금지** +- 폰트 크기는 `Theme.of(context).textTheme` 활용 + +--- + +## 상태 관리 + +```dart +// Riverpod 예시 (권장) +@riverpod +Future fetchUser(FetchUserRef ref, int id) async { + final repo = ref.watch(userRepositoryProvider); + return repo.getUser(id); +} + +// 위젯에서 사용 +class UserWidget extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final userAsync = ref.watch(fetchUserProvider(userId)); + return userAsync.when( + data: (user) => Text(user.name), + loading: () => const CircularProgressIndicator(), + error: (e, _) => const ErrorWidget(), + ); + } +} +``` + +- **Riverpod** 권장 (Provider는 레거시로 간주) +- `StatefulWidget`은 애니메이션, 폼 등 로컬 UI 상태에만 제한적 사용 + +--- + +## API 통신 (Dio + 인터셉터) + +실제 구현(`lib/core/api/dio_client.dart`)은 싱글톤 + `InterceptorsWrapper` 인라인 방식이다. 인터셉터 로직 추가 시 이 파일의 패턴을 그대로 따른다. + +```dart +// core/api/dio_client.dart (실제 구조 요약) +class DioClient { + DioClient._internal() { + // flutter_dotenv 로 .env 로드, 미설정 시 로컬 기본값 + final baseUrl = dotenv.maybeGet('API_URL') ?? 'http://localhost:3000/api'; + _dio = Dio(BaseOptions( + baseUrl: baseUrl, + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + )); + _dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) async { + // jwt_token 읽어 Authorization 헤더 첨부 + final token = await _storage.read(key: 'jwt_token'); + if (token != null) options.headers['Authorization'] = 'Bearer $token'; + handler.next(options); + }, + onResponse: (response, handler) { + // success==true → data 언래핑, false → 에러 코드로 reject + handler.next(response); + }, + onError: (err, handler) { + // 상태코드/서버 error.code → ErrorLexicon 한국어 메시지 ([[error-codes]] 참조) + handler.next(err); + }, + )); + } +} +``` + +> 인터셉터를 별도 클래스(`AuthInterceptor`/`ErrorInterceptor`)로 분리하고 싶다면 리팩터링 가능하나, 현재는 위 인라인 구조가 SSOT다. + +--- + +## 보안 (토큰 저장) + +```dart +// flutter_secure_storage 사용 +const storage = FlutterSecureStorage(); + +// 저장 (키 이름은 dio_client 인터셉터와 통일: jwt_token) +await storage.write(key: 'jwt_token', value: token); + +// 읽기 +final token = await storage.read(key: 'jwt_token'); + +// 삭제 (로그아웃) +await storage.deleteAll(); +``` + +- JWT, 민감 정보는 반드시 `flutter_secure_storage` 사용 +- `SharedPreferences`에 토큰 저장 **금지** +- 추가 보안 규칙은 [[security]] 참조. + +--- + +## 성능 & 최신 트렌드 추가 규칙 + +- **모델 직렬화**: `json_serializable` + `freezed` 사용 권장 (불변 모델) +- **이미지**: `cached_network_image`로 네트워크 이미지 캐싱 +- **목록**: 긴 목록은 `ListView.builder` 사용 (전체 렌더링 금지) +- **에러 핸들링**: `Either` 패턴 고려 (`fpdart` 패키지) +- **라우팅**: `go_router` 사용 권장 (딥링크, 중첩 라우팅 지원) +- **환경 분리**: `--dart-define` 또는 `flutter_dotenv`로 dev/prod 분리 + +--- + +## 작업 점검 체크리스트 + +(전체 확장 지점·점검 목록은 [[checklist]]) + +- [ ] **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` 저장 금지. +- [ ] **상태관리**: 새 기능은 Riverpod 프로바이더/노티파이어로 작성 (`ProviderScope` 는 기본 제공). +- [ ] **구조 보완 시**: `app.dart` 분리, `core/{constants,theme,storage}` 생성, 공통 위젯 `lib/widgets/` → `shared/widgets/` 이동 고려. +- [ ] **권장 패키지**: 모델(`freezed`/`json_serializable`)·라우팅(`go_router`) 도입 시 `pubspec.yaml` 추가 후 `flutter pub get`. +- [ ] **analyze**: 커밋 전 `flutter analyze` 통과. +- [ ] **env**: `flutter/.env.example` → `.env` 복사 후 `API_URL` 설정 (Android 에뮬레이터는 `10.0.2.2`). diff --git a/.claude/commands/nestjs.md b/.claude/skills/nestjs/SKILL.md similarity index 50% rename from .claude/commands/nestjs.md rename to .claude/skills/nestjs/SKILL.md index 0f65ab3..ec731be 100644 --- a/.claude/commands/nestjs.md +++ b/.claude/skills/nestjs/SKILL.md @@ -1,9 +1,34 @@ --- -description: NestJS 백엔드 개발 규칙 +name: nestjs +description: NestJS 백엔드(backend/) 개발 규칙 — 컨트롤러/서비스/DTO 관심사 분리, 표준 응답 인터셉터, 전역 예외 필터, Swagger, JWT 인증, 캐싱/Rate Limiting/페이지네이션. backend/ 에서 작업하거나 REST API·모듈·DTO·인터셉터·필터를 만들 때 사용한다. --- -> 이 파일은 `.claude/commands/nestjs.md`에 위치한다. -> 사용법: `/nestjs [요청 내용]` +> 이 스킬은 `.claude/skills/nestjs/SKILL.md`에 위치한다. +> 사용법: `/nestjs [요청 내용]` 또는 backend/ 작업 시 자동 적용. +> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]] + +--- + +## 템플릿 기본 제공 골격 + +> 이 템플릿은 아래 인프라를 **기본 제공**한다. 새 코드는 이 파일들의 패턴을 재사용한다(같은 역할을 새로 만들지 말 것). + +| 제공 항목 | 위치 | 비고 | +| --- | --- | --- | +| 표준 응답 인터셉터 | `src/common/interceptors/transform.interceptor.ts` | main.ts 전역 등록 — 컨트롤러는 raw 데이터만 반환 | +| 전역 예외 필터 | `src/common/filters/http-exception.filter.ts` | `HttpException` → `{success:false,error:{code,message}}` ([[error-codes]]) | +| 전역 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 데모 저장소 | +| 순수 유틸 | `shared/utils/` | 순수 함수 분리 위치 | + +> **확장 시 따르는 규칙** (기능이 필요해지면 규칙대로 구현): +> - **인증(JWT)**: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/AUTH_003 흐름 검증 +> - **DB 영속화**: TypeORM 엔티티/리포지토리 추가 (예제의 in-memory 저장 대체) +> - **페이지네이션**: 목록 API 에 커서/오프셋 적용 +> - `common/{guards,decorators,pipes}/` 는 해당 관심사 등장 시 생성 (아래 "모듈 구조"는 목표 구조) --- @@ -90,6 +115,7 @@ export class TransformInterceptor implements NestInterceptor { - `main.ts`에서 전역 등록: `app.useGlobalInterceptors(new TransformInterceptor())` - 실패 응답(`success: false`)은 아래 Exception Filter가 생성한다. +- 응답 포맷 SSOT는 [[response-format]] 참조. --- @@ -104,11 +130,13 @@ export class HttpExceptionFilter implements ExceptionFilter { const response = ctx.getResponse(); // HTTP 상태 → 에러 코드 매핑 후 { success: false, error: { code, message } } 반환 - // SYS_001 / AUTH_001 / VAL_001 등 CLAUDE.md 에러 맵 참조 + // SYS_001 / AUTH_001 / VAL_001 등 에러 맵은 [[error-codes]] 참조 } } ``` +이 필터는 세 플랫폼 공유 에러 코드 맵의 **단일 진실 공급원(SSOT)** 이다. 메시지 수정 시 [[error-codes]] 의 3개 파일을 함께 갱신한다. + --- ## Swagger 필수 적용 @@ -134,18 +162,36 @@ SwaggerModule.setup('api-docs', app, document); ## 인증 (JWT) +> 📐 **확장 지점.** 이 템플릿은 JWT 인증을 강제하지 않는다(규칙만 제공). **인증이 필요해지면** 아래 규칙대로 구현한다: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/AUTH_003 흐름 검증. + - Access Token: 만료 시간 짧게 (15분 ~ 1시간) - Refresh Token: HttpOnly Secure 쿠키로 전달 - `@nestjs/jwt` + `@nestjs/passport` 사용 - Guard를 통한 라우트 보호 +- 토큰 저장·전송 보안 규칙은 [[security]] 참조. --- ## 성능 & 최신 트렌드 추가 규칙 - **캐싱**: 자주 조회되는 데이터는 `@nestjs/cache-manager` (Redis) 활용 고려 -- **Rate Limiting**: `@nestjs/throttler`로 API 남용 방지 +- **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` 엔드포인트 구성 권장 - **Versioning**: API 버전 관리 (`/v1/`, `/v2/`) 적용 고려 + +--- + +## 작업 점검 체크리스트 + +코드 작성/수정 후 아래를 점검한다. (전체 확장 지점·점검 목록은 [[checklist]]) + +- [ ] **응답 포맷**: 컨트롤러는 raw 데이터만 반환 — `{success,data}` 래핑은 인터셉터가 담당. 직접 래핑 금지. +- [ ] **에러**: 사용자 노출 실패는 `HttpException` 으로 던져 필터가 코드 매핑하게 한다. 새 코드 추가 시 [[error-codes]] 의 3개 파일 동시 갱신. +- [ ] **DTO**: 모든 입력 DTO 에 `class-validator` + 한국어 메시지 + `@ApiProperty()`. +- [ ] **Swagger**: 새 엔드포인트에 `@ApiTags`/`@ApiOperation`/`@ApiResponse`. +- [ ] **lint**: 커밋 전 `npm run lint` 통과 (Warning 0). +- [ ] **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/commands/vue3.md b/.claude/skills/vue3/SKILL.md similarity index 51% rename from .claude/commands/vue3.md rename to .claude/skills/vue3/SKILL.md index d9e1e8e..efef90b 100644 --- a/.claude/commands/vue3.md +++ b/.claude/skills/vue3/SKILL.md @@ -1,9 +1,35 @@ --- -description: Vue 3 프론트엔드 개발 규칙 +name: vue3 +description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API +