merge: 예약 명령 인자 요약을 사령부 호환에 통합

# Conflicts:
#	app/game-frontend/src/components/command/ReservedCommandEditor.vue
This commit is contained in:
2026-08-17 10:51:18 +00:00
10 changed files with 566 additions and 30 deletions
+12 -10
View File
@@ -165,20 +165,22 @@ const loadGeneralTurns = async (db: DatabaseClient, generalId: number): Promise<
return buildTurnListFromRows(rows, MAX_GENERAL_TURNS);
};
const loadGeneralAutorunLimit = async (db: DatabaseClient, generalId: number): Promise<number | null> => {
const general = await db.general.findUnique({ where: { id: generalId }, select: { meta: true } });
const rawAutorunLimit = isRecord(general?.meta) ? general.meta.autorun_limit : undefined;
return typeof rawAutorunLimit === 'number' && Number.isFinite(rawAutorunLimit) ? Math.trunc(rawAutorunLimit) : null;
};
export const getGeneralTurnSnapshot = async (db: DatabaseClient, generalId: number): Promise<ReservedTurnSnapshot> => {
const [turns, revisionRow, general] = await Promise.all([
const [turns, revisionRow, autorunLimit] = await Promise.all([
loadGeneralTurns(db, generalId),
db.generalTurnRevision.findUnique({ where: { generalId } }),
db.general.findUnique({ where: { id: generalId }, select: { meta: true } }),
loadGeneralAutorunLimit(db, generalId),
]);
const rawAutorunLimit = isRecord(general?.meta) ? general.meta.autorun_limit : undefined;
return {
revision: revisionRow?.revision ?? 0,
turns: serializeTurnList(turns),
autorunLimit:
typeof rawAutorunLimit === 'number' && Number.isFinite(rawAutorunLimit)
? Math.trunc(rawAutorunLimit)
: null,
autorunLimit,
};
};
@@ -331,7 +333,7 @@ export const setGeneralTurns = async (
}
}
await persistGeneralTurns(db, generalId, turns);
return { revision, turns: serializeTurnList(turns) };
return { revision, turns: serializeTurnList(turns), autorunLimit: await loadGeneralAutorunLimit(db, generalId) };
};
export const setGeneralTurn = async (
@@ -357,7 +359,7 @@ export const shiftGeneralTurns = async (
const turns = await loadGeneralTurns(db, generalId);
const shifted = applyShift(turns, amount);
await persistGeneralTurns(db, generalId, shifted);
return { revision, turns: serializeTurnList(shifted) };
return { revision, turns: serializeTurnList(shifted), autorunLimit: await loadGeneralAutorunLimit(db, generalId) };
};
export const repeatGeneralTurns = async (
@@ -372,7 +374,7 @@ export const repeatGeneralTurns = async (
const revision = await claimGeneralRevision(db, generalId, expectedRevision);
const turns = applyRepeat(await loadGeneralTurns(db, generalId), amount);
await persistGeneralTurns(db, generalId, turns);
return { revision, turns: serializeTurnList(turns) };
return { revision, turns: serializeTurnList(turns), autorunLimit: await loadGeneralAutorunLimit(db, generalId) };
};
export const setNationTurns = async (
+8 -4
View File
@@ -18,7 +18,7 @@ import {
ReservedTurnRevisionConflictError,
} from '../src/turns/reservedTurns.js';
const buildDb = () => {
const buildDb = (autorunLimit: number | null = null) => {
const generalTurns = new Map<number, GeneralTurnRow[]>();
const nationTurns = new Map<string, NationTurnRow[]>();
const generalRevisions = new Map<number, number>();
@@ -47,7 +47,7 @@ const buildDb = () => {
findFirst: async () => null,
},
general: {
findUnique: async () => null,
findUnique: async () => ({ meta: autorunLimit === null ? {} : { autorun_limit: autorunLimit } }),
},
city: {
findUnique: async () => null,
@@ -202,22 +202,25 @@ const buildDb = () => {
describe('reservedTurns', () => {
it('sets and shifts general turns', async () => {
const { db } = buildDb();
const { db } = buildDb(2408);
const initial = await setGeneralTurn(db, 1, 0, 'che_화계', { destCityId: 10 }, 0);
expect(initial.revision).toBe(1);
expect(initial.turns).toHaveLength(MAX_GENERAL_TURNS);
expect(initial.turns[0]?.action).toBe('che_화계');
expect(initial.autorunLimit).toBe(2408);
const pushed = await shiftGeneralTurns(db, 1, 1, initial.revision);
expect(pushed.revision).toBe(2);
expect(pushed.turns[0]?.action).toBe('휴식');
expect(pushed.turns[1]?.action).toBe('che_화계');
expect(pushed.autorunLimit).toBe(2408);
const pulled = await shiftGeneralTurns(db, 1, -1, pushed.revision);
expect(pulled.turns[0]?.action).toBe('che_화계');
expect(pulled.turns[MAX_GENERAL_TURNS - 1]?.action).toBe('휴식');
expect(pulled.autorunLimit).toBe(2408);
await expect(setGeneralTurn(db, 1, 2, 'che_훈련', {}, 1)).rejects.toBeInstanceOf(
ReservedTurnRevisionConflictError
@@ -287,7 +290,7 @@ describe('reservedTurns', () => {
});
it('repeats the leading general pattern at the legacy interval', async () => {
const { db } = buildDb();
const { db } = buildDb(2408);
const seeded = await setGeneralTurns(
db,
4,
@@ -311,6 +314,7 @@ describe('reservedTurns', () => {
'che_사기진작',
'che_징병',
]);
expect(repeated.autorunLimit).toBe(2408);
});
it('supports nation bulk/repeat and preserves the legacy amount-12 no-op', async () => {
+2 -1
View File
@@ -869,7 +869,7 @@ describe('appRouter', () => {
});
it('validates and persists general command arguments from the authenticated owner', async () => {
const general = buildGeneralRow({ id: 13 });
const general = buildGeneralRow({ id: 13, meta: { autorun_limit: 2408 } });
const writes: unknown[] = [];
const changeJournal = new ChangeJournal();
const caller = appRouter.createCaller(
@@ -885,6 +885,7 @@ describe('appRouter', () => {
});
expect(response.turns[0]).toMatchObject({ action: 'che_화계', args: { destCityId: 7 } });
expect(response.autorunLimit).toBe(2408);
expect(writes).toHaveLength(1);
const written = writes[0] as { data: unknown[] };
expect(written.data).toHaveLength(30);