생성 때 고른 행성 형태·색 표시 · HOME 주간 정보

This commit is contained in:
Sn-Kinos
2026-10-05 23:21:44 +09:00
parent 55424bd4a2
commit c8c1a1ae67
40 changed files with 688 additions and 40 deletions
+6
View File
@@ -283,6 +283,12 @@ service cloud.firestore {
// 활동 로그(걸음·스밍·위치·출석). 어뷰징 판정 입력이라 클라이언트 쓰기 금지.
// 화면: S-H-WALK · S-H-STREAM · S-H-PLACE · S-H-ATTEND · S-AD-ABUSE
// 성장 귀속 행(원장과 같은 트랜잭션) — HOME 주간 정보(A-37) 「이번 주 기여량 · 비율」이 본인 행을 합한다.
match /users/{uid}/growthEvents/{txId} {
allow read: if isSelf(uid) || isViewer();
allow write: if false;
}
match /users/{uid}/activityLogs/{logId} {
allow read: if isSelf(uid) || isViewer();
allow write: if false;
+3 -1
View File
@@ -30,12 +30,14 @@ export async function snapshotWeeklyRanking(now = new Date()): Promise<{ week: s
const db = getFirestore();
const week = weekKeyOf(now);
const fandoms = await db.collection('fandoms').where('rankingEligible', '==', true)
.select('name', 'fpiScore', 'fpiAxes', 'stage', 'level').get();
.select('name', 'fpiScore', 'fpiAxes', 'stage', 'level', 'growthPop', 'sfpi').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,
// HOME 주간 정보(A-37)의 기준선 — 「이번 주 성장」은 지금 누계에서 이 값을 뺀 두 축 증분이다.
growthPop: Number(item.get('growthPop') ?? 0), sfpi: Number(item.get('sfpi') ?? 0),
}));
try {
await db.doc(`rankingSnapshots/${week}`).create({
+4 -1
View File
@@ -8,7 +8,7 @@ const ids = ['test-week-a', 'test-week-b', 'test-week-c', 'test-week-out'];
beforeAll(async () => {
bootAdmin();
const db = getFirestore();
await db.doc('fandoms/test-week-a').set({ name: 'A', fpiScore: 300, rankingEligible: true });
await db.doc('fandoms/test-week-a').set({ name: 'A', fpiScore: 300, rankingEligible: true, growthPop: 1200, sfpi: 34 });
await db.doc('fandoms/test-week-b').set({ name: 'B', fpiScore: 300, rankingEligible: true });
await db.doc('fandoms/test-week-c').set({ name: 'C', fpiScore: 100, rankingEligible: true });
await db.doc('fandoms/test-week-out').set({ name: 'X', fpiScore: 999, rankingEligible: false });
@@ -36,6 +36,9 @@ it('모집단 행성의 공동 순위를 한 번만 영구 보관한다', async
expect(ours.map(([id]) => id)).toEqual(['test-week-a', 'test-week-b', 'test-week-c']);
// 같은 점수는 같은 순위(1·1·3) — 다른 테스트 행성이 섞여도 상대 순서는 같다.
expect(ours[0]![1]).toBe(ours[1]![1]);
// HOME 「이번 주 성장」 기준선(A-37) — 그 시각의 Growth POP · sFPI 누계를 함께 남긴다.
const a = (snap.planets as Array<Record<string, unknown>>).find((p) => p.fandomId === 'test-week-a')!;
expect([a.growthPop, a.sfpi]).toEqual([1200, 34]);
// 다시 돌아도 덮지 않는다.
await db.doc('fandoms/test-week-c').update({ fpiScore: 9_999 });
expect(await snapshotWeeklyRanking(at)).toEqual({ week: '2026-W40', created: false });
@@ -253,6 +253,17 @@ describe('원칙 2 — 개인 리소스와 쓰기 필드 제한', () => {
});
});
describe('성장 귀속 행 — users/{uid}/growthEvents (HOME 주간 정보 A-37)', () => {
test('본인만 읽고 누구도 직접 쓰지 못한다', async () => {
await seedDocuments(testEnvironment, {
'users/alice/growthEvents/tx-1': { fandomIdAtPayout: 'fandom-a', amount: 10 },
});
await assertSucceeds(getDoc(doc(dbFor('alice'), 'users/alice/growthEvents/tx-1')));
await assertFails(getDoc(doc(dbFor('bob'), 'users/alice/growthEvents/tx-1')));
await assertFails(setDoc(doc(dbFor('alice'), 'users/alice/growthEvents/tx-2'), { amount: 999 }));
});
});
describe('가입 목록 — users/{uid}/memberships (C-15)', () => {
beforeEach(async () => {
await seedDocuments(testEnvironment, {