fix: 플레이 감사 기간을 실제 초기 달력으로 제한

This commit is contained in:
2026-09-16 04:07:56 +00:00
parent 31c5927449
commit 9e14306fe4
8 changed files with 160 additions and 8 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ export const generalLogs = auditProcedure
const world = await readAuditWorld(tx);
if (
input.month &&
(input.month.year < world.startYear ||
(monthOrdinal(input.month.year, input.month.month) < monthOrdinal(world.startYear, world.startMonth) ||
monthOrdinal(input.month.year, input.month.month) > monthOrdinal(world.year, world.month))
) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '현재 기수 안의 로그 월을 선택해 주세요.' });
@@ -112,9 +112,9 @@ export const nationSeries = auditProcedure
const current = monthOrdinal(world.year, world.month);
const from = input.from
? monthOrdinal(input.from.year, input.from.month)
: Math.max(world.startYear * 12, current - 5);
: Math.max(monthOrdinal(world.startYear, world.startMonth), current - 5);
const to = input.to ? monthOrdinal(input.to.year, input.to.month) : current;
if (from < world.startYear * 12 || to > current || from > to) {
if (from < monthOrdinal(world.startYear, world.startMonth) || to > current || from > to) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '현재 기수 안에서 시작·종료 월을 선택해 주세요.' });
}
const width = input.resolution === 'month' ? 1 : 6;
+20 -2
View File
@@ -66,17 +66,35 @@ export const readAuditWorld = async (tx: GamePrisma.TransactionClient) => {
const serverId = typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId : null;
const scenario = asRecord(meta.scenarioMeta);
const config = asRecord(world.config);
const startYear =
const scenarioStartYear =
typeof scenario.startYear === 'number'
? scenario.startYear
: typeof asRecord(config.scenarioMeta).startYear === 'number'
? Number(asRecord(config.scenarioMeta).startYear)
: world.currentYear;
// 동기화 개방은 시나리오 시작 전년도에 시작할 수 있다. 저장된 실제 달력을
// 조회 경계로 쓰며 gameplay 규칙인 scenario.startYear는 변경하지 않는다.
const hasInitialCalendar =
typeof meta.initYear === 'number' &&
Number.isInteger(meta.initYear) &&
meta.initYear >= 0 &&
typeof meta.initMonth === 'number' &&
Number.isInteger(meta.initMonth) &&
meta.initMonth >= 1 &&
meta.initMonth <= 12 &&
monthOrdinal(meta.initYear, meta.initMonth) <= monthOrdinal(world.currentYear, world.currentMonth);
const startYear = hasInitialCalendar
? Number(meta.initYear)
: Number.isInteger(scenarioStartYear) && scenarioStartYear >= 0
? Math.min(scenarioStartYear, world.currentYear)
: world.currentYear;
const startMonth = hasInitialCalendar ? Number(meta.initMonth) : 1;
return {
serverId,
year: world.currentYear,
month: world.currentMonth,
startYear,
startMonth,
tick: world.lastTurnTick?.toString() ?? null,
asOf: new Date().toISOString(),
};
@@ -89,7 +107,7 @@ export const findAuditMonth = async (
at: z.infer<typeof zAuditMonth>
) => {
const ordinal = monthOrdinal(at.year, at.month);
if (at.year < world.startYear || ordinal > monthOrdinal(world.year, world.month)) {
if (ordinal < monthOrdinal(world.startYear, world.startMonth) || ordinal > monthOrdinal(world.year, world.month)) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '현재 기수의 게임 연월을 선택해 주세요.' });
}
if (!world.serverId) return null;
@@ -2570,6 +2570,71 @@ integration('game API security over HTTP transport', () => {
result: { data: { items: [{ complete: false, stock: null, flows: { incomeGold: null } }] } },
});
// A synchronized opening may start before the scenario's gameplay year.
await db.worldState.update({
where: { id: fixtureWorldId },
data: {
currentYear: 189,
currentMonth: 10,
meta: { serverId: seasonId, initYear: 189, initMonth: 10, scenarioMeta: { startYear: 190 } },
},
});
await db.playAuditMonth.create({
data: {
id: `${seasonId}:initial`,
serverId: seasonId,
year: 189,
month: 10,
kind: 'MONTH_END',
settlementsComplete: false,
hash: 'initial-calendar',
},
});
await db.logEntry.create({
data: {
serverId: seasonId,
scope: 'GENERAL',
category: 'HISTORY',
generalId: logGeneralId,
year: 189,
month: 10,
text: `${seasonId}:initial-log`,
},
});
const initial = { year: 189, month: 10 };
for (const [path, input] of [
['nationSnapshot', { nationId: ownerNationId, at: initial }],
['generalDetail', { id: generalId, at: initial }],
['cityDetail', { id: 99123, at: initial }],
] as const) {
const response = await get(path, admin, input);
expect(response.status).toBe(200);
expect(response.body).toMatchObject({
result: { data: { startYear: 189, startMonth: 10, collected: true } },
});
expect((await get(path, admin, { ...input, at: { year: 189, month: 9 } })).status).toBe(400);
}
expect((await get('nationSeries', admin, { nationId: ownerNationId })).body).toMatchObject({
result: { data: { items: [{ from: initial, to: initial }] } },
});
expect(
(await get('nationSeries', admin, { nationId: ownerNationId, from: { year: 189, month: 9 } })).status
).toBe(400);
expect(
(await get('generalLogs', admin, { generalId: logGeneralId, type: 'generalHistory', month: initial }))
.body
).toMatchObject({ result: { data: { items: [{ text: `${seasonId}:initial-log` }] } } });
expect(
(
await get('generalLogs', admin, {
generalId: logGeneralId,
type: 'generalHistory',
month: { year: 189, month: 9 },
})
).status
).toBe(400);
await db.worldState.update({ where: { id: fixtureWorldId }, data: { currentYear: 190, currentMonth: 7 } });
await db.worldState.update({
where: { id: fixtureWorldId },
data: {