From 7dfec294583225dd505c22a4d068750b0176218d Mon Sep 17 00:00:00 2001 From: hided62 Date: Fri, 21 Aug 2026 16:28:30 +0000 Subject: [PATCH] =?UTF-8?q?feat(game-ui):=20=EC=A7=80=EB=8F=84=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=EC=9D=84=20=ED=99=95=EC=9E=A5=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=EB=A1=9C=20=EB=AC=B6=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-frontend/e2e/inGameInfo.spec.ts | 87 +++++++++-- .../src/components/main/MapViewer.vue | 135 +++++++++++++++--- 2 files changed, 193 insertions(+), 29 deletions(-) diff --git a/app/game-frontend/e2e/inGameInfo.spec.ts b/app/game-frontend/e2e/inGameInfo.spec.ts index 9ef6d6c5..e90843f6 100644 --- a/app/game-frontend/e2e/inGameInfo.spec.ts +++ b/app/game-frontend/e2e/inGameInfo.spec.ts @@ -475,6 +475,27 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or await page.setViewportSize({ width: 1200, height: 900 }); await go(page, 'global-info'); + const desktopOptionsTrigger = page.getByRole('button', { name: '지도 옵션' }); + await expect(desktopOptionsTrigger).toBeVisible(); + await expect(desktopOptionsTrigger).toHaveAttribute('aria-expanded', 'false'); + await expect(desktopOptionsTrigger).toHaveCSS('background-color', 'rgb(52, 92, 133)'); + await desktopOptionsTrigger.hover(); + await expect(desktopOptionsTrigger).toHaveCSS('background-color', 'rgb(40, 73, 105)'); + await expect(page.getByRole('button', { name: '도시명 표기 끄기' })).not.toBeVisible(); + await desktopOptionsTrigger.click(); + const cityNameToggle = page.getByRole('button', { name: '도시명 표기 끄기' }); + await expect(cityNameToggle).toBeVisible(); + await expect(desktopOptionsTrigger).toHaveAttribute('aria-expanded', 'true'); + await expect(page.locator('.map-options-menu .map-toggle')).toHaveCount(1); + await page.keyboard.press('Tab'); + await desktopOptionsTrigger.focus(); + await expect(desktopOptionsTrigger).toHaveCSS('outline-style', 'solid'); + await page.screenshot({ path: testInfo.outputPath('desktop-map-options-open.png'), fullPage: true }); + await cityNameToggle.click(); + await expect(page.locator('.map-area .city-name')).toHaveCount(0); + await page.keyboard.press('Escape'); + await expect(desktopOptionsTrigger).toHaveAttribute('aria-expanded', 'false'); + const desktopCity = page.locator('.city-base').first(); await expect(page.locator('.map-toggle-single-tap')).toHaveCount(0); await desktopCity.hover(); @@ -508,20 +529,48 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or await install(mobilePage); await go(mobilePage, 'global-info'); + const mobileOptionsTrigger = mobilePage.getByRole('button', { name: '지도 옵션' }); + await expect(mobileOptionsTrigger).toBeVisible(); + await expect(mobileOptionsTrigger).toHaveAttribute('aria-expanded', 'false'); + await mobileOptionsTrigger.tap(); + await expect(mobileOptionsTrigger).toHaveAttribute('aria-expanded', 'true'); + await expect(mobilePage.locator('.map-options-menu .map-toggle')).toHaveCount(2); + const twoTapButton = mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 끄기' }); await expect(twoTapButton).toBeVisible(); await expect(twoTapButton).toHaveAttribute('aria-pressed', 'false'); - const controlGeometry = await twoTapButton.evaluate((element) => { - const rect = element.getBoundingClientRect(); + const controlGeometry = await mobilePage.locator('.map-controls').evaluate((element) => { + const triggerRect = element.querySelector('.map-options-trigger')!.getBoundingClientRect(); + const menuRect = element.querySelector('.map-options-menu')!.getBoundingClientRect(); + const optionRects = Array.from(element.querySelectorAll('.map-options-menu .map-toggle')).map((option) => { + const rect = option.getBoundingClientRect(); + return { top: rect.top, bottom: rect.bottom }; + }); const mapRect = element.closest('.map-area')?.getBoundingClientRect(); - const style = getComputedStyle(element); + const optionStyle = getComputedStyle(element.querySelector('.map-toggle')!); return { - right: rect.right, - bottom: rect.bottom, - mapRight: mapRect?.right, - mapBottom: mapRect?.bottom, - fontSize: style.fontSize, - lineHeight: style.lineHeight, + trigger: { + right: triggerRect.right, + bottom: triggerRect.bottom, + top: triggerRect.top, + }, + menu: { + left: menuRect.left, + right: menuRect.right, + top: menuRect.top, + bottom: menuRect.bottom, + }, + optionRects, + map: mapRect + ? { + left: mapRect.left, + right: mapRect.right, + top: mapRect.top, + bottom: mapRect.bottom, + } + : null, + fontSize: optionStyle.fontSize, + lineHeight: optionStyle.lineHeight, documentWidth: document.documentElement.scrollWidth, viewportWidth: document.documentElement.clientWidth, overflowing: Array.from(document.querySelectorAll('body *')) @@ -543,8 +592,20 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or overflowing: [], }); expect(controlGeometry.documentWidth).toBeLessThanOrEqual(controlGeometry.viewportWidth + 1); - expect(controlGeometry.mapRight! - controlGeometry.right).toBeCloseTo(4, 1); - expect(controlGeometry.mapBottom! - controlGeometry.bottom).toBeCloseTo(4, 1); + expect(controlGeometry.map).not.toBeNull(); + expect(controlGeometry.map!.right - controlGeometry.trigger.right).toBeCloseTo(4, 1); + expect(controlGeometry.map!.bottom - controlGeometry.trigger.bottom).toBeCloseTo(4, 1); + expect(controlGeometry.menu.left).toBeGreaterThanOrEqual(controlGeometry.map!.left); + expect(controlGeometry.menu.right).toBeLessThanOrEqual(controlGeometry.map!.right); + expect(controlGeometry.menu.top).toBeGreaterThanOrEqual(controlGeometry.map!.top); + expect(controlGeometry.menu.bottom).toBeLessThan(controlGeometry.trigger.top); + expect(controlGeometry.optionRects).toHaveLength(2); + expect(controlGeometry.optionRects[0]!.bottom).toBeLessThanOrEqual(controlGeometry.optionRects[1]!.top); + expect(controlGeometry.optionRects[1]!.bottom).toBeLessThanOrEqual(controlGeometry.menu.bottom); + await mobilePage.screenshot({ path: testInfo.outputPath('mobile-map-options-open.png'), fullPage: true }); + + await mobilePage.locator('.map-area').tap({ position: { x: 10, y: 10 } }); + await expect(mobileOptionsTrigger).toHaveAttribute('aria-expanded', 'false'); const mobileCities = mobilePage.locator('.city-base'); await mobileCities.nth(0).tap(); @@ -560,17 +621,19 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or await expect(mobilePage).toHaveURL(/\/current-city\?cityId=2$/u); await go(mobilePage, 'global-info'); + await mobilePage.getByRole('button', { name: '지도 옵션' }).tap(); await mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 끄기' }).click(); const singleTapButton = mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 켜기' }); await expect(singleTapButton).toHaveAttribute('aria-pressed', 'true'); expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('yes'); await mobilePage.reload(); - await expect(singleTapButton).toBeVisible(); + await expect(singleTapButton).not.toBeVisible(); await mobilePage.locator('.city-base').nth(2).tap(); await expect(mobilePage).toHaveURL(/\/current-city\?cityId=3$/u); await go(mobilePage, 'global-info'); + await mobilePage.getByRole('button', { name: '지도 옵션' }).tap(); await mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 켜기' }).click(); expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('no'); await mobilePage.locator('.city-base').first().tap(); diff --git a/app/game-frontend/src/components/main/MapViewer.vue b/app/game-frontend/src/components/main/MapViewer.vue index c9c01be9..4a11e038 100644 --- a/app/game-frontend/src/components/main/MapViewer.vue +++ b/app/game-frontend/src/components/main/MapViewer.vue @@ -1,7 +1,7 @@