feat(game-ui): 토너먼트 단계별 메인 메뉴를 전환한다
This commit is contained in:
@@ -29,6 +29,8 @@ type NavigationFixture = {
|
|||||||
permission: number;
|
permission: number;
|
||||||
nationLevel: number;
|
nationLevel: number;
|
||||||
stage: number;
|
stage: number;
|
||||||
|
tournamentType?: 0 | 1 | 2 | 3;
|
||||||
|
tournamentWinnerId?: number;
|
||||||
npcMode: number;
|
npcMode: number;
|
||||||
generalMeCalls: number;
|
generalMeCalls: number;
|
||||||
operations: string[];
|
operations: string[];
|
||||||
@@ -758,7 +760,16 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
|
|||||||
canSecret: state.permission >= 2,
|
canSecret: state.permission >= 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (operation === 'tournament.getState') return response({ stage: state.stage });
|
if (operation === 'tournament.getState') {
|
||||||
|
if (state.stage === 0 && state.tournamentType === undefined && state.tournamentWinnerId === undefined) {
|
||||||
|
return response(null);
|
||||||
|
}
|
||||||
|
return response({
|
||||||
|
stage: state.stage,
|
||||||
|
type: state.tournamentType ?? 0,
|
||||||
|
winnerId: state.tournamentWinnerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
return response({ ok: true });
|
return response({ ok: true });
|
||||||
});
|
});
|
||||||
operations.forEach((operation, index) => {
|
operations.forEach((operation, index) => {
|
||||||
@@ -1273,9 +1284,9 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
|
|||||||
);
|
);
|
||||||
await tournamentToggle.click();
|
await tournamentToggle.click();
|
||||||
await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'true');
|
await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'true');
|
||||||
await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]')).toHaveText(
|
await expect(
|
||||||
'토너먼트'
|
nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]')
|
||||||
);
|
).toHaveText('토너먼트');
|
||||||
await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]')).toHaveAttribute(
|
await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]')).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
`${basePath}/betting`
|
`${basePath}/betting`
|
||||||
@@ -1437,6 +1448,95 @@ test('shows the persisted official game index beside the scenario title without
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tournament split main action follows recruitment, betting, finals, and tournament type on desktop and mobile', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
const state: NavigationFixture = {
|
||||||
|
officerLevel: 5,
|
||||||
|
permission: 2,
|
||||||
|
nationLevel: 3,
|
||||||
|
stage: 1,
|
||||||
|
tournamentType: 3,
|
||||||
|
npcMode: 1,
|
||||||
|
scenarioTitle: '토너먼트 동적 메뉴 검증 시나리오',
|
||||||
|
generalMeCalls: 0,
|
||||||
|
operations: [],
|
||||||
|
};
|
||||||
|
await installFixture(page, state);
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
{ stage: 1, type: 3 as const, label: '설 전', route: '/tournament' },
|
||||||
|
{ stage: 5, type: 2 as const, label: '일 기 토', route: '/tournament' },
|
||||||
|
{ stage: 6, type: 2 as const, label: '베 팅 장', route: '/betting' },
|
||||||
|
{ stage: 7, type: 2 as const, label: '일 기 토', route: '/tournament' },
|
||||||
|
{ stage: 10, type: 3 as const, label: '설 전', route: '/tournament' },
|
||||||
|
{ stage: 0, type: 3 as const, label: '설 전', route: '/tournament', winnerId: 17 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const viewport of [
|
||||||
|
{ width: 1200, height: 900 },
|
||||||
|
{ width: 500, height: 900 },
|
||||||
|
]) {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
for (const lifecycle of cases) {
|
||||||
|
state.stage = lifecycle.stage;
|
||||||
|
state.tournamentType = lifecycle.type;
|
||||||
|
state.tournamentWinnerId = lifecycle.winnerId;
|
||||||
|
if (page.url() === 'about:blank') {
|
||||||
|
await waitForMain(page);
|
||||||
|
} else {
|
||||||
|
await page.reload();
|
||||||
|
await waitForMain(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nationMenu = page.locator('.main-nation-menu:visible');
|
||||||
|
const main = nationMenu.locator('[data-navigation-id="tournament"]');
|
||||||
|
await expect(main).toHaveText(lifecycle.label);
|
||||||
|
await expect(main).toHaveAttribute('href', `${basePath}${lifecycle.route}`);
|
||||||
|
await expect(main).toHaveClass(/highlight/);
|
||||||
|
|
||||||
|
const toggle = nationMenu.locator('[data-menu-id="tournament-betting"]');
|
||||||
|
await toggle.click();
|
||||||
|
const tournamentItem = nationMenu.locator(
|
||||||
|
'#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]'
|
||||||
|
);
|
||||||
|
const bettingItem = nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]');
|
||||||
|
if (lifecycle.stage === 6) {
|
||||||
|
await expect(tournamentItem).not.toHaveClass(/highlight/);
|
||||||
|
await expect(bettingItem).toHaveClass(/highlight/);
|
||||||
|
} else {
|
||||||
|
await expect(tournamentItem).toHaveClass(/highlight/);
|
||||||
|
await expect(bettingItem).not.toHaveClass(/highlight/);
|
||||||
|
}
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
if (viewport.width === 500) {
|
||||||
|
const nationTrigger = page.locator('[data-bottom-menu="nation"]');
|
||||||
|
await nationTrigger.click();
|
||||||
|
const mobileTournament = page.locator('#mobile-nation-menu [data-navigation-id="tournament-menu"]');
|
||||||
|
const mobileBetting = page.locator('#mobile-nation-menu [data-navigation-id="betting"]');
|
||||||
|
if (lifecycle.stage === 6) {
|
||||||
|
await expect(mobileTournament).not.toHaveClass(/highlight/);
|
||||||
|
await expect(mobileBetting).toHaveClass(/highlight/);
|
||||||
|
} else {
|
||||||
|
await expect(mobileTournament).toHaveClass(/highlight/);
|
||||||
|
await expect(mobileBetting).not.toHaveClass(/highlight/);
|
||||||
|
}
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('.main-nation-menu:visible [data-menu-id="tournament-betting"]')
|
||||||
|
.locator('..')
|
||||||
|
.screenshot({
|
||||||
|
path: artifactRoot
|
||||||
|
? resolve(artifactRoot, `tournament-dynamic-main-${viewport.width}.png`)
|
||||||
|
: testInfo.outputPath(`tournament-dynamic-main-${viewport.width}.png`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('shows game index zero for a profile whose first game starts at zero', async ({ page }) => {
|
test('shows game index zero for a profile whose first game starts at zero', async ({ page }) => {
|
||||||
const state: NavigationFixture = {
|
const state: NavigationFixture = {
|
||||||
officerLevel: 5,
|
officerLevel: 5,
|
||||||
@@ -3986,9 +4086,7 @@ for (const viewport of [
|
|||||||
await picker.getByLabel('장비 종류', { exact: true }).selectOption('weapon');
|
await picker.getByLabel('장비 종류', { exact: true }).selectOption('weapon');
|
||||||
const equipment = picker.getByLabel('장비', { exact: true });
|
const equipment = picker.getByLabel('장비', { exact: true });
|
||||||
await equipment.selectOption('청룡언월도');
|
await equipment.selectOption('청룡언월도');
|
||||||
const optionLabelBeforeRefresh = await equipment
|
const optionLabelBeforeRefresh = await equipment.locator('option[value="청룡언월도"]').textContent();
|
||||||
.locator('option[value="청룡언월도"]')
|
|
||||||
.textContent();
|
|
||||||
await equipment.evaluate((element) => {
|
await equipment.evaluate((element) => {
|
||||||
const select = element as HTMLSelectElement;
|
const select = element as HTMLSelectElement;
|
||||||
const valueDescriptor = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value');
|
const valueDescriptor = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value');
|
||||||
@@ -4118,8 +4216,8 @@ test('keeps an Android Chromium native command select untouched while a turn sig
|
|||||||
await waitForMain(mobilePage);
|
await waitForMain(mobilePage);
|
||||||
await expect
|
await expect
|
||||||
.poll(() =>
|
.poll(() =>
|
||||||
mobilePage.evaluate(
|
mobilePage.evaluate(() =>
|
||||||
() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()
|
(window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.toBe(true);
|
.toBe(true);
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { legacyNationTextColor } from '../../utils/legacyNationColor';
|
|||||||
import MainNavigationLink from './MainNavigationLink.vue';
|
import MainNavigationLink from './MainNavigationLink.vue';
|
||||||
import {
|
import {
|
||||||
buildGlobalNavigation,
|
buildGlobalNavigation,
|
||||||
|
buildNationNavigation,
|
||||||
isNationNavigationEnabled,
|
isNationNavigationEnabled,
|
||||||
isNavigationConfigured,
|
isNavigationConfigured,
|
||||||
nationNavigation,
|
|
||||||
quickNavigation,
|
quickNavigation,
|
||||||
type MainNavigationLink as MainNavigationLinkItem,
|
type MainNavigationLink as MainNavigationLinkItem,
|
||||||
type MainNavigationEntry,
|
type MainNavigationEntry,
|
||||||
@@ -18,6 +18,7 @@ import { useMenuPopup } from './useMenuPopup';
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
access: NationNavigationAccess;
|
access: NationNavigationAccess;
|
||||||
tournamentStage: number;
|
tournamentStage: number;
|
||||||
|
tournamentType: number | null;
|
||||||
nationColor: string;
|
nationColor: string;
|
||||||
npcMode: number;
|
npcMode: number;
|
||||||
realtimeEnabled: boolean;
|
realtimeEnabled: boolean;
|
||||||
@@ -35,6 +36,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const { setRoot, openId, close, toggle } = useMenuPopup();
|
const { setRoot, openId, close, toggle } = useMenuPopup();
|
||||||
const globalEntries = computed(() => buildGlobalNavigation(props.npcMode, props.entries));
|
const globalEntries = computed(() => buildGlobalNavigation(props.npcMode, props.entries));
|
||||||
|
const nationEntries = computed(() => buildNationNavigation(props.tournamentStage, props.tournamentType));
|
||||||
const nationMenuColor = computed(() => props.nationColor || '#000000');
|
const nationMenuColor = computed(() => props.nationColor || '#000000');
|
||||||
const nationMenuTextColor = computed(() => legacyNationTextColor(nationMenuColor.value));
|
const nationMenuTextColor = computed(() => legacyNationTextColor(nationMenuColor.value));
|
||||||
const isActive = (link: MainNavigationLinkItem) =>
|
const isActive = (link: MainNavigationLinkItem) =>
|
||||||
@@ -150,7 +152,7 @@ const onAction = (action: NonNullable<MainNavigationLinkItem['action']>) => {
|
|||||||
<span class="dropup-caret" aria-hidden="true"></span>
|
<span class="dropup-caret" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul v-if="openId === 'nation'" id="mobile-nation-menu" class="bottom-popup" role="menu">
|
<ul v-if="openId === 'nation'" id="mobile-nation-menu" class="bottom-popup" role="menu">
|
||||||
<template v-for="entry in nationNavigation" :key="entry.id">
|
<template v-for="entry in nationEntries" :key="entry.id">
|
||||||
<li v-if="entry.kind === 'link'" role="none">
|
<li v-if="entry.kind === 'link'" role="none">
|
||||||
<MainNavigationLink
|
<MainNavigationLink
|
||||||
:link="entry"
|
:link="entry"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
import MainNavigationLink from './MainNavigationLink.vue';
|
import MainNavigationLink from './MainNavigationLink.vue';
|
||||||
import {
|
import {
|
||||||
|
buildNationNavigation,
|
||||||
isNationNavigationEnabled,
|
isNationNavigationEnabled,
|
||||||
nationNavigation,
|
|
||||||
type MainNavigationLink as MainNavigationLinkItem,
|
type MainNavigationLink as MainNavigationLinkItem,
|
||||||
type NationNavigationAccess,
|
type NationNavigationAccess,
|
||||||
} from './mainNavigation';
|
} from './mainNavigation';
|
||||||
@@ -12,10 +13,12 @@ import { legacyNationTextColor } from '../../utils/legacyNationColor';
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
access: NationNavigationAccess;
|
access: NationNavigationAccess;
|
||||||
tournamentStage: number;
|
tournamentStage: number;
|
||||||
|
tournamentType: number | null;
|
||||||
nationColor: string;
|
nationColor: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { setRoot, openId, close, toggle } = useMenuPopup();
|
const { setRoot, openId, close, toggle } = useMenuPopup();
|
||||||
|
const entries = computed(() => buildNationNavigation(props.tournamentStage, props.tournamentType));
|
||||||
const isActive = (link: MainNavigationLinkItem) =>
|
const isActive = (link: MainNavigationLinkItem) =>
|
||||||
link.highlightStage === props.tournamentStage ||
|
link.highlightStage === props.tournamentStage ||
|
||||||
link.highlightStages?.some((stage) => stage === props.tournamentStage) === true;
|
link.highlightStages?.some((stage) => stage === props.tournamentStage) === true;
|
||||||
@@ -29,7 +32,7 @@ const isActive = (link: MainNavigationLinkItem) =>
|
|||||||
:class="{ 'dark-label': legacyNationTextColor(nationColor) === '#000000' }"
|
:class="{ 'dark-label': legacyNationTextColor(nationColor) === '#000000' }"
|
||||||
aria-label="국가 메뉴"
|
aria-label="국가 메뉴"
|
||||||
>
|
>
|
||||||
<template v-for="entry in nationNavigation" :key="entry.id">
|
<template v-for="entry in entries" :key="entry.id">
|
||||||
<MainNavigationLink
|
<MainNavigationLink
|
||||||
v-if="entry.kind === 'link'"
|
v-if="entry.kind === 'link'"
|
||||||
:link="entry"
|
:link="entry"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
RuntimeNavigationLink,
|
RuntimeNavigationLink,
|
||||||
} from '@sammo-ts/common/navigation/menuConfig';
|
} from '@sammo-ts/common/navigation/menuConfig';
|
||||||
import defaultNavigationJson from '../../../../../resources/navigation.json';
|
import defaultNavigationJson from '../../../../../resources/navigation.json';
|
||||||
|
import { resolveTournamentMainPresentation } from '../../utils/tournamentNavigation';
|
||||||
|
|
||||||
export type NationAccessRule =
|
export type NationAccessRule =
|
||||||
'always' | 'meeting' | 'secret' | 'nation-member' | 'nation-established' | 'nation-secret';
|
'always' | 'meeting' | 'secret' | 'nation-member' | 'nation-established' | 'nation-secret';
|
||||||
@@ -11,8 +12,8 @@ export type NationAccessRule =
|
|||||||
export type MainNavigationLink = RuntimeNavigationLink & {
|
export type MainNavigationLink = RuntimeNavigationLink & {
|
||||||
compactLabel?: string;
|
compactLabel?: string;
|
||||||
access?: NationAccessRule;
|
access?: NationAccessRule;
|
||||||
highlightStage?: 1 | 6;
|
highlightStage?: number;
|
||||||
highlightStages?: readonly [1, 6];
|
highlightStages?: readonly number[];
|
||||||
unavailableReason?: string;
|
unavailableReason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,7 +156,6 @@ export const nationNavigation: MainNavigationEntry[] = [
|
|||||||
to: '/tournament',
|
to: '/tournament',
|
||||||
newTab: true,
|
newTab: true,
|
||||||
access: 'always',
|
access: 'always',
|
||||||
highlightStages: [1, 6],
|
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@@ -246,6 +246,39 @@ export const nationNavigation: MainNavigationEntry[] = [
|
|||||||
{ kind: 'link', id: 'my-settings', label: '화면 설정', to: '/my-settings', access: 'always' },
|
{ kind: 'link', id: 'my-settings', label: '화면 설정', to: '/my-settings', access: 'always' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const buildNationNavigation = (
|
||||||
|
tournamentStage: number,
|
||||||
|
tournamentType: number | null,
|
||||||
|
source: MainNavigationEntry[] = nationNavigation
|
||||||
|
): MainNavigationEntry[] =>
|
||||||
|
source.map((entry) => {
|
||||||
|
if (entry.kind !== 'split' || entry.id !== 'tournament-betting') return entry;
|
||||||
|
|
||||||
|
const presentation = resolveTournamentMainPresentation(tournamentStage, tournamentType);
|
||||||
|
const main: MainNavigationLink = {
|
||||||
|
...entry.main,
|
||||||
|
label: presentation.label,
|
||||||
|
compactLabel: presentation.compactLabel,
|
||||||
|
to: presentation.to,
|
||||||
|
highlightStage: presentation.active ? tournamentStage : undefined,
|
||||||
|
highlightStages: undefined,
|
||||||
|
};
|
||||||
|
const items = entry.items.map((item) => {
|
||||||
|
if (item.kind !== 'link') return item;
|
||||||
|
if (item.id === 'tournament-menu') {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
highlightStage: presentation.active && !presentation.bettingActive ? tournamentStage : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.id === 'betting') {
|
||||||
|
return { ...item, highlightStage: presentation.bettingActive ? tournamentStage : undefined };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
return { ...entry, main, items };
|
||||||
|
});
|
||||||
|
|
||||||
export const quickNavigation: Array<QuickNavigationItem | MainNavigationDivider> = [
|
export const quickNavigation: Array<QuickNavigationItem | MainNavigationDivider> = [
|
||||||
{ kind: 'divider', id: 'quick-nation-heading', label: '국가 정보' },
|
{ kind: 'divider', id: 'quick-nation-heading', label: '국가 정보' },
|
||||||
{ id: 'policy', label: '방침', tab: 'map', selector: '[data-main-target="policy"]' },
|
{ id: 'policy', label: '방침', tab: 'map', selector: '[data-main-target="policy"]' },
|
||||||
@@ -265,7 +298,8 @@ export const quickNavigation: Array<QuickNavigationItem | MainNavigationDivider>
|
|||||||
{ id: 'diplomacy-message', label: '외교', tab: 'messages', selector: '[data-message-type="diplomacy"]' },
|
{ id: 'diplomacy-message', label: '외교', tab: 'messages', selector: '[data-message-type="diplomacy"]' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const isNavigationConfigured = (link: MainNavigationLink): boolean => Boolean(link.to || link.href || link.action);
|
export const isNavigationConfigured = (link: MainNavigationLink): boolean =>
|
||||||
|
Boolean(link.to || link.href || link.action);
|
||||||
|
|
||||||
export const isNationNavigationEnabled = (link: MainNavigationLink, access: NationNavigationAccess): boolean => {
|
export const isNationNavigationEnabled = (link: MainNavigationLink, access: NationNavigationAccess): boolean => {
|
||||||
const rule = link.access ?? 'always';
|
const rule = link.access ?? 'always';
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
type RecentRecord = Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>['global'][number];
|
type RecentRecord = Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>['global'][number];
|
||||||
type FrontStatus = Awaited<ReturnType<typeof trpc.general.getFrontStatus.query>>;
|
type FrontStatus = Awaited<ReturnType<typeof trpc.general.getFrontStatus.query>>;
|
||||||
type TournamentState = Awaited<ReturnType<typeof trpc.tournament.getState.query>>;
|
type TournamentState = Awaited<ReturnType<typeof trpc.tournament.getState.query>>;
|
||||||
|
type TournamentType = NonNullable<TournamentState>['type'];
|
||||||
type ContextBundleDelta = Awaited<ReturnType<typeof trpc.dashboard.getContextBundleDelta.query>>;
|
type ContextBundleDelta = Awaited<ReturnType<typeof trpc.dashboard.getContextBundleDelta.query>>;
|
||||||
type DashboardReadModelPatch = {
|
type DashboardReadModelPatch = {
|
||||||
contextSnapshot?: GeneralContext;
|
contextSnapshot?: GeneralContext;
|
||||||
@@ -76,6 +77,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
worldHistory?: RecentRecord[];
|
worldHistory?: RecentRecord[];
|
||||||
frontStatus?: FrontStatus | null;
|
frontStatus?: FrontStatus | null;
|
||||||
tournamentStage?: number;
|
tournamentStage?: number;
|
||||||
|
tournamentType?: TournamentType | null;
|
||||||
};
|
};
|
||||||
type DashboardTabMessage =
|
type DashboardTabMessage =
|
||||||
{ kind: 'patch'; patch: DashboardReadModelPatch } | { kind: 'status'; status: 'idle' | 'connected' };
|
{ kind: 'patch'; patch: DashboardReadModelPatch } | { kind: 'status'; status: 'idle' | 'connected' };
|
||||||
@@ -117,6 +119,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
const worldHistory = ref<RecentRecord[]>([]);
|
const worldHistory = ref<RecentRecord[]>([]);
|
||||||
const frontStatus = ref<FrontStatus | null>(null);
|
const frontStatus = ref<FrontStatus | null>(null);
|
||||||
const tournamentStage = ref(0);
|
const tournamentStage = ref(0);
|
||||||
|
const tournamentType = ref<TournamentType | null>(null);
|
||||||
const surveyNotice = ref<NonNullable<FrontStatus['latestVote']> | null>(null);
|
const surveyNotice = ref<NonNullable<FrontStatus['latestVote']> | null>(null);
|
||||||
let lastGeneralRecordId = 0;
|
let lastGeneralRecordId = 0;
|
||||||
let lastWorldHistoryId = 0;
|
let lastWorldHistoryId = 0;
|
||||||
@@ -440,6 +443,9 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
if (patch.tournamentStage !== undefined) {
|
if (patch.tournamentStage !== undefined) {
|
||||||
tournamentStage.value = patch.tournamentStage;
|
tournamentStage.value = patch.tournamentStage;
|
||||||
}
|
}
|
||||||
|
if (patch.tournamentType !== undefined) {
|
||||||
|
tournamentType.value = patch.tournamentType;
|
||||||
|
}
|
||||||
if (patch.contextRevision !== undefined) {
|
if (patch.contextRevision !== undefined) {
|
||||||
contextRevision = patch.contextRevision;
|
contextRevision = patch.contextRevision;
|
||||||
contextSourceRevision = patch.contextSourceRevision ?? null;
|
contextSourceRevision = patch.contextSourceRevision ?? null;
|
||||||
@@ -487,6 +493,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
patch.worldHistory = toRaw(worldHistory.value);
|
patch.worldHistory = toRaw(worldHistory.value);
|
||||||
patch.frontStatus = toRaw(frontStatus.value);
|
patch.frontStatus = toRaw(frontStatus.value);
|
||||||
patch.tournamentStage = tournamentStage.value;
|
patch.tournamentStage = tournamentStage.value;
|
||||||
|
patch.tournamentType = tournamentType.value;
|
||||||
return patch;
|
return patch;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -637,6 +644,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
}
|
}
|
||||||
if (tournamentState !== undefined) {
|
if (tournamentState !== undefined) {
|
||||||
tournamentStage.value = tournamentState?.stage ?? 0;
|
tournamentStage.value = tournamentState?.stage ?? 0;
|
||||||
|
tournamentType.value = tournamentState?.type ?? null;
|
||||||
}
|
}
|
||||||
if (initializedMailboxGeneralId !== id) {
|
if (initializedMailboxGeneralId !== id) {
|
||||||
targetMailbox.value = MESSAGE_MAILBOX_NATIONAL_BASE + context.general.nationId;
|
targetMailbox.value = MESSAGE_MAILBOX_NATIONAL_BASE + context.general.nationId;
|
||||||
@@ -765,7 +773,10 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
patch.worldHistory = nextWorldHistory;
|
patch.worldHistory = nextWorldHistory;
|
||||||
}
|
}
|
||||||
if (nextFrontStatus) patch.frontStatus = nextFrontStatus;
|
if (nextFrontStatus) patch.frontStatus = nextFrontStatus;
|
||||||
if (tournamentState !== undefined) patch.tournamentStage = tournamentState?.stage ?? 0;
|
if (tournamentState !== undefined) {
|
||||||
|
patch.tournamentStage = tournamentState?.stage ?? 0;
|
||||||
|
patch.tournamentType = tournamentState?.type ?? null;
|
||||||
|
}
|
||||||
applyDashboardPatch(patch);
|
applyDashboardPatch(patch);
|
||||||
publishDashboardPatch(patch);
|
publishDashboardPatch(patch);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1290,6 +1301,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
worldHistory,
|
worldHistory,
|
||||||
frontStatus,
|
frontStatus,
|
||||||
tournamentStage,
|
tournamentStage,
|
||||||
|
tournamentType,
|
||||||
surveyNotice,
|
surveyNotice,
|
||||||
messageDraftText,
|
messageDraftText,
|
||||||
targetMailbox,
|
targetMailbox,
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
export interface TournamentMainPresentation {
|
||||||
|
label: string;
|
||||||
|
compactLabel: string;
|
||||||
|
to: '/tournament' | '/betting';
|
||||||
|
active: boolean;
|
||||||
|
bettingActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tournamentLabels = [
|
||||||
|
{ label: '전 력 전', compactLabel: '전력전' },
|
||||||
|
{ label: '통 솔 전', compactLabel: '통솔전' },
|
||||||
|
{ label: '일 기 토', compactLabel: '일기토' },
|
||||||
|
{ label: '설 전', compactLabel: '설전' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const resolveTournamentMainPresentation = (
|
||||||
|
tournamentStage: number,
|
||||||
|
tournamentType: number | null
|
||||||
|
): TournamentMainPresentation => {
|
||||||
|
const active = tournamentStage > 0 || tournamentType !== null;
|
||||||
|
const bettingActive = tournamentStage === 6;
|
||||||
|
const tournamentLabel = tournamentType === null ? undefined : tournamentLabels[tournamentType];
|
||||||
|
return {
|
||||||
|
label: bettingActive ? '베 팅 장' : (tournamentLabel?.label ?? '토 너 먼 트'),
|
||||||
|
compactLabel: bettingActive ? '베팅장' : (tournamentLabel?.compactLabel ?? '토너먼트'),
|
||||||
|
to: bettingActive ? '/betting' : '/tournament',
|
||||||
|
active,
|
||||||
|
bettingActive,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -83,6 +83,7 @@ const {
|
|||||||
worldHistory,
|
worldHistory,
|
||||||
frontStatus,
|
frontStatus,
|
||||||
tournamentStage,
|
tournamentStage,
|
||||||
|
tournamentType,
|
||||||
surveyNotice,
|
surveyNotice,
|
||||||
messageDraftText,
|
messageDraftText,
|
||||||
targetMailbox,
|
targetMailbox,
|
||||||
@@ -301,6 +302,7 @@ watch(
|
|||||||
class="nation-menu-middle"
|
class="nation-menu-middle"
|
||||||
:access="nationAccess"
|
:access="nationAccess"
|
||||||
:tournament-stage="tournamentStage"
|
:tournament-stage="tournamentStage"
|
||||||
|
:tournament-type="tournamentType"
|
||||||
:nation-color="nationColor"
|
:nation-color="nationColor"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -480,6 +482,7 @@ watch(
|
|||||||
class="nation-menu-middle"
|
class="nation-menu-middle"
|
||||||
:access="nationAccess"
|
:access="nationAccess"
|
||||||
:tournament-stage="tournamentStage"
|
:tournament-stage="tournamentStage"
|
||||||
|
:tournament-type="tournamentType"
|
||||||
:nation-color="nationColor"
|
:nation-color="nationColor"
|
||||||
/>
|
/>
|
||||||
<section class="record-zone">
|
<section class="record-zone">
|
||||||
@@ -570,6 +573,7 @@ watch(
|
|||||||
v-if="isMobile"
|
v-if="isMobile"
|
||||||
:access="nationAccess"
|
:access="nationAccess"
|
||||||
:tournament-stage="tournamentStage"
|
:tournament-stage="tournamentStage"
|
||||||
|
:tournament-type="tournamentType"
|
||||||
:nation-color="nationColor"
|
:nation-color="nationColor"
|
||||||
:npc-mode="npcMode"
|
:npc-mode="npcMode"
|
||||||
:realtime-enabled="realtimeEnabled"
|
:realtime-enabled="realtimeEnabled"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { resolveTournamentMainPresentation } from '../src/utils/tournamentNavigation.ts';
|
||||||
|
|
||||||
|
void test('routes every active tournament stage except betting to its type-specific tournament button', () => {
|
||||||
|
const expectedLabels = ['전력전', '통솔전', '일기토', '설전'];
|
||||||
|
for (const [type, expectedLabel] of expectedLabels.entries()) {
|
||||||
|
for (const stage of [1, 2, 3, 4, 5, 7, 8, 9, 10, 0]) {
|
||||||
|
const presentation = resolveTournamentMainPresentation(stage, type);
|
||||||
|
assert.equal(presentation.compactLabel, expectedLabel);
|
||||||
|
assert.equal(presentation.to, '/tournament');
|
||||||
|
assert.equal(presentation.active, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('routes the betting stage to the betting hall regardless of tournament type', () => {
|
||||||
|
for (const type of [0, 1, 2, 3]) {
|
||||||
|
const presentation = resolveTournamentMainPresentation(6, type);
|
||||||
|
assert.equal(presentation.compactLabel, '베팅장');
|
||||||
|
assert.equal(presentation.to, '/betting');
|
||||||
|
assert.equal(presentation.active, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('keeps the generic tournament button when no tournament state exists', () => {
|
||||||
|
const presentation = resolveTournamentMainPresentation(0, null);
|
||||||
|
assert.equal(presentation.compactLabel, '토너먼트');
|
||||||
|
assert.equal(presentation.to, '/tournament');
|
||||||
|
assert.equal(presentation.active, false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user