From 8c26a30977462e43a0db858236bdc78806bbfebf Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 23 Aug 2026 15:10:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AA=85=EC=98=88=EC=9D=98=20=EC=A0=84?= =?UTF-8?q?=EB=8B=B9=EC=97=90=20=EC=9C=A0=EC=82=B0=20=ED=9A=8D=EB=93=9D?= =?UTF-8?q?=EB=9F=89=EC=9D=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 명장 일람과 같은 inherit_earned 기록을 은퇴와 통일 정산에서 Hall에 보존하고 기수별 순위로 표시한다. API, 저장 경로와 실제 Chromium 회귀 검사를 함께 보강한다. --- app/game-api/src/router/ranking/index.ts | 1 + app/game-api/test/rankingRouter.test.ts | 17 +++++- ...rnLifecyclePersistence.integration.test.ts | 14 +++++ .../generalTurnLifecyclePersistence.test.ts | 58 +++++++++++++++++++ .../test/unificationPersistence.test.ts | 6 ++ .../e2e/legacyArchiveViews.spec.ts | 26 ++++++++- packages/common/src/ranking/types.ts | 1 + 7 files changed, 119 insertions(+), 4 deletions(-) diff --git a/app/game-api/src/router/ranking/index.ts b/app/game-api/src/router/ranking/index.ts index e550c294..755449f4 100644 --- a/app/game-api/src/router/ranking/index.ts +++ b/app/game-api/src/router/ranking/index.ts @@ -476,6 +476,7 @@ export const rankingRouter = router({ { key: 'betwin', title: '베 팅 당 첨', type: 'int' }, { key: 'betwingold', title: '베 팅 수 익 금', type: 'int' }, { key: 'betrate', title: '베 팅 수 익 률', type: 'percent' }, + { key: 'inherit_earned', title: '유 산 획 득 량', type: 'int' }, ]; const allowedTypes = new Set(HALL_OF_FAME_TYPES); diff --git a/app/game-api/test/rankingRouter.test.ts b/app/game-api/test/rankingRouter.test.ts index 0b9431f9..6ad3b566 100644 --- a/app/game-api/test/rankingRouter.test.ts +++ b/app/game-api/test/rankingRouter.test.ts @@ -205,11 +205,11 @@ const buildContext = (options?: { }, hallOfFame: { findMany: async (args: { where: { type: string } }) => - args.where.type === 'experience' + args.where.type === 'experience' || args.where.type === 'inherit_earned' ? [ { generalNo: 1, - value: 1200, + value: args.where.type === 'experience' ? 1200 : 4321, aux: { name: '유비', ownerName: 'private-hall-user-id', @@ -450,4 +450,17 @@ describe('ranking hall of fame', () => { .ranking.getHallOfFame({ season: 3 }); expect(redacted.sections[0]?.entries[0]?.ownerName).toBeNull(); }); + + it('returns the inheritance earned ranking for each completed season', async () => { + const result = await appRouter + .createCaller(buildContext({ authenticated: false })) + .ranking.getHallOfFame({ season: 3 }); + + expect(result.sections).toHaveLength(25); + expect(result.sections.at(-1)).toMatchObject({ + title: '유 산 획 득 량', + valueType: 'int', + entries: [expect.objectContaining({ generalId: 1, value: 4321, printValue: '4,321' })], + }); + }); }); diff --git a/app/game-engine/test/generalTurnLifecyclePersistence.integration.test.ts b/app/game-engine/test/generalTurnLifecyclePersistence.integration.test.ts index e9cf54e3..26e3fe01 100644 --- a/app/game-engine/test/generalTurnLifecyclePersistence.integration.test.ts +++ b/app/game-engine/test/generalTurnLifecyclePersistence.integration.test.ts @@ -251,6 +251,9 @@ integration('general turn lifecycle persistence', () => { await db.rankData.create({ data: { generalId: general.id, nationId: 0, type: 'warnum', value: 10 }, }); + await db.rankData.create({ + data: { generalId: general.id, nationId: 0, type: 'inherit_earned', value: 4_321 }, + }); await db.$transaction((tx) => persistGeneralLifecycleEvents( @@ -278,6 +281,17 @@ integration('general turn lifecycle persistence', () => { }, }) ).toMatchObject({ value: 1_000 }); + expect( + await db.hallOfFame.findUnique({ + where: { + serverId_type_generalNo: { + serverId, + type: 'inherit_earned', + generalNo: general.id, + }, + }, + }) + ).toMatchObject({ value: 4_321 }); expect( await db.inheritancePoint.findUnique({ where: { userId_key: { userId: general.userId!, key: 'previous' } }, diff --git a/app/game-engine/test/generalTurnLifecyclePersistence.test.ts b/app/game-engine/test/generalTurnLifecyclePersistence.test.ts index 0d144dd8..87dda617 100644 --- a/app/game-engine/test/generalTurnLifecyclePersistence.test.ts +++ b/app/game-engine/test/generalTurnLifecyclePersistence.test.ts @@ -103,4 +103,62 @@ describe('general lifecycle archive history', () => { }) ); }); + + it('stores the inheritance earned rank in the hall before a rebirth resets ranks', async () => { + const general = archivedGeneral(); + const hallCreateMany = vi.fn(async () => ({ count: 1 })); + const prisma = { + generalAccessLog: { + updateMany: vi.fn(async () => ({ count: 1 })), + }, + rankData: { + findMany: vi.fn(async () => [{ type: 'inherit_earned', value: 4_321 }]), + updateMany: vi.fn(async () => ({ count: 1 })), + }, + nation: { + findUnique: vi.fn(async () => null), + }, + gameHistory: { + count: vi.fn(async () => 2), + }, + hallOfFame: { + findUnique: vi.fn(async () => null), + createMany: hallCreateMany, + update: vi.fn(async () => undefined), + }, + } as unknown as GamePrisma.TransactionClient; + const event: GeneralLifecycleEvent = { + generalId: general.id, + outcome: 'retired', + before: general, + after: general, + year: 200, + month: 1, + }; + + await persistGeneralLifecycleEvents( + prisma, + [event], + { serverId: 'hall-fixture', season: 4, scenarioId: 22, isUnited: 0 }, + {} + ); + + expect(hallCreateMany).toHaveBeenCalledWith({ + data: [ + expect.objectContaining({ + serverId: 'hall-fixture', + season: 4, + scenario: 22, + generalNo: general.id, + type: 'inherit_earned', + value: 4_321, + }), + ], + skipDuplicates: true, + }); + expect(prisma.rankData.updateMany).toHaveBeenCalledWith({ + where: { generalId: general.id }, + data: { value: 0 }, + }); + }); }); diff --git a/app/game-engine/test/unificationPersistence.test.ts b/app/game-engine/test/unificationPersistence.test.ts index f19e886d..d95a61c1 100644 --- a/app/game-engine/test/unificationPersistence.test.ts +++ b/app/game-engine/test/unificationPersistence.test.ts @@ -214,6 +214,7 @@ describe('persistUnificationFinalization', () => { { generalId: 1, type: 'firenum', value: 4 }, { generalId: 1, type: 'killcrew', value: 1_200 }, { generalId: 1, type: 'deathcrew', value: 800 }, + { generalId: 1, type: 'inherit_earned', value: 4_321 }, { generalId: 1, type: 'ttw', value: 3 }, { generalId: 1, type: 'ttd', value: 2 }, { generalId: 1, type: 'ttl', value: 1 }, @@ -268,6 +269,11 @@ describe('persistUnificationFinalization', () => { }), }) ); + expect(hallCreate).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ type: 'inherit_earned', value: 4_321 }), + }) + ); expect(gameHistoryUpdate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ winnerNation: 1 }) }) ); diff --git a/app/game-frontend/e2e/legacyArchiveViews.spec.ts b/app/game-frontend/e2e/legacyArchiveViews.spec.ts index 12b1b42b..5a0750f8 100644 --- a/app/game-frontend/e2e/legacyArchiveViews.spec.ts +++ b/app/game-frontend/e2e/legacyArchiveViews.spec.ts @@ -60,6 +60,24 @@ const installArchiveViews = async (page: Page) => { }, ], }, + { + title: '유 산 획 득 량', + valueType: 'int', + entries: [ + { + generalId: 1, + name: legacy ? '이전장수' : '현재장수', + ownerName: null, + nationName: legacy ? '이전국' : '현재국', + bgColor: '#330000', + fgColor: '#ffffff', + picture: null, + imageServer: 0, + value: 4321, + printValue: '4,321', + }, + ], + }, ], }); } @@ -201,9 +219,13 @@ test('명예의 전당은 현재 profile의 현재·이전 서버 기록만 조 await page.setViewportSize({ width: 1000, height: 800 }); await page.goto('hall-of-fame'); - await expect(page.getByText('현재장수')).toBeVisible(); + await expect(page.getByText('현재장수')).toHaveCount(2); + await expect(page.getByRole('heading', { name: '유 산 획 득 량' })).toBeVisible(); + await expect(page.getByText('4,321')).toBeVisible(); await page.getByLabel('기록 구분').selectOption('legacy'); - await expect(page.getByText('이전장수')).toBeVisible(); + await expect(page.getByText('이전장수')).toHaveCount(2); + await expect(page.getByRole('heading', { name: '유 산 획 득 량' })).toBeVisible(); + await expect(page.getByText('4,321')).toBeVisible(); await expect(page.getByLabel('시나리오 검색')).toContainText('이전 시나리오'); await expect(page.getByLabel('시나리오 검색')).not.toContainText('HWE /'); expect(state.hallRequests.some((request) => request.includes('legacy'))).toBe(true); diff --git a/packages/common/src/ranking/types.ts b/packages/common/src/ranking/types.ts index 714f823c..44712b6a 100644 --- a/packages/common/src/ranking/types.ts +++ b/packages/common/src/ranking/types.ts @@ -137,6 +137,7 @@ export const HALL_OF_FAME_TYPES = [ 'betwin', 'betwingold', 'betrate', + 'inherit_earned', ] as const; export type HallOfFameType = (typeof HALL_OF_FAME_TYPES)[number];