Passport Tier 팬덤 단위 · 활동 Badge · 진화 Emblem · Eclipse 참여 기록
This commit is contained in:
@@ -2,7 +2,7 @@ import { getFirestore, FieldValue } from 'firebase-admin/firestore';
|
||||
import type { DocumentSnapshot, Transaction } from 'firebase-admin/firestore';
|
||||
|
||||
import type { FpiAxis } from '../core/config.js';
|
||||
import { levelOf, passportTierOf, type PassportTier } from '../core/level.js';
|
||||
import { levelOf } from '../core/level.js';
|
||||
|
||||
/**
|
||||
* FPI 원천 이벤트 — 정책 원장 **C-34 · C-35 · G-05**, 설계서 §4.1.1.
|
||||
@@ -79,6 +79,10 @@ export function writeFpiEvent(
|
||||
): void {
|
||||
if (event.units <= 0) return;
|
||||
if (event.grantsExp !== false) writeUserExp(tx, event.uid, user);
|
||||
// 활동 카테고리별 누적 — 활동 Badge 7단계의 입력(상세정의 4장 · `account/passport.ts`).
|
||||
tx.set(getFirestore().doc(`users/${event.uid}`), {
|
||||
activityCounts: { [event.axis]: FieldValue.increment(event.units) },
|
||||
}, { merge: true });
|
||||
if (!event.fandomId) return;
|
||||
tx.create(
|
||||
getFirestore().doc(`fandoms/${event.fandomId}/fpiEvents/${event.uid}_${event.sourceId}`),
|
||||
@@ -109,7 +113,6 @@ export function writeUserExp(tx: Transaction, uid: string, user: DocumentSnapsho
|
||||
tx.set(getFirestore().doc(`users/${uid}`), {
|
||||
userExp: exp,
|
||||
userLevel: level,
|
||||
passportTier: passportTierOf(level, user?.get('passportTier') as PassportTier | undefined),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
}, { merge: true });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { DocumentReference, DocumentSnapshot, Firestore, Transaction } from
|
||||
|
||||
import { ERR, fail } from '../core/errors.js';
|
||||
import { lotIdOf, txIdOf } from '../core/ids.js';
|
||||
import { levelOf, passportRank, passportTierOf, type PassportTier } from '../core/level.js';
|
||||
import { levelOf } from '../core/level.js';
|
||||
import { fandomOfUser, isMemberOf, voidCompletions, writeFpiEvent } from './fpi.js';
|
||||
import { publicNameOf } from '../core/names.js';
|
||||
|
||||
@@ -809,7 +809,7 @@ export async function reconcileUser(uid: string): Promise<ReconcileRow> {
|
||||
actual: statsActual,
|
||||
});
|
||||
|
||||
// User Level ↔ EXP, Passport Tier 단조(설계서 §3.4 · B-65 · B-66). 두 체계를 따로 본다.
|
||||
// User Level ↔ EXP(설계서 §3.4 · B-66). Passport Tier 는 팬덤별이라 `account/passport.ts` 가 단조로 산출한다.
|
||||
const storedLevel = userSnap.get('userLevel') as number | undefined;
|
||||
if (storedLevel !== undefined) {
|
||||
checks.push({
|
||||
@@ -818,18 +818,6 @@ export async function reconcileUser(uid: string): Promise<ReconcileRow> {
|
||||
actual: storedLevel,
|
||||
});
|
||||
}
|
||||
const storedPassport = userSnap.get('passportTier') as PassportTier | undefined;
|
||||
if (storedPassport !== undefined) {
|
||||
// 레벨이 요구하는 등급보다 낮게 저장돼 있으면(강등 · 누락) 불일치다.
|
||||
checks.push({
|
||||
name: 'passport-monotonic',
|
||||
// 레벨 위조가 이 검사까지 오염시키지 않도록 저장된 레벨이 아니라 EXP 로 본다.
|
||||
expected: passportRank(passportTierOf(
|
||||
levelOf((userSnap.get('userExp') as number | undefined) ?? 0), storedPassport)),
|
||||
actual: passportRank(storedPassport),
|
||||
});
|
||||
}
|
||||
|
||||
const ok = checks.every((c) => c.expected === c.actual);
|
||||
await walletRef(db, uid).set({
|
||||
lastReconciledAt: Timestamp.now(),
|
||||
|
||||
Reference in New Issue
Block a user