docs(rules): 서버화에 맞춰 규칙·스킬 문서 갱신

구현한 구조를 규칙 SSOT 에 반영한다 — 다음 작업이 같은 패턴을 쓰도록.

- security: 모바일 리프레시 토큰은 secure storage(쿠키 저장소가 없다) — 대신 해시 저장·회전·재사용 탐지.
  소셜 토큰은 서버가 제공자에게 확인, 개발용 로그인은 운영에서 코드로 닫힘, 업로드 매직 바이트 검사·소유자 확인
- nestjs 스킬: 인증·회원·도메인 모듈·커서 헬퍼·공통 검증·snake_case 이름 규칙·아이 삭제 정리 리스너,
  새 라우트는 기본 로그인 필요(@Public 예외)·새 엔티티는 소유자 조건 조회
- flutter 스킬: DATA_MODE, 세션·토큰 갱신, 실패 알림·succeeded(), 서버 사진, 저장소 계약(Local·Api) 패턴
- env·응답 형식·에러 코드·테스트·체크리스트: DATA_MODE·소셜 키, keysetPaginate·fetchAllPages,
  도메인 코드 목록, 앱의 AUTH_002 흐름, 가짜 서버·라이브 점검 테스트
- vue3 스킬: 웹 로그인 연동 시 필요한 것 (지금 인증은 모바일용)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:43:48 +09:00
parent 373d50576a
commit 5331362215
10 changed files with 108 additions and 59 deletions
+35 -30
View File
@@ -16,18 +16,23 @@ 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(코드 매핑) — 인라인 |
| 데이터 모드 | `lib/core/config/app_config.dart` | `.env` 의 `DATA_MODE` — `api`(기본, 서버) / `local`(기기 저장 · 서버 없이 화면 확인 · 테스트) |
| Dio 클라이언트 | `lib/core/api/dio_client.dart` + `dio_provider.dart` | Provider 로 주입(`ref.watch(dioProvider)`), baseUrl=dotenv `API_URL`, 타임아웃 |
| Auth/Error 인터셉터 | `dio_client.dart` 의 `InterceptorsWrapper` | onRequest(토큰 첨부)·onResponse(언래핑)·onError(**401 → 토큰 갱신 후 한 번 재시도**·코드 매핑) — 인라인 |
| **ApiException** | `lib/core/api/api_exception.dart` | `{code, message, traceId, statusCode}` 정규화 예외. 인터셉터가 `DioException.error` 에 실어준다 |
| 실패 알림 | `apiErrorReporterProvider` → `rup_toast.dart` 의 `apiErrorToastReporter` | 사전 문구 토스트, 같은 코드는 잠깐 한 번만. 호출부가 처리하는 코드는 `kSilentCodesExtra` 로 끈다 |
| 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`(UserModel)·`data/repositories`(단건+커서 목록)·`presentation/providers` 수직 슬라이스 |
| 세션 | `lib/core/api/session_store.dart` | 액세스·리프레시 토큰(secure storage `kJwtTokenKey`·`kRefreshTokenKey`)을 들고 있다. 회원이 바뀌면 `sessionEpochProvider` 가 오른다 |
| 세션 만료 처리 | `dio_client.dart` 의 `_toApiException()` | 토큰 갱신을 서버가 거절하면 `AUTH_002` + 세션 삭제 → 진입 상태가 로그인 화면으로 보낸다 |
| 화면의 실패 처리 | `lib/core/api/api_guard.dart` | `succeeded(future)` / `isReportedApiFailure(e)` — 알림은 이미 떴으니 화면은 멈추기만 한다 |
| 목록 | `lib/core/api/api_paging.dart` | `fetchAllPages()` — `{items, pageInfo}` 를 마지막 페이지까지 이어 읽는다 |
| 사진 | `server_file.dart` · `file_uploader.dart` · `shared/widgets/photo_image.dart` | 사진 경로 자리에 `server-file:<id>`. 기기 사진은 저장할 때 내용으로 형식을 확인해 올린다. 서버 사진은 Dio 로 받아 그린다 |
| 상태관리 초기화 | `main.dart` 의 `ProviderScope` | 세션(토큰)을 첫 프레임 전에 읽어 override 한다 |
| 도메인 feature(패턴 참고) | `lib/features/pet/` | 저장소 **계약 + `Local*` + `Api*`**, 프로바이더가 `DATA_MODE` 로 고른다. 알림기 build 는 `sessionEpochProvider` 를 watch |
> **확장 시 따르는 규칙** (아래 "프로젝트 구조"는 목표 구조 — 기능 추가 시 점진적으로 채운다):
> - **상태관리**: `features/user` 의 model→repository→provider 패턴을 복제. 위젯에서 `ref.watch(userProvider(id))`
> - **상태관리**: `features/pet` 의 저장소 계약(Local·Api)→알림기 패턴을 복제. 서버가 이미 한 정리(아이 삭제에 따른 일정 정리 등)는 Api 저장소에서 아무것도 보내지 않는다
> - **소셜 로그인 SDK**: 앱 키를 받으면 `ApiAuthRepository.signIn` 만 SDK 토큰 → `POST /auth/sessions` 로 바꾼다 (지금은 기기 키로 개발용 로그인)
> - **앱 구조**: 라우터 도입 시 `MyApp` 을 `app.dart` 로 분리, `core/{constants,theme,storage}` 채움, 공통 위젯은 `shared/widgets/`
> - **토큰 저장 추상화(선택)**: 필요 시 `core/storage/` 래퍼로 분리
> - **권장 패키지(선택)**: `go_router`·`freezed`·`json_serializable`·`cached_network_image`·`fpdart` 도입 시 `pubspec.yaml` 추가
@@ -162,38 +167,33 @@ class UserWidget extends ConsumerWidget {
## API 통신 (Dio + 인터셉터)
실제 구현(`lib/core/api/dio_client.dart`)은 싱글톤 + `InterceptorsWrapper` 인라인 방식이다. 인터셉터 로직 추가 시 이 파일의 패턴을 그대로 따른다.
실제 구현(`lib/core/api/dio_client.dart`)은 `InterceptorsWrapper` 인라인 방식이고, `dio_provider.dart` 가 세션·실패 알림을 넣어 만든다.
인터셉터 로직 추가 시 이 파일의 패턴을 그대로 따른다.
```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),
));
DioClient({required String baseUrl, required SessionStore session, ApiErrorReporter? onError}) {
_dio = Dio(BaseOptions(baseUrl: baseUrl, connectTimeout: ..., receiveTimeout: ...));
_authDio = Dio(...); // 갱신 전용 — 인터셉터 없음
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
// jwt_token 읽어 Authorization 헤더 첨부
final token = await _storage.read(key: kJwtTokenKey);
if (token != null) options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
onRequest: (options, handler) {
// 세션의 액세스 토큰을 Authorization 헤더에 붙인다 (토큰은 SessionStore 가 secure storage 에서 읽어 둔다)
},
onResponse: (response, handler) {
// success==true → data 언래핑, false → ApiException 으로 reject
handler.next(response);
// success==true → data 언래핑, false → ApiException 으로 reject (+ 알림)
},
onError: (err, handler) async {
// 상태코드/서버 error.code → ApiException{code,message,traceId} ([[error-codes]] 참조)
// 토큰 보유 상태의 401 은 AUTH_002(세션 만료)로 승격하고 토큰을 삭제한다.
handler.next(err.copyWith(error: await _toApiException(err)));
// 401 이면 리프레시 토큰으로 갱신해 한 번 다시 보낸다 (동시에 여러 요청이 와도 갱신은 한 번)
// 그래도 실패하면 상태코드/서버 error.code → ApiException{code,message,traceId} ([[error-codes]] 참조)
// 서버가 갱신을 거절하면 AUTH_002(세션 만료) + 세션 삭제, 서버에 닿지 못했으면 SYS_001
},
));
}
}
// 리포지토리는 ref.watch(dioProvider) 로 주입받는다
final dioProvider = Provider<Dio>((ref) => ref.watch(dioClientProvider).dio);
```
> 인터셉터를 별도 클래스(`AuthInterceptor`/`ErrorInterceptor`)로 분리하고 싶다면 리팩터링 가능하나, 현재는 위 인라인 구조가 SSOT다.
@@ -206,7 +206,8 @@ class DioClient {
// flutter_secure_storage 사용
const storage = FlutterSecureStorage();
// 저장 (키 이름은 dio_client 의 상수로 통일: kJwtTokenKey == 'jwt_token')
// 저장 (키 이름은 session_store 의 상수로 통일: kJwtTokenKey == 'jwt_token', kRefreshTokenKey == 'refresh_token')
// 앱 코드는 직접 쓰지 않고 SessionStore(SecureTokenVault)가 맡는다
await storage.write(key: kJwtTokenKey, value: token);
// 읽기
@@ -242,9 +243,12 @@ await storage.deleteAll();
- [ ] **위젯**: depth 깊어지면 private 위젯 분리, `const` 적극, `build()` 내 비즈니스 로직 금지.
- [ ] **반응형**: 하드코딩 픽셀 금지 → `MediaQuery`/`Expanded`/`Flexible`.
- [ ] **API**: `dio_client.dart` 의 인터셉터 패턴 재사용. 리포지토리에서 try/catch 로 메시지를 만들지 않는다.
- [ ] **새 저장소**: 계약 + `Local*`(기기) + `Api*`(서버), 프로바이더가 `DATA_MODE` 로 고른다. 로그인 전에는 서버를 부르지 않는다. `test/api_*_test.dart` 처럼 가짜 서버(`test/support/fake_api.dart`)로 테스트한다.
- [ ] **화면 실패 처리**: 서버 호출은 `succeeded()` 또는 `catch (e) { if (!isReportedApiFailure(e)) rethrow; }` — 입력을 지운 채 넘어가지 않는다.
- [ ] **사진**: 화면은 `PetPhotoImage`/`PersonAvatar`(→ `PhotoImage`)로 그린다 — 서버 파일·기기 파일을 같은 자리에서 처리한다.
- [ ] **에러 분기**: `e.error as ApiException` 의 **`code` 로 분기**한다. 문구는 사전에서만 온다.
- [ ] **에러 사전**: `error_lexicon.g.dart` 직접 수정 금지 — 백엔드 레지스트리 수정 후 `npm run sync:error-codes`.
- [ ] **목록**: 응답은 `{items, pageInfo}` — `UserRepository.getUsers()` 패턴 재사용 ([[response-format]]).
- [ ] **목록**: 응답은 `{items, pageInfo}` — `fetchAllPages()` 재사용 ([[response-format]]).
- [ ] **토큰**: `flutter_secure_storage` + 키 상수 `kJwtTokenKey` 통일. `SharedPreferences` 저장 금지.
- [ ] **상태관리**: 새 기능은 Riverpod 프로바이더/노티파이어로 작성 (`ProviderScope` 는 기본 제공).
- [ ] **구조 보완 시**: `app.dart` 분리, `core/{constants,theme,storage}` 생성. 공통 위젯은 `lib/shared/widgets/` 에 둔다(이동 완료).
@@ -253,5 +257,6 @@ await storage.deleteAll();
CI(`ci-flutter.yml`)는 **flutter/ 가 바뀐 커밋에서만** 자동 실행된다 (SDK 다운로드가 느려 분리).
다른 시점에 검사가 필요하면 Actions 탭의 "Run workflow" 로 수동 실행한다.
- [ ] **린트로 강제되는 항목**: `dynamic` 호출·`print`·`const` 누락·await 누락·작은따옴표·import 정렬 (`analysis_options.yaml`).
- [ ] **env**: `flutter/.env.example` → `.env` 복사 후 `API_URL` 설정 (Android 에뮬레이터는 `10.0.2.2`).
- [ ] **env**: `flutter/.env.example` → `.env` 복사 후 `API_URL`·`DATA_MODE` 설정 (Android 에뮬레이터는 `10.0.2.2`).
`.env` 는 `pubspec.yaml` 의 asset 이라 **없으면 `flutter analyze` 가 경고**한다.
- [ ] **서버 계약 점검**: 서버 쪽 응답 모양을 바꿨다면 개발 서버를 띄우고 `flutter test test_live` (CI 에서는 돌지 않는다).
+30 -14
View File
@@ -31,18 +31,24 @@ description: NestJS 백엔드(backend/) 개발 규칙 — 컨트롤러/서비스
| DB 설정 | `shared/config/database.config.ts` | 앱·CLI 공유 SSOT. `synchronize` 기본 off |
| 마이그레이션 | `src/data-source.ts` + `src/migrations/` | `npm run migration:*` (아래 "DB 마이그레이션") |
| Graceful shutdown | `main.ts` 의 `enableShutdownHooks()` + `shared/logger/shutdown.logger.ts` | SIGTERM 시 요청/커넥션 정리 후 종료 |
| 예제 모듈(참조용) | `modules/user`(DTO+Swagger+페이지네이션) | 새 모듈 패턴 참고. in-memory 데모 저장소 |
| 파일 업로드 | `modules/upload` | 로컬 디스크 저장 + 형식·크기 검증 + 안전한 파일명 생성 ([[security]]) |
| **인증(JWT)** | `modules/auth` + `common/guards/jwt-auth.guard.ts` (APP_GUARD) | **모든 라우트가 기본으로 로그인 필요** — 공개 라우트만 `@Public()`. 핸들러는 `@CurrentUser()` 로 회원을 받는다. 아래 "인증" 참조 |
| 회원 | `modules/user` | 내 정보 조회·수정·탈퇴(`/users/me`), 소셜 계정 연결 |
| 도메인 모듈(패턴 참고) | `modules/pet` · `schedule` · `diary` · `pet-card` | 새 모듈은 `modules/pet` 을 복제한다 (엔티티·DTO·서비스·컨트롤러·소유자 조회) |
| 커서 페이지네이션 헬퍼 | `common/dto/keyset-pagination.ts` | `keysetPaginate(qb, 정렬키, query)` + `mapPage()`. 목록 Swagger 는 `@ApiPaginatedResponse` |
| 공통 검증 데코레이터 | `common/validators` | `IsIsoDate`·`IsHexColor`·`IsHourMinute`·`IsTargetIdList`·`IsShortStringList` |
| 컬럼 이름 규칙 | `shared/config/snake-naming.strategy.ts` | 엔티티 속성 camelCase ↔ 컬럼 snake_case (database.config.ts 에 연결) |
| 도메인 간 정리 | `modules/pet/pet-lifecycle.ts` | 아이 삭제·보관을 따라 일정·다이어리·명함이 같은 트랜잭션에서 정리 (`onModuleInit` 에서 등록) |
| 파일 업로드 | `modules/upload` | 소유자 기록·매직 바이트 검사·`GET /files/:id`(본인만, attachment)·`releaseIfUnused()` ([[security]]) |
| 헬스체크 | `modules/health` | `@SkipThrottle`+`@SkipTransform` — 기계가 읽는 엔드포인트 패턴 ([[response-format]]) |
| 순수 유틸 | `shared/utils/` | 순수 함수 분리 위치 |
> **확장 시 따르는 규칙** (기능이 필요해지면 규칙대로 구현):
> - **인증(JWT)**: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/AUTH_003 흐름 검증
> - **DB 영속화**: TypeORM 엔티티/리포지토리 추가 (예제의 in-memory 저장 대체).
> `app.module.ts` 는 이미 `TypeOrmModule.forRootAsync` 로 연결되어 있다.
> - **소셜 로그인 운영**: 제공자 콘솔의 값을 `backend/.env.{env}` 에 넣는다 (`KAKAO_APP_ID`·`NAVER_CLIENT_ID`·`GOOGLE_CLIENT_IDS`·`APPLE_CLIENT_IDS`). 비어 있는 제공자는 로그인을 거절한다
> - **새 엔티티**: 소유자 컬럼(`owner_id`, 회원 삭제 시 CASCADE)을 두고 **조회는 항상 소유자 조건**으로 — 남의 것과 없는 것은 똑같이 `RES_001`.
> 스키마는 **마이그레이션으로 관리**하며 `synchronize` 는 기본 off 다 (아래 "DB 마이그레이션" 참조)
> - **파일을 참조하는 컬럼**: FK 는 `onDelete: 'NO ACTION'` 으로 둔다 — `UploadService.releaseIfUnused()` 가 이 제약으로 "아직 쓰는 파일"을 가린다
> - **도메인 에러 코드**: `error-codes.ts` 의 `DOMAIN_ERROR_CODES` 에 등록 → `npm run sync:error-codes`
> - `common/{guards,pipes}/` 는 해당 관심사 등장 시 생성 (아래 "모듈 구조"는 목표 구조)
> - `common/pipes/` 는 해당 관심사 등장 시 생성 (아래 "모듈 구조"는 목표 구조)
---
@@ -181,6 +187,7 @@ SwaggerModule.setup('api-docs', app, document);
```typescript
@ApiStandardResponse(UserResponseDto, { description: '사용자 조회 성공' })
@ApiPaginatedResponse(PetResponseDto, '아이 목록') // 목록 — { items, pageInfo } 로 그린다
@ApiErrorResponse('RES_001', '사용자를 찾을 수 없음') // 상태·메시지를 레지스트리에서 가져온다
```
@@ -190,12 +197,20 @@ SwaggerModule.setup('api-docs', app, document);
## 인증 (JWT)
> 📐 **확장 지점.** 이 템플릿은 JWT 인증을 강제하지 않는다(규칙만 제공). **인증이 필요해지면** 아래 규칙대로 구현한다: `@nestjs/jwt`·`@nestjs/passport` 설치 → `common/guards/` 생성 → AUTH_001/AUTH_003 흐름 검증.
`modules/auth` 가 구현되어 있다. 경로는 리소스 이름으로 둔다 ([[api-design]] — 동사 금지).
- Access Token: 만료 시간 짧게 (15분 ~ 1시간)
- Refresh Token: HttpOnly Secure 쿠키로 전달
- `@nestjs/jwt` + `@nestjs/passport` 사용
- Guard를 통한 라우트 보호
| 경로 | 하는 일 |
| --- | --- |
| `POST /auth/sessions` | 소셜 로그인(처음이면 가입) — 앱이 SDK 로 받은 토큰을 **서버가 제공자에게 직접 확인**한다 |
| `POST /auth/dev-sessions` | 개발용 로그인(기기 키) — 소셜 앱 키 전까지. **운영에서는 닫힌다**(RES_001) |
| `POST /auth/tokens` | 토큰 갱신 — 리프레시 토큰을 폐기하고 새 한 쌍 (회전) |
| `POST /auth/token-revocation` | 로그아웃 — 리프레시 토큰 폐기 (204) |
- Access Token: 1시간(`JWT_ACCESS_EXPIRES_IN`), HS256. 페이로드 `{ sub, typ: 'access' }`
- Refresh Token: 불투명 랜덤 값, **DB 에는 SHA-256 해시만**. 폐기된 토큰이 다시 오면 탈취로 보고 그 회원의 토큰을 모두 폐기한다
- 모바일은 리프레시 토큰을 응답 본문으로 받아 secure storage 에 둔다 (웹을 붙이면 HttpOnly 쿠키로 내려주는 경로를 따로 둔다 — [[security]])
- 전역 `JwtAuthGuard` — 토큰이 없거나 틀렸거나 탈퇴한 회원이면 원인을 가리지 않고 AUTH_001
- 로그인 엔드포인트는 전역보다 엄격한 rate limit(분당 10회)을 건다
- 토큰 저장·전송 보안 규칙은 [[security]] 참조.
---
@@ -217,7 +232,7 @@ SwaggerModule.setup('api-docs', app, document);
- **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]])
- **Pagination**: 목록 API는 반드시 페이지네이션 적용 — `common/dto/pagination.dto.ts` 의 쿼리 DTO + `common/dto/keyset-pagination.ts` 의 `keysetPaginate()` 사용. 응답은 `{items, pageInfo}` 고정 ([[response-format]]). Swagger 는 `@ApiPaginatedResponse(Dto)`
- **Logging**: `winston` 또는 `@nestjs/common Logger` 사용, `console.log` 금지. 레벨 기준·민감정보 마스킹은 [[logging]]
- **Graceful shutdown**: `app.enableShutdownHooks()` 가 적용되어 있다. 종료 시 정리가 필요한 리소스(큐 소비자, 워커, 외부 연결)는
`OnApplicationShutdown` 훅으로 정리한다. **`process.on(signal)` 로 직접 잡지 말 것** — Nest 의 종료 흐름에 간섭해 종료 코드가 달라진다
@@ -270,6 +285,7 @@ dc exec backend npm run migration:run
- 생성된 SQL 을 **검토 없이 커밋하지 않는다.** 컬럼 rename 이 DROP+ADD 로 생성되면 데이터가 날아간다.
- 마이그레이션 파일은 **한번 커밋되면 수정하지 않는다.** 잘못됐으면 새 마이그레이션으로 고친다.
- 운영 배포 시 `migrationsRun: true` 로 자동 실행된다 — 별도 명령이 필요 없다.
테스트 환경(`NODE_ENV=test`)도 자동 실행한다 — CI 의 e2e 는 빈 DB 에서 시작하기 때문이다.
- 빠른 실험이 필요하면 개발 환경에서만 `DB_SYNCHRONIZE=true` 로 임시 활성화하되,
**커밋 전에는 끄고 마이그레이션을 생성**한다. 운영에서는 이 값과 무관하게 항상 비활성화된다.
- 접속 설정은 `shared/config/database.config.ts` 하나만 수정한다 (앱과 CLI 가 공유).
@@ -317,6 +333,6 @@ dc exec backend npm uninstall <패키지>
- [ ] **의존성 변경 시**: 호스트가 아니라 **컨테이너 안에서** `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 흐름 확인.
- [ ] **새 라우트**: 기본으로 로그인이 필요하다 — 공개해야 하는 라우트만 `@Public()`. 회원은 `@CurrentUser()` 로 받는다.
- [ ] **새 엔티티**: 소유자 조건으로만 조회(남의 것 = RES_001), 마이그레이션 생성·검토, 파일 FK 는 NO ACTION.
- [ ] **env**: DB/Redis/CORS/포트는 루트 `.env` 에서 주입된다. 백엔드 고유 시크릿(`JWT_SECRET` 등)만 `backend/.env.{env}` 에 작성(compose `env_file` 로 로드).
+3 -1
View File
@@ -36,7 +36,9 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API
> 두 포트 모두 `vite.config.ts` 가 **루트 `.env.{mode}` 에서 직접 읽는다** — 프론트 쪽에 포트를 중복 작성하지 않는다.
> **확장 시 따르는 규칙:**
> - **로그인 연동**: 실제 `/auth/login` 엔드포인트 연결 → 성공 시 `useAuth` 가 `authStore.markAuthenticated()` 호출
> - **로그인 연동**: 백엔드 인증(`modules/auth`)은 지금 **모바일용**이다 — 소셜 토큰으로 `POST /auth/sessions`, 토큰은 응답 본문.
> 웹을 붙일 때는 리프레시 토큰을 HttpOnly 쿠키로 내려주는 경로를 백엔드에 추가한 뒤 `useAuth` 를 연결하고,
> 성공 시 `authStore.markAuthenticated()` 를 호출한다 ([[security]]). `useAuth`·`useUser` 는 아직 템플릿 예제 경로를 가리킨다 (내 정보는 `GET /users/me`)
> - **로그인 페이지**: 추가 시 라우터 가드 리다이렉트를 `{ name: 'login' }` 으로 변경
> - **공통 컴포넌트**: `BaseButton` 패턴을 따라 `src/components/` 에 추가