refactor: .claude 규칙 체계를 skills·rules 구조로 재편
- 슬래시 커맨드(nestjs/vue3/flutter)를 정식 스킬(.claude/skills)로 전환하고 docker 스킬을 신설(루트 command.md 이관) - 공통 규칙을 .claude/rules 로 모듈화(coding-common·response-format·error-codes· git-convention·security·env-structure·testing·checklist)하고 CLAUDE.md 에서 @import - 파일·식별자 네이밍 규약, 테스트 규칙 추가 - .claude/settings.json(권한·환경) 추가 - CLAUDE.md·README.md 참조 갱신, 단독 실행(non-Docker) 안내 정리 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 접속 (<DB_USER>·<DB_NAME> 은 루트 .env 값으로 치환)
|
||||
dc exec db psql -U <DB_USER> -d <DB_NAME>
|
||||
|
||||
# Redis 접속 (<REDIS_PASSWORD> 은 루트 .env 값)
|
||||
dc exec redis redis-cli -a <REDIS_PASSWORD>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 규칙
|
||||
|
||||
- **빌드 최소화**: 의존성(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`.
|
||||
@@ -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<User> 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<Failure, Success>` 패턴 고려 (`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`).
|
||||
@@ -0,0 +1,197 @@
|
||||
---
|
||||
name: nestjs
|
||||
description: NestJS 백엔드(backend/) 개발 규칙 — 컨트롤러/서비스/DTO 관심사 분리, 표준 응답 인터셉터, 전역 예외 필터, Swagger, JWT 인증, 캐싱/Rate Limiting/페이지네이션. backend/ 에서 작업하거나 REST API·모듈·DTO·인터셉터·필터를 만들 때 사용한다.
|
||||
---
|
||||
|
||||
> 이 스킬은 `.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}/` 는 해당 관심사 등장 시 생성 (아래 "모듈 구조"는 목표 구조)
|
||||
|
||||
---
|
||||
|
||||
## 아키텍처 원칙
|
||||
|
||||
### 관심사 분리 (Separation of Concerns)
|
||||
|
||||
```
|
||||
Controller → HTTP 라우팅, 요청/응답 파싱만 담당
|
||||
Service → 모든 비즈니스 로직 처리
|
||||
Repository → 데이터 접근 계층 (TypeORM)
|
||||
DTO → 요청/응답 데이터 형식 정의 및 검증
|
||||
```
|
||||
|
||||
- `Controller`에 비즈니스 로직 작성 **금지**
|
||||
- `new` 키워드로 수동 객체 생성 **지양** → 의존성 주입(DI) 활용
|
||||
|
||||
### 모듈 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── modules/
|
||||
│ └── user/
|
||||
│ ├── user.module.ts
|
||||
│ ├── user.controller.ts
|
||||
│ ├── user.service.ts
|
||||
│ ├── user.repository.ts ← 커스텀 Repository (선택)
|
||||
│ └── dto/
|
||||
│ ├── create-user.dto.ts
|
||||
│ └── update-user.dto.ts
|
||||
├── common/
|
||||
│ ├── interceptors/ ← 응답 래핑, 에러 처리
|
||||
│ ├── filters/ ← 전역 Exception Filter
|
||||
│ ├── guards/ ← 인증/권한 Guard
|
||||
│ ├── decorators/ ← 커스텀 데코레이터
|
||||
│ └── pipes/ ← 전역 Validation Pipe
|
||||
└── shared/
|
||||
└── utils/ ← 순수 유틸 함수
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DTO 작성 규칙
|
||||
|
||||
```typescript
|
||||
import { IsString, IsEmail, IsNotEmpty, MinLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiProperty({ description: '사용자 이메일', example: 'user@example.com' })
|
||||
@IsEmail({}, { message: '올바른 이메일 형식을 입력해 주세요.' })
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ description: '비밀번호 (최소 8자)', example: 'password123' })
|
||||
@IsString()
|
||||
@MinLength(8, { message: '비밀번호는 최소 8자 이상이어야 합니다.' })
|
||||
password: string;
|
||||
}
|
||||
```
|
||||
|
||||
- 모든 DTO는 `class-validator` + `class-transformer` 사용
|
||||
- 에러 메시지는 **한국어**로 작성
|
||||
- `@ApiProperty()` 반드시 포함 (Swagger 명세용)
|
||||
|
||||
---
|
||||
|
||||
## 표준 응답 인터셉터
|
||||
|
||||
```typescript
|
||||
// common/interceptors/transform.interceptor.ts
|
||||
@Injectable()
|
||||
export class TransformInterceptor<T> implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
success: true,
|
||||
data: data ?? null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `main.ts`에서 전역 등록: `app.useGlobalInterceptors(new TransformInterceptor())`
|
||||
- 실패 응답(`success: false`)은 아래 Exception Filter가 생성한다.
|
||||
- 응답 포맷 SSOT는 [[response-format]] 참조.
|
||||
|
||||
---
|
||||
|
||||
## 전역 Exception Filter
|
||||
|
||||
```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>();
|
||||
|
||||
// HTTP 상태 → 에러 코드 매핑 후 { success: false, error: { code, message } } 반환
|
||||
// SYS_001 / AUTH_001 / VAL_001 등 에러 맵은 [[error-codes]] 참조
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이 필터는 세 플랫폼 공유 에러 코드 맵의 **단일 진실 공급원(SSOT)** 이다. 메시지 수정 시 [[error-codes]] 의 3개 파일을 함께 갱신한다.
|
||||
|
||||
---
|
||||
|
||||
## Swagger 필수 적용
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('API 문서')
|
||||
.setDescription('프로젝트 API 명세서')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api-docs', app, document);
|
||||
```
|
||||
|
||||
- 모든 Controller에 `@ApiTags()` 적용
|
||||
- 모든 엔드포인트에 `@ApiOperation()`, `@ApiResponse()` 작성
|
||||
- 모든 DTO에 `@ApiProperty()` 작성
|
||||
|
||||
---
|
||||
|
||||
## 인증 (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**: 템플릿은 `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` 로 로드).
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
name: vue3
|
||||
description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API + <script setup>, Props/Emits 타입 정의, Pinia 스토어, Composables+Axios API 호출, Vue Router 코드 스플리팅·가드. frontend/ 에서 작업하거나 컴포넌트·스토어·컴포저블·라우터를 만들 때 사용한다.
|
||||
---
|
||||
|
||||
> 이 스킬은 `.claude/skills/vue3/SKILL.md`에 위치한다.
|
||||
> 사용법: `/vue3 [요청 내용]` 또는 frontend/ 작업 시 자동 적용.
|
||||
> 공통 규칙: [[response-format]] · [[error-codes]] · [[security]] · [[coding-common]]
|
||||
|
||||
---
|
||||
|
||||
## 템플릿 기본 제공 골격
|
||||
|
||||
> 이 템플릿은 아래를 **기본 제공**한다. 새 코드는 이 파일들의 패턴을 복제한다(같은 역할을 새로 만들지 말 것).
|
||||
|
||||
| 제공 항목 | 위치 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| 중앙 Axios 인스턴스 | `src/composables/useApi.ts` | baseURL=`VITE_API_BASE_URL`, `withCredentials` |
|
||||
| 응답 언래핑 인터셉터 | `useApi.ts` | `{success,data}→data`, 에러 코드 처리 |
|
||||
| ErrorCodeLexicon | `useApi.ts` 내부 | 에러 코드 → 한국어 메시지 ([[error-codes]]) |
|
||||
| 도메인 Composable 예제 | `src/composables/useUser.ts` | 새 도메인 API 패턴 참고 |
|
||||
| 인증 Composable 예제 | `src/composables/useAuth.ts` | 로그인/로그아웃 흐름(쿠키 기반) 시작점 |
|
||||
| Pinia 스토어 예제 | `src/stores/user.store.ts` | 새 스토어 패턴 참고 |
|
||||
| 인증 스토어 | `src/stores/auth.store.ts` | 세션 인증 여부(`isLoggedIn`) 관리 |
|
||||
| 공통 컴포넌트 예제 | `src/components/BaseButton.vue` | 재사용 컴포넌트 기준 패턴(타입 Props/Emits·scoped·rem·a11y) |
|
||||
| 라우터 | `src/router/index.ts` | lazy import + `auth.store` 연동 `requiresAuth` 가드 |
|
||||
| env 타입 선언 | `frontend/env.d.ts` | `VITE_API_BASE_URL` |
|
||||
|
||||
> **확장 시 따르는 규칙:**
|
||||
> - **로그인 연동**: 실제 `/auth/login` 엔드포인트 연결 → 성공 시 `useAuth` 가 `authStore.markAuthenticated()` 호출
|
||||
> - **로그인 페이지**: 추가 시 라우터 가드 리다이렉트를 `{ name: 'login' }` 으로 변경
|
||||
> - **공통 컴포넌트**: `BaseButton` 패턴을 따라 `src/components/` 에 추가
|
||||
|
||||
---
|
||||
|
||||
## 컴포넌트 작성 원칙
|
||||
|
||||
### 필수 구조
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 1. import
|
||||
// 2. Props / Emits 정의
|
||||
// 3. Store / Composable 호출
|
||||
// 4. 반응형 상태 (ref, reactive, computed)
|
||||
// 5. 함수 정의
|
||||
// 6. Lifecycle Hooks
|
||||
// 7. watch
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 시맨틱 HTML 태그 사용 -->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* rem 단위 사용, 인라인 스타일 금지 */
|
||||
</style>
|
||||
```
|
||||
|
||||
- `Options API` 사용 **금지** → `Composition API` + `<script setup>` 필수
|
||||
- 인라인 스타일(`style=""`) 사용 **금지** → `<style scoped>` 사용
|
||||
- 모든 px 값은 **rem으로 변환** (기준: 16px = 1rem)
|
||||
|
||||
---
|
||||
|
||||
## Props / Emits 타입 정의
|
||||
|
||||
```typescript
|
||||
// Props
|
||||
interface Props {
|
||||
title: string
|
||||
count?: number
|
||||
items: string[]
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 0,
|
||||
})
|
||||
|
||||
// Emits
|
||||
interface Emits {
|
||||
(e: 'update', value: string): void
|
||||
(e: 'close'): void
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 상태 관리 (Pinia)
|
||||
|
||||
```typescript
|
||||
// stores/user.store.ts
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 상태
|
||||
const user = ref<User | null>(null)
|
||||
const isLoggedIn = computed(() => !!user.value)
|
||||
|
||||
// 액션
|
||||
async function fetchUser(id: number) {
|
||||
// API 호출
|
||||
}
|
||||
|
||||
return { user, isLoggedIn, fetchUser }
|
||||
})
|
||||
```
|
||||
|
||||
- 도메인별 Store 파일 분리 (`user.store.ts`, `auth.store.ts` 등)
|
||||
- Store 내 API 호출은 허용, 단 에러 처리는 인터셉터에 위임
|
||||
|
||||
---
|
||||
|
||||
## API 호출 (Composables + Axios)
|
||||
|
||||
```typescript
|
||||
// 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)
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
```typescript
|
||||
// composables/useUser.ts — 도메인별 API Composable
|
||||
export function useUser() {
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function getUser(id: number) {
|
||||
loading.value = true
|
||||
try {
|
||||
return await api.get(`/users/${id}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { loading, error, getUser }
|
||||
}
|
||||
```
|
||||
|
||||
- API 호출 로직은 반드시 `composables/` 폴더에 분리
|
||||
- 컴포넌트 내 `axios.get()` 직접 호출 **금지**
|
||||
- `ErrorCodeLexicon` 은 세 플랫폼 공유 에러 맵의 일부 — [[error-codes]] 참조.
|
||||
|
||||
---
|
||||
|
||||
## 라우터 (Vue Router)
|
||||
|
||||
```typescript
|
||||
// 코드 스플리팅 — 모든 페이지 컴포넌트는 lazy import
|
||||
const routes = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: () => import('@/pages/DashboardPage.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
]
|
||||
|
||||
// 네비게이션 가드 — 인증 처리
|
||||
router.beforeEach((to) => {
|
||||
const authStore = useAuthStore()
|
||||
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
||||
return '/login'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 성능 & 최신 트렌드 추가 규칙
|
||||
|
||||
- **컴포넌트 분리**: 200줄 이상 컴포넌트는 분리 고려
|
||||
- **`defineAsyncComponent`**: 무거운 컴포넌트는 비동기 로딩 적용
|
||||
- **`v-memo`**: 반복 렌더링 최적화 필요 시 활용
|
||||
- **`<Suspense>`**: 비동기 컴포넌트 로딩 상태 처리에 활용
|
||||
- **환경변수**: 모든 설정값은 `import.meta.env.VITE_*` 형태로 관리 ([[env-structure]] 참조)
|
||||
- **절대경로**: `@/` 경로 alias 사용 (상대경로 `../../` 지양)
|
||||
- **접근성(a11y)**: `aria-label`, `role`, `tabindex` 등 기본 접근성 속성 포함
|
||||
|
||||
---
|
||||
|
||||
## 작업 점검 체크리스트
|
||||
|
||||
(전체 확장 지점·점검 목록은 [[checklist]])
|
||||
|
||||
- [ ] **API 호출**: 컴포넌트에서 `axios` 직접 호출 금지 → `composables/` 경유.
|
||||
- [ ] **에러 처리**: 개별 try/catch 로 메시지를 만들지 말고 `useApi` 인터셉터 + [[error-codes]] 에 위임.
|
||||
- [ ] **컴포넌트**: `<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` 통과.
|
||||
- [ ] **env**: `VITE_API_BASE_URL` 은 빌드 시 루트 `BACKEND_API_URL` 에서 주입된다 — `frontend/.env` 에 별도 작성 불필요.
|
||||
Reference in New Issue
Block a user