From 445db7bbe0b86e894143776803cd93354519bec0 Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 12 Aug 2026 07:43:50 +0000 Subject: [PATCH] fix(game-frontend): align main UI grid with ref --- app/game-frontend/e2e/mainNavigation.spec.ts | 250 +++++++++++++++--- .../command/ReservedCommandEditor.vue | 6 +- .../src/components/main/CommandSelectForm.vue | 2 +- .../src/components/main/GeneralBasicCard.vue | 86 +----- app/game-frontend/src/views/MainView.vue | 6 +- 5 files changed, 235 insertions(+), 115 deletions(-) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index ea4db25b..fb10f760 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -31,6 +31,7 @@ type NavigationFixture = { forceSnapshotCalls?: number; refreshDelayMs?: number; largeCommandTable?: boolean; + reservedTurns?: Array<{ index: number; action: string; args: Record }>; dashboardResponses?: Array<{ bytes: number; contextKind: string | null; @@ -116,12 +117,13 @@ const emitReadModelChanges = (page: Page, changes: ReturnType ({ general: large - ? [ - { - category: '일반', - values: Array.from({ length: 48 }, (_, index) => ({ + ? ['내정', '군사', '계략'].map((category, categoryIndex) => ({ + category, + values: Array.from({ length: 16 }, (_, localIndex) => { + const index = categoryIndex * 16 + localIndex; + return { key: `command-${index}`, - name: `명령 ${index}`, + name: index === 0 ? '주민 선정과 장기 도시 개발' : `명령 ${index}`, reqArg: index % 2 === 0, possible: index >= blockedCount, status: index >= blockedCount ? 'available' : 'blocked', @@ -135,9 +137,9 @@ const commandTableFixture = (large: boolean, blockedCount = 0) => ({ max: 10_000, }, ], - })), - }, - ] + }; + }), + })) : [], nation: [], inputOptions: { @@ -229,6 +231,7 @@ const generalContext = (state: NavigationFixture) => ({ dex: [350, 100_000, 500_000, 1_000_000, 1_275_975], }, items: { horse: null, weapon: null, book: null, item: null }, + turnTime: '0185-01-01T00:00:00.000Z', }, city: { id: 1, @@ -400,7 +403,7 @@ const installFixture = async (page: Page, state: NavigationFixture) => { } if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] }); if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') { - return response({ turns: [], revision: 0 }); + return response({ turns: state.reservedTurns ?? [], revision: 0 }); } if (operation === 'messages.getRecent') return response(emptyMessages(state.permission)); if (operation === 'messages.getContacts') return response({ nation: [] }); @@ -628,9 +631,7 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`); }); -test('main city and general cards render every ref bar plus dual dexterity progress at ref heights', async ({ - page, -}) => { +test('main cards and command input stay inside their Ref-sized grid slots', async ({ page }) => { const state: NavigationFixture = { officerLevel: 1, permission: 0, @@ -639,6 +640,12 @@ test('main city and general cards render every ref bar plus dual dexterity progr npcMode: 1, generalMeCalls: 0, operations: [], + largeCommandTable: true, + reservedTurns: Array.from({ length: 14 }, (_, index) => ({ + index, + action: `command-${index}`, + args: {}, + })), }; await installFixture(page, state); await page.setViewportSize({ width: 1200, height: 900 }); @@ -647,28 +654,15 @@ test('main city and general cards render every ref bar plus dual dexterity progr const cityBars = page.locator('[data-main-target="city"] [role="progressbar"]'); const statBars = page.locator('[data-stat-progress] [role="progressbar"]'); const experienceBar = page.locator('[data-experience-progress] [role="progressbar"]'); - const dexRows = page.locator('[data-dex-progress]'); await expect(cityBars).toHaveCount(8); await expect(statBars).toHaveCount(3); await expect(experienceBar).toHaveCount(1); - await expect(dexRows).toHaveCount(5); - await expect(dexRows.locator('[role="progressbar"]')).toHaveCount(10); + await expect(page.locator('[data-main-target="general"] [data-dex-progress]')).toHaveCount(0); + await expect(page.locator('[data-main-target="general"] [role="progressbar"]')).toHaveCount(4); expect(await cityBars.first().evaluate((element) => element.getBoundingClientRect().height)).toBe(9); expect(await statBars.first().evaluate((element) => element.getBoundingClientRect().height)).toBe(12); expect(await experienceBar.evaluate((element) => element.getBoundingClientRect().height)).toBe(12); - const firstDexBars = dexRows.first().locator('[role="progressbar"]'); - expect(await firstDexBars.nth(0).evaluate((element) => element.getBoundingClientRect().height)).toBe(12); - expect(await firstDexBars.nth(1).evaluate((element) => element.getBoundingClientRect().height)).toBe(9); - - await expect(dexRows.nth(3).locator('[role="progressbar"]').nth(0)).toHaveAttribute('aria-valuemax', '100'); - await expect(dexRows.nth(3).locator('[role="progressbar"]').nth(0)).toHaveAttribute( - 'aria-label', - /1,000,000 \/ 1,275,975 \(EX\+\)/ - ); - await expect(dexRows.nth(3).locator('[role="progressbar"]').nth(1)).toHaveAttribute('aria-label', /까지 .* 남음/); - await expect(dexRows.nth(4).locator('[role="progressbar"]').nth(1)).toHaveAttribute('aria-label', /EX\+ 달성/); - const texture = await cityBars.first().evaluate((element) => getComputedStyle(element).backgroundImage); const fillTexture = await cityBars .first() @@ -677,6 +671,100 @@ test('main city and general cards render every ref bar plus dual dexterity progr expect(texture).toContain('/game/pr5.gif'); expect(fillTexture).toContain('/game/pb5.gif'); + const desktopGeometry = await page.evaluate(() => { + const box = (element: Element) => { + const rect = element.getBoundingClientRect(); + return { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + right: rect.right, + bottom: rect.bottom, + }; + }; + const target = (name: string) => { + const panel = document.querySelector(`[data-main-target="${name}"]`); + if (!panel) throw new Error(`${name} panel missing`); + return panel; + }; + const general = target('general'); + const nation = target('nation'); + const city = target('city'); + const commands = target('commands'); + const generalBody = general.querySelector('.general-body'); + const editor = commands.querySelector('.reserved-command-editor'); + const controlPad = commands.querySelector('.control-pad'); + const bottomActions = commands.querySelector('.bottom-actions'); + if (!generalBody || !editor || !controlPad || !bottomActions) throw new Error('main layout probe missing'); + const visibleControlBoxes = [...controlPad.children] + .filter((element) => getComputedStyle(element).display !== 'none') + .map(box); + const bottomActionBoxes = [...bottomActions.children].map(box); + return { + general: box(general), + generalBody: box(generalBody), + nation: box(nation), + city: box(city), + commands: box(commands), + editor: box(editor), + controlPad: box(controlPad), + controlColumns: getComputedStyle(controlPad).gridTemplateColumns, + visibleControlBoxes, + bottomActionBoxes, + commandHorizontalOverflow: commands.scrollWidth - commands.clientWidth, + commandVerticalOverflow: commands.scrollHeight - commands.clientHeight, + }; + }); + expect(desktopGeometry.general.y).toBe(desktopGeometry.nation.y); + expect(desktopGeometry.general.bottom).toBe(desktopGeometry.nation.bottom); + expect(desktopGeometry.commands.bottom).toBe(desktopGeometry.city.bottom); + expect(desktopGeometry.generalBody.bottom).toBeLessThanOrEqual(desktopGeometry.general.bottom); + expect(desktopGeometry.editor.right).toBeLessThanOrEqual(desktopGeometry.commands.right); + expect(desktopGeometry.commandHorizontalOverflow).toBeLessThanOrEqual(0); + expect(desktopGeometry.commandVerticalOverflow).toBeLessThanOrEqual(0); + expect(desktopGeometry.controlColumns.split(' ')).toHaveLength(3); + expect(desktopGeometry.visibleControlBoxes).toHaveLength(3); + expect(new Set(desktopGeometry.visibleControlBoxes.map(({ y }) => y)).size).toBe(1); + expect(desktopGeometry.bottomActionBoxes).toHaveLength(3); + expect(new Set(desktopGeometry.bottomActionBoxes.map(({ y }) => y)).size).toBe(1); + + const modeButton = page.locator('[data-main-target="commands"] .control-pad').getByRole('button', { + name: '고급 모드', + }); + await modeButton.hover(); + await modeButton.focus(); + await expect(modeButton).toBeFocused(); + await modeButton.click(); + await page.locator('[data-main-target="commands"] .select-command').click(); + const picker = page.getByTestId('command-picker'); + await expect(picker).toBeVisible(); + const pickerGeometry = await picker.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const editor = element.closest('.reserved-command-editor')?.getBoundingClientRect(); + const categories = element.querySelector('.category-list'); + if (!editor || !categories) throw new Error('command picker geometry missing'); + return { + left: rect.left, + right: rect.right, + editorLeft: editor.left, + editorRight: editor.right, + categoryColumns: getComputedStyle(categories).gridTemplateColumns, + categoryBoxes: [...categories.children].map((child) => { + const childRect = child.getBoundingClientRect(); + return { x: childRect.x, y: childRect.y, width: childRect.width, height: childRect.height }; + }), + horizontalOverflow: element.scrollWidth - element.clientWidth, + }; + }); + expect(pickerGeometry.left).toBeGreaterThanOrEqual(pickerGeometry.editorLeft); + expect(pickerGeometry.right).toBeLessThanOrEqual(pickerGeometry.editorRight); + expect(pickerGeometry.categoryColumns.split(' ')).toHaveLength(3); + expect(pickerGeometry.categoryBoxes).toHaveLength(3); + expect(new Set(pickerGeometry.categoryBoxes.map(({ y }) => y)).size).toBe(1); + expect(pickerGeometry.horizontalOverflow).toBeLessThanOrEqual(0); + await picker.getByRole('button', { name: '명령 입력 닫기' }).click(); + const captureProgress = async (name: string) => { if (!artifactRoot) return; await mkdir(artifactRoot, { recursive: true }); @@ -696,27 +784,123 @@ test('main city and general cards render every ref bar plus dual dexterity progr }; }) ); + const layout = await page.evaluate(() => { + const describe = (selector: string) => { + const element = document.querySelector(selector); + if (!element) return null; + const rect = element.getBoundingClientRect(); + return { + rect: { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + right: rect.right, + bottom: rect.bottom, + }, + clientWidth: element.clientWidth, + clientHeight: element.clientHeight, + scrollWidth: element.scrollWidth, + scrollHeight: element.scrollHeight, + overflowX: getComputedStyle(element).overflowX, + overflowY: getComputedStyle(element).overflowY, + boxSizing: getComputedStyle(element).boxSizing, + cssWidth: getComputedStyle(element).width, + minWidth: getComputedStyle(element).minWidth, + paddingInline: getComputedStyle(element).paddingInline, + borderInline: `${getComputedStyle(element).borderLeftWidth} ${getComputedStyle(element).borderRightWidth}`, + }; + }; + return { + viewport: { width: innerWidth, height: innerHeight }, + documentWidth: document.documentElement.scrollWidth, + roots: { + html: describe('html'), + body: describe('body'), + app: describe('#app'), + main: describe('main.main-page'), + }, + overflowingElements: [...document.body.querySelectorAll('*')] + .map((element) => { + const rect = element.getBoundingClientRect(); + return { + tag: element.tagName.toLowerCase(), + className: element.className, + testId: element.dataset.testid ?? null, + left: rect.left, + right: rect.right, + width: rect.width, + }; + }) + .filter(({ left, right }) => left < 0 || right > innerWidth) + .slice(0, 20), + city: describe('[data-main-target="city"]'), + nation: describe('[data-main-target="nation"]'), + general: describe('[data-main-target="general"]'), + commands: describe('[data-main-target="commands"]'), + commandEditor: describe('[data-main-target="commands"] .reserved-command-editor'), + commandPicker: describe('[data-testid="command-picker"]'), + }; + }); await Promise.all([ page.screenshot({ path: resolve(artifactRoot, `progress-bars-${name}.png`), fullPage: true }), - writeFile(resolve(artifactRoot, `progress-bars-${name}.json`), `${JSON.stringify(measurement, null, 2)}\n`), + writeFile( + resolve(artifactRoot, `progress-bars-${name}.json`), + `${JSON.stringify({ layout, progress: measurement }, null, 2)}\n` + ), ]); }; await captureProgress('desktop-1200'); + await page.locator('[data-main-target="commands"] .control-pad').getByRole('button', { name: '일반 모드' }).click(); await page.setViewportSize({ width: 500, height: 900 }); await expect(page.locator('.layout-mobile')).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(500); await expect(page.locator('[data-main-target="city"] [role="progressbar"]')).toHaveCount(8); - await expect(page.locator('[data-main-target="general"] [role="progressbar"]')).toHaveCount(14); + await expect(page.locator('[data-main-target="general"] [role="progressbar"]')).toHaveCount(4); expect( await page .locator('[data-main-target="city"] [role="progressbar"]') .first() .evaluate((element) => element.getBoundingClientRect().height) ).toBe(9); + const mobileGeometry = await page.locator('[data-main-target="commands"]').evaluate((commands) => { + const editor = commands.querySelector('.reserved-command-editor'); + const controls = commands.querySelector('.control-pad'); + if (!editor || !controls) throw new Error('mobile command layout probe missing'); + const panelRect = commands.getBoundingClientRect(); + const editorRect = editor.getBoundingClientRect(); + const controlBoxes = [...controls.children] + .filter((element) => getComputedStyle(element).display !== 'none') + .map((element) => { + const rect = element.getBoundingClientRect(); + return { y: rect.y, width: rect.width }; + }); + return { + panelWidth: panelRect.width, + panelHeight: panelRect.height, + editorLeft: editorRect.left, + editorRight: editorRect.right, + panelLeft: panelRect.left, + panelRight: panelRect.right, + horizontalOverflow: commands.scrollWidth - commands.clientWidth, + verticalOverflow: commands.scrollHeight - commands.clientHeight, + controlColumns: getComputedStyle(controls).gridTemplateColumns, + controlBoxes, + }; + }); + expect(mobileGeometry.panelHeight).toBe(645); + expect(mobileGeometry.editorLeft).toBeGreaterThanOrEqual(mobileGeometry.panelLeft); + expect(mobileGeometry.editorRight).toBeLessThanOrEqual(mobileGeometry.panelRight); + expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0); + expect(mobileGeometry.verticalOverflow).toBeLessThanOrEqual(0); + expect(mobileGeometry.controlColumns.split(' ')).toHaveLength(3); + expect(mobileGeometry.controlBoxes).toHaveLength(3); + expect(new Set(mobileGeometry.controlBoxes.map(({ y }) => y)).size).toBe(1); await captureProgress('mobile-500'); }); -test('the 939/940 boundary switches to the Ref-style 502px single document', async ({ page }) => { +test('the 939/940 boundary switches to the Ref-style 500px single document', async ({ page }) => { const state: NavigationFixture = { officerLevel: 5, permission: 2, @@ -738,7 +922,7 @@ test('the 939/940 boundary switches to the Ref-style 502px single document', asy await expect(page.locator('.layout-mobile')).toBeVisible(); expect(await gridColumnCount(page, '.main-global-menu')).toBe(4); expect(await gridColumnCount(page, '.main-nation-menu')).toBe(5); - await expect(page.locator('.main-mobile-bottom')).toHaveCount(0); + await expect(page.locator('.main-mobile-bottom')).toBeVisible(); await page.setViewportSize({ width: 500, height: 900 }); const documentGeometry = await page.locator('.main-page').evaluate((element) => { @@ -752,8 +936,8 @@ test('the 939/940 boundary switches to the Ref-style 502px single document', asy }); expect(documentGeometry).toMatchObject({ x: 0, - width: 502, - scrollWidth: 502, + width: 500, + scrollWidth: 500, }); expect(documentGeometry.height).toBeGreaterThan(900); for (const selector of [ diff --git a/app/game-frontend/src/components/command/ReservedCommandEditor.vue b/app/game-frontend/src/components/command/ReservedCommandEditor.vue index 6f07ad16..54db3422 100644 --- a/app/game-frontend/src/components/command/ReservedCommandEditor.vue +++ b/app/game-frontend/src/components/command/ReservedCommandEditor.vue @@ -246,8 +246,8 @@ const clickOutsideMenu = (event: Event) => { {{ props.name ?? '-' }}{{ props.title }} -
{{ props.currentTime }}
+
{{ props.currentTime }}
반복
-
+
당기기
-
+
미루기 명성 {{ props.general.experience.toLocaleString() }} - -
숙 련 도
-
- {{ row.label }} - {{ row.progress.name }} - {{ (row.value / 1_000).toFixed(1) }}K -
- - -
-