From 360530ad1382b3646e3a19de502075c6a5e63c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Fri, 24 Apr 2026 08:19:44 +0900 Subject: [PATCH] Implement scoreboard system with rank snapshots and delta tracking. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체 및 팀별 스코어보드 기능을 구현하고 순위 변동 시각화를 위한 스냅샷 시스템을 도입했습니다. - `dailyArchive` 시점에 `rankSnapshot`을 기록하여 이전 대비 순위 변화량을 정확히 산출합니다. - 상위권 데이터를 RTDB에 사전 계산하여 저장하고 `MemCache`를 적용해 조회 성능을 최적화했습니다. - Firestore 복합 색인과 Count Aggregation으로 동점자 처리 및 상위 퍼센타일 산출 로직을 구현했습니다. - 주간 결과 타입을 `DailyJudgment`로 전환하여 판정 세부 상태가 통계 응답에 포함되도록 수정했습니다. --- src/services/statsService.ts | 22 +++++++--------------- src/types/panit.ts | 2 +- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 615a58e..4a23aa1 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -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 { +function weeklyResultsOf(entries: Array<{ date: DateString; doc: VoteHistoryDoc }>): Array { 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 = []; + const results: Array = []; 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; } diff --git a/src/types/panit.ts b/src/types/panit.ts index b0b20b1..8b50bd1 100644 --- a/src/types/panit.ts +++ b/src/types/panit.ts @@ -85,7 +85,7 @@ export interface VoteHistoryDoc { export interface StatsResponse { streakDays: number; highestStreak: number; - weeklyResults: Array; + weeklyResults: Array; winRates: { overall: number; weekly: number;