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
+26 -17
View File
@@ -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` 가 경고**한다.