/** * `/userVotes/{uid}/{date}/{gameId}` → `/userVotesByDate/{date}/{uid}/{gameId}` 백필. * * `dailyArchive`는 날짜별 인덱스만 읽는다(원래는 하루치를 위해 `/userVotes` 트리 * 전체를 내려받았다). 인덱스 도입 이전에 기록된 투표는 이 스크립트로 옮겨야 * 잡의 레거시 폴백 경로를 타지 않는다. * * 멱등하다 — 여러 번 돌려도 같은 값으로 덮어쓸 뿐이다. * * 실행: * npx tsx scripts/backfill-user-votes-by-date.ts # 미리보기 * npx tsx scripts/backfill-user-votes-by-date.ts --apply # 실제 쓰기 */ import "./_bootstrap"; import { rtdb } from "../src/firebase"; type VoteEntry = { team: string; result?: boolean; cancelled?: boolean }; type ByUid = Record>>; /** RTDB update는 한 번에 너무 많은 경로를 담으면 실패하므로 나눠 커밋한다. */ const CHUNK = 500; async function main(): Promise { const apply = process.argv.includes("--apply"); const snap = await rtdb.ref("/userVotes").get(); if (!snap.exists()) { console.log("no /userVotes data — nothing to backfill"); if (apply) await markBackfilled(); return; } const byUid = snap.val() as ByUid; const updates: Record = {}; let uidCount = 0; let entryCount = 0; const dates = new Set(); for (const uid of Object.keys(byUid)) { const byDate = byUid[uid] ?? {}; let touched = false; for (const date of Object.keys(byDate)) { const games = byDate[date] ?? {}; for (const gameId of Object.keys(games)) { updates[`/userVotesByDate/${date}/${uid}/${gameId}`] = games[gameId]; entryCount += 1; dates.add(date); touched = true; } } if (touched) uidCount += 1; } const paths = Object.keys(updates); console.log( `${entryCount} entries / ${uidCount} uids / ${dates.size} dates` + (apply ? "" : " (dry run — pass --apply to write)") ); if (!apply) return; for (let i = 0; i < paths.length; i += CHUNK) { const slice: Record = {}; for (const p of paths.slice(i, i + CHUNK)) slice[p] = updates[p]; await rtdb.ref().update(slice); console.log(` written ${Math.min(i + CHUNK, paths.length)}/${paths.length}`); } await markBackfilled(); console.log("backfill complete"); } /** * 완료 표시. 이 값이 있으면 `dailyArchive`는 인덱스가 비어 있어도 * "그날 투표가 없었다"로 해석하고 레거시 전체 스캔 폴백을 쓰지 않는다. */ async function markBackfilled(): Promise { await rtdb.ref("/userVotesByDateMeta/backfilledAt").set(new Date().toISOString()); console.log("marked /userVotesByDateMeta/backfilledAt"); } main() .then(() => process.exit(0)) .catch((err) => { console.error(err); process.exit(1); });