팬덤 탈퇴 · 성운 창립자 권한 승계 · 대표 팬덤 자동 지정

This commit is contained in:
Sn-Kinos
2026-10-05 17:41:20 +09:00
parent 8a1b824307
commit 3a3be4c3d3
21 changed files with 229 additions and 54 deletions
@@ -129,25 +129,62 @@ describe('가입 · 탈퇴 · 대표 팬덤', () => {
});
});
describe('창립자 탈퇴 · 대표 팬덤 자동 지정(Q-PN-03 · Q-PN-04 권고안)', () => {
describe('창립자 탈퇴 승계 · 대표 팬덤 자동 지정(원장 C-100 · C-101 · S20)', () => {
const setPolicy = (patch: Record<string, unknown>) => db().doc('config/policy').set(patch, { merge: true });
const THIRD = 'test-planet-create-third';
afterEach(async () => {
await db().doc('config/policy').set({
nebulaFounderLeaveBlocked: FieldValue.delete(), primaryOnLeave: FieldValue.delete(),
}, { merge: true });
await db().doc('config/policy').set({ primaryOnLeave: FieldValue.delete() }, { merge: true });
await wipeUser(THIRD);
await db().recursiveDelete(db().collection(`users/${THIRD}/memberships`));
await db().recursiveDelete(db().collection(`users/${OTHER}/notifications`));
await db().recursiveDelete(db().collection(`users/${THIRD}/notifications`));
});
it('성운 창립자는 탄생 전 탈퇴 E-FDM-05 · 탄생 뒤에는 탈퇴 가능 · 설정을 끄면 막지 않는다', async () => {
const id = (await call(fnNebulaCreate, { name: 'Founder Stay' })).fandomId as string;
expect(await codeOf(() => call(fnFandomLeave, { fandomId: id }))).toBe('E-FDM-05');
expect((await fandom(id)).memberCount).toBe(1);
it('탄생 전 창립자 탈퇴 허용 → 가장 먼저 가입한 남은 멤버가 탄생 권한자 · 알림 · 이전 창립자 탄생 거부 · 순번 비재사용', async () => {
const id = (await call(fnNebulaCreate, { name: 'Founder Go' })).fandomId as string;
await call(fnFandomJoin, { fandomId: id }, OTHER);
await call(fnFandomJoin, { fandomId: id }, THIRD);
// 가입 시각을 OTHER < THIRD 로 못박는다.
await db().doc(`fandoms/${id}/members/${OTHER}`).update({ joinedAt: Timestamp.fromMillis(Date.now() - 2 * 86_400_000) });
await db().doc(`fandoms/${id}/members/${THIRD}`).update({ joinedAt: Timestamp.fromMillis(Date.now() - 86_400_000) });
const out = await call(fnFandomLeave, { fandomId: id });
expect(out).toMatchObject({ left: true, founderUid: OTHER });
expect(await fandom(id)).toMatchObject({ founderUid: OTHER, memberCount: 2, memberSeq: 3 });
const notes = await db().collection(`users/${OTHER}/notifications`).get();
expect(notes.docs.map((d) => d.get('title'))).toEqual(['「Founder Go」 성운의 행성 탄생 권한을 이어받았어요.']);
await db().doc(`fandoms/${id}`).update({ memberCount: 10, growthPop: 1_000 });
const pick = { fandomId: id, planetType: 'shape1', colorKey: 'blue' };
expect(await codeOf(() => call(fnPlanetCreate, pick))).toBe('E-PERM-01');
// 떠난 창립자가 다시 가입해도 #001 이 아니라 다음 순번(#4)을 받는다.
await call(fnFandomJoin, { fandomId: id });
expect((await db().doc(`fandoms/${id}/members/${UID}`).get()).get('memberNo')).toBe(4);
expect((await fandom(id)).founderUid).toBe(OTHER);
await call(fnPlanetCreate, pick, OTHER);
expect((await fandom(id)).stage).toBe(1);
});
it('창립자 혼자인 성운 — 탈퇴 허용 · 성운 유지 · 탄생 권한은 비웠다가 다음 가입자에게(S20)', async () => {
const id = (await call(fnNebulaCreate, { name: 'Founder Alone' })).fandomId as string;
expect(await call(fnFandomLeave, { fandomId: id })).toMatchObject({ left: true, founderUid: null });
expect(await fandom(id)).toMatchObject({ stage: 0, memberCount: 0, founderUid: null, name: 'Founder Alone' });
await call(fnFandomJoin, { fandomId: id }, OTHER);
expect((await fandom(id)).founderUid).toBe(OTHER);
// 이미 권한자가 있으면 다음 가입자는 이어받지 않는다.
await call(fnFandomJoin, { fandomId: id }, THIRD);
expect((await fandom(id)).founderUid).toBe(OTHER);
});
it('탄생 뒤 창립자 탈퇴는 탄생 권한을 건드리지 않는다', async () => {
const id = (await call(fnNebulaCreate, { name: 'Founder Born' })).fandomId as string;
await call(fnFandomJoin, { fandomId: id }, OTHER);
await db().doc(`fandoms/${id}`).update({ stage: 1 });
expect((await call(fnFandomLeave, { fandomId: id })).left).toBe(true);
const other = (await call(fnNebulaCreate, { name: 'Founder Go' }, OTHER)).fandomId as string;
await setPolicy({ nebulaFounderLeaveBlocked: false });
expect((await call(fnFandomLeave, { fandomId: other }, OTHER)).left).toBe(true);
expect(await call(fnFandomLeave, { fandomId: id })).not.toHaveProperty('founderUid');
expect((await fandom(id)).founderUid).toBe(UID);
expect((await db().collection(`users/${OTHER}/notifications`).get()).empty).toBe(true);
});
/** A(대표) · B · C 에 가입 — 가입 시각은 B < C 로 못박는다. */