fix: 메인 예약 명령 요약과 자율 행동 상태를 복원

예약 명령 mutation 응답에도 autorunLimit을 포함해 입력 직후 색상과 상태가 사라지지 않게 한다. Ref getBrief 계약에 맞춰 명령 인자를 요약하고 자율 행동 종료 연월과 현재 시각을 표시한다.
This commit is contained in:
2026-08-17 10:49:14 +00:00
parent f31f2181bc
commit ba29528cf1
10 changed files with 554 additions and 32 deletions
+88 -10
View File
@@ -43,6 +43,7 @@ const inputOptions = {
cities: [
{ value: 1, label: '업 (아국)' },
{ value: 2, label: '허창 (적국)', description: '적국 · 예주 · 대도시' },
{ value: 3, label: '단양 (오)' },
],
nations: [
{ value: 1, label: '아국', color: '#008000' },
@@ -182,6 +183,27 @@ const commandTable = {
},
],
},
{
category: '군사',
values: [
{
key: 'che_출병',
name: '출병',
reqArg: true,
possible: true,
status: 'needsInput',
inputFields: [
{
key: 'destCityId',
label: '대상 도시',
kind: 'select',
required: true,
optionSource: 'cities',
},
],
},
],
},
],
nation: [
{
@@ -312,6 +334,7 @@ const generalContext = {
experience: 0,
dedication: 0,
items: { horse: 'None', weapon: 'None', book: 'None', item: 'None' },
turnTime: '2026-08-17T12:00:00.000Z',
},
city: {
id: 1,
@@ -483,7 +506,7 @@ const install = async (page: Page, rejectGeneral = false, commandTableResponse:
if (name === 'turns.getCommandTable') return response(commandTableResponse);
if (name === 'nation.getChiefCenter') return response(chiefCenter);
if (name === 'turns.reserved.getGeneral')
return response({ turns: generalTurns, revision: generalRevision });
return response({ turns: generalTurns, revision: generalRevision, autorunLimit: 2403 });
if (name === 'turns.reserved.getNation') return response({ turns: nationTurns, revision: nationRevision });
if (name === 'general.getRecentRecords') return response({ global: [], general: [], history: [] });
if (name === 'general.getFrontStatus')
@@ -520,7 +543,7 @@ const install = async (page: Page, rejectGeneral = false, commandTableResponse:
generalTurns[index] = { index, action: entry.action, args: entry.args ?? {} };
}
generalRevision += 1;
return response({ ok: true, revision: generalRevision, turns: generalTurns });
return response({ ok: true, revision: generalRevision, turns: generalTurns, autorunLimit: 2403 });
}
if (name === 'turns.reserved.setNationBulk') {
requests.push(body);
@@ -561,7 +584,7 @@ test('renders and accepts every Ref strategy command at mobile width', async ({
await button.click();
const form = picker.getByTestId('command-argument-form');
await expect(form.getByTestId('command-argument-guidance')).toContainText(strategy.guidance);
await expect(form.locator('select option')).toHaveCount(2);
await expect(form.locator('select option')).toHaveCount(3);
await picker.getByRole('button', { name: '명령 다시 선택', exact: true }).click();
}
await picker.screenshot({ path: test.info().outputPath('all-strategy-commands-mobile.png') });
@@ -654,7 +677,7 @@ test('reserves force move, retirement, and resignation from the user command pic
await expect(forceMoveForm.getByTestId('command-argument-guidance')).toContainText('선택한 도시로 강행합니다.');
await forceMoveForm.locator('select').selectOption('2');
await picker.getByRole('button', { name: '입력', exact: true }).click();
await expect(editor.locator('.action-column > div').nth(2)).toHaveText('강행');
await expect(editor.locator('.action-column > div').nth(2)).toHaveText('【허창】으로 강행');
const serialized = JSON.stringify(requests);
expect(serialized).toContain('"action":"che_은퇴","args":{}');
@@ -801,7 +824,9 @@ test('enters general and nation command arguments and sends exact values', async
return { width: rect.width, height: rect.height };
});
await page.getByTestId('command-picker').getByRole('button', { name: '입력', exact: true }).click();
await expect(page.locator('[data-command-scope="general"] .action-column > div').first()).toHaveText('화계');
await expect(page.locator('[data-command-scope="general"] .action-column > div').first()).toHaveText(
'【허창】에 화계실행'
);
await page.goto('/che/chief-center');
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
@@ -824,7 +849,9 @@ test('enters general and nation command arguments and sends exact values', async
};
});
await chiefPicker.getByRole('button', { name: '입력', exact: true }).click();
await expect(page.locator('[data-command-scope="nation"] .action-column > div').first()).toHaveText('포상');
await expect(page.locator('[data-command-scope="nation"] .action-column > div').first()).toHaveText(
'【관우】 쌀 300 포상'
);
expect(JSON.stringify(requests)).toContain('"destCityId":2');
expect(JSON.stringify(requests)).toContain('"isGold":false');
@@ -906,7 +933,9 @@ test('uses a Ref-style full recruitment page without horizontal overflow on desk
await infantry.getByRole('button', { name: '절반', exact: true }).click();
await page.screenshot({ path: testInfo.outputPath('recruitment-desktop.png') });
await picker.getByRole('button', { name: '입력', exact: true }).click();
await expect(page.locator('[data-command-scope="general"] .action-column > div').first()).toHaveText('징병');
await expect(page.locator('[data-command-scope="general"] .action-column > div').first()).toHaveText(
'【보병】 3500명 징병'
);
expect(JSON.stringify(requests)).toContain('"crewType":1100');
expect(JSON.stringify(requests)).toContain('"amount":3500');
@@ -1112,6 +1141,53 @@ test('keeps the entered command visible and reports a server validation error',
await expect(page.getByTestId('command-argument-form').locator('select')).toHaveValue('2');
});
test('keeps Ref command briefs and autonomous-action state after a turn mutation', async ({ page }) => {
await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('/');
const editor = page.locator('[data-command-scope="general"]');
const status = editor.locator('[data-command-autorun-status]');
const firstRow = editor.locator('.action-column > div').first();
await expect(status).toHaveText(/ : 200 3 · \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/u);
await expect(firstRow).toContainText('휴식(자율 행동)');
expect(await firstRow.evaluate((element) => getComputedStyle(element).color)).toBe('rgb(170, 255, 255)');
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
const picker = page.getByTestId('command-picker');
await picker.getByRole('button', { name: '군사', exact: true }).click();
await picker.getByRole('button', { name: '출병', exact: true }).click();
await picker.getByTestId('command-argument-form').locator('select').selectOption('3');
await picker.getByRole('button', { name: '입력', exact: true }).click();
await expect(firstRow).toHaveText('【단양】으로 출병');
await expect(firstRow).toHaveAttribute('title', / /u);
expect(await firstRow.evaluate((element) => getComputedStyle(element).color)).toBe('rgb(170, 255, 255)');
await expect(status).toHaveText(/ : 200 3 · .*/u);
const desktopGeometry = await editor.evaluate((element) => {
const statusElement = element.querySelector<HTMLElement>('[data-command-autorun-status]');
const row = element.querySelector<HTMLElement>('.action-column > div');
if (!statusElement || !row) throw new Error('autonomous command geometry is missing');
return {
horizontalOverflow: element.scrollWidth - element.clientWidth,
statusWidth: statusElement.getBoundingClientRect().width,
editorWidth: element.getBoundingClientRect().width,
rowHeight: row.getBoundingClientRect().height,
};
});
expect(desktopGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
expect(desktopGeometry.statusWidth).toBeLessThanOrEqual(desktopGeometry.editorWidth);
expect(desktopGeometry.rowHeight).toBeGreaterThanOrEqual(20);
await page.screenshot({ path: test.info().outputPath('command-brief-autorun-desktop-1200.png'), fullPage: true });
await page.setViewportSize({ width: 500, height: 900 });
await expect(firstRow).toHaveText('【단양】으로 출병');
await expect(status).toBeVisible();
expect(await editor.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(0);
await page.screenshot({ path: test.info().outputPath('command-brief-autorun-mobile-500.png'), fullPage: true });
});
test('uses drag selection, clipboard paste, and a stored template in advanced mode', async ({ page }) => {
const requests = await install(page);
await page.goto('/');
@@ -1139,7 +1215,7 @@ test('uses drag selection, clipboard paste, and a stored template in advanced mo
await blockedFire.click();
await picker.getByTestId('command-argument-form').locator('select').selectOption('2');
await picker.getByRole('button', { name: '입력', exact: true }).click();
await expect(editor.locator('.action-column > div').nth(2)).toHaveText('화계');
await expect(editor.locator('.action-column > div').nth(2)).toHaveText('【허창】에 화계실행');
await drag(0, 2);
await editor.locator('details.selected-menu > summary').click();
@@ -1150,7 +1226,7 @@ test('uses drag selection, clipboard paste, and a stored template in advanced mo
await expect(editor.locator('.index-column > button.selected')).toHaveCount(15);
await editor.locator('details.selected-menu > summary').click();
await editor.getByRole('button', { name: '붙여넣기', exact: true }).click();
await expect(editor.locator('.action-column > div').nth(5)).toHaveText('화계');
await expect(editor.locator('.action-column > div').nth(5)).toHaveText('【허창】에 화계실행');
await drag(0, 2);
page.once('dialog', (dialog) => dialog.accept('화계 세트'));
@@ -1229,7 +1305,9 @@ test('keeps the shared main and chief shell geometry and interaction states', as
await chiefArgumentForm.locator('input[type=number]').fill('300');
await chiefArgumentForm.locator('select').selectOption('2');
await page.getByTestId('command-picker').getByRole('button', { name: '입력', exact: true }).click();
await expect(page.locator('[data-command-scope="nation"] .action-column > div').first()).toHaveText('포상');
await expect(page.locator('[data-command-scope="nation"] .action-column > div').first()).toHaveText(
'【관우】 쌀 300 포상'
);
expect(JSON.stringify(requests)).toContain('"action":"che_포상"');
expect(JSON.stringify(requests)).toContain('"destGeneralId":2');
const chiefDesktop = await page.locator('.chief-page').evaluate((element) => ({