fix(game-api): 시나리오 아이템 표시 순서 통일

JSONB에 저장된 품목과 수량을 유지하며 원본 시나리오 순서로 구매·명장일람·유산·전투 시뮬레이터 선택지를 구성한다.
This commit is contained in:
2026-09-23 01:32:35 +00:00
parent 908ae72cba
commit f5264352f9
10 changed files with 222 additions and 44 deletions
+33
View File
@@ -353,6 +353,39 @@ describe('battle router orchestration', () => {
expect(worldStateReads).toBe(1);
});
it('offers simulator equipment and consumables in source order for the selected scenario', async () => {
const state: WorldStateRow = {
id: 1,
scenarioCode: '905',
currentYear: 200,
currentMonth: 1,
tickSeconds: 600,
config: {
const: {
allItems: {
item: { che_필살_둔갑천서: 1, che_의술_정력견혈산: 1, che_치료_환약: 0 },
horse: { che_명마_15_적토마: 1, che_명마_07_백마: 1, che_명마_01_노기: 0 },
},
},
},
meta: {},
updatedAt: new Date('2026-01-01T00:00:00Z'),
};
const caller = appRouter.createCaller(buildContext({ state, battleSim: new QueuedBattleSimTransport() }));
const context = await caller.battle.getSimulatorContext();
expect(context.items.horse.map(({ key }) => key)).toEqual([
'che_명마_01_노기',
'che_명마_07_백마',
'che_명마_15_적토마',
]);
expect(context.items.item.map(({ key }) => key)).toEqual([
'che_치료_환약',
'che_의술_정력견혈산',
'che_필살_둔갑천서',
]);
});
it('rejects the neutral storage nation type before preparing or queuing a simulation', async () => {
const battleSim = new QueuedBattleSimTransport();
const state: WorldStateRow = {
+23 -1
View File
@@ -110,6 +110,7 @@ const buildContext = (options: {
rankRows?: Array<{ type: string; value: number }>;
inheritanceLogs?: Array<{ id: number; year: number; month: number; text: string; createdAt: Date }>;
configConst?: Record<string, unknown>;
scenarioCode?: string;
configMap?: Record<string, unknown>;
daemonResult?: TurnDaemonCommandResult;
requestId?: string;
@@ -141,9 +142,10 @@ const buildContext = (options: {
const webPushOutboxCreateMany = vi.fn(async () => ({ count: 1 }));
const activeWorldState =
options.configConst === undefined && options.configMap === undefined
? worldState
? { ...worldState, scenarioCode: options.scenarioCode ?? worldState.scenarioCode }
: {
...worldState,
scenarioCode: options.scenarioCode ?? worldState.scenarioCode,
config: {
...worldState.config,
...(options.configConst === undefined ? {} : { const: options.configConst }),
@@ -435,6 +437,26 @@ describe('inherit router actor and permission boundaries', () => {
]);
});
it('restores source order for unique candidates when a scenario config has JSONB key order', async () => {
const fixture = buildContext({
scenarioCode: '905',
configConst: {
allItems: {
item: { che_필살_둔갑천서: 1, che_의술_정력견혈산: 1 },
horse: { che_명마_15_적토마: 1, che_명마_07_백마: 1 },
},
},
});
const status = await appRouter.createCaller(fixture.context).inherit.getStatus();
expect(status.availableUnique.map(({ key }) => key)).toEqual([
'che_명마_07_백마',
'che_명마_15_적토마',
'che_의술_정력견혈산',
'che_필살_둔갑천서',
]);
});
it('loads the first inheritance-log page without an out-of-range integer cursor', async () => {
const createdAt = new Date('2026-07-26T00:00:00Z');
const fixture = buildContext({
+25 -1
View File
@@ -107,6 +107,8 @@ const buildContext = (options?: {
includeOwnerDisplayName?: boolean;
profileId?: string;
generals?: RankingGeneralRow[];
scenarioCode?: string;
allItems?: Record<string, Record<string, number>>;
rankRows?: Array<{ generalId: number; type: string; value: number }>;
gameHistoryFindMany?: (args: unknown) => Promise<Array<{ season: number; scenario: number; scenarioName: string }>>;
}): GameApiContext => {
@@ -157,10 +159,11 @@ const buildContext = (options?: {
},
worldState: {
findFirst: async () => ({
scenarioCode: options?.scenarioCode ?? 'default',
meta: { isUnited: options?.isUnited ? 1 : 0 },
config: {
const: {
allItems: {
allItems: options?.allItems ?? {
horse: { che_명마_15_적토마: 2 },
weapon: {},
book: {},
@@ -287,6 +290,27 @@ describe('ranking.getBestGeneral', () => {
expect(JSON.stringify(result)).not.toContain('private-user-id');
});
it('uses the source scenario order for unique equipment and item cards after JSONB reorders their keys', async () => {
const result = await appRouter
.createCaller(
buildContext({
scenarioCode: '905',
allItems: {
horse: { che_명마_07_백마: 1, che_명마_15_적토마: 1 },
item: { che_의술_정력견혈산: 1, che_필살_둔갑천서: 1 },
},
})
)
.ranking.getBestGeneral({ view: 'user' });
expect(
result.uniqueItems.find((section) => section.slot === 'horse')?.entries.map((entry) => entry.itemKey)
).toEqual(['che_명마_15_적토마', 'che_명마_07_백마']);
expect(
result.uniqueItems.find((section) => section.slot === 'item')?.entries.map((entry) => entry.itemKey)
).toEqual(['che_필살_둔갑천서', 'che_의술_정력견혈산']);
});
it('separates autonomous NPCs from users and possessed generals', async () => {
const result = await appRouter.createCaller(buildContext()).ranking.getBestGeneral({ view: 'npc' });
expect(result.sections[0]?.entries.map((entry) => entry.id)).toEqual([3]);