mmday-firebase/src/handlers/predictionHandlers.ts
윤정민 b25e78dde8 Implement scoreboard system with rank snapshots and delta tracking.
- 전체 및 팀별 스코어보드 기능을 구현하고, 순위 변동(delta)을 시각화하기 위한 스냅샷 시스템을 도입했습니다.
- `dailyArchive` 과정에서 사용자의 현재 순위를 `rankSnapshot`으로 기록하여, 다음 판정 시점의 순위 변화량을 정확히 계산합니다.
- 상위권 데이터는 RTDB에 사전 계산(precompute)하여 저장함으로써 대규모 조회 요청에 대응하고, `MemCache`를 통해 API 응답 성능을 최적화했습니다.
- Firestore 복합 색인과 Count Aggregation을 활용하여 동점자 처리가 포함된 랭킹 및 상위 퍼센타일 산출 로직을 구현했습니다.
- 관련 서비스 레이어, 저장소 함수, API 핸들러 및 단위 테스트 코드를 추가했습니다.
2026-04-23 09:19:04 +09:00

72 lines
2.3 KiB
TypeScript

import { onRequest } from "firebase-functions/https";
import { requireAuth } from "../middleware/auth.js";
import { HttpError, sendError } from "../middleware/errors.js";
import {
createPrediction,
updatePrediction,
getMyVotes,
getSummary,
listGamesByDate,
} from "../services/predictionService.js";
import { getScoreboard } from "../services/scoreboardService.js";
export const prediction = onRequest(async (req, res) => {
const segs = req.path.replace(/^\/+|\/+$/g, "").split("/");
const tail = segs[segs.length - 1] ?? "";
try {
if (tail === "games" && req.method === "GET") {
const date = String(req.query.date ?? "");
const games = await listGamesByDate(date);
res.status(200).json({ date, games });
return;
}
if (tail === "scoreboard" && req.method === "GET") {
const uid = await requireAuth(req);
const type = String(req.query.type ?? "");
if (type !== "team" && type !== "overall") {
throw new HttpError(400, `invalid type: ${type}`);
}
const result = await getScoreboard(uid, type);
res.set("Cache-Control", "private, max-age=60");
res.status(200).json(result);
return;
}
if (tail === "summary" && req.method === "GET") {
const gameId = String(req.query.gameId ?? "");
const result = await getSummary(gameId);
res.set("Cache-Control", "public, max-age=5");
res.status(200).json(result);
return;
}
if (tail === "prediction" || segs.length === 1) {
if (req.method === "POST") {
const uid = await requireAuth(req);
const result = await createPrediction(uid, req.body ?? {});
res.status(201).json(result);
return;
}
if (req.method === "PUT") {
const uid = await requireAuth(req);
const result = await updatePrediction(uid, req.body ?? {});
res.status(200).json(result);
return;
}
if (req.method === "GET") {
const uid = await requireAuth(req);
const date = String(req.query.date ?? "");
const result = await getMyVotes(uid, date);
res.status(200).json(result);
return;
}
}
res.status(404).json({ error: `Unknown: ${req.method} ${req.path}` });
} catch (err) {
sendError(res, err);
}
});