From 5549f0c1e82f4be66a04010282d3c4dcd141c2f5 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 22 Aug 2026 05:11:50 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20profile=20=EC=83=88=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=95=88=EB=82=B4=20toast=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배포 bundle과 같은 commit의 정적 버전 문서를 생성하고, 열린 탭이 DB mutation 없이 변경을 감지하도록 한다. 동일 버전은 tab session에서 한 번만 안내하며 강제 새로고침은 하지 않는다. --- README.md | 3 +- .../e2e/deploymentVersionNotice.spec.ts | 64 +++++++++++ app/game-frontend/e2e/playwright.config.mjs | 1 + app/game-frontend/src/App.vue | 3 + .../composables/useDeploymentVersionNotice.ts | 45 ++++++++ .../src/config/deploymentVersion.ts | 78 +++++++++++++ .../test/deploymentVersionNotice.test.ts | 105 ++++++++++++++++++ app/game-frontend/test/viteConfig.test.ts | 11 ++ app/game-frontend/vite.config.ts | 18 ++- .../test/profileFrontendMaterializer.test.ts | 4 + docs/architecture/runtime.md | 10 ++ docs/release-operations.md | 5 + 12 files changed, 343 insertions(+), 4 deletions(-) create mode 100644 app/game-frontend/e2e/deploymentVersionNotice.spec.ts create mode 100644 app/game-frontend/src/composables/useDeploymentVersionNotice.ts create mode 100644 app/game-frontend/src/config/deploymentVersion.ts create mode 100644 app/game-frontend/test/deploymentVersionNotice.test.ts diff --git a/README.md b/README.md index 43b5ad73..db4c9008 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,8 @@ Node.js는 `.nvmrc`에서 24.x로 고정합니다. 의존성 설치와 검증에 `package.json`과 `pnpm-lock.yaml`을 함께 사용해 주세요. 모든 workspace package는 내부 전용(`private`)이므로 manifest의 `0.0.0`은 배포 버전이 아닙니다. 배포 source는 full Git commit으로 고정하고, 실험 릴리스 같은 milestone은 annotated Git tag로 -식별합니다. +식별합니다. Profile frontend는 같은 commit의 정적 버전 문서를 주기적으로 확인하고, +현재 열린 bundle과 달라졌을 때만 강제 reload 없이 공용 toast로 한 번 안내합니다. ## 개발 환경 diff --git a/app/game-frontend/e2e/deploymentVersionNotice.spec.ts b/app/game-frontend/e2e/deploymentVersionNotice.spec.ts new file mode 100644 index 00000000..f8a40e43 --- /dev/null +++ b/app/game-frontend/e2e/deploymentVersionNotice.spec.ts @@ -0,0 +1,64 @@ +import { expect, test, type Page } from '@playwright/test'; +import { gamePath } from './gameTestPaths.js'; + +const currentCommitSha = process.env.PLAYWRIGHT_BUILD_COMMIT_SHA ?? '0123456789abcdef0123456789abcdef01234567'; +const nextCommitSha = '89abcdef0123456789abcdef0123456789abcdef'; +const noticeMessage = '새 버전이 준비되었습니다. 새로고침하면 변경사항이 반영됩니다.'; + +const installVersionFixture = async (page: Page) => { + let availableCommitSha = currentCommitSha; + let requests = 0; + await page.route('**/deployment-version.json*', async (route) => { + requests += 1; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ commitSha: availableCommitSha }), + }); + }); + return { + deployNextVersion: () => { + availableCommitSha = nextCommitSha; + }, + requestCount: () => requests, + }; +}; + +for (const viewport of [ + { name: 'desktop', width: 1280, height: 800 }, + { name: 'mobile', width: 390, height: 844 }, +]) { + test(`shows one quiet update toast without forcing reload on ${viewport.name}`, async ({ page }) => { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + const fixture = await installVersionFixture(page); + await page.goto(gamePath('/version-notice-fixture')); + await expect(page.getByRole('heading', { name: 'Not Found' })).toBeVisible(); + await expect.poll(fixture.requestCount).toBeGreaterThan(0); + await page.evaluate(() => { + Object.assign(window, { __versionNoticePageMarker: 'kept' }); + }); + + fixture.deployNextVersion(); + await expect + .poll(async () => { + await page.evaluate(() => window.dispatchEvent(new Event('online'))); + return fixture.requestCount(); + }) + .toBeGreaterThan(1); + const toast = page.getByTestId('game-toast').filter({ hasText: noticeMessage }); + await expect(toast).toBeVisible(); + await expect(toast).toHaveAttribute('data-feedback-kind', 'info'); + await expect(toast).toHaveCSS('transform', 'none'); + const box = await toast.boundingBox(); + expect(box).not.toBeNull(); + expect(box!.x).toBeGreaterThanOrEqual(0); + expect(box!.y).toBeGreaterThanOrEqual(0); + expect(box!.x + box!.width).toBeLessThanOrEqual(viewport.width); + expect(box!.y + box!.height).toBeLessThanOrEqual(viewport.height); + expect(await page.evaluate(() => Reflect.get(window, '__versionNoticePageMarker'))).toBe('kept'); + + await page.getByRole('button', { name: '알림 닫기' }).click(); + await page.evaluate(() => window.dispatchEvent(new Event('online'))); + await expect(toast).toHaveCount(0); + }); +} diff --git a/app/game-frontend/e2e/playwright.config.mjs b/app/game-frontend/e2e/playwright.config.mjs index 55766660..5f422500 100644 --- a/app/game-frontend/e2e/playwright.config.mjs +++ b/app/game-frontend/e2e/playwright.config.mjs @@ -42,6 +42,7 @@ export default defineConfig({ 'session-auth.spec.ts', 'npcPossession.spec.ts', 'joinLayout.spec.ts', + 'deploymentVersionNotice.spec.ts', ], fullyParallel: false, workers: 1, diff --git a/app/game-frontend/src/App.vue b/app/game-frontend/src/App.vue index 6baf484f..eae06717 100644 --- a/app/game-frontend/src/App.vue +++ b/app/game-frontend/src/App.vue @@ -1,6 +1,9 @@