From 3a52bb7830d9e7dcb880214cca5410ea360d71b2 Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 17 Aug 2026 15:32:48 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A9=94=EC=9D=B8=20=EA=B0=B1=EC=8B=A0?= =?UTF-8?q?=20=EC=A4=91=20=ED=81=B4=EB=A6=AD=EC=9D=84=20toast=EB=A1=9C=20?= =?UTF-8?q?=EC=95=88=EB=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 실시간 또는 수동 갱신 중에도 직접 갱신 버튼의 활성 시각 상태를 유지한다. 중복 클릭은 추가 조회 없이 공통 안내 toast로 전달하고 데스크톱·모바일 Chromium 회귀 검증을 보강한다. --- app/game-frontend/e2e/mainNavigation.spec.ts | 26 +++++++++++++++++-- .../components/main/MainMobileBottomBar.vue | 7 ----- app/game-frontend/src/views/MainView.vue | 15 ++++++++--- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 57383be0..ee9cbfc4 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -2107,16 +2107,24 @@ test('mobile bottom controls share Ref pressed geometry while their color bases await expect .poll(() => page.locator(manualRefreshSelector).evaluate((element) => getComputedStyle(element).boxShadow)) .not.toBe('none'); + const callsBeforeManualRefresh = state.generalMeCalls; await page.locator(manualRefreshSelector).click(); - await expect(page.locator(manualRefreshSelector)).toBeDisabled(); + await expect(page.locator(manualRefreshSelector)).toBeEnabled(); + await expect(page.locator(manualRefreshSelector)).toHaveAttribute('aria-busy', 'true'); + await page.locator(manualRefreshSelector).click(); + await expect(page.getByTestId('game-toast')).toContainText('이미 정보를 갱신하고 있습니다.'); + await page.mouse.move(1, 1); expect(await buttonStyle(manualRefreshSelector)).toMatchObject({ backgroundColor: 'rgb(33, 37, 41)', borderBottomWidth: '4px', marginTop: '0px', height: 45, }); - await expect(page.locator(manualRefreshSelector)).toHaveCSS('opacity', '0.55'); + await expect(page.locator(manualRefreshSelector)).toHaveCSS('opacity', '1'); + await page.screenshot({ path: testInfo.outputPath('mobile-refresh-busy-toast.png'), fullPage: true }); + await expect(page.locator(manualRefreshSelector)).toHaveAttribute('aria-busy', 'false'); await expect(page.locator(manualRefreshSelector)).toBeEnabled(); + expect(state.generalMeCalls).toBe(callsBeforeManualRefresh + 1); await persistArtifact(page, `${basePath.slice(1)}-mobile-bottom-ref-buttons`); }); @@ -2685,6 +2693,7 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl const operationsBeforeChangedBurst = state.operations.length; state.generalName = '부드럽게갱신된장수'; + state.refreshDelayMs = 1_000; await page.evaluate(() => { const emit = (window as unknown as { __emitMainRealtime: (type: string, payload: unknown) => void }) .__emitMainRealtime; @@ -2706,7 +2715,20 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl } }); + const manualRefresh = page.getByRole('button', { name: '갱 신' }); + await expect(manualRefresh).toHaveAttribute('aria-busy', 'true'); + await expect(manualRefresh).toBeEnabled(); + await manualRefresh.click(); + await expect(page.getByTestId('game-toast')).toContainText('이미 정보를 갱신하고 있습니다.'); + if (autoRefreshArtifactRoot) { + await page.screenshot({ + path: resolve(autoRefreshArtifactRoot, 'auto-refresh-busy-feedback.png'), + fullPage: true, + }); + } + await expect.poll(() => state.generalMeCalls, { timeout: 3_000 }).toBe(callsBeforeRefresh + 1); + state.refreshDelayMs = 300; await expect(page.locator('[data-main-target="general"] .skeleton-line')).toHaveCount(0); await expect(page.locator('[data-main-target="city"] .skeleton-line')).toHaveCount(0); await expect(page.getByRole('button', { name: '갱 신' })).toHaveAttribute('aria-busy', 'false'); diff --git a/app/game-frontend/src/components/main/MainMobileBottomBar.vue b/app/game-frontend/src/components/main/MainMobileBottomBar.vue index 11deec41..f3de99be 100644 --- a/app/game-frontend/src/components/main/MainMobileBottomBar.vue +++ b/app/game-frontend/src/components/main/MainMobileBottomBar.vue @@ -226,7 +226,6 @@ const onQuick = (item: QuickNavigationItem) => { data-bottom-menu="manual-refresh" aria-label="직접 갱신" title="직접 갱신" - :disabled="refreshing" :aria-busy="refreshing" @click="emit('refresh')" > @@ -306,12 +305,6 @@ const onQuick = (item: QuickNavigationItem) => { line-height: 1; } -.manual-refresh-trigger:disabled { - cursor: wait; - filter: grayscale(0.6); - opacity: 0.55; -} - .bottom-trigger.legacy-button:not(:disabled, [aria-disabled='true']):hover, .bottom-trigger.legacy-button:not(:disabled, [aria-disabled='true'])[aria-expanded='true'] { height: 44px; diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index 1e4daab3..3ab408fe 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -20,11 +20,13 @@ import type { QuickNavigationItem } from '../components/main/mainNavigation'; import { formatLog } from '../utils/formatLog'; import { useSessionStore } from '../stores/session'; import { useMainDashboardStore } from '../stores/mainDashboard'; +import { useGameFeedback } from '../composables/useGameFeedback'; import { trpc } from '../utils/trpc'; import type { CommandPatternEntry } from '../components/command/types'; const session = useSessionStore(); const dashboard = useMainDashboardStore(); +const { info: showInfoToast } = useGameFeedback(); const isMobile = useMediaQuery('(max-width: 939.98px)'); const npcMode = ref(0); @@ -115,6 +117,14 @@ const loadMainData = async () => { npcMode.value = worldState?.config.npcMode ?? 0; }; +const requestManualRefresh = () => { + if (refreshing.value) { + showInfoToast('이미 정보를 갱신하고 있습니다.'); + return; + } + void loadMainData(); +}; + const moveLobby = () => { window.location.replace(import.meta.env.VITE_GATEWAY_WEB_URL?.trim() || '/gateway/'); }; @@ -154,9 +164,8 @@ watch( @@ -451,7 +460,7 @@ watch( :npc-mode="npcMode" :realtime-enabled="realtimeEnabled" :refreshing="refreshing" - @refresh="loadMainData" + @refresh="requestManualRefresh" @toggle-realtime="dashboard.setRealtimeEnabled(!realtimeEnabled)" @lobby="moveLobby" @quick="moveQuick"