46 lines
2.1 KiB
TypeScript
46 lines
2.1 KiB
TypeScript
import { Timestamp, type DocumentReference } from 'firebase-admin/firestore';
|
||
import { dayKey } from '../core/time.js';
|
||
|
||
/**
|
||
* 2026-09-16 EC02·EC05: 누락된 집계를 0으로 채우지 않는다.
|
||
* 원장 C-26 `target = max(1, ceil(avgDau × rate))` · C-27 생성 후 완료 집계일 ≤ [windowDays] 일 평균 ·
|
||
* C-21 집계 기간은 운영값(기본 7일).
|
||
*/
|
||
export async function eclipseTarget(
|
||
fandom: DocumentReference,
|
||
createdAt: unknown,
|
||
day: string,
|
||
rate = 0.05,
|
||
windowDays = 7,
|
||
): Promise<
|
||
{ target: number; averageDau: number | null; dauDays: string[] }
|
||
| { result: 'cancelled_error'; cancelReason: string }
|
||
> {
|
||
const invalid = (cancelReason: string) => ({ result: 'cancelled_error' as const, cancelReason });
|
||
if (!Number.isFinite(rate) || rate <= 0 || rate > 1) return invalid('INVALID_TARGET_RATE');
|
||
if (!Number.isSafeInteger(windowDays) || windowDays < 1) return invalid('INVALID_DAU_WINDOW');
|
||
if (!(createdAt instanceof Timestamp)) return invalid('MISSING_FANDOM_CREATED_AT');
|
||
// 기존 scheduler/dayKey와 같은 날짜 키로 완료된 일별 집계를 조회한다.
|
||
const createdDay = dayKey(createdAt.toDate());
|
||
if (createdDay > day) return invalid('FUTURE_FANDOM_CREATED_AT');
|
||
if (createdDay === day) return { target: 1, averageDau: null, dauDays: [] };
|
||
const days: string[] = [];
|
||
for (let i = 1; i <= windowDays; i++) {
|
||
const previous = new Date(`${day}T00:00:00Z`);
|
||
previous.setUTCDate(previous.getUTCDate() - i);
|
||
const key = previous.toISOString().slice(0, 10);
|
||
if (key < createdDay) break;
|
||
days.push(key);
|
||
}
|
||
const docs = await fandom.firestore.getAll(
|
||
...days.map((key) => fandom.collection('fandomDailyStats').doc(key)),
|
||
);
|
||
if (docs.some((doc) => !doc.exists || !Number.isSafeInteger(doc.get('dau')) || doc.get('dau') < 0)) {
|
||
return invalid('MISSING_OR_INVALID_DAU');
|
||
}
|
||
const sum = docs.reduce((total, doc) => total + (doc.get('dau') as number), 0);
|
||
if (!Number.isSafeInteger(sum)) return invalid('INVALID_DAU_TOTAL');
|
||
const averageDau = sum / days.length;
|
||
return { target: Math.max(1, Math.ceil(averageDau * rate)), averageDau, dauDays: days };
|
||
}
|