From c741ef4be5e1296c3e66ff603109d37e0397664f Mon Sep 17 00:00:00 2001 From: hided62 Date: Fri, 28 Aug 2026 06:56:44 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=B0=94?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=B9=EC=B9=A8=EA=B3=BC=20=EC=A0=84=EC=9A=A9=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EB=B0=B0=EA=B2=BD=EC=9D=84=20?= =?UTF-8?q?=EB=B0=94=EB=A1=9C=EC=9E=A1=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/mainNavigation.spec.ts | 163 +++++++++++++++++- .../src/components/main/GeneralBasicCard.vue | 13 +- .../src/components/main/MainGlobalMenu.vue | 4 + .../src/components/main/MessagePanel.vue | 4 + app/game-frontend/src/views/MainView.vue | 3 +- 5 files changed, 177 insertions(+), 10 deletions(-) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 4db5f643..0852890a 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -35,6 +35,8 @@ type NavigationFixture = { generalMeCalls: number; operations: string[]; generalName?: string; + generalPicture?: string | null; + generalImageServer?: number; generalTurnTime?: string; nextTurnMonthOffset?: 0 | 1; serverTime?: string; @@ -446,8 +448,8 @@ const generalContext = (state: NavigationFixture) => ({ officerCityName: state.officerLevel >= 2 && state.officerLevel <= 4 ? '업' : null, generalType: '용장', leadershipBonus: state.officerLevel === 12 ? state.nationLevel * 2 : 0, - picture: null, - imageServer: 0, + picture: state.generalPicture ?? null, + imageServer: state.generalImageServer ?? 0, stats: { leadership: 70, strength: 60, intelligence: 50 }, gold: 1_000, rice: 2_000, @@ -1428,6 +1430,85 @@ test('main info fills the eighth slot with Ref-compatible autorun status and an await expect(page.getByRole('tooltip')).toHaveCount(0); }); +test('main user icon keeps transparent pixels clear and only falls back after an image error', async ({ + page, +}, testInfo) => { + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 0, + npcMode: 1, + generalPicture: 'transparent-main.png', + generalImageServer: 1, + generalMeCalls: 0, + operations: [], + }; + await page.route('**/icons/transparent-main.png', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'image/png', + body: Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAF/gL+X1N6WQAAAABJRU5ErkJggg==', + 'base64' + ), + }); + }); + await installFixture(page, state); + await page.setViewportSize({ width: 500, height: 900 }); + await waitForMain(page); + + const icon = page.locator('[data-main-target="general"] .general-icon'); + await expect(icon).toHaveJSProperty('tagName', 'IMG'); + await expect(icon).toHaveAttribute('src', /\/icons\/transparent-main\.png$/u); + await expect(icon).toHaveCSS('background-image', 'none'); + await expect(icon).toHaveCSS('object-fit', 'contain'); + await icon.screenshot({ path: testInfo.outputPath('transparent-main-user-icon.png') }); +}); + +test('mobile top menu popup stays above the eight-cell game information strip', async ({ page }, testInfo) => { + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 0, + npcMode: 1, + generalMeCalls: 0, + operations: [], + }; + await installFixture(page, state); + await page.setViewportSize({ width: 500, height: 900 }); + await waitForMain(page); + + const topMenu = page.locator('[data-menu-position="top"]'); + const gameInfoButton = topMenu.locator('[data-menu-id="game-info"]'); + await gameInfoButton.click(); + const popup = topMenu.locator('#global-menu-game-info'); + await expect(popup).toBeVisible(); + + const stacking = await popup.evaluate((element) => { + const menu = element.closest('.main-global-menu'); + const gameInfo = document.querySelector('.legacy-game-info'); + if (!menu || !gameInfo) throw new Error('mobile top menu stacking targets are missing'); + const popupRect = element.getBoundingClientRect(); + const gameInfoRect = gameInfo.getBoundingClientRect(); + const left = Math.max(popupRect.left, gameInfoRect.left); + const right = Math.min(popupRect.right, gameInfoRect.right); + const top = Math.max(popupRect.top, gameInfoRect.top); + const bottom = Math.min(popupRect.bottom, gameInfoRect.bottom); + if (right <= left || bottom <= top) throw new Error('mobile popup does not overlap the game information strip'); + const hit = document.elementFromPoint((left + right) / 2, (top + bottom) / 2); + return { + menuZIndex: Number(getComputedStyle(menu).zIndex), + gameInfoZIndex: Number(getComputedStyle(gameInfo).zIndex), + hitInsidePopup: hit === element || element.contains(hit), + }; + }); + expect(stacking.menuZIndex).toBeGreaterThan(stacking.gameInfoZIndex); + expect(stacking.hitInsidePopup).toBe(true); + await page.screenshot({ path: testInfo.outputPath('mobile-top-menu-game-info-stacking.png') }); +}); + test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({ page, }, testInfo) => { @@ -2026,6 +2107,84 @@ test('the repeated bottom global menu opens upward on the mobile document', asyn await persistArtifact(page, `${basePath.slice(1)}-mobile-bottom-dropup`); }); +test('mobile message content owns its full height and leaves the diplomacy action clear of the bottom bar', async ({ + page, +}, testInfo) => { + const target = (generalId: number, generalName: string) => ({ + generalId, + generalName, + nationId: 1, + nationName: '위', + color: '#008000', + icon: '', + }); + const entries = (type: 'public' | 'national' | 'private' | 'diplomacy', startId: number) => + Array.from({ length: 12 }, (_, index) => ({ + id: startId + index, + text: `${type} 모바일 높이 검증 메시지 ${index + 1}`, + time: '2026-08-12 12:00:00', + msgType: type, + src: target(21, '보낸장수'), + dest: type === 'public' ? null : target(7, '메뉴검증장수'), + option: {}, + })); + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 0, + npcMode: 1, + generalMeCalls: 0, + operations: [], + messages: { + ...emptyMessages(2), + public: entries('public', 100), + national: entries('national', 200), + private: entries('private', 300), + diplomacy: entries('diplomacy', 400), + }, + }; + await installFixture(page, state); + await page.setViewportSize({ width: 500, height: 900 }); + await waitForMain(page); + + const panel = page.locator('.mobile-message-panel'); + const diplomacyLoadOlder = panel.locator('.DiplomacyTalk .load-older'); + await expect(diplomacyLoadOlder).toBeVisible(); + const flowGeometry = await panel.evaluate((element) => { + const repeated = document.querySelector('[data-menu-position="bottom"]'); + const privateTalk = element.querySelector('.PrivateTalk'); + const actions = element.querySelector('.DiplomacyTalk .Actions'); + if (!repeated || !privateTalk || !actions) throw new Error('mobile message flow targets are missing'); + return { + panel: element.getBoundingClientRect().toJSON(), + panelClientHeight: element.clientHeight, + panelScrollHeight: element.scrollHeight, + privateTalk: privateTalk.getBoundingClientRect().toJSON(), + actionsColumns: getComputedStyle(actions).gridTemplateColumns, + repeatedMenu: repeated.getBoundingClientRect().toJSON(), + }; + }); + expect(flowGeometry.panelClientHeight).toBeGreaterThanOrEqual(flowGeometry.panelScrollHeight); + expect(flowGeometry.repeatedMenu.top).toBeGreaterThanOrEqual(flowGeometry.panel.bottom - 0.5); + expect(flowGeometry.repeatedMenu.top).toBeGreaterThanOrEqual(flowGeometry.privateTalk.bottom - 0.5); + expect(flowGeometry.actionsColumns.split(' ')).toHaveLength(2); + + await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight)); + const bottomClearance = await diplomacyLoadOlder.evaluate((button) => { + const bottomBar = document.querySelector('.main-mobile-bottom'); + const spacer = document.querySelector('.main-mobile-bottom-spacer'); + if (!bottomBar || !spacer) throw new Error('mobile bottom clearance targets are missing'); + return { + clearance: bottomBar.getBoundingClientRect().top - button.getBoundingClientRect().bottom, + spacerHeight: spacer.getBoundingClientRect().height, + }; + }); + expect(bottomClearance.clearance).toBeGreaterThanOrEqual(12); + expect(bottomClearance.spacerHeight).toBeGreaterThanOrEqual(61); + await page.screenshot({ path: testInfo.outputPath('mobile-diplomacy-bottom-clearance.png') }); +}); + test('first reserved month crosses December only after the general turn has passed', async ({ page }, testInfo) => { const state: NavigationFixture = { officerLevel: 0, diff --git a/app/game-frontend/src/components/main/GeneralBasicCard.vue b/app/game-frontend/src/components/main/GeneralBasicCard.vue index 64ff9d7c..c0233704 100644 --- a/app/game-frontend/src/components/main/GeneralBasicCard.vue +++ b/app/game-frontend/src/components/main/GeneralBasicCard.vue @@ -6,7 +6,7 @@ import LegacyProgressBar from '../ui/LegacyProgressBar.vue'; import RichTooltip from '../ui/RichTooltip.vue'; import { formatLocalTimeSeconds } from '../../utils/legacyDateTime'; import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress'; -import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconBackgroundImage } from '../../utils/generalIcon'; +import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconUrl, useDefaultGeneralIcon } from '../../utils/generalIcon'; import { configuredGameAssetUrl } from '../../utils/imageAssets'; import { legacyLuminanceTextColor } from '../../utils/legacyNationColor'; @@ -164,7 +164,7 @@ const experiencePercent = computed(() => const itemNames = computed(() => props.general?.itemNames ?? props.general?.equipmentNames ?? {}); -const generalIconBackground = computed(() => resolveGeneralIconBackgroundImage(props.general ?? {})); +const generalIconUrl = computed(() => resolveGeneralIconUrl(props.general ?? {})); const crewTypeIconBackground = computed(() => { const crewTypeId = props.general?.crewTypeId; @@ -251,11 +251,11 @@ const specialText = computed(() => {
장수 정보를 불러오지 못했습니다.