Implement scoreboard system with rank snapshots and delta tracking.

- 전체 및 팀별 스코어보드 기능을 구현하고 순위 변동 시각화를 위한 스냅샷 시스템을 도입했습니다.
- `dailyArchive` 시점에 `rankSnapshot`을 기록하여 이전 대비 순위 변화량을 정확히 산출합니다.
- 상위권 데이터를 RTDB에 사전 계산하여 저장하고 `MemCache`를 적용해 조회 성능을 최적화했습니다.
- Firestore 복합 색인과 Count Aggregation으로 동점자 처리 및 상위 퍼센타일 산출 로직을 구현했습니다.
- 주간 결과 타입을 `DailyJudgment`로 전환하여 판정 세부 상태가 통계 응답에 포함되도록 수정했습니다.
This commit is contained in:
윤정민 2026-04-24 08:19:44 +09:00
parent b25e78dde8
commit 360530ad13
2 changed files with 8 additions and 16 deletions

View File

@ -4,7 +4,7 @@ import {computeLevel} from "../constants/levels.js";
import {tierOf} from "../constants/tiers.js";
import {getAll, getDay} from "../repositories/voteHistoryRepository.js";
import {getUser} from "../repositories/userRepository.js";
import type {StatsResponse, VoteHistoryDoc} from "../types/panit.js";
import type {DailyJudgment, StatsResponse, VoteHistoryDoc} from "../types/panit.js";
import {
parseDateString,
toDateString,
@ -124,16 +124,14 @@ function computeStreak(entries: Array<{ date: DateString; doc: VoteHistoryDoc }>
}
/**
* (~) 7 .
*
* - `true`
* - `false`
* - `null`
* (~) 7 .
* `VoteHistoryDoc.judgment`(perfect/success/fail/skip) ,
* `null` .
*
* @param entries -
* @returns 7
*/
function weeklyResultsOf(entries: Array<{ date: DateString; doc: VoteHistoryDoc }>): Array<boolean | null> {
function weeklyResultsOf(entries: Array<{ date: DateString; doc: VoteHistoryDoc }>): Array<DailyJudgment | null> {
const byDate = new Map(entries.map((e) => [e.date, e.doc] as const));
const today = todayKst();
const todayMs = startOfDayKst(today).getTime();
@ -146,17 +144,11 @@ function weeklyResultsOf(entries: Array<{ date: DateString; doc: VoteHistoryDoc
const tuesdayMs = todayMs - offset * 86400000;
const results: Array<boolean | null> = [];
const results: Array<DailyJudgment | null> = [];
for (let i = 0; i < 7; i++) {
const key = toDateString(new Date(tuesdayMs + i * 86400000));
const doc = byDate.get(key);
if (!doc || doc.data.length === 0) {
results.push(null);
} else {
const anyCorrect = doc.data.some((v) => v.result === true);
const allWrong = doc.data.every((v) => v.result === false);
results.push(anyCorrect ? true : allWrong ? false : null);
}
results.push(doc?.judgment ?? null);
}
return results;
}

View File

@ -85,7 +85,7 @@ export interface VoteHistoryDoc {
export interface StatsResponse {
streakDays: number;
highestStreak: number;
weeklyResults: Array<boolean | null>;
weeklyResults: Array<DailyJudgment | null>;
winRates: {
overall: number;
weekly: number;