// 누적 랭킹 — 투표 결과가 반영된 포인트 축적 TOP 10 // ───────────────────────────────────────────────────────────── // UI(Leaderboard.tsx)는 fetchLeaderboard 의 "시그니처(입출력 타입)"에만 의존한다. // 백엔드 개발자는 fetchLeaderboard 의 *본문만* 콜러블 함수(getLeaderboard) // 호출로 교체하면 UI 수정 불필요. // const { fns } = getFirebase(); // const res = await httpsCallable(fns, "getLeaderboard")({ tab }); // return res.data as LeaderEntry[]; // 누적 포인트 내림차순, 최대 10 // // 포인트 산정(서버 책임): 종료 경기마다 예측 정확도로 가점 → 누적. // PII: 참가자 label 은 *마스킹된 이메일 아이디*만 반환할 것(전체 이메일 금지). export type LeaderTab = "voters" | "ai"; /** 랭킹 1행 — 표시용 2개 항목(이름/아이디 + 누적 포인트) */ export type LeaderEntry = { label: string; // 참가자: 마스킹된 이메일 아이디 / AI: 모델명(GPT·Claude·Gemini) points: number; // 누적 포인트 }; /** 공개 노출용 이메일 아이디 마스킹: "haewonkam@x.com" → "hae****m" */ export function maskEmailId(email: string): string { const id = (email.split("@")[0] || email).trim(); if (id.length <= 2) return id[0] + "*"; if (id.length <= 4) return id.slice(0, 2) + "*".repeat(id.length - 2); return id.slice(0, 3) + "*".repeat(Math.min(4, id.length - 4)) + id.slice(-1); } // ── 데모 목업 (결정론적) — 운영에서는 getLeaderboard 콜러블로 대체 ── const DEMO_VOTERS: LeaderEntry[] = [ { label: "cha***n", points: 1840 }, { label: "jun***0", points: 1715 }, { label: "min***k", points: 1660 }, { label: "soo***2", points: 1590 }, { label: "hae***i", points: 1505 }, { label: "yul***7", points: 1430 }, { label: "dae***n", points: 1388 }, { label: "seo***3", points: 1322 }, { label: "par***k", points: 1260 }, { label: "won***9", points: 1195 }, ]; const DEMO_AI: LeaderEntry[] = [ { label: "Gemini", points: 1420 }, { label: "GPT", points: 1305 }, { label: "Claude", points: 1180 }, ]; /* ============================================================ * 백엔드 연동 지점 — 누적 랭킹 TOP 10 조회 * 개발자: 본문을 getLeaderboard 콜러블 호출로 교체. * ============================================================ */ export async function fetchLeaderboard(tab: LeaderTab): Promise { const list = tab === "ai" ? DEMO_AI : DEMO_VOTERS; return [...list].sort((a, b) => b.points - a.points).slice(0, 10); }