From 70b80e7afa4805f450b3fe42150435a208da9d2f Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 16 Sep 2026 15:16:18 +0000 Subject: [PATCH] =?UTF-8?q?fix(ui):=20Gateway=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EB=A9=94=EB=89=B4=EC=99=80=20=EB=A1=9C=EB=B9=84=20?= =?UTF-8?q?=EB=B3=B5=EA=B7=80=20=EB=A7=81=ED=81=AC=20=EA=B2=B9=EC=B9=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../e2e/lobby-admin-navigation.spec.ts | 63 ++++++++++++++++++- .../src/layouts/AdminConsoleLayout.vue | 21 +++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts b/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts index 7d4b39dd..18907ddc 100644 --- a/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts +++ b/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts @@ -8,7 +8,7 @@ const operationNames = (route: Route): string[] => { return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6)).split(','); }; -const installGatewayFixture = async (page: Page, roles: string[]) => { +const installGatewayFixture = async (page: Page, roles: string[], manyProfiles = false) => { const requests: Array<{ method: string; url: string; body: unknown }> = []; await page.addInitScript(() => { window.localStorage.setItem('sammo-session-token', 'playwright-admin-session'); @@ -62,6 +62,16 @@ const installGatewayFixture = async (page: Page, roles: string[]) => { ); } if (operation === 'admin.profiles.listNavigation') { + if (manyProfiles) + return response( + ['che', 'kwe', 'twe', 'hwe', 'nya', 'pwe', 'pya'].map((profile) => ({ + profileName: `${profile}:default`, + profile, + instanceKey: 'default', + displayName: `${profile.toUpperCase()} 서버`, + currentScenario: '1010', + })) + ); return response( roles.some((role) => role === 'superuser' || role.includes(':hwe:2')) ? [ @@ -378,3 +388,54 @@ test('scoped administrators see the same navigation while ordinary users do not' await expect(userPage.getByRole('link', { name: '관리자 페이지' })).toHaveCount(0); await userContext.close(); }); + +for (const viewport of [ + { width: 1280, height: 720 }, + { width: 1200, height: 500 }, + { width: 390, height: 844 }, +]) { + test(`administrator exit stays separate from seven-profile navigation at ${viewport.width}x${viewport.height}`, async ({ + page, + }, testInfo) => { + await page.setViewportSize(viewport); + await installGatewayFixture(page, ['superuser'], true); + await page.goto('admin'); + if (viewport.width < 860) await page.getByRole('button', { name: '관리자 메뉴' }).click(); + else await page.evaluate(() => window.scrollTo(0, 76)); + const nav = page.getByRole('navigation', { name: '관리자 메뉴' }); + await expect(nav.getByRole('link', { name: 'PYA 서버' })).toBeVisible(); + const lastLink = nav.getByRole('link', { name: '감사 로그' }); + await lastLink.scrollIntoViewIfNeeded(); + await lastLink.focus(); + await expect(lastLink).toBeFocused(); + const exit = page.getByRole('link', { name: '← 로비로 돌아가기', exact: true }); + await exit.scrollIntoViewIfNeeded(); + const geometry = await page.locator('#admin-navigation').evaluate((sidebar) => { + const nav = sidebar.querySelector('nav')!; + const exit = sidebar.querySelector('.admin-exit')!; + const last = sidebar.querySelector('.admin-nav-group:last-child .admin-nav-link:last-child')!; + return { + navBottom: nav.getBoundingClientRect().bottom, + exitTop: exit.getBoundingClientRect().top, + lastBottom: last.getBoundingClientRect().bottom, + width: document.documentElement.scrollWidth, + navHeight: nav.clientHeight, + navScrollHeight: nav.scrollHeight, + exitPosition: getComputedStyle(exit).position, + }; + }); + expect(geometry.navBottom).toBeLessThanOrEqual(geometry.exitTop); + expect(geometry.lastBottom).toBeLessThanOrEqual(geometry.exitTop); + expect(geometry.width).toBeLessThanOrEqual(viewport.width); + if (viewport.width >= 860) expect(geometry.navScrollHeight).toBeGreaterThan(geometry.navHeight); + await page.evaluate(() => document.fonts.ready); + await writeFile(testInfo.outputPath('sidebar-geometry.json'), JSON.stringify(geometry)); + await writeFile(testInfo.outputPath('sidebar.html'), await page.content()); + await page.screenshot({ path: testInfo.outputPath('sidebar.png'), fullPage: true }); + await exit.hover(); + await exit.focus(); + await expect(exit).toBeFocused(); + await exit.click(); + await expect(page).toHaveURL(/\/gateway\/lobby$/); + }); +} diff --git a/app/gateway-frontend/src/layouts/AdminConsoleLayout.vue b/app/gateway-frontend/src/layouts/AdminConsoleLayout.vue index 01615a26..f6bdf901 100644 --- a/app/gateway-frontend/src/layouts/AdminConsoleLayout.vue +++ b/app/gateway-frontend/src/layouts/AdminConsoleLayout.vue @@ -212,12 +212,22 @@ onMounted(async () => { position: sticky; top: 0; height: 100vh; + display: flex; + flex-direction: column; border-right: 1px solid #27272a; background: #111113; padding: 24px 16px; } +.admin-sidebar > nav { + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior-y: contain; +} + .admin-identity { + flex-shrink: 0; display: flex; align-items: center; gap: 11px; @@ -317,10 +327,9 @@ onMounted(async () => { } .admin-exit { - position: absolute; - right: 24px; - bottom: 24px; - left: 24px; + flex-shrink: 0; + display: block; + margin: 16px 8px 0; border-top: 1px solid #27272a; padding-top: 17px; color: #71717a; @@ -418,6 +427,10 @@ onMounted(async () => { padding: 18px 14px 14px; } + .admin-sidebar > nav { + overflow-y: visible; + } + .admin-sidebar.open { display: block; }