From a35dcc78b31ae1b9216c7548337daf95b3535f1f Mon Sep 17 00:00:00 2001 From: hided62 Date: Thu, 27 Aug 2026 10:24:44 +0000 Subject: [PATCH] =?UTF-8?q?fix(game-ui):=20=EB=A9=94=EC=9D=B8=EA=B3=BC=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=ED=99=94=EB=A9=B4=20=EB=AA=A8=EB=93=9C=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=EB=A5=BC=20=ED=86=B5=EC=9D=BC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-frontend/e2e/mainNavigation.spec.ts | 61 +++++++++++++++++++ .../src/components/main/MapViewer.vue | 3 +- .../src/utils/screenModeViewport.ts | 3 + app/game-frontend/src/views/MainView.vue | 3 +- .../test/screenModeViewport.test.ts | 9 +++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 34c3b21a..1aa67dbd 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -3083,6 +3083,51 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn await captureProgress('mobile-500'); }); +test('main layout and map use the same mobile/desktop boundary', async ({ page }, testInfo) => { + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 6, + npcMode: 1, + scenarioTitle: '모바일 검증 시나리오', + lastExecuted: null, + latestVote: null, + generalMeCalls: 0, + operations: [], + validMapImages: true, + }; + await installFixture(page, state); + await page.setViewportSize({ width: 1000, height: 900 }); + await waitForMain(page); + await expect(page.locator('.layout-desktop [data-main-target="map"] .map-area')).toBeVisible(); + expect( + await page + .locator('.layout-desktop [data-main-target="map"] .map-area') + .evaluate((element) => element.getBoundingClientRect().width) + ).toBe(700); + await page.screenshot({ path: testInfo.outputPath('screen-mode-desktop-1000.png'), fullPage: true }); + + await page.setViewportSize({ width: 940, height: 900 }); + await expect(page.locator('.layout-desktop')).toBeVisible(); + await expect(page.locator('.layout-desktop [data-main-target="map"] .map-area')).toBeVisible(); + expect( + await page + .locator('.layout-desktop [data-main-target="map"] .map-area') + .evaluate((element) => element.getBoundingClientRect().width) + ).toBeGreaterThan(500); + + await page.setViewportSize({ width: 939, height: 900 }); + await expect(page.locator('.layout-mobile')).toBeVisible(); + await expect(page.locator('.layout-mobile [data-main-target="map"] .map-area')).toBeVisible(); + expect( + await page + .locator('.layout-mobile [data-main-target="map"] .map-area') + .evaluate((element) => element.getBoundingClientRect().width) + ).toBe(500); + await page.screenshot({ path: testInfo.outputPath('screen-mode-mobile-939.png'), fullPage: true }); +}); + test('the 939/940 boundary switches to the Ref-style 500px single document', async ({ page }) => { const state: NavigationFixture = { officerLevel: 5, @@ -3630,12 +3675,20 @@ test('automatic screen mode switches wide mobile screens to the 1000px layout at npcMode: 1, generalMeCalls: 0, operations: [], + validMapImages: true, }; await installFixture(mobilePage, state); await waitForMain(mobilePage); const expectedWideLayout = deviceWidth >= 700; await expect(mobilePage.locator(expectedWideLayout ? '.layout-desktop' : '.layout-mobile')).toBeVisible(); + const activeMap = mobilePage.locator( + `${expectedWideLayout ? '.layout-desktop' : '.layout-mobile'} [data-main-target="map"] .map-area` + ); + await expect(activeMap).toBeVisible(); + expect(await activeMap.evaluate((element) => element.getBoundingClientRect().width)).toBe( + expectedWideLayout ? 700 : 500 + ); expect( await mobilePage.evaluate(() => document.querySelector('meta[name="viewport"]')?.content) ).toBe(expectedWideLayout ? 'width=1000' : 'width=device-width, initial-scale=1'); @@ -3664,6 +3717,10 @@ test('automatic screen mode switches wide mobile screens to the 1000px layout at document.dispatchEvent(new CustomEvent('tryChangeScreenMode')); }); await expect(mobilePage.locator('.layout-mobile')).toBeVisible(); + await expect(mobilePage.locator('.layout-mobile [data-main-target="map"] .map-area')).toHaveCSS( + 'width', + '500px' + ); expect( await mobilePage.evaluate( () => document.querySelector('meta[name="viewport"]')?.content @@ -3686,6 +3743,10 @@ test('automatic screen mode switches wide mobile screens to the 1000px layout at document.dispatchEvent(new CustomEvent('tryChangeScreenMode')); }); await expect(mobilePage.locator('.layout-desktop')).toBeVisible(); + await expect(mobilePage.locator('.layout-desktop [data-main-target="map"] .map-area')).toHaveCSS( + 'width', + '700px' + ); expect( await mobilePage.evaluate( () => document.querySelector('meta[name="viewport"]')?.content diff --git a/app/game-frontend/src/components/main/MapViewer.vue b/app/game-frontend/src/components/main/MapViewer.vue index d757caee..5de4e844 100644 --- a/app/game-frontend/src/components/main/MapViewer.vue +++ b/app/game-frontend/src/components/main/MapViewer.vue @@ -9,6 +9,7 @@ import { useMapViewerStore } from '../../stores/mapViewer'; import { buildAssetUrl } from '../../utils/mapAssets'; import { resolveMapBackgroundPath, resolveMapSeason, resolveNextMapSeason } from '../../utils/mapBackground'; import { configuredGameAssetUrl } from '../../utils/imageAssets'; +import { SCREEN_MODE_DESKTOP_MEDIA_QUERY } from '../../utils/screenModeViewport'; interface MapSummary { year: number; @@ -133,7 +134,7 @@ const preloadDecodedImage = (url: string): Promise => { return pending; }; -const isWide = useMediaQuery('(min-width: 1024px)'); +const isWide = useMediaQuery(SCREEN_MODE_DESKTOP_MEDIA_QUERY); const mapStore = useMapViewerStore(); const { showCityName, diff --git a/app/game-frontend/src/utils/screenModeViewport.ts b/app/game-frontend/src/utils/screenModeViewport.ts index 260fec55..d72978ba 100644 --- a/app/game-frontend/src/utils/screenModeViewport.ts +++ b/app/game-frontend/src/utils/screenModeViewport.ts @@ -1,5 +1,8 @@ export const SCREEN_MODE_KEY = 'sam.screenMode'; export const SCREEN_MODE_CHANGE_EVENT = 'tryChangeScreenMode'; +export const SCREEN_MODE_DESKTOP_MIN_WIDTH = 940; +export const SCREEN_MODE_DESKTOP_MEDIA_QUERY = `(min-width: ${SCREEN_MODE_DESKTOP_MIN_WIDTH}px)`; +export const SCREEN_MODE_MOBILE_MEDIA_QUERY = `(max-width: ${SCREEN_MODE_DESKTOP_MIN_WIDTH - 0.02}px)`; export type ScreenMode = 'auto' | '500px' | '1000px'; diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index 0411673b..4548993e 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -37,11 +37,12 @@ import { MOBILE_MAIN_PANEL_ORDER_STORAGE_KEY, type MobileMainPanelId, } from '../utils/mobileMainPanelOrder'; +import { SCREEN_MODE_MOBILE_MEDIA_QUERY } from '../utils/screenModeViewport'; const session = useSessionStore(); const dashboard = useMainDashboardStore(); const { info: showInfoToast } = useGameFeedback(); -const isMobile = useMediaQuery('(max-width: 939.98px)'); +const isMobile = useMediaQuery(SCREEN_MODE_MOBILE_MEDIA_QUERY); const npcMode = ref(0); const globalNavigation = ref(defaultGlobalNavigation); diff --git a/app/game-frontend/test/screenModeViewport.test.ts b/app/game-frontend/test/screenModeViewport.test.ts index 45e426a9..0ac60f72 100644 --- a/app/game-frontend/test/screenModeViewport.test.ts +++ b/app/game-frontend/test/screenModeViewport.test.ts @@ -5,8 +5,17 @@ import { normalizeScreenMode, resolveAutoViewportContent, resolveViewportContent, + SCREEN_MODE_DESKTOP_MEDIA_QUERY, + SCREEN_MODE_DESKTOP_MIN_WIDTH, + SCREEN_MODE_MOBILE_MEDIA_QUERY, } from '../src/utils/screenModeViewport.ts'; +void test('main and map layouts share one 939/940 screen-mode boundary', () => { + assert.equal(SCREEN_MODE_DESKTOP_MIN_WIDTH, 940); + assert.equal(SCREEN_MODE_DESKTOP_MEDIA_QUERY, '(min-width: 940px)'); + assert.equal(SCREEN_MODE_MOBILE_MEDIA_QUERY, '(max-width: 939.98px)'); +}); + void test('automatic mode follows the Ref physical-screen thresholds', () => { assert.equal(resolveAutoViewportContent({ deviceWidth: 390, viewportHeight: 844 }), 'width=500'); assert.equal(