merge: 최신 main을 전투 통계 행 정렬에 통합한다

This commit is contained in:
2026-08-21 16:46:38 +00:00
8 changed files with 62 additions and 20 deletions
+3 -3
View File
@@ -349,7 +349,7 @@ describeDb('scenario database seed', () => {
databaseUrl,
now: new Date('2030-01-01T00:00:00Z'),
installOptions: {
turnTermMinutes: 60,
turnTermMinutes: 3,
sync: false,
fiction: 1,
extend: false,
@@ -383,7 +383,7 @@ describeDb('scenario database seed', () => {
if (!worldState) {
return;
}
expect(worldState.tickSeconds).toBe(3600);
expect(worldState.tickSeconds).toBe(180);
expect(worldState.currentMonth).toBe(1);
const config = (worldState.config ?? {}) as Record<string, unknown>;
@@ -395,7 +395,7 @@ describeDb('scenario database seed', () => {
expect(meta.develcost).toBe(
(worldState.currentYear - (scenario.startYear ?? worldState.currentYear) + 10) * 2
);
expect(meta.killturn).toBe(80);
expect(meta.killturn).toBe(1600);
const autorun = (meta.autorun_user ?? {}) as Record<string, unknown>;
const autorunOptions = (autorun.options ?? {}) as Record<string, unknown>;
expect(autorunOptions.develop).toBe(true);
+5 -4
View File
@@ -49,7 +49,6 @@ const zServerAction = z.enum([
'SHUTDOWN',
]);
const TURN_TERM_MINUTES = [1, 2, 5, 10, 20, 30, 60, 120] as const;
const AUTORUN_USER_OPTIONS = ['develop', 'warp', 'recruit', 'recruit_high', 'train', 'battle', 'chief'] as const;
const ADMIN_ROLE_PREFIX = 'admin.';
@@ -430,7 +429,9 @@ const zInstallAutorun = z.object({
options: z.array(z.enum(AUTORUN_USER_OPTIONS)),
});
const isAllowedTurnTerm = (value: number): boolean => TURN_TERM_MINUTES.some((term) => term === value);
// Ref aligns a 12-month game year inside a 24-hour wall-clock day. That makes
// every positive divisor of 120 valid, including 3 and 8 minutes.
const isAllowedTurnTerm = (value: number): boolean => Number.isInteger(value) && value > 0 && 120 % value === 0;
const zRuntimeSettings = z
.object({
@@ -438,7 +439,7 @@ const zRuntimeSettings = z
.number()
.int()
.refine((value) => isAllowedTurnTerm(value), {
message: 'turnTermMinutes must divide 120.',
message: '턴 시간은 120의 약수여야 합니다.',
})
.optional(),
blockGeneralCreate: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(),
@@ -464,7 +465,7 @@ const zInstallOptions = z.object({
.number()
.int()
.refine((value) => isAllowedTurnTerm(value), {
message: 'turnTermMinutes must divide 120.',
message: '턴 시간은 120의 약수여야 합니다.',
}),
sync: z.boolean(),
fiction: z.number().int().min(0).max(1),
+39 -1
View File
@@ -825,7 +825,7 @@ describe('admin operation API', () => {
{ adminRoles: ['admin.profiles.settings:che:2'], firstUserIsAdmin: false }
);
const resetDefaults = {
turnTermMinutes: 10,
turnTermMinutes: 3,
sync: true,
fiction: 1 as const,
extend: true,
@@ -853,6 +853,44 @@ describe('admin operation API', () => {
).rejects.toBeDefined();
});
it('queues a three-minute reset because it is a valid divisor of 120', async () => {
const harness = await buildCaller(async (input) => ({
id: '66666666-6666-4666-8666-666666666666',
profileName: input.profileName,
type: 'RESET',
status: 'QUEUED',
sourceMode: input.sourceMode,
sourceRef: input.sourceRef,
payload: input.payload ?? {},
requestedBy: input.requestedBy,
createdAt: '2026-08-21T00:00:00.000Z',
updatedAt: '2026-08-21T00:00:00.000Z',
}));
await harness.caller.admin.operations.requestReset({
profileName: 'che:2',
sourceMode: 'COMMIT',
sourceRef: 'HEAD',
install: {
scenarioId: 1010,
turnTermMinutes: 3,
sync: true,
fiction: 1,
extend: true,
blockGeneralCreate: 0,
npcMode: 0,
showImgLevel: 3,
tournamentTrig: true,
joinMode: 'full',
},
});
expect(harness.createdInputs[0]).toMatchObject({
type: 'RESET',
payload: { install: { turnTermMinutes: 3 } },
});
});
it('stores event season zero as the next season number', async () => {
const harness = await buildCaller(
async () => {
@@ -983,7 +983,7 @@ test('loads server metadata defaults into the reset form and submits them', asyn
runtimeRunning: true,
requestBodies: [],
resetDefaults: {
turnTermMinutes: 20,
turnTermMinutes: 3,
sync: false,
fiction: 0,
extend: false,
@@ -999,7 +999,7 @@ test('loads server metadata defaults into the reset form and submits them', asyn
page.on('dialog', (dialog) => dialog.accept());
await page.goto('admin/servers/che%3Adefault/scenario');
await expect(page.getByTestId('reset-turn-term')).toHaveValue('20');
await expect(page.getByTestId('reset-turn-term')).toHaveValue('3');
await page.getByText('고급 시나리오 옵션').click();
await expect(page.getByTestId('reset-defaults-source')).toContainText('서버의 메타');
await expect(page.getByTestId('reset-npc-mode')).toHaveValue('1');
@@ -1013,7 +1013,7 @@ test('loads server metadata defaults into the reset form and submits them', asyn
const request = JSON.stringify(
state.requestBodies.find((entry) => entry.operation === 'admin.operations.requestReset')?.body
);
expect(request).toContain('"turnTermMinutes":20');
expect(request).toContain('"turnTermMinutes":3');
expect(request).toContain('"npcMode":1');
expect(request).toContain('"joinMode":"onlyRandom"');
expect(request).toContain('"limitMinutes":720');
@@ -1125,7 +1125,7 @@ test('edits server reset defaults through profile metadata settings', async ({ p
await page.goto('admin/servers/che%3Adefault');
await page.getByText('서버 리셋 기본 옵션').click();
await page.getByTestId('meta-reset-turn-term').selectOption('10');
await page.getByTestId('meta-reset-turn-term').selectOption('3');
await page.getByTestId('meta-reset-npc-mode').selectOption('2');
await page.getByRole('button', { name: '메타 저장' }).click();
@@ -1172,7 +1172,7 @@ test('edits server reset defaults through profile metadata settings', async ({ p
state.requestBodies.find((entry) => entry.operation === 'admin.profiles.updateMeta')?.body
);
expect(request).toContain('"resetDefaults"');
expect(request).toContain('"turnTermMinutes":10');
expect(request).toContain('"turnTermMinutes":3');
expect(request).toContain('"npcMode":2');
});
@@ -1,4 +1,6 @@
export const PROFILE_TURN_TERM_MINUTES = [1, 2, 5, 10, 20, 30, 60, 120] as const;
// A 12-month game year must fit an integer number of times in one wall-clock
// day, so every positive divisor of 120 is valid.
export const PROFILE_TURN_TERM_MINUTES = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 24, 30, 40, 60, 120] as const;
export const RESET_AUTORUN_OPTIONS = [
'develop',
'warp',
@@ -24,7 +26,7 @@ export const RESET_AUTORUN_LABELS: ReadonlyArray<{ value: ResetAutorunOption; la
export const RESET_OPTION_COPY = {
turnTerm: {
label: '턴 시간(분)',
help: '한 턴이 진행되는 실제 시간입니다. ref 초기화 화면과 같은 분 단위 값입니다.',
help: '한 턴이 진행되는 실제 시간입니다. 하루 경계를 정수 게임 연도로 유지하기 위해 120의 약수만 사용할 수 있습니다.',
},
sync: {
label: '시간 동기화',
@@ -8,6 +8,7 @@ import { useToast } from '../composables/useToast';
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
import {
normalizeProfileResetDefaults,
PROFILE_TURN_TERM_MINUTES,
RESET_AUTORUN_LABELS,
RESET_OPTION_COPY,
SYSTEM_PROFILE_RESET_DEFAULTS,
@@ -1078,11 +1079,7 @@ onBeforeUnmount(() => {
class="w-full rounded border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-white"
data-testid="reset-turn-term"
>
<option
v-for="minutes in [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 24, 30, 40, 60, 120]"
:key="minutes"
:value="minutes"
>
<option v-for="minutes in PROFILE_TURN_TERM_MINUTES" :key="minutes" :value="minutes">
{{ minutes }}
</option>
</select>
+2
View File
@@ -71,6 +71,8 @@ Gateway 관리자 콘솔은 `/gateway/admin`에서 시작합니다. 공개 로
턴 간격, 동기화, 가상 장수, 연장, 가입 방식, 장수 생성 제한, NPC, 이미지,
토너먼트와 유저 자동턴 기본값을 저장합니다. 시나리오 초기화 화면은 대상
서버를 URL에서 결정한 뒤 이 메타를 별도 권한 검사로 읽어 폼에 적용합니다.
턴 간격은 하루를 `12개월 × 턴 간격`의 정수 개 게임 연도로 나눌 수 있는
120의 양의 약수만 허용하므로 3분과 8분도 유효합니다.
유저 자동턴은 내정, 이동, 징병, 고급 징병, 훈련, 전투, 중신의 7개 동작을
각각 보존해 RESET payload로 전달합니다.
메타가 없거나 유효하지 않으면 기존 시스템 기본값을 사용합니다. 시나리오와
+2
View File
@@ -175,6 +175,8 @@ migration과 scenario seed부터 진행합니다. 상위 권한자가 버전 업
장수·연장·토너먼트 사용, 기본 NPC, 이미지 단계 3, 전체 가입과 유저 자동턴
미사용이라는 기존 시스템 기본값으로 돌아갑니다. 실행마다 달라지는 scenario와
예약·가오픈·정식 오픈 시각은 자동 입력하지 않습니다.
턴 간격은 120의 양의 약수만 허용하며 3분과 8분도 RESET·저장 기본값·실행 중
변경에서 동일하게 사용할 수 있습니다.
이 모드는 build와 migration 후 기존 season/tick metadata를 읽고 scenario seeder를 실행합니다.
빈 profile schema도 migration을 먼저 적용하므로 최초 `world_state` 조회가