Passport Tier 팬덤 단위 · 활동 Badge · 진화 Emblem · Eclipse 참여 기록

This commit is contained in:
Sn-Kinos
2026-10-05 22:09:10 +09:00
parent 71ec17dc3c
commit cc0f2fb292
36 changed files with 877 additions and 81 deletions
+22 -9
View File
@@ -4,7 +4,8 @@ import { txIdOf, lotIdOf, deviceHashOf } from '../src/core/ids.js';
import { dayKey, isPlausibleOccurredAt } from '../src/core/time.js';
import { DEFAULT_POLICY, FPI_AXES, weightsAreUsable } from '../src/core/config.js';
import { CONTRIBUTION_REASONS } from '../src/ledger/ledger.js';
import { expForLevel, levelOf, passportTierOf, titleOf } from '../src/core/level.js';
import { expForLevel, levelOf, titleOf } from '../src/core/level.js';
import { badgeStageOf, BADGE_STAGE_THRESHOLDS, DEFAULT_TIER_RULES, tierOf } from '../src/account/passport.js';
import { FPI_UNIT_FACTOR, rankWithTies } from '../src/ledger/fpi.js';
import { fnIapRefundWebhook, fnSubscriptionWebhook } from '../src/payment/iap.js';
import {
@@ -223,7 +224,7 @@ describe('User Level · 칭호 · Passport — C-18 · B-65 · B-66', () => {
it('신규 회원은 Lv.1 GLOW · SILVER 다', () => {
expect(levelOf(0)).toBe(1);
expect(titleOf(levelOf(0))).toBe('GLOW');
expect(passportTierOf(1)).toBe('SILVER');
expect(tierOf(DEFAULT_TIER_RULES, { userLevel: 1, activeDays: 0, badgeStages: [], contributedPop: 0 })).toBe('SILVER');
});
it('레벨은 EXP 요구량 경계에서 정확히 오른다', () => {
@@ -250,13 +251,25 @@ describe('User Level · 칭호 · Passport — C-18 · B-65 · B-66', () => {
expect(titleOf(levelOf(3_000))).toBe('GLOW');
});
it('Passport 는 Gold Lv.30+ · Diamond Lv.70+ 이고 강등하지 않는다', () => {
expect(passportTierOf(29)).toBe('SILVER');
expect(passportTierOf(30)).toBe('GOLD');
expect(passportTierOf(70)).toBe('DIAMOND');
expect(passportTierOf(1, 'DIAMOND')).toBe('DIAMOND');
expect(passportTierOf(40, 'DIAMOND')).toBe('DIAMOND');
expect(passportTierOf(75, 'GOLD')).toBe('DIAMOND');
it('Passport Tier 는 팬덤별 규칙(Level · 활동기간 · Badge · 누적기여)을 모두 채워야 오르고 강등하지 않는다', () => {
const base = { userLevel: 30, activeDays: 30, badgeStages: [1], contributedPop: 0 };
expect(tierOf(DEFAULT_TIER_RULES, base)).toBe('GOLD');
// 하나라도 모자라면 그대로 — Level 만으로 오르지 않는다(상세정의 4장 · 10/3 #10).
expect(tierOf(DEFAULT_TIER_RULES, { ...base, userLevel: 29 })).toBe('SILVER');
expect(tierOf(DEFAULT_TIER_RULES, { ...base, activeDays: 29 })).toBe('SILVER');
expect(tierOf(DEFAULT_TIER_RULES, { ...base, badgeStages: [] })).toBe('SILVER');
const diamond = { userLevel: 70, activeDays: 180, badgeStages: [3, 3], contributedPop: 100_000 };
expect(tierOf(DEFAULT_TIER_RULES, diamond)).toBe('DIAMOND');
expect(tierOf(DEFAULT_TIER_RULES, { ...diamond, contributedPop: 99_999 })).toBe('GOLD');
expect(tierOf(DEFAULT_TIER_RULES, { ...base, userLevel: 1 }, 'DIAMOND')).toBe('DIAMOND');
// 운영값이 규칙을 끄면 그 Tier 로 오르지 않는다.
expect(tierOf(DEFAULT_TIER_RULES.map((r) => ({ ...r, enabled: false })), diamond)).toBe('SILVER');
});
it('활동 Badge 는 7단계 10/100/500/1,500/4,000/10,000/30,000(상세정의 4장)', () => {
expect(BADGE_STAGE_THRESHOLDS).toEqual([10, 100, 500, 1_500, 4_000, 10_000, 30_000]);
expect([0, 9, 10, 99, 100, 1_500, 29_999, 30_000, 1e9].map((n) => badgeStageOf(BADGE_STAGE_THRESHOLDS, n)))
.toEqual([0, 0, 1, 1, 2, 4, 6, 7, 7]);
});
});