fix: 토너먼트 단계의 메인 실시간 갱신을 연결

토너먼트 state의 stage 전이만 공용 실시간 채널에 발행하고 공개 invalidation으로 변환한다.
메인 dashboard store가 단계 값을 소유해 상단 문구와 메뉴 강조를 단일/다중 탭에서 함께 갱신한다.
Redis 통합 테스트와 production Chromium 회귀 검증을 추가한다.
This commit is contained in:
2026-08-17 01:41:37 +00:00
parent 1a4cd1a53f
commit 50068224c8
17 changed files with 392 additions and 31 deletions
+20 -2
View File
@@ -46,6 +46,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
type ReservedTurnView = Awaited<ReturnType<typeof trpc.turns.reserved.getGeneral.query>>['turns'][number];
type RecentRecord = Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>['global'][number];
type FrontStatus = Awaited<ReturnType<typeof trpc.general.getFrontStatus.query>>;
type TournamentState = Awaited<ReturnType<typeof trpc.tournament.getState.query>>;
type ContextBundleDelta = Awaited<ReturnType<typeof trpc.dashboard.getContextBundleDelta.query>>;
type DashboardReadModelPatch = {
contextSnapshot?: GeneralContext;
@@ -72,6 +73,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
generalRecords?: RecentRecord[];
worldHistory?: RecentRecord[];
frontStatus?: FrontStatus | null;
tournamentStage?: number;
};
type DashboardTabMessage =
{ kind: 'patch'; patch: DashboardReadModelPatch } | { kind: 'status'; status: 'idle' | 'connected' };
@@ -112,6 +114,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
const generalRecords = ref<RecentRecord[]>([]);
const worldHistory = ref<RecentRecord[]>([]);
const frontStatus = ref<FrontStatus | null>(null);
const tournamentStage = ref(0);
const surveyNotice = ref<NonNullable<FrontStatus['latestVote']> | null>(null);
let lastGeneralRecordId = 0;
let lastWorldHistoryId = 0;
@@ -430,6 +433,9 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
} else if (patch.frontStatus !== undefined) {
updateFrontStatus(patch.frontStatus);
}
if (patch.tournamentStage !== undefined) {
tournamentStage.value = patch.tournamentStage;
}
if (patch.contextRevision !== undefined) {
contextRevision = patch.contextRevision;
contextSourceRevision = patch.contextSourceRevision ?? null;
@@ -476,6 +482,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
patch.generalRecords = toRaw(generalRecords.value);
patch.worldHistory = toRaw(worldHistory.value);
patch.frontStatus = toRaw(frontStatus.value);
patch.tournamentStage = tournamentStage.value;
return patch;
};
@@ -585,7 +592,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
frontStatusError.value = resolveErrorMessage(err);
return null;
});
const [layout, lobby, map, messageData, contacts, generalTurns, records, nextFrontStatus] =
const tournamentPromise = trpc.tournament.getState.query().catch(() => undefined);
const [layout, lobby, map, messageData, contacts, generalTurns, records, nextFrontStatus, tournamentState] =
await Promise.all([
layoutPromise,
trpc.lobby.info.query(),
@@ -595,6 +603,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
generalTurnsPromise,
recordsPromise,
frontStatusPromise,
tournamentPromise,
]);
general.value = structurallyShare(general.value, context.general);
@@ -617,6 +626,9 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
if (nextFrontStatus) {
updateFrontStatus(nextFrontStatus);
}
if (tournamentState !== undefined) {
tournamentStage.value = tournamentState?.stage ?? 0;
}
if (initializedMailboxGeneralId !== id) {
targetMailbox.value = MESSAGE_MAILBOX_NATIONAL_BASE + context.general.nationId;
initializedMailboxGeneralId = id;
@@ -705,14 +717,18 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
return null;
})
: Promise.resolve(undefined);
const tournamentPromise: Promise<TournamentState | undefined> = plan.tournament
? trpc.tournament.getState.query().catch(() => undefined)
: Promise.resolve(undefined);
const [lobby, map, contacts, generalTurns, records, nextFrontStatus] = await Promise.all([
const [lobby, map, contacts, generalTurns, records, nextFrontStatus, tournamentState] = await Promise.all([
lobbyPromise,
mapPromise,
contactsPromise,
reservedPromise,
recordsPromise,
frontPromise,
tournamentPromise,
]);
const patch: DashboardReadModelPatch = { ...contextPatch };
@@ -733,6 +749,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
patch.worldHistory = nextWorldHistory;
}
if (nextFrontStatus) patch.frontStatus = nextFrontStatus;
if (tournamentState !== undefined) patch.tournamentStage = tournamentState?.stage ?? 0;
applyDashboardPatch(patch);
publishDashboardPatch(patch);
} catch (err) {
@@ -1241,6 +1258,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
generalRecords,
worldHistory,
frontStatus,
tournamentStage,
surveyNotice,
messageDraftText,
targetMailbox,
+2 -4
View File
@@ -27,7 +27,6 @@ const session = useSessionStore();
const dashboard = useMainDashboardStore();
const isMobile = useMediaQuery('(max-width: 939.98px)');
const tournamentStage = ref(0);
const npcMode = ref(0);
const {
@@ -52,6 +51,7 @@ const {
generalRecords,
worldHistory,
frontStatus,
tournamentStage,
surveyNotice,
messageDraftText,
targetMailbox,
@@ -107,12 +107,10 @@ const repeatGeneralTurns = (amount: number) => {
};
const loadMainData = async () => {
const [, state, worldState] = await Promise.all([
const [, worldState] = await Promise.all([
dashboard.loadMainData(),
trpc.tournament.getState.query().catch(() => null),
trpc.world.getState.query().catch(() => null),
]);
tournamentStage.value = state?.stage ?? 0;
npcMode.value = worldState?.config.npcMode ?? 0;
};