diff --git a/docs/data-patterns/backend-writes.md b/docs/data-patterns/backend-writes.md index 6df6f02..0b9b90a 100644 --- a/docs/data-patterns/backend-writes.md +++ b/docs/data-patterns/backend-writes.md @@ -18,8 +18,8 @@ | 항목 | 내용 | |---|---| | 주요 필드 | `displayName, email, provider, knowledgeLevel, photoUrl, favoriteTeamCode, createdAt` (생성), `currentStreak, highestStreak, tierPoints, lastJudgedDate, rankSnapshot, notifications` (운영 중 갱신) | -| Writer / 위치 | `createUser` `userRepository.ts:128-139` · `updateUser` `:145-150` · `applyDailyJudgmentTx` `:182-254` · `deleteUser` `:155-157` · `snapshotRankForUser` `rankSnapshotService.ts:26-54` | -| Trigger | 생성=POST `/user`(`createMe`, userService.ts:120) · 갱신=PATCH `/user`(`updateMe`), PATCH `/user/notifications`, **GET `/user`(`getMe`) 의 photoUrl 자동 동기화**(userService.ts:101-105) · 판정=`dailyArchive` cron→`judgeDay`→`applyDailyJudgmentTx` · rankSnapshot=`dailyArchive` cron(judge **이전**) · 삭제=DELETE `/user` | +| Writer / 위치 | `createUser` `userRepository.ts:128-139` · `updateUser` `:145-150` · `applyDailyJudgmentTx` `:182-254` · `deleteUser` `:155-157` · `snapshotRankForUser` `rankSnapshotService.ts:26-54` · `settleUsers` `seasonRepository.ts`(tierPoints=0 리셋 + rankSnapshot 삭제) | +| Trigger | 생성=POST `/user`(`createMe`, userService.ts:120) · 갱신=PATCH `/user`(`updateMe`), PATCH `/user/notifications`, **GET `/user`(`getMe`) 의 photoUrl 자동 동기화**(userService.ts:101-105) · 판정=`dailyArchive` cron→`judgeDay`→`applyDailyJudgmentTx` · rankSnapshot=`dailyArchive` cron(judge **이전**) · 시즌 리셋=`dailyArchive` cron→`maybeSettleSeason`(시즌 endDate 다음 날 1회, §1.8) · 삭제=DELETE `/user` | | Mechanism | 생성 `set({merge:false})`; 일반 갱신 `set({merge:true})`; 판정 `runTransaction`+`set(merge:true)`+`FieldValue.serverTimestamp()`(createdAt); 삭제 `recursiveDelete`(서브컬렉션 voteHistory/attendance/pointLedger 포함) | | 빈도/볼륨 | 가입 1회/유저 · 프로필 수정 드묾 · **rankSnapshot + 판정 = 유저당 매일 2회 write** (전체 활성 유저 N명 × 매일) | | 비용 관찰 | ⚠️ **`getMe`(읽기 경로)에서 토큰 사진이 다르면 매 요청 write 발생** — 사진 변경이 잦은 토큰이면 read마다 hot write. ⚠️ `rankSnapshot` write(archive 중)와 `applyDailyJudgmentTx` write가 **같은 doc을 같은 cron run에서 2번** 건드림 → 1 write로 합칠 여지. | @@ -89,6 +89,17 @@ --- +### 1.8 `users/{uid}/seasonHistory/{seasonId}` (시즌 최종 성적 아카이브) + +| 항목 | 내용 | +|---|---| +| 데이터 | `{ seasonId, tierPoints, tier, rank, totalRanked, settledAt }` (문서 ID = seasonId) | +| Writer | `settleUsers` `seasonRepository.ts` — 유저별 아카이브 `set` + 루트 doc `tierPoints=0`/`rankSnapshot` 삭제를 **같은 배치**로 커밋(200유저/배치) | +| Trigger | `dailyArchive` cron → `maybeSettleSeason` (seasonService.ts) — `config/season.endDate` 다음 날, `settledAt` 마커가 없을 때 1회 | +| Mechanism | 부분 실패 재실행 시 collectionGroup(`seasonId ==`) 조회로 기정산 유저 점수를 순위 산정에 합류시켜 rank 보존(멱등). `settledAt` 마커는 전원 완료 후 `config/season`에 기록 | +| 빈도/볼륨 | **시즌당 1회** × 랭킹 대상 유저 수(유저당 2 write). append-only, 시즌 수만큼만 증가 | +| 읽기 | GET `/stats/seasons` (`listUserSeasonHistory`, seasonId desc) — 클라이언트 마이페이지 "지난 시즌 기록" | + ## 2. Realtime Database 쓰기 대상 ### 2.1 `/votes/{gameId}/counts/{homeCount|awayCount}` — 🔴 핫 카운터