플레이 감사를 3단 대시보드와 국가 추이 그래프로 개선한다

This commit is contained in:
2026-09-26 04:36:31 +00:00
parent 2b2edeaeff
commit e35679335c
12 changed files with 1040 additions and 321 deletions
@@ -1,12 +1,15 @@
import { describe, expect, it } from 'vitest';
import {
summarizeNationPeriod,
projectCurrentSettlement,
zAuditNation,
type AuditNationData,
type NationMonthPoint,
} from '../src/router/playAudit/nationSeries.js';
const population = {
count: 0,
crew: null,
gold: 0,
rice: 0,
dex: { dex1: 0, dex2: 0, dex3: 0, dex4: 0, dex5: 0 },
@@ -89,3 +92,44 @@ describe('play audit half-year summary', () => {
});
});
});
describe('current committed settlement', () => {
it.each([
[1, 'gold'],
[7, 'rice'],
] as const)('exposes month %s settlement before month-end', (month, resource) => {
const meta = {
playAuditFlows: {
year: 200,
month,
complete: true,
entries: { [`2:${resource}`]: { nationId: 2, resource, income: 1234.5, paid: 234 } },
},
};
expect(projectCurrentSettlement(meta, 200, month, 2)[resource]).toEqual({ income: 1234.5, paid: 234 });
expect(projectCurrentSettlement(meta, 200, month + 1, 2)[resource]).toBeNull();
expect(projectCurrentSettlement(meta, 200, month, 3)[resource]).toBeNull();
});
it('keeps absent and malformed observations unknown, preserves actual zero', () => {
expect(projectCurrentSettlement({}, 200, 1, 2).gold).toBeNull();
const meta = {
playAuditFlows: {
year: 200,
month: 1,
complete: false,
entries: { '2:gold': { nationId: 2, resource: 'gold', income: 0, paid: 0 } },
},
};
expect(projectCurrentSettlement(meta, 200, 1, 2)).toMatchObject({
gold: { income: 0, paid: 0 },
complete: false,
});
meta.playAuditFlows.entries['2:gold'].income = NaN;
expect(projectCurrentSettlement(meta, 200, 1, 2).gold).toBeNull();
});
it('reads old snapshots without inventing troop counts', () => {
const old = JSON.parse(JSON.stringify(nation(1)));
delete old.populations.human.crew;
expect(zAuditNation.parse(old).populations.human.crew).toBeNull();
});
});
@@ -3119,6 +3119,39 @@ integration('game API security over HTTP transport', () => {
},
},
});
await db.playAuditNation.update({
where: { sampleId_nationId: { sampleId: `${seasonId}:190:6`, nationId: ownerNationId } },
data: {
data: {
...asRecord(finalNation.data),
populations: {
human: { ...population, count: 2 },
npc: { ...population, count: 1 },
troopNpc: population,
},
},
},
});
await db.playAuditGeneral.createMany({
data: [1, 2].map((id) => ({
sampleId: `${seasonId}:190:6`,
generalId: id,
nationId: ownerNationId,
cityId: 0,
npcState: 0,
data: { crew: id * 1200 },
})),
});
await db.playAuditGeneral.create({
data: {
sampleId: `${seasonId}:190:6`,
generalId: 3,
nationId: ownerNationId,
cityId: 0,
npcState: 2,
data: { crew: 'invalid' },
},
});
const series = await get('nationSeries', admin, {
nationId: ownerNationId,
from: { year: 190, month: 1 },
@@ -3134,7 +3167,10 @@ integration('game API security over HTTP transport', () => {
year: 190,
month: 1,
complete: true,
stock: { gold: 600 },
stock: {
gold: 600,
populations: { human: { crew: 3600 }, npc: { crew: null }, troopNpc: { crew: 0 } },
},
flows: { incomeGold: 21, incomeRice: 0 },
},
],
@@ -3166,6 +3202,59 @@ integration('game API security over HTTP transport', () => {
result: { data: { items: [{ complete: false, stock: null, flows: { incomeGold: null } }] } },
});
// 정산 commit 직후, 아직 월말 표본이 없는 상태를 HTTP로 확인한다.
for (const [settlementMonth, resource] of [
[1, 'gold'],
[7, 'rice'],
] as const) {
await db.worldState.update({
where: { id: fixtureWorldId },
data: {
currentYear: 191,
currentMonth: settlementMonth,
meta: {
serverId: seasonId,
scenarioMeta: { startYear: 190 },
playAuditFlows: {
year: 191,
month: settlementMonth,
complete: true,
entries: {
[`${ownerNationId}:${resource}`]: {
nationId: ownerNationId,
resource,
income: 8765.5,
paid: 4321,
},
},
},
},
},
});
const input = {
nationId: ownerNationId,
from: { year: 191, month: settlementMonth },
to: { year: 191, month: settlementMonth },
};
expect((await get('nationSeries', admin, input)).body).toMatchObject({
result: {
data: {
currentSettlement: {
year: 191,
month: settlementMonth,
[resource]: { income: 8765.5, paid: 4321 },
},
items: [{ stock: null, complete: false }],
},
},
});
expect((await get('nationSeries', undefined, input)).status).toBe(401);
expect((await get('nationSeries', await token(['admin']), input)).status).toBe(403);
expect(
(await get('nationSeries', await token(['admin.playAudit.read:other:default']), input)).status
).toBe(403);
}
// A synchronized opening may start before the scenario's gameplay year.
await db.worldState.update({
where: { id: fixtureWorldId },