feat: 명예의 전당에 유산 획득량을 추가

명장 일람과 같은 inherit_earned 기록을 은퇴와 통일 정산에서 Hall에 보존하고 기수별 순위로 표시한다. API, 저장 경로와 실제 Chromium 회귀 검사를 함께 보강한다.
This commit is contained in:
2026-08-23 15:10:10 +00:00
parent 440de3843c
commit 8c26a30977
7 changed files with 119 additions and 4 deletions
+1
View File
@@ -476,6 +476,7 @@ export const rankingRouter = router({
{ key: 'betwin', title: '베 팅 당 첨', type: 'int' }, { key: 'betwin', title: '베 팅 당 첨', type: 'int' },
{ key: 'betwingold', title: '베 팅 수 익 금', type: 'int' }, { key: 'betwingold', title: '베 팅 수 익 금', type: 'int' },
{ key: 'betrate', title: '베 팅 수 익 률', type: 'percent' }, { key: 'betrate', title: '베 팅 수 익 률', type: 'percent' },
{ key: 'inherit_earned', title: '유 산 획 득 량', type: 'int' },
]; ];
const allowedTypes = new Set(HALL_OF_FAME_TYPES); const allowedTypes = new Set(HALL_OF_FAME_TYPES);
+15 -2
View File
@@ -205,11 +205,11 @@ const buildContext = (options?: {
}, },
hallOfFame: { hallOfFame: {
findMany: async (args: { where: { type: string } }) => findMany: async (args: { where: { type: string } }) =>
args.where.type === 'experience' args.where.type === 'experience' || args.where.type === 'inherit_earned'
? [ ? [
{ {
generalNo: 1, generalNo: 1,
value: 1200, value: args.where.type === 'experience' ? 1200 : 4321,
aux: { aux: {
name: '유비', name: '유비',
ownerName: 'private-hall-user-id', ownerName: 'private-hall-user-id',
@@ -450,4 +450,17 @@ describe('ranking hall of fame', () => {
.ranking.getHallOfFame({ season: 3 }); .ranking.getHallOfFame({ season: 3 });
expect(redacted.sections[0]?.entries[0]?.ownerName).toBeNull(); 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' })],
});
});
}); });
@@ -251,6 +251,9 @@ integration('general turn lifecycle persistence', () => {
await db.rankData.create({ await db.rankData.create({
data: { generalId: general.id, nationId: 0, type: 'warnum', value: 10 }, 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) => await db.$transaction((tx) =>
persistGeneralLifecycleEvents( persistGeneralLifecycleEvents(
@@ -278,6 +281,17 @@ integration('general turn lifecycle persistence', () => {
}, },
}) })
).toMatchObject({ value: 1_000 }); ).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( expect(
await db.inheritancePoint.findUnique({ await db.inheritancePoint.findUnique({
where: { userId_key: { userId: general.userId!, key: 'previous' } }, where: { userId_key: { userId: general.userId!, key: 'previous' } },
@@ -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 },
});
});
}); });
@@ -214,6 +214,7 @@ describe('persistUnificationFinalization', () => {
{ generalId: 1, type: 'firenum', value: 4 }, { generalId: 1, type: 'firenum', value: 4 },
{ generalId: 1, type: 'killcrew', value: 1_200 }, { generalId: 1, type: 'killcrew', value: 1_200 },
{ generalId: 1, type: 'deathcrew', value: 800 }, { generalId: 1, type: 'deathcrew', value: 800 },
{ generalId: 1, type: 'inherit_earned', value: 4_321 },
{ generalId: 1, type: 'ttw', value: 3 }, { generalId: 1, type: 'ttw', value: 3 },
{ generalId: 1, type: 'ttd', value: 2 }, { generalId: 1, type: 'ttd', value: 2 },
{ generalId: 1, type: 'ttl', value: 1 }, { 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(gameHistoryUpdate).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ winnerNation: 1 }) }) expect.objectContaining({ data: expect.objectContaining({ winnerNation: 1 }) })
); );
@@ -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.setViewportSize({ width: 1000, height: 800 });
await page.goto('hall-of-fame'); 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 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('시나리오 검색')).toContainText('이전 시나리오');
await expect(page.getByLabel('시나리오 검색')).not.toContainText('HWE /'); await expect(page.getByLabel('시나리오 검색')).not.toContainText('HWE /');
expect(state.hallRequests.some((request) => request.includes('legacy'))).toBe(true); expect(state.hallRequests.some((request) => request.includes('legacy'))).toBe(true);
+1
View File
@@ -137,6 +137,7 @@ export const HALL_OF_FAME_TYPES = [
'betwin', 'betwin',
'betwingold', 'betwingold',
'betrate', 'betrate',
'inherit_earned',
] as const; ] as const;
export type HallOfFameType = (typeof HALL_OF_FAME_TYPES)[number]; export type HallOfFameType = (typeof HALL_OF_FAME_TYPES)[number];