From bd894398b5b34d0a127bf95ddf88db878d95abda Mon Sep 17 00:00:00 2001 From: hided62 Date: Fri, 21 Aug 2026 23:01:48 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat(game-ui):=20=ED=86=A0=EB=84=88?= =?UTF-8?q?=EB=A8=BC=ED=8A=B8=EC=99=80=20=EB=B2=A0=ED=8C=85=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=EB=A5=BC=20=ED=95=A9=EC=B9=98=EA=B3=A0=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 토너먼트 기본 동작과 베팅 선택을 split 메뉴로 묶고 빈 국가 메뉴 칸에 화면 설정을 배치한다. 내 정보 화면에는 게임 상태 설정만 남기며 화면 폭, 모바일 패널 순서, 개인 CSS는 기기별 전용 화면에서 관리한다. --- app/game-frontend/e2e/inGameMenus.spec.ts | 82 +- app/game-frontend/e2e/mainNavigation.spec.ts | 39 +- app/game-frontend/src/assets/main.css | 1 + .../components/main/MainMobileBottomBar.vue | 5 +- .../src/components/main/MainNationMenu.vue | 5 +- .../src/components/main/mainNavigation.ts | 52 +- app/game-frontend/src/main.ts | 2 + app/game-frontend/src/router/index.ts | 4 +- app/game-frontend/src/utils/customCss.ts | 19 + app/game-frontend/src/views/MyPageView.vue | 334 +------- .../src/views/MySettingsView.vue | 792 +++++++++--------- 11 files changed, 567 insertions(+), 768 deletions(-) create mode 100644 app/game-frontend/src/utils/customCss.ts diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index 7b51451d..3e5f697a 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -22,6 +22,13 @@ const persistParityArtifact = async (page: Page, name: string, geometry: unknown ]); }; +const waitForVisualAssets = async (page: Page): Promise => { + await page.waitForLoadState('networkidle'); + await page.evaluate(async () => { + await document.fonts.ready; + }); +}; + const readGeneralPanelImages = async (panel: Locator) => panel.evaluate((element) => [...element.querySelectorAll('.general-image')].map((image) => { @@ -1270,6 +1277,9 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide await expect(page.locator('#container')).not.toContainText('che_'); await expect(page.locator('.title-row')).toContainText('내 정 보'); await expect(page.locator('#set_my_setting')).toBeVisible(); + await expect(page.getByRole('radiogroup', { name: '화면 폭 모드' })).toHaveCount(0); + await expect(page.getByRole('button', { name: '순서 바꾸기', exact: true })).toHaveCount(0); + await expect(page.locator('#custom_css')).toHaveCount(0); await expect(page.locator('.general-column [role="progressbar"]')).toHaveCount(14); await expect(page.locator('.general-column [aria-label*="1,275,975 (EX+)"]')).toHaveCount(5); await expect.poll(() => state.generalMeQueries).toBeGreaterThan(0); @@ -1290,7 +1300,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide const settings = element.querySelector('.settings-column')!.getBoundingClientRect(); const saveButton = element.querySelector('#set_my_setting')!; const save = saveButton.getBoundingClientRect(); - const customCss = element.querySelector('#custom_css')!.getBoundingClientRect(); const columns = getComputedStyle(element.querySelector('.top-grid')!).gridTemplateColumns; return { width: rect.width, @@ -1302,8 +1311,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide saveWidth: save.width, saveHeight: save.height, saveBackground: getComputedStyle(saveButton).backgroundColor, - customCssWidth: customCss.width, - customCssHeight: customCss.height, backgroundImage: getComputedStyle(element).backgroundImage, sectionBackgroundImage: getComputedStyle(element.querySelector('.section-title')!).backgroundImage, }; @@ -1317,8 +1324,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide expect(desktop.saveWidth).toBe(160); expect(desktop.saveHeight).toBe(30); expect(desktop.saveBackground).toBe('rgb(34, 85, 0)'); - expect(desktop.customCssWidth).toBe(420); - expect(desktop.customCssHeight).toBe(150); expect(desktop.backgroundImage).toContain('back_walnut.jpg'); expect(desktop.sectionBackgroundImage).toContain('back_green.jpg'); await expectLumenButtonStates(page, page.locator('#set_my_setting'), 'rgb(34, 85, 0)'); @@ -1583,11 +1588,66 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오 } }); -test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({ page }) => { +test('화면 설정에서 화면 폭과 개인 CSS를 저장하고 게임 설정 API는 호출하지 않는다', async ({ page }, testInfo) => { + const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] }; + await install(page, state); + await page.setViewportSize({ width: 1000, height: 900 }); + await page.goto('my-settings'); + await waitForVisualAssets(page); + + await expect(page.locator('.title-row')).toContainText('화 면 설 정'); + await expect(page.locator('.scope-note')).toContainText('게임 상태에는 영향을 주지 않습니다.'); + await expect(page.locator('#set_my_setting')).toHaveCount(0); + await expect(page.locator('#custom_css')).toBeVisible(); + expect(state.settingMutations).toHaveLength(0); + + const desktop = await page.locator('#interface-settings').evaluate((element) => { + const rect = element.getBoundingClientRect(); + const css = element.querySelector('#custom_css')!.getBoundingClientRect(); + return { + width: rect.width, + columns: getComputedStyle(element.querySelector('.settings-grid')!).gridTemplateColumns, + cssWidth: css.width, + cssHeight: css.height, + backgroundImage: getComputedStyle(element).backgroundImage, + sectionBackgroundImage: getComputedStyle(element.querySelector('.section-title')!).backgroundImage, + }; + }); + expect(desktop.width).toBe(1000); + expect(desktop.columns.split(' ')).toHaveLength(2); + expect(desktop.cssWidth).toBe(420); + expect(desktop.cssHeight).toBe(150); + expect(desktop.backgroundImage).toContain('back_walnut.jpg'); + expect(desktop.sectionBackgroundImage).toContain('back_green.jpg'); + await page.screenshot({ path: testInfo.outputPath('interface-settings-desktop.png'), fullPage: true }); + + await page.getByRole('radio', { name: '500px' }).check(); + await expect.poll(() => page.evaluate(() => localStorage.getItem('sam.screenMode'))).toBe('500px'); + await expect(page.locator('meta[name="viewport"]')).toHaveAttribute('content', 'width=500'); + + const cssText = '#interface-settings { --ui-settings-e2e: 23px; }'; + await page.getByLabel('개인용 CSS').fill(cssText); + await expect(page.locator('.custom-css span')).toHaveText('(저장 중)'); + await expect.poll(() => page.evaluate(() => localStorage.getItem('sam_customCSS'))).toBe(cssText); + await expect.poll(() => page.locator('#sammo-custom-css').textContent()).toBe(cssText); + await page.reload(); + await expect.poll(() => page.locator('#sammo-custom-css').textContent()).toBe(cssText); + expect(state.settingMutations).toHaveLength(0); + + await page.getByLabel('개인용 CSS').fill(''); + await expect.poll(() => page.evaluate(() => localStorage.getItem('sam_customCSS'))).toBe(''); + await page.getByRole('radio', { name: '자동' }).check(); + await persistParityArtifact(page, 'core-interface-settings-desktop', desktop); +}); + +test('화면 설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({ + page, +}, testInfo) => { const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] }; await install(page, state); await page.setViewportSize({ width: 390, height: 844 }); - await page.goto('my-page'); + await page.goto('my-settings'); + await waitForVisualAssets(page); await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click(); const dialog = page.getByRole('dialog', { name: '모바일 레이아웃 순서 바꾸기' }); @@ -1637,7 +1697,8 @@ test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버 expect(dialogGeometry.firstItem?.height).toBeGreaterThanOrEqual(44); expect(dialogGeometry.moveButton?.width).toBeGreaterThanOrEqual(36); expect(dialogGeometry.documentWidth).toBe(390); - await persistParityArtifact(page, 'core-my-page-mobile-layout-order-dialog', dialogGeometry); + await dialog.screenshot({ path: testInfo.outputPath('interface-settings-mobile-layout-dialog.png') }); + await persistParityArtifact(page, 'core-interface-settings-mobile-layout-order-dialog', dialogGeometry); await dialog.getByRole('button', { name: '적용', exact: true }).click(); await expect(dialog).toBeHidden(); @@ -1654,7 +1715,7 @@ test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버 .toEqual(defaultOrder); }); -test('실제 모바일 터치로 메인 패널 순서를 재정렬한다', async ({ browser }, testInfo) => { +test('화면 설정에서 실제 모바일 터치로 메인 패널 순서를 재정렬한다', async ({ browser }, testInfo) => { const configuredBaseUrl = testInfo.project.use.baseURL; if (typeof configuredBaseUrl !== 'string') { throw new Error('Playwright baseURL is required for the mobile touch contract'); @@ -1672,7 +1733,8 @@ test('실제 모바일 터치로 메인 패널 순서를 재정렬한다', async try { const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] }; await install(mobilePage, state); - await mobilePage.goto('my-page'); + await mobilePage.goto('my-settings'); + await waitForVisualAssets(mobilePage); await mobilePage.getByRole('button', { name: '순서 바꾸기', exact: true }).click(); const dialog = mobilePage.getByRole('dialog', { name: '모바일 레이아웃 순서 바꾸기' }); diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 9f78ed3f..bf0b1807 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -1260,7 +1260,27 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro await expect(global.locator('[data-navigation-id="board-community"]')).toHaveAttribute('href', '/xe/community'); await expect(global.locator('[data-navigation-id="official-chat"]')).toHaveAttribute('target', '_blank'); await expect(global.locator('[data-navigation-id="survey"]')).toHaveClass(/highlight/); - await expect(page.locator('.main-nation-menu [data-navigation-id="tournament"]')).toHaveClass(/highlight/); + const nationMenu = page.locator('.main-nation-menu:visible'); + await expect(nationMenu.locator(':scope > *')).toHaveCount(20); + const tournamentMain = nationMenu.locator('[data-navigation-id="tournament"]'); + const tournamentToggle = nationMenu.locator('[data-menu-id="tournament-betting"]'); + await expect(tournamentMain).toHaveClass(/highlight/); + await expect(tournamentMain).toHaveAttribute('href', `${basePath}/tournament`); + await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'false'); + await expect(nationMenu.locator('[data-navigation-id="my-settings"]')).toHaveAttribute( + 'href', + `${basePath}/my-settings` + ); + await tournamentToggle.click(); + await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'true'); + await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]')).toHaveText( + '토너먼트' + ); + await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]')).toHaveAttribute( + 'href', + `${basePath}/betting` + ); + await page.keyboard.press('Escape'); const gameInfoButton = global.locator('[data-menu-id="game-info"]'); const bettingButton = global.locator('[data-navigation-id="nation-betting"]'); @@ -1455,7 +1475,7 @@ test('nation split buttons keep square inner corners and a single divider in eve officerLevel: 5, permission: 2, nationLevel: 3, - stage: 1, + stage: 6, npcMode: 1, scenarioTitle: '분할 버튼 이음새 검증 시나리오', generalMeCalls: 0, @@ -1515,14 +1535,21 @@ test('nation split buttons keep square inner corners and a single divider in eve for (const width of [1200, 500]) { await page.setViewportSize({ width, height: 900 }); await waitForMain(page); - const nationSplit = page.locator('.main-nation-menu:visible .nation-menu-split').first(); const pairs: Array<[string, Locator, Locator]> = [ [ - 'nation', - nationSplit.locator('[data-navigation-id="auction-resource"]'), - nationSplit.locator('[data-menu-id="auction"]'), + 'tournament', + page.locator('.main-nation-menu:visible [data-navigation-id="tournament"]'), + page.locator('.main-nation-menu:visible [data-menu-id="tournament-betting"]'), + ], + [ + 'auction', + page.locator('.main-nation-menu:visible [data-navigation-id="auction-resource"]'), + page.locator('.main-nation-menu:visible [data-menu-id="auction"]'), ], ]; + await expect(page.locator('.main-nation-menu:visible [data-navigation-id="tournament"]')).toHaveClass( + /highlight/ + ); for (const [label, main, toggle] of pairs) { await expect(main).toBeVisible(); diff --git a/app/game-frontend/src/assets/main.css b/app/game-frontend/src/assets/main.css index 65148076..4912c428 100644 --- a/app/game-frontend/src/assets/main.css +++ b/app/game-frontend/src/assets/main.css @@ -42,6 +42,7 @@ body { /* These redesigned screens own a true handheld layout. */ #app:has(.responsive-settings-page), +#app:has(.interface-settings-page), #app:has(#tournament-container), #app:has(#tournament-betting-container), #app:has(#personnel-container) { diff --git a/app/game-frontend/src/components/main/MainMobileBottomBar.vue b/app/game-frontend/src/components/main/MainMobileBottomBar.vue index dc492b03..4ccd28c7 100644 --- a/app/game-frontend/src/components/main/MainMobileBottomBar.vue +++ b/app/game-frontend/src/components/main/MainMobileBottomBar.vue @@ -37,7 +37,9 @@ const { setRoot, openId, close, toggle } = useMenuPopup(); const globalEntries = computed(() => buildGlobalNavigation(props.npcMode, props.entries)); const nationMenuColor = computed(() => props.nationColor || '#000000'); const nationMenuTextColor = computed(() => legacyNationTextColor(nationMenuColor.value)); -const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props.tournamentStage; +const isActive = (link: MainNavigationLinkItem) => + link.highlightStage === props.tournamentStage || + link.highlightStages?.some((stage) => stage === props.tournamentStage) === true; const onQuick = (item: QuickNavigationItem) => { close(); @@ -165,6 +167,7 @@ const onAction = (action: NonNullable) => { (); const { setRoot, openId, close, toggle } = useMenuPopup(); -const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props.tournamentStage; +const isActive = (link: MainNavigationLinkItem) => + link.highlightStage === props.tournamentStage || + link.highlightStages?.some((stage) => stage === props.tournamentStage) === true;