feat: profile 새 버전 안내 toast를 추가한다
배포 bundle과 같은 commit의 정적 버전 문서를 생성하고, 열린 탭이 DB mutation 없이 변경을 감지하도록 한다. 동일 버전은 tab session에서 한 번만 안내하며 강제 새로고침은 하지 않는다.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import GameFeedbackLayer from './components/ui/GameFeedbackLayer.vue';
|
||||
import { useDeploymentVersionNotice } from './composables/useDeploymentVersionNotice';
|
||||
|
||||
useDeploymentVersionNotice();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import { createDeploymentVersionChecker } from '../config/deploymentVersion';
|
||||
import { useGameFeedback } from './useGameFeedback';
|
||||
|
||||
const pollIntervalMs = 60_000;
|
||||
const noticeMessage = '새 버전이 준비되었습니다. 새로고침하면 변경사항이 반영됩니다.';
|
||||
|
||||
const resolveSessionStorage = (): Pick<Storage, 'getItem' | 'setItem'> | undefined => {
|
||||
try {
|
||||
return window.sessionStorage;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const useDeploymentVersionNotice = (): void => {
|
||||
const currentCommitSha = import.meta.env.VITE_BUILD_COMMIT_SHA?.trim() ?? '';
|
||||
const versionUrl = `${import.meta.env.BASE_URL}deployment-version.json`;
|
||||
const { info: showInfoToast } = useGameFeedback();
|
||||
const checker = createDeploymentVersionChecker({
|
||||
currentCommitSha,
|
||||
versionUrl,
|
||||
storage: resolveSessionStorage(),
|
||||
onVersionChanged: () => showInfoToast(noticeMessage),
|
||||
});
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const checkWhenVisible = (): void => {
|
||||
if (document.visibilityState === 'visible') void checker.check();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
void checker.check();
|
||||
pollTimer = setInterval(checkWhenVisible, pollIntervalMs);
|
||||
document.addEventListener('visibilitychange', checkWhenVisible);
|
||||
window.addEventListener('online', checkWhenVisible);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
document.removeEventListener('visibilitychange', checkWhenVisible);
|
||||
window.removeEventListener('online', checkWhenVisible);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
const fullCommitShaPattern = /^[0-9a-f]{40,64}$/iu;
|
||||
|
||||
type VersionStorage = Pick<Storage, 'getItem' | 'setItem'>;
|
||||
|
||||
export type DeploymentVersionCheckerOptions = {
|
||||
currentCommitSha: string;
|
||||
versionUrl: string;
|
||||
fetchVersion?: typeof fetch;
|
||||
storage?: VersionStorage;
|
||||
now?: () => number;
|
||||
onVersionChanged: (availableCommitSha: string) => void;
|
||||
};
|
||||
|
||||
export const deploymentVersionAssetSource = (buildCommitSha: string): string =>
|
||||
`${JSON.stringify({ commitSha: buildCommitSha })}\n`;
|
||||
|
||||
export const parseDeploymentCommitSha = (payload: unknown): string | null => {
|
||||
if (!payload || typeof payload !== 'object' || !('commitSha' in payload)) return null;
|
||||
const commitSha = String(payload.commitSha).trim().toLowerCase();
|
||||
return fullCommitShaPattern.test(commitSha) ? commitSha : null;
|
||||
};
|
||||
|
||||
const notificationStorageKey = (versionUrl: string, availableCommitSha: string): string =>
|
||||
`sammo:deployment-version-notice:${versionUrl}:${availableCommitSha}`;
|
||||
|
||||
export const createDeploymentVersionChecker = (options: DeploymentVersionCheckerOptions) => {
|
||||
const currentCommitSha = options.currentCommitSha.trim().toLowerCase();
|
||||
const fetchVersion = options.fetchVersion ?? fetch;
|
||||
const now = options.now ?? Date.now;
|
||||
let inFlight: Promise<void> | null = null;
|
||||
let lastNotifiedCommitSha: string | null = null;
|
||||
|
||||
const wasNotified = (key: string): boolean => {
|
||||
try {
|
||||
return options.storage?.getItem(key) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const rememberNotification = (key: string): void => {
|
||||
try {
|
||||
options.storage?.setItem(key, '1');
|
||||
} catch {
|
||||
// Session storage can be unavailable under restrictive browser policies.
|
||||
}
|
||||
};
|
||||
|
||||
const run = async (): Promise<void> => {
|
||||
if (!fullCommitShaPattern.test(currentCommitSha)) return;
|
||||
const separator = options.versionUrl.includes('?') ? '&' : '?';
|
||||
const response = await fetchVersion(`${options.versionUrl}${separator}t=${now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const availableCommitSha = parseDeploymentCommitSha(await response.json());
|
||||
if (!availableCommitSha || availableCommitSha === currentCommitSha) return;
|
||||
|
||||
const storageKey = notificationStorageKey(options.versionUrl, availableCommitSha);
|
||||
if (lastNotifiedCommitSha === availableCommitSha || wasNotified(storageKey)) return;
|
||||
lastNotifiedCommitSha = availableCommitSha;
|
||||
rememberNotification(storageKey);
|
||||
options.onVersionChanged(availableCommitSha);
|
||||
};
|
||||
|
||||
return {
|
||||
check: (): Promise<void> => {
|
||||
if (inFlight) return inFlight;
|
||||
inFlight = run()
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
inFlight = null;
|
||||
});
|
||||
return inFlight;
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user