From d4e83661ca877900e5246d5db6737244a0a33fd4 Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 31 Aug 2026 02:49:36 +0000 Subject: [PATCH] =?UTF-8?q?fix(game-ui):=20=EC=9D=B8=EC=82=AC=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=20=EC=9E=A5=EC=88=98=20=EC=84=A0=ED=83=9D=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-api/src/turns/commandTargets.ts | 21 +++- app/game-api/test/commandTargets.test.ts | 14 ++- .../e2e/commandArguments.spec.ts | 110 ++++++++++++------ 3 files changed, 104 insertions(+), 41 deletions(-) diff --git a/app/game-api/src/turns/commandTargets.ts b/app/game-api/src/turns/commandTargets.ts index b551a8db..834772a4 100644 --- a/app/game-api/src/turns/commandTargets.ts +++ b/app/game-api/src/turns/commandTargets.ts @@ -56,27 +56,36 @@ export const buildRefGeneralTargetOptions = (options: { }): RefGeneralTargetOptions => { const toOption = (entry: GeneralTargetSource, action?: string): TurnCommandOption => { const troopName = entry.troopId ? options.troopNames?.get(entry.troopId) : undefined; + const cityName = options.cityNames.get(entry.cityId) ?? '재야'; + const troopLabel = entry.troopId + ? `${troopName ?? `#${entry.troopId}`}${entry.troopId === entry.id ? ' (부대장)' : ''}` + : '부대 없음'; const isTroopMember = Boolean(entry.troopId && entry.troopId !== entry.id); const isTroopExit = action === 'che_부대탈퇴지시'; const availableNow = isTroopExit ? isTroopMember && entry.id !== options.actorId : undefined; + const label = (() => { + if (action === 'che_발령') return `${entry.name} (${troopLabel} · ${cityName})`; + if (action === 'che_포상' || action === 'che_몰수') return `${entry.name} (${cityName})`; + return `${entry.name} (${options.nationNames.get(entry.nationId) ?? '무소속'} · ${cityName})`; + })(); const details = [ entry.gold === undefined ? null : `금 ${entry.gold.toLocaleString()}`, entry.rice === undefined ? null : `쌀 ${entry.rice.toLocaleString()}`, entry.crew === undefined ? null : `병력 ${entry.crew.toLocaleString()}`, entry.train === undefined ? null : `훈련 ${entry.train.toLocaleString()}`, entry.atmos === undefined ? null : `사기 ${entry.atmos.toLocaleString()}`, - entry.troopId - ? `탑승 부대 ${troopName ?? `#${entry.troopId}`}${entry.troopId === entry.id ? ' (부대장)' : ''}` - : '탑승 부대 없음', + action === 'che_발령' || action === 'che_포상' || action === 'che_몰수' + ? null + : entry.troopId + ? `탑승 부대 ${troopLabel}` + : '탑승 부대 없음', ].filter((value): value is string => Boolean(value)); if (isTroopExit) { details.unshift(availableNow ? '현재 탈퇴 지시 가능' : '현재 탈퇴 지시 불가'); } return { value: entry.id, - label: `${entry.name} (${options.nationNames.get(entry.nationId) ?? '무소속'} · ${ - options.cityNames.get(entry.cityId) ?? '재야' - })`, + label, description: details.join(' · '), ...(availableNow === undefined ? {} : { availableNow }), ...(entry.gold === undefined ? {} : { gold: entry.gold }), diff --git a/app/game-api/test/commandTargets.test.ts b/app/game-api/test/commandTargets.test.ts index f56d7daa..676ac5de 100644 --- a/app/game-api/test/commandTargets.test.ts +++ b/app/game-api/test/commandTargets.test.ts @@ -87,7 +87,19 @@ describe('Ref command general targets', () => { troopId: 3, description: expect.stringContaining('탑승 부대 청룡대'), }); - expect(detailed.generalTargets.che_포상?.[0]?.description).toContain('금 5,000 · 쌀 4,000 · 병력 1,000'); + expect(detailed.generalTargets.che_발령?.map((entry) => entry.label)).toEqual([ + '본인 (부대 없음 · 업)', + '부대원 (청룡대 · 업)', + '부대장 (청룡대 (부대장) · 업)', + ]); + expect(detailed.generalTargets.che_발령?.[1]?.description).not.toContain('탑승 부대'); + expect(detailed.generalTargets.che_포상?.map((entry) => entry.label)).toEqual([ + '본인 (업)', + '부대원 (업)', + '부대장 (업)', + ]); + expect(detailed.generalTargets.che_포상?.[0]?.description).toBe('금 5,000 · 쌀 4,000 · 병력 1,000'); + expect(detailed.generalTargets.che_몰수?.[1]?.description).not.toContain('탑승 부대'); }); }); diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index 33ae42eb..035f791d 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -128,80 +128,80 @@ const inputOptions = { che_포상: [ { value: 1, - label: '장수 (아국 · 업)', + label: '장수 (업)', gold: 5000, rice: 400, crew: 500, - description: '금 5,000 · 쌀 400 · 병력 500 · 탑승 부대 없음', + description: '금 5,000 · 쌀 400 · 병력 500', }, { value: 2, - label: '관우 (아국 · 업)', + label: '관우 (업)', gold: 100, rice: 4000, crew: 1200, troopId: 2, - description: '금 100 · 쌀 4,000 · 병력 1,200 · 탑승 부대 청룡대 (부대장)', + description: '금 100 · 쌀 4,000 · 병력 1,200', }, { value: 3, - label: '여포NPC (아국 · 업)', + label: '여포NPC (업)', npcState: 2, gold: 3000, rice: 500, crew: 1500, troopId: 2, - description: '금 3,000 · 쌀 500 · 병력 1,500 · 탑승 부대 청룡대', + description: '금 3,000 · 쌀 500 · 병력 1,500', }, ], che_몰수: [ { value: 1, - label: '장수 (아국 · 업)', + label: '장수 (업)', gold: 5000, rice: 400, crew: 500, - description: '금 5,000 · 쌀 400 · 병력 500 · 탑승 부대 없음', + description: '금 5,000 · 쌀 400 · 병력 500', }, { value: 2, - label: '관우 (아국 · 업)', + label: '관우 (업)', gold: 100, rice: 4000, crew: 1200, troopId: 2, - description: '금 100 · 쌀 4,000 · 병력 1,200 · 탑승 부대 청룡대 (부대장)', + description: '금 100 · 쌀 4,000 · 병력 1,200', }, { value: 3, - label: '여포NPC (아국 · 업)', + label: '여포NPC (업)', gold: 3000, rice: 500, crew: 1500, troopId: 2, - description: '금 3,000 · 쌀 500 · 병력 1,500 · 탑승 부대 청룡대', + description: '금 3,000 · 쌀 500 · 병력 1,500', }, ], che_발령: [ { value: 1, - label: '장수 (아국 · 업)', + label: '장수 (부대 없음 · 업)', crew: 500, - description: '금 5,000 · 쌀 400 · 병력 500 · 탑승 부대 없음', + description: '금 5,000 · 쌀 400 · 병력 500', }, { value: 2, - label: '관우 (아국 · 업)', + label: '관우 (청룡대 (부대장) · 업)', crew: 1200, troopId: 2, - description: '금 100 · 쌀 4,000 · 병력 1,200 · 탑승 부대 청룡대 (부대장)', + description: '금 100 · 쌀 4,000 · 병력 1,200', }, { value: 3, - label: '여포NPC (아국 · 업)', + label: '여포NPC (청룡대 · 업)', crew: 1500, troopId: 2, - description: '금 3,000 · 쌀 500 · 병력 1,500 · 탑승 부대 청룡대', + description: '금 3,000 · 쌀 500 · 병력 1,500', }, ], che_부대탈퇴지시: [ @@ -2485,22 +2485,21 @@ test('offers Ref amount presets and rich, command-specific general lists', async await expect(amount).toHaveValue('1375'); let generalList = form.getByTestId('general-target-list'); - await expect(generalList.locator('.target-option strong')).toHaveText([ - '관우 (아국 · 업)', - '여포NPC (아국 · 업)', - '장수 (아국 · 업)', + await expect(form.locator('#command-arg-destGeneralId option')).toHaveText([ + '관우 (업)', + '여포NPC (업)', + '장수 (업)', ]); + await expect(generalList.locator('.target-option strong')).toHaveText(['관우 (업)', '여포NPC (업)', '장수 (업)']); await expect(generalList.locator('.target-option').filter({ hasText: '여포NPC' }).locator('strong')).toHaveCSS( 'color', 'rgb(0, 255, 255)' ); - await expect(generalList).toContainText('금 100 · 쌀 4,000 · 병력 1,200 · 탑승 부대 청룡대 (부대장)'); + await expect(generalList).toContainText('금 100 · 쌀 4,000 · 병력 1,200'); + await expect(generalList).not.toContainText('아국'); + await expect(generalList).not.toContainText('탑승 부대'); await form.getByRole('button', { name: '쌀', exact: true }).click(); - await expect(generalList.locator('.target-option strong')).toHaveText([ - '장수 (아국 · 업)', - '여포NPC (아국 · 업)', - '관우 (아국 · 업)', - ]); + await expect(generalList.locator('.target-option strong')).toHaveText(['장수 (업)', '여포NPC (업)', '관우 (업)']); await generalList.locator('.target-option').filter({ hasText: '여포NPC' }).click(); const awardResponse = page.waitForResponse((response) => response.url().includes('turns.reserved.setNationBulk')); await picker.getByRole('button', { name: '입력', exact: true }).click(); @@ -2513,19 +2512,34 @@ test('offers Ref amount presets and rich, command-specific general lists', async await picker.getByRole('button', { name: /몰수/ }).click(); form = picker.getByTestId('command-argument-form'); generalList = form.getByTestId('general-target-list'); - await expect(generalList.locator('.target-option strong')).toHaveText([ - '장수 (아국 · 업)', - '여포NPC (아국 · 업)', - '관우 (아국 · 업)', + await expect(form.locator('#command-arg-destGeneralId option')).toHaveText([ + '장수 (업)', + '여포NPC (업)', + '관우 (업)', ]); - + await expect(generalList.locator('.target-option strong')).toHaveText(['장수 (업)', '여포NPC (업)', '관우 (업)']); + await expect(generalList).not.toContainText('아국'); + await expect(generalList).not.toContainText('탑승 부대'); await page.goto('/che/chief-center'); await page.getByRole('button', { name: '3턴 명령 입력', exact: true }).click(); picker = page.getByTestId('command-picker'); await picker.getByRole('button', { name: /^(?:국가:)?인사$/, exact: true }).click(); await picker.getByRole('button', { name: /발령/ }).click(); form = picker.getByTestId('command-argument-form'); - await expect(form.getByTestId('general-target-list')).toContainText('병력 1,200 · 탑승 부대 청룡대 (부대장)'); + generalList = form.getByTestId('general-target-list'); + await expect(form.locator('#command-arg-destGeneralId option')).toHaveText([ + '장수 (부대 없음 · 업)', + '관우 (청룡대 (부대장) · 업)', + '여포NPC (청룡대 · 업)', + ]); + await expect(generalList.locator('.target-option strong')).toHaveText([ + '장수 (부대 없음 · 업)', + '관우 (청룡대 (부대장) · 업)', + '여포NPC (청룡대 · 업)', + ]); + await expect(generalList).not.toContainText('아국'); + await expect(generalList).not.toContainText('탑승 부대'); + await picker.screenshot({ path: test.info().outputPath('chief-assignment-option-format-desktop.png') }); await page.goto('/che/chief-center'); await page.getByRole('button', { name: '4턴 명령 입력', exact: true }).click(); @@ -2560,6 +2574,34 @@ test('offers Ref amount presets and rich, command-specific general lists', async expect(serialized).toContain('"destGeneralId":3'); expect(serialized).toContain('"amountList":[20000,1370]'); await page.screenshot({ path: test.info().outputPath('chief-ref-guidance-controls.png'), fullPage: true }); + + await page.setViewportSize({ width: 500, height: 900 }); + const mobilePersonnelCases = [ + { turn: 5, command: '포상', labels: ['관우 (업)', '여포NPC (업)', '장수 (업)'] }, + { turn: 6, command: '몰수', labels: ['장수 (업)', '여포NPC (업)', '관우 (업)'] }, + { + turn: 7, + command: '발령', + labels: ['장수 (부대 없음 · 업)', '관우 (청룡대 (부대장) · 업)', '여포NPC (청룡대 · 업)'], + }, + ]; + for (const entry of mobilePersonnelCases) { + await page.goto('/che/chief-center'); + await page.getByRole('button', { name: `${entry.turn + 1}턴 명령 입력`, exact: true }).click(); + picker = page.getByTestId('command-picker'); + await picker.getByRole('button', { name: /^(?:국가:)?인사$/, exact: true }).click(); + await picker.getByRole('button', { name: new RegExp(entry.command) }).click(); + form = picker.getByTestId('command-argument-form'); + await expect(form.locator('#command-arg-destGeneralId option')).toHaveText(entry.labels); + await expect(form.getByTestId('general-target-list')).not.toContainText('아국'); + } + const mobileGeometry = await picker.evaluate((element) => ({ + pickerWidth: element.getBoundingClientRect().width, + pickerOverflow: element.scrollWidth - element.clientWidth, + documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, + })); + expect(mobileGeometry).toEqual({ pickerWidth: 500, pickerOverflow: 0, documentOverflow: 0 }); + await page.screenshot({ path: test.info().outputPath('chief-personnel-option-format-mobile.png'), fullPage: true }); }); test('fits the city map option window inside the Ref-compatible 500px mobile page', async ({ page }) => {