- Firestore/RTDB read/write 패턴 전수 조사 결과를 문서화 - backend-writes.md, backend-reads.md, data-lifecycle.md, optimization-plan.md 추가
7.3 KiB
7.3 KiB
Panit 데이터 생애주기 — 생성 주체 · 소비자 맵
각 데이터(Firestore 컬렉션 / RTDB 경로)에 대해 어떻게 생성·갱신되는지(writer/trigger) 와 누가 읽는지(소비자 = 백엔드 경로 + 클라 화면) 를 정리한다. 근거:
backend-writes.md,backend-reads.md,../../../AndroidStudioProjects/Panit/docs/data-patterns/client-patterns.md종합. 핵심 사실: 클라이언트는 Firestore/RTDB를 직접 읽지 않는다.users/{uid}.fcmTokens직접 write 1곳을 제외한 모든 접근은 Cloud Functions HTTP 경유. (regionasia-northeast3)
데이터 흐름 한눈에
외부 KBO (koreabaseball.com)
│ scrape/fetch (락+캐시 보호)
▼
kboCache(Firestore) / MemCache(인메모리) ──GET /kbo/*──▶ 클라 화면(일정·순위·선수·경기상세)
│ kboDailyRefresh cron(02:00) + 미스 시 fetch
▼
games(Firestore) ◀─syncGamesForMonth cron─ KBO 일정
│ onDocumentUpdated(games) = onGameCompleted 트리거
▼
votes / userVotes(RTDB) ◀─POST/PUT /prediction─ 유저 투표
│ 경기 종료 → result 주입 → invalidateStats
▼
voteHistory · attendance · pointLedger(Firestore 서브컬렉션) ◀─dailyArchive cron(03:00)+checkIn─
│ 판정/스냅샷
▼
users(Firestore) · scoreboardCache · cache/stats(RTDB) ──GET /stats·/scoreboard─▶ 클라(성적·랭킹)
Firestore
users/{uid} — 유저 루트
- 생성:
POST /user(온보딩 가입) →createUser(userRepository.ts:128),set(merge:false). - 갱신:
PATCH /user·/user/notifications→updateUser(프로필·알림 토글)GET /user(getMe)에서 토큰 사진 변경 시 photoUrl 자동 write (userService.ts:101)dailyArchivecron(03:00):snapshotRankForUser(rank 스냅샷) →applyDailyJudgmentTx(streak/tierPoints/tickets) → 일요일applyWeeklyMasterTx— 유저당 매일 2 write- 직접 write: 클라
users/{uid}.fcmTokensarrayUnion/Remove (fcm_service.dart:40,58) — 유일한 클라 직접 쓰기
- 소비자(읽기):
GET /user→클라 마이페이지/홈/캘린더currentUser,getStats·스코어보드·판정·rank 스냅샷이 내부적으로getUser. 랭킹 정렬은users[favoriteTeamCode+tierPoints]인덱스로 조회.
users/{uid}/voteHistory/{YYYY-MM-DD} — 날짜별 예측 채점 이력
- 생성/갱신:
dailyArchivecron이 ① data 기록(dailyArchive.ts:130) → ②judgeDay판정 채워 재기록(judgmentService.ts:94) — 동일 run 2회 full-set. - 소비자:
GET /stats의computeStats가 서브컬렉션 전체 스캔(누적 성적 집계),GET /stats/history가 단일 일자 doc. 클라userStats·voteHistory(date)·DayPredictionRecord.
users/{uid}/attendance/{YYYY-MM} — 월별 출석
- 생성/갱신:
POST /attendance/check-in트랜잭션,days[]누적 +lastResult(attendanceService.ts:162,243). - 소비자:
GET /attendance/month→클라currentMonthAttendance(출석현황·포인트잔액·오늘출석여부 파생).
users/{uid}/pointLedger/{autoId} — 포인트 원장(append-only)
- 생성:
checkIn트랜잭션 내tx.create(daily 1 + 주간/월간 보너스 조건부, 출석당 1~3행) (pointLedgerRepository.ts:61). - 소비자: 잔액 조회
getLatestBalanceTx(orderBy createdAt desc,seq desc limit 1,pointLedger[createdAt+seq]인덱스).GET /attendance/month응답에 잔액 포함.
games/{gameId} — 경기 (쓰기 증폭 1위)
- 생성/갱신:
syncGamesForMonth=kboDailyRefreshcron(02:00): 한 달치 전 경기를 변경검사 없이merge:truebatch로 매일 덮어씀 (gameSyncService.ts:63)forceSyncDay= cron(어제분)+debug/forceSync: getAll+diff 후 변경분만 writemarkGameEnded=POST /admin/game/end: status/승팀 update →onGameCompleted트리거 점화
- 소비자:
GET /prediction/games(listByDate,time범위쿼리)→클라 예측 화면. 판정/아카이브가listByDate반복 호출. status 변경이onGameCompleted트리거 구동.
kboCache/{key} — 외부 KBO 데이터 캐시
- 생성/갱신:
GET /kbo/*미스 시setCached(rank/player 1h, schedule/gameDetail 동적 TTL), 월간 일정은 일자별 ~30 write.kboDailyRefreshcron이 prefix 전량 삭제 후 rank/schedule 재생성 (kboRefresh.ts:16). - 소비자:
GET /kbo/schedule·rank·player·gameDetail→클라 일정/순위/선수/경기상세.kboLocks로 thundering-herd 차단.
kboLocks/{key} — 분산 락(TTL 30s)
- 생성/삭제: 캐시 미스 fetch 직렬화용
acquireLock/releaseLock(kboCacheRepository.ts:88). 소비자=캐시 미스 경로 내부 전용.
Realtime Database
/votes/{gameId}/counts/{home|away} — 투표 카운터(핫)
- 생성/갱신:
submitVote/changeVoteServerValue.increment(±1)(voteRepository.ts:66,92). POST/PUT/prediction. - 소비자:
GET /prediction/summary(getCounts, MemCache 5s)→클라voteSummary10초 폴링. counts는 RTDB public read 허용.
/votes/{gameId}/users/{uid} & /userVotes/{uid}/{date}/{gameId} — 투표 내역
- 생성/갱신: 투표 시 두 경로 동시 기록 → 경기 종료 시
result주입(fan-out,gameResultService.ts:33) →onGameCompleted/dailyArchive가 취소·정리·remove. - 소비자:
GET /prediction(getUserDateVotes)→클라 그날 예측 목록. 판정(judgeDay)·streak 보정·dailyArchive가 읽음.
/cache/stats/{uid}/{period} — 유저 통계 캐시(파생)
- 생성:
GET /stats미스 시set(statsService.ts:262).forDate로 staleness 판단. - 무효화:
invalidateStats가/cache/stats/{uid}전체 삭제 — 경기 종료마다 투표자 전원 fan-out (gameResultService.ts:42,onGameCompleted.ts:59). - 소비자:
GET /stats→클라userStats/홈 성적.
/scoreboardCache/{date}/{overall|team/{code}} — 랭킹 precompute
- 생성:
dailyArchivecron finally(dailyArchive.ts:163)→precomputeScoreboardCache: overall+10팀 = 11 scopeset, 각 scopelistTopByTierPoints+countRankedUsers. - 소비자:
GET /prediction/scoreboard(MemCache scope 30s)→클라scoreboard(type). miss이고 미precompute면 503(즉시계산 안 함).
/nicknames/{name} & /userNicknames/{uid} — 닉네임 예약
- 생성/갱신:
GET /user/check-nickname(reserveNickname트랜잭션 TTL 10분), 가입 시deleteReservation, 탈퇴 시releaseReservation. - 소비자: 온보딩 닉네임 중복확인 한정.
클라이언트 직접 Firebase 사용처(HTTP 외)
- firebase_auth: Google/Apple 로그인 +
authStateChanges()스트림(라우터 redirect·스플래시·currentUser게이트). read 비용 ~0. - Remote Config:
team_order_by_standing1키, 앱 시작 시 1회 fetch. - FCM:
users/{uid}.fcmTokens직접 write(로그인/토큰갱신 union, 로그아웃 remove). - SWR 캐시(
swr_cache.dart): 일정/경기상세/내기록/기록날짜 4개 도메인. TTL 없음, hit 즉시반환+bg 재검증,skipRevalidateIf로 확정 데이터 재검증 생략.