diff --git a/.claude/rules/error-codes.md b/.claude/rules/error-codes.md index d8b68a5..9c2a21a 100644 --- a/.claude/rules/error-codes.md +++ b/.claude/rules/error-codes.md @@ -45,7 +45,10 @@ backend/src/common/errors/error-codes.ts ← 여기만 수정한다 | FILE_001 / FILE_002 | 400 / 413 | 허용되지 않는 파일 형식(내용 검사 포함) / 크기 초과 | | DIARY_001 | 409 | 다이어리 책장이 가득 참 (최대 6권 — 추억 보관함의 아이만 담은 다이어리는 세지 않는다) | | DIARY_002 | 409 | 한 사람이 한 다이어리에 하루 한 번 | +| DIARY_003 / 004 / 005 | 404 / 409 / 409 | 공유 다이어리 초대 코드 없음 / 참여 인원 가득 참(4명) / 이미 참여 중 | | CARD_001 | 409 | 펫 명함 최대 10장 | +| CARD_002 / 003 | 404 / 400 | 받을 명함 코드 없음 / 내 명함은 받을 수 없음 | +| SCHED_001 ~ 005 | 404 / 409 / 400 / 409 / 409 | 일정 연동 — 초대 코드 없음 / 4명 가득 참 / 내 코드 / 이미 다른 사람과 연동 / 이미 연동된 사람 | ### AUTH_002 (클라이언트 전용) diff --git a/.claude/rules/security.md b/.claude/rules/security.md index 6b2b46f..63d1f55 100644 --- a/.claude/rules/security.md +++ b/.claude/rules/security.md @@ -31,7 +31,11 @@ - **확장자와 MIME 을 둘 다** 검사한다 (`upload.config.ts`). 확장자만 보면 위장이 쉽고, MIME 만 보면 클라이언트가 보낸 값이라 신뢰할 수 없다. 저장한 뒤 **파일 앞부분(매직 바이트)도 확인**해 형식과 다르면 지운다. -- 파일은 **올린 회원의 것**으로 기록하고, 내려받기(`GET /files/:id`)·다른 기록에서의 참조 모두 본인 파일만 허용한다. +- 파일은 **올린 회원의 것**으로 기록하고, 다른 기록에서의 참조는 본인 파일만 허용한다. + 내려받기(`GET /files/:id`)는 본인 파일 + **함께 쓰는 기능이 등록한 열람 정책**(`FileAccessPolicy`)이 허락한 파일만이다 — + 공유 다이어리(표지·기록 사진·대상 아이·멤버 프로필)와 일정 함께 관리(연동된 사람의 프로필·아이 사진). 그 밖은 남의 파일과 없는 파일 모두 RES_001. +- **초대 코드만으로 남의 기록에 들어오는 기능**(공유 다이어리·일정 연동)은 코드가 곧 열쇠다. 짐작하기 어렵게(`RUP-` + 6자, 혼동 글자 뺀 32자) + 만들고, 조회·참여 엔드포인트에 엄격한 rate limit(분당 30회)을 건다. 내보내거나 나가면 다이어리 코드를 바꾼다. - **허용 목록(allowlist) 방식**으로만 받는다. 차단 목록은 언제나 빠뜨린다. `.html`·`.svg`·`.js` 등 **브라우저가 실행할 수 있는 형식은 넣지 않는다.** - **저장 파일명은 서버가 생성**한다(UUID + 검증된 확장자). 클라이언트가 보낸 이름을 경로에 쓰면 diff --git a/.claude/skills/flutter/SKILL.md b/.claude/skills/flutter/SKILL.md index 6a97f34..6a53796 100644 --- a/.claude/skills/flutter/SKILL.md +++ b/.claude/skills/flutter/SKILL.md @@ -26,7 +26,9 @@ description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null S | 세션 만료 처리 | `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:`. 기기 사진은 저장할 때 내용으로 형식을 확인해 올린다. 서버 사진은 Dio 로 받아 그린다 | +| 사진 | `server_file.dart` · `file_uploader.dart` · `shared/widgets/photo_image.dart` | 사진 경로 자리에 `server-file:` (내 것이 아닌 서버 경로는 `server-url:<경로>` — 받기 전 명함 미리보기). 기기 사진은 저장할 때 내용으로 형식을 확인해 올린다. 서버 사진은 Dio 로 받아 그린다 | +| QR 스캐너 | `shared/widgets/qr_scanner_page.dart` | `QrScannerPage` — 문구·코드 꺼내기·불러오기만 넘긴다(명함 받기·다이어리 참여·일정 연동). 결과는 `ScanFound` / `ScanTypeCode`. 테스트는 `scannerCameraProvider` 로 카메라를 바꿔 끼운다 | +| 함께 쓰는 기록 | `features/diary` · `features/schedule` | 서버가 준 작성자·멤버의 회원 id 가 나면 `kMeAuthorId`/`isMe` 로 바꿔 담는다. 남의 아이는 내 목록에 없으므로 서버가 준 이름·사진 요약(`targets`)으로 그린다. 기기 저장 모드는 `canShare`/`available` 이 false 라 안내만 띄운다 | | 상태관리 초기화 | `main.dart` 의 `ProviderScope` | 세션(토큰)을 첫 프레임 전에 읽어 override 한다 | | 도메인 feature(패턴 참고) | `lib/features/pet/` | 저장소 **계약 + `Local*` + `Api*`**, 프로바이더가 `DATA_MODE` 로 고른다. 알림기 build 는 `sessionEpochProvider` 를 watch | diff --git a/.claude/skills/nestjs/SKILL.md b/.claude/skills/nestjs/SKILL.md index e657ad2..63797e0 100644 --- a/.claude/skills/nestjs/SKILL.md +++ b/.claude/skills/nestjs/SKILL.md @@ -38,7 +38,9 @@ description: NestJS 백엔드(backend/) 개발 규칙 — 컨트롤러/서비스 | 공통 검증 데코레이터 | `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/upload` | 소유자 기록·매직 바이트 검사·`GET /files/:id`(본인 + 열람 정책, attachment)·`releaseIfUnused()` ([[security]]) | +| 파일 열람 정책 | `modules/upload/file-access-policy.ts` | 남과 함께 쓰는 기능은 `FileAccessPolicy` 를 만들어 `onModuleInit` 에서 `UploadService.registerAccessPolicy()` 로 등록한다 (`diary-file-access.ts`·`calendar-file-access.ts` 참고) | +| 초대 코드 | `shared/utils/invite-code.ts` | `inviteCodeFormat(길이)` — `RUP-` + 혼동 글자 뺀 대문자·숫자. 저장은 `retryOnUniqueViolation()` 으로 충돌 시 다시 만든다 | | 헬스체크 | `modules/health` | `@SkipThrottle`+`@SkipTransform` — 기계가 읽는 엔드포인트 패턴 ([[response-format]]) | | 순수 유틸 | `shared/utils/` | 순수 함수 분리 위치 |