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"