From 005a61b9823201b784d742649eec47d082a1165c Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 17 Aug 2026 15:44:11 +0000 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=EC=A7=95=EB=B3=91=EA=B3=BC=20?= =?UTF-8?q?=EB=AA=A8=EB=B3=91=20=EC=A7=81=EC=A0=91=20=EC=88=98=EB=9F=89?= =?UTF-8?q?=EC=9D=84=20=EB=B3=B4=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이미 선택된 병종의 행 실행 버튼이 수량을 채우기 값으로 다시 초기화하지 않도록 수정한다. 징병과 모병의 보병·궁병·기병·귀병 직접 입력, 예약 payload, 턴 정보 재조회를 production Chromium 회귀 테스트로 고정한다. --- .../e2e/commandArguments.spec.ts | 157 +++++++++++++++++- .../command/RecruitmentCommandForm.vue | 2 +- 2 files changed, 154 insertions(+), 5 deletions(-) diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index b1b03475..9f5037fd 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -265,6 +265,71 @@ const commandTable = { ], inputOptions, }; +const basicRecruitmentCrewTypes = [ + { + id: 1100, + armType: 1, + name: '보병', + attack: 100, + defence: 150, + speed: 7, + avoid: 10, + baseCost: 9, + baseRice: 9, + info: ['표준적인 보병입니다.'], + }, + { + id: 1200, + armType: 2, + name: '궁병', + attack: 100, + defence: 100, + speed: 7, + avoid: 20, + baseCost: 10, + baseRice: 10, + info: ['표준적인 궁병입니다.'], + }, + { + id: 1300, + armType: 3, + name: '기병', + attack: 150, + defence: 100, + speed: 7, + avoid: 5, + baseCost: 11, + baseRice: 11, + info: ['표준적인 기병입니다.'], + }, + { + id: 1400, + armType: 4, + name: '귀병', + attack: 80, + defence: 80, + speed: 7, + avoid: 5, + baseCost: 9, + baseRice: 9, + info: ['계략을 사용하는 병종입니다.'], + }, +].map((crewType) => ({ ...crewType, available: true, special: false })); +const fourArmRecruitmentCommandTable = { + ...commandTable, + inputOptions: { + ...inputOptions, + crewTypes: basicRecruitmentCrewTypes.map((crewType) => ({ value: crewType.id, label: crewType.name })), + recruitment: { + ...inputOptions.recruitment, + groups: basicRecruitmentCrewTypes.map((crewType) => ({ + armType: crewType.armType, + armName: crewType.name, + values: [crewType], + })), + }, + }, +}; const buildSimpleCommand = (key: string, name: string) => ({ key, name, @@ -379,14 +444,25 @@ const generalContext = { name: '아국', color: '#008000', level: 1, - levelName: '호족', gold: 5000, rice: 6000, tech: 100, - typeCode: 'che_중립', typeName: '중립', - capitalCityId: 1, - capitalCityName: '업', + typePros: '', + typeCons: '', + population: { cityCount: 1, current: 1000, max: 2000 }, + crew: { generalCount: 2, current: 500, max: 7000 }, + power: 1234, + bill: 100, + taxRate: 20, + strategicCommandLimit: 0, + diplomaticLimit: 0, + prohibitScout: false, + prohibitWar: false, + techLevel: 1, + techLimited: false, + topChiefs: {}, + impossibleStrategicCommands: [], }, settings: {}, penalties: {}, @@ -1210,6 +1286,79 @@ test('uses a Ref-style full recruitment page without horizontal overflow on desk await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).overflow)).not.toBe('hidden'); }); +test('keeps arbitrary direct recruitment and mercenary amounts for all four arms after turn refresh', async ({ + page, +}, testInfo) => { + const requests = await install(page, false, fourArmRecruitmentCommandTable); + await page.setViewportSize({ width: 1200, height: 900 }); + await page.goto('/'); + + const entries = [ + { turn: 1, command: '징병', crewTypeId: 1100, name: '보병', inputAmount: 13, savedAmount: 1300 }, + { turn: 2, command: '징병', crewTypeId: 1200, name: '궁병', inputAmount: 27, savedAmount: 2700 }, + { turn: 3, command: '징병', crewTypeId: 1300, name: '기병', inputAmount: 41, savedAmount: 4100 }, + { turn: 4, command: '징병', crewTypeId: 1400, name: '귀병', inputAmount: 59, savedAmount: 5900 }, + { turn: 5, command: '모병', crewTypeId: 1100, name: '보병', inputAmount: 17, savedAmount: 1700 }, + { turn: 6, command: '모병', crewTypeId: 1200, name: '궁병', inputAmount: 31, savedAmount: 3100 }, + { turn: 7, command: '모병', crewTypeId: 1300, name: '기병', inputAmount: 43, savedAmount: 4300 }, + { turn: 8, command: '모병', crewTypeId: 1400, name: '귀병', inputAmount: 61, savedAmount: 6100 }, + ]; + + for (const entry of entries) { + await page.getByRole('button', { name: `${entry.turn}턴 명령 입력`, exact: true }).click(); + const picker = page.getByTestId('command-picker'); + await picker.getByRole('button', { name: '내정', exact: true }).click(); + await picker.getByRole('button', { name: entry.command, exact: true }).click(); + + const row = picker.getByRole('button', { name: `${entry.name} 선택 가능`, exact: true }); + const amountInput = row.locator('input[type=number]'); + await amountInput.fill(String(entry.inputAmount)); + await expect(amountInput).toHaveValue(String(entry.inputAmount)); + if (entry.turn === 1) { + const inputGeometry = await amountInput.evaluate((element) => { + if (!(element instanceof HTMLInputElement)) throw new Error('Expected recruitment amount input'); + const rect = element.getBoundingClientRect(); + return { + width: rect.width, + height: rect.height, + textAlign: getComputedStyle(element).textAlign, + value: element.value, + }; + }); + expect(inputGeometry).toMatchObject({ height: 28, textAlign: 'right', value: '13' }); + expect(inputGeometry.width).toBeGreaterThan(0); + await picker.screenshot({ path: testInfo.outputPath('recruitment-direct-amount-desktop.png') }); + } + await row.getByRole('button', { name: entry.command, exact: true }).click(); + + await expect(picker).toHaveCount(0); + await expect(page.locator('[data-command-scope="general"] .action-column > div').nth(entry.turn - 1)).toHaveText( + `【${entry.name}】 ${entry.savedAmount}명 ${entry.command}` + ); + } + + const refreshResponse = page.waitForResponse((apiResponse) => + decodeURIComponent(apiResponse.url()).includes('turns.reserved.getGeneral') + ); + await page.getByRole('button', { name: '갱 신', exact: true }).click(); + await refreshResponse; + + for (const entry of entries) { + await expect(page.locator('[data-command-scope="general"] .action-column > div').nth(entry.turn - 1)).toHaveText( + `【${entry.name}】 ${entry.savedAmount}명 ${entry.command}` + ); + } + await page + .locator('[data-command-scope="general"]') + .screenshot({ path: testInfo.outputPath('recruitment-arbitrary-amounts-after-refresh.png') }); + + const serializedRequests = JSON.stringify(requests); + for (const entry of entries) { + expect(serializedRequests).toContain(`"crewType":${entry.crewTypeId}`); + expect(serializedRequests).toContain(`"amount":${entry.savedAmount}`); + } +}); + test('uses the map to choose a nation target in the chief command window', async ({ page }) => { await install(page); await page.setViewportSize({ width: 1200, height: 900 }); diff --git a/app/game-frontend/src/components/command/RecruitmentCommandForm.vue b/app/game-frontend/src/components/command/RecruitmentCommandForm.vue index fb5b3cc1..ed2f3ae1 100644 --- a/app/game-frontend/src/components/command/RecruitmentCommandForm.vue +++ b/app/game-frontend/src/components/command/RecruitmentCommandForm.vue @@ -69,7 +69,7 @@ const updateAmount = (event: Event) => { }; const submit = async (crewType?: RecruitmentCrewType) => { - if (crewType) selectCrewType(crewType); + if (crewType && crewType.id !== selectedCrewTypeId.value) selectCrewType(crewType); await nextTick(); if (valid.value) emit('submit'); };