rules_version='2' // Reward data model: // products/{id}, orders/{id}: Admin SDK API only. // users/{uid}/wallet/current, pointLedger/{txId}: // owner-readable and server-write-only. Order recipient PII is never client-readable. service cloud.firestore { match /databases/{database}/documents { match /users/{uid} { allow read: if request.auth != null && request.auth.uid == uid; allow update: if request.auth != null && request.auth.uid == uid && request.resource.data.diff(resource.data) .affectedKeys().hasOnly(['fcmToken']) && (!('fcmToken' in request.resource.data) || (request.resource.data.fcmToken is string && request.resource.data.fcmToken.size() <= 4096)); allow create, delete: if false; match /voteHistory/{date} { allow read: if request.auth != null && request.auth.uid == uid; allow write: if false; } // 시즌 종료 시 서버가 기록하는 최종 성적 아카이브 — 본인 조회 전용. match /seasonHistory/{seasonId} { allow read: if request.auth != null && request.auth.uid == uid; allow write: if false; } match /attendance/{month} { allow read: if request.auth != null && request.auth.uid == uid; allow write: if false; } match /pointLedger/{id} { allow read: if request.auth != null && request.auth.uid == uid; allow write: if false; } match /wallet/{id} { allow read: if request.auth != null && request.auth.uid == uid; allow write: if false; } // 배송지 주소록 — 본인 전용 클라이언트 직접 CRUD. // 주문 recipient는 API(POST /reward/orders)에 인라인 전달되므로 // 서버는 이 컬렉션을 읽지 않는다. 문서 id는 클라이언트 uuid. match /addresses/{addressId} { allow read, delete: if request.auth != null && request.auth.uid == uid; allow create, update: if request.auth != null && request.auth.uid == uid && request.resource.data.keys().hasOnly([ 'recipientName', 'phone', 'postalCode', 'address1', 'address2', 'deliveryMemo', 'isDefault', 'order', 'updatedAt' ]) && request.resource.data.recipientName is string && request.resource.data.recipientName.size() > 0 && request.resource.data.recipientName.size() <= 50 && request.resource.data.phone is string && request.resource.data.phone.size() <= 30 && request.resource.data.postalCode is string && request.resource.data.postalCode.size() <= 10 && request.resource.data.address1 is string && request.resource.data.address1.size() <= 300 && request.resource.data.address2 is string && request.resource.data.address2.size() <= 300 && request.resource.data.deliveryMemo is string && request.resource.data.deliveryMemo.size() <= 300 && request.resource.data.isDefault is bool && request.resource.data.order is int && request.resource.data.order >= 0 && request.resource.data.updatedAt == request.time; } // AI 채팅(짹) — 전부 Admin SDK(서버) 전용. 클라이언트가 직접 읽으면 // 프롬프트 노출, 직접 쓰면 한도 우회·이력 위조(영속 인젝션)가 가능해진다. // 이력 조회도 GET /chat/messages 경유. (ai-chat-tech-design.md §4.6) match /chatThreads/{threadId} { match /{document=**} { allow read, write: if false; } allow read, write: if false; } match /chatQuota/{date} { allow read, write: if false; } match /chatRequests/{clientMessageId} { allow read, write: if false; } } // 상품·재고 및 주문 PII는 모두 API 전용이다. match /products/{productId} { allow read, write: if false; } match /orders/{orderId} { allow read, write: if false; } // 시스템 프롬프트·필터 사전·한도 설정 평문 보관 — 노출 시 §7.5 무력화 match /config/{doc} { allow read, write: if false; } // 신고 적재함 — Admin(운영) 전용. 생성도 서버(POST /chat/.../report) 경유 match /chatReports/{reportId} { allow read, write: if false; } match /games/{gameId} { allow read: if true; allow write: if false; } match /kboCache/{key} { allow read: if true; allow write: if false; } match /kboLocks/{key} { allow read, write: if false; } match /{document=**} { allow read, write: if false; } } }