From d1950070d0dbded80ae5c2133f4220946ec32290 Mon Sep 17 00:00:00 2001 From: hided62 Date: Thu, 27 Aug 2026 06:00:18 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EA=B0=80=EC=98=A4=ED=94=88=20=EC=84=A4?= =?UTF-8?q?=EB=AC=B8=20=EC=95=8C=EB=A6=BC=EC=9D=98=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 동일한 front status가 연속 도착해도 방금 열린 설문 알림을 즉시 닫지 않도록 cursor 처리를 멱등하게 만든다. production Chromium 회귀로 실시간 재조회 뒤 알림 유지를 검증한다. --- app/game-frontend/e2e/mainNavigation.spec.ts | 9 +++++++++ app/game-frontend/src/stores/mainDashboard.ts | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index edbe8e46..2cbfbddc 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -1158,6 +1158,7 @@ test('scopes the new-survey notice cursor to the reset-specific server ID', asyn generalMeCalls: 0, operations: [], }; + await installRealtimeHarness(page); await installFixture(page, state); await page.addInitScript(() => { localStorage.setItem('state.che.lastVote', '99'); @@ -1170,6 +1171,14 @@ test('scopes the new-survey notice cursor to the reset-specific server ID', asyn .poll(() => page.evaluate(() => localStorage.getItem('state.che_260819_new_season.lastVote'))) .toBe('1'); expect(await page.evaluate(() => localStorage.getItem('state.che.lastVote'))).toBe('99'); + + await waitForMainRealtime(page); + const operationsBeforeDuplicate = state.operations.length; + await emitReadModelInvalidation(page, readModelInvalidation({ frontStatus: true })); + await expect + .poll(() => state.operations.slice(operationsBeforeDuplicate), { timeout: 3_000 }) + .toEqual(['dashboard.getContextBundleDelta', 'general.getFrontStatus']); + await expect(page.locator('.survey-notice')).toContainText('새로운 설문조사가 있습니다.'); }); test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({ diff --git a/app/game-frontend/src/stores/mainDashboard.ts b/app/game-frontend/src/stores/mainDashboard.ts index 43d406ca..8fbd5447 100644 --- a/app/game-frontend/src/stores/mainDashboard.ts +++ b/app/game-frontend/src/stores/mainDashboard.ts @@ -306,7 +306,13 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => { const storageKey = `state.${serverId}.lastVote`; const lastSeenVoteId = Number.parseInt(window.localStorage.getItem(storageKey) ?? '0', 10); if (latestVote.id <= (Number.isFinite(lastSeenVoteId) ? lastSeenVoteId : 0)) { - surveyNotice.value = null; + // Initial mounting, general creation and realtime invalidation can fetch + // the same front status in quick succession. The first response records + // the cursor; a duplicate response must not immediately dismiss the + // notice that response just opened. + if (surveyNotice.value?.id !== latestVote.id) { + surveyNotice.value = null; + } return; } window.localStorage.setItem(storageKey, latestVote.id.toString());