라이브 티커 · 갤럭시 검색 색인 · 주간 랭킹 스냅샷 · 연속 ZAP 상한 · 가입 행성 Zap · 성운 목표 · 랭킹 갱신 안내 · 행성 터치 반응
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { onSchedule } from 'firebase-functions/v2/scheduler';
|
||||
import { logger } from 'firebase-functions';
|
||||
import { FieldValue, getFirestore } from 'firebase-admin/firestore';
|
||||
|
||||
import { rankWithTies } from '../ledger/fpi.js';
|
||||
|
||||
/**
|
||||
* Weekly Ranking Snapshot — 별첨 B 「Ranking Snapshot — 주 1 회 Snapshot 영구 Archive」(B-20) ·
|
||||
* 「Weekly Ranking — 주간 공식 Snapshot 은 영구 Archive」 · 9/16 EC06 「주간 Snapshot 유지」.
|
||||
*
|
||||
* - 모집단 · 순위는 FPI 랭킹과 같다(별첨 B 「FPI 랭킹」 — 정식 행성 중 최근 7일 유효 활동, 1·1·3 공동 순위).
|
||||
* - **영구 Archive** — `create` 라 한 주에 한 번만 생기고 다시 쓰지 않는다. 규칙도 클라이언트 쓰기를 막는다.
|
||||
* - Ranking Campaign Draft 자동 생성 · Admin 승인 게시는 Campaign 범위(3차)라 여기서 하지 않는다.
|
||||
*
|
||||
* 🟡 **임시값 — 회신 대기**(해석 확인 요청 2026-10-07 영역A Q-A11): 스냅샷 시각은 자료에 없다. KST 월요일 00:10
|
||||
* (일일 마감 00:00 뒤)으로 두고, 그 시점의 집계값을 지난주(월~일) 공식 순위로 남긴다.
|
||||
*/
|
||||
export function weekKeyOf(at: Date): string {
|
||||
// KST 기준 ISO 주차. 월요일 00:10 에 돌면 「방금 끝난 주」를 가리키도록 하루 전으로 계산한다.
|
||||
const kst = new Date(at.getTime() + 9 * 3_600_000 - 86_400_000);
|
||||
const day = (kst.getUTCDay() + 6) % 7;
|
||||
const thursday = new Date(Date.UTC(kst.getUTCFullYear(), kst.getUTCMonth(), kst.getUTCDate() - day + 3));
|
||||
const firstThursday = new Date(Date.UTC(thursday.getUTCFullYear(), 0, 4));
|
||||
const week = 1 + Math.round(((thursday.getTime() - firstThursday.getTime()) / 86_400_000
|
||||
- 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7);
|
||||
return `${thursday.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export async function snapshotWeeklyRanking(now = new Date()): Promise<{ week: string; created: boolean }> {
|
||||
const db = getFirestore();
|
||||
const week = weekKeyOf(now);
|
||||
const fandoms = await db.collection('fandoms').where('rankingEligible', '==', true)
|
||||
.select('name', 'fpiScore', 'fpiAxes', 'stage', 'level').get();
|
||||
const planets = rankWithTies(fandoms.docs, (d) => Number(d.get('fpiScore') ?? 0), (d) => d.id)
|
||||
.map(({ item, rank }) => ({
|
||||
fandomId: item.id, name: item.get('name') ?? item.id, rank,
|
||||
fpiScore: Number(item.get('fpiScore') ?? 0), fpiAxes: item.get('fpiAxes') ?? {},
|
||||
stage: item.get('stage') ?? null, level: item.get('level') ?? null,
|
||||
}));
|
||||
try {
|
||||
await db.doc(`rankingSnapshots/${week}`).create({
|
||||
week, kind: 'weekly_fpi', planets, population: planets.length,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
});
|
||||
return { week, created: true };
|
||||
} catch (error) {
|
||||
const code = (error as { code?: number | string }).code;
|
||||
if (code !== 6 && code !== 'already-exists') throw error;
|
||||
return { week, created: false };
|
||||
}
|
||||
}
|
||||
|
||||
export const fnWeeklyRankingSnapshot = onSchedule(
|
||||
{ schedule: '10 0 * * 1', timeZone: 'Asia/Seoul' },
|
||||
async () => {
|
||||
logger.info('[ranking] 주간 Snapshot', await snapshotWeeklyRanking());
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user