merge: 토너먼트 단계별 노출을 main에 통합한다
This commit is contained in:
@@ -126,7 +126,15 @@ const persistScreenshot = async (page: Page, name: string, fallbackPath: string)
|
|||||||
await page.screenshot({ path: resolve(responsiveArtifactDir, `${name}.webp`), fullPage: true });
|
await page.screenshot({ path: resolve(responsiveArtifactDir, `${name}.webp`), fullPage: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const installFixture = async (page: Page, options: { applicationOpen?: boolean; tournamentType?: number } = {}) => {
|
const installFixture = async (
|
||||||
|
page: Page,
|
||||||
|
options: {
|
||||||
|
applicationOpen?: boolean;
|
||||||
|
tournamentType?: number;
|
||||||
|
tournamentStage?: number;
|
||||||
|
joinedGroupId?: number;
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
let joined = false;
|
let joined = false;
|
||||||
await page.addInitScript((profile) => {
|
await page.addInitScript((profile) => {
|
||||||
window.localStorage.setItem('sammo-game-token', 'ga_tournament_bracket_playwright');
|
window.localStorage.setItem('sammo-game-token', 'ga_tournament_bracket_playwright');
|
||||||
@@ -148,9 +156,11 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean;
|
|||||||
if (operation === 'general.me') return response({ general: { id: 1, name: names[0] } });
|
if (operation === 'general.me') return response({ general: { id: 1, name: names[0] } });
|
||||||
if (operation === 'tournament.getAdminStatus') return response({ ok: false });
|
if (operation === 'tournament.getAdminStatus') return response({ ok: false });
|
||||||
if (operation === 'tournament.getSnapshot') {
|
if (operation === 'tournament.getSnapshot') {
|
||||||
|
const tournamentStage = options.tournamentStage ?? (options.applicationOpen ? 1 : 0);
|
||||||
|
const joinedGroupId = options.joinedGroupId ?? 0;
|
||||||
return response({
|
return response({
|
||||||
state: {
|
state: {
|
||||||
stage: options.applicationOpen ? 1 : 0,
|
stage: tournamentStage,
|
||||||
phase: 0,
|
phase: 0,
|
||||||
type: options.tournamentType ?? 0,
|
type: options.tournamentType ?? 0,
|
||||||
auto: false,
|
auto: false,
|
||||||
@@ -158,7 +168,7 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean;
|
|||||||
openMonth: 1,
|
openMonth: 1,
|
||||||
termSeconds: 60,
|
termSeconds: 60,
|
||||||
nextAt: '2026-08-02T00:00:00.000Z',
|
nextAt: '2026-08-02T00:00:00.000Z',
|
||||||
winnerId: 1,
|
winnerId: tournamentStage === 0 ? 1 : undefined,
|
||||||
},
|
},
|
||||||
participants:
|
participants:
|
||||||
options.applicationOpen && !joined
|
options.applicationOpen && !joined
|
||||||
@@ -167,8 +177,10 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean;
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
...participants[0],
|
...participants[0],
|
||||||
groupId: 0,
|
groupId: joinedGroupId,
|
||||||
groupNo: 0,
|
groupNo: 0,
|
||||||
|
preliminaryGroupId: joinedGroupId,
|
||||||
|
preliminaryGroupNo: 0,
|
||||||
win: 0,
|
win: 0,
|
||||||
draw: 0,
|
draw: 0,
|
||||||
lose: 0,
|
lose: 0,
|
||||||
@@ -322,20 +334,36 @@ test('desktop bracket connects every real general slot to the next round', async
|
|||||||
await persistScreenshot(page, 'tournament-desktop', testInfo.outputPath('tournament-bracket-desktop.webp'));
|
await persistScreenshot(page, 'tournament-desktop', testInfo.outputPath('tournament-bracket-desktop.webp'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('join refresh shows the assigned preliminary group immediately with accessible controls', async ({ page }) => {
|
test('join refresh shows the assigned preliminary group immediately with accessible controls', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await installFixture(page, { applicationOpen: true });
|
await installFixture(page, { applicationOpen: true, joinedGroupId: 5 });
|
||||||
await page.goto('tournament');
|
await page.goto('tournament');
|
||||||
|
|
||||||
const refresh = page.getByRole('button', { name: '갱신' });
|
const refresh = page.getByRole('button', { name: '갱신' });
|
||||||
const join = page.getByRole('button', { name: '참가' });
|
const join = page.getByRole('button', { name: '참가' });
|
||||||
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
||||||
await expect(join).toBeEnabled();
|
await expect(join).toBeEnabled();
|
||||||
|
await expect(page.getByText('조별 예선 순위')).toBeVisible();
|
||||||
|
await expect(page.getByText('조별 본선 순위')).toHaveCount(0);
|
||||||
|
await expect(page.getByLabel('토너먼트 대진표')).toHaveCount(0);
|
||||||
await join.click();
|
await join.click();
|
||||||
|
|
||||||
await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다.');
|
await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다. 六조에 배정되었습니다.');
|
||||||
await expect(join).toBeDisabled();
|
await expect(join).toBeDisabled();
|
||||||
await expect(page.locator('.preliminary-grid .general-identity', { hasText: names[0] })).toBeVisible();
|
const preliminaryTabs = page.getByRole('tablist', { name: '예선 조 선택' });
|
||||||
|
await expect(preliminaryTabs.getByRole('tab').nth(5)).toHaveAttribute('aria-selected', 'true');
|
||||||
|
const assignedGroup = page.locator('[data-preliminary-group="5"]');
|
||||||
|
await expect(assignedGroup.locator('.general-identity', { hasText: names[0] })).toBeVisible();
|
||||||
|
const assignedGroupBounds = await assignedGroup.boundingBox();
|
||||||
|
expect(assignedGroupBounds?.y).toBeLessThan(844);
|
||||||
|
expect((assignedGroupBounds?.y ?? 0) + (assignedGroupBounds?.height ?? 0)).toBeGreaterThan(0);
|
||||||
|
await persistScreenshot(
|
||||||
|
page,
|
||||||
|
'tournament-joined-group-mobile',
|
||||||
|
testInfo.outputPath('tournament-joined-group.webp')
|
||||||
|
);
|
||||||
|
|
||||||
for (const control of [refresh, join, close]) {
|
for (const control of [refresh, join, close]) {
|
||||||
const box = await control.boundingBox();
|
const box = await control.boundingBox();
|
||||||
@@ -349,6 +377,41 @@ test('join refresh shows the assigned preliminary group immediately with accessi
|
|||||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('desktop join scrolls the assigned preliminary group into view without future sections', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
await page.setViewportSize({ width: 1365, height: 900 });
|
||||||
|
await installFixture(page, { applicationOpen: true, joinedGroupId: 7 });
|
||||||
|
await page.goto('tournament');
|
||||||
|
|
||||||
|
await expect(page.getByText('조별 본선 순위')).toHaveCount(0);
|
||||||
|
await expect(page.getByLabel('토너먼트 대진표')).toHaveCount(0);
|
||||||
|
await page.getByRole('button', { name: '참가' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다. 八조에 배정되었습니다.');
|
||||||
|
const assignedGroup = page.locator('[data-preliminary-group="7"]');
|
||||||
|
await expect(assignedGroup.locator('.general-identity', { hasText: names[0] })).toBeVisible();
|
||||||
|
const bounds = await assignedGroup.boundingBox();
|
||||||
|
expect(bounds?.y).toBeLessThan(900);
|
||||||
|
expect((bounds?.y ?? 0) + (bounds?.height ?? 0)).toBeGreaterThan(0);
|
||||||
|
await persistScreenshot(
|
||||||
|
page,
|
||||||
|
'tournament-joined-group-desktop',
|
||||||
|
testInfo.outputPath('tournament-joined-group.webp')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('final group section appears before the later knockout section', async ({ page }, testInfo) => {
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await installFixture(page, { tournamentStage: 3 });
|
||||||
|
await page.goto('tournament');
|
||||||
|
|
||||||
|
await expect(page.getByText('조별 예선 순위')).toBeVisible();
|
||||||
|
await expect(page.getByText('조별 본선 순위')).toBeVisible();
|
||||||
|
await expect(page.getByLabel('토너먼트 대진표')).toHaveCount(0);
|
||||||
|
await persistScreenshot(page, 'tournament-final-stage-mobile', testInfo.outputPath('tournament-final-stage.webp'));
|
||||||
|
});
|
||||||
|
|
||||||
test('mobile bracket exposes every round through tabs with standard horizontal identities', async ({
|
test('mobile bracket exposes every round through tabs with standard horizontal identities', async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
|
|||||||
@@ -13,3 +13,18 @@ export const tournamentStageNames = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const resolveTournamentStageName = (stage: number): string => tournamentStageNames[stage] ?? '상태 확인 중';
|
export const resolveTournamentStageName = (stage: number): string => tournamentStageNames[stage] ?? '상태 확인 중';
|
||||||
|
|
||||||
|
export interface TournamentSectionVisibility {
|
||||||
|
preliminary: boolean;
|
||||||
|
final: boolean;
|
||||||
|
knockout: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveTournamentSectionVisibility = (stage: number, winnerId?: number): TournamentSectionVisibility => {
|
||||||
|
const completed = stage === 0 && winnerId !== undefined;
|
||||||
|
return {
|
||||||
|
preliminary: stage >= 1 || completed,
|
||||||
|
final: stage >= 3 || completed,
|
||||||
|
knockout: stage >= 5 || completed,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
|
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
|
||||||
import TournamentPageHeader from '../components/tournament/TournamentPageHeader.vue';
|
import TournamentPageHeader from '../components/tournament/TournamentPageHeader.vue';
|
||||||
import GeneralIdentity from '../components/ui/GeneralIdentity.vue';
|
import GeneralIdentity from '../components/ui/GeneralIdentity.vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { resolveTournamentStageName } from '../utils/tournamentStatus';
|
import { resolveTournamentSectionVisibility, resolveTournamentStageName } from '../utils/tournamentStatus';
|
||||||
|
|
||||||
type Snapshot = Awaited<ReturnType<typeof trpc.tournament.getSnapshot.query>>;
|
type Snapshot = Awaited<ReturnType<typeof trpc.tournament.getSnapshot.query>>;
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ const actionMessage = ref<string | null>(null);
|
|||||||
const adminEnabled = ref(false);
|
const adminEnabled = ref(false);
|
||||||
const activeFinalGroup = ref(0);
|
const activeFinalGroup = ref(0);
|
||||||
const activePreliminaryGroup = ref(0);
|
const activePreliminaryGroup = ref(0);
|
||||||
|
const tournamentContainer = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const typeNames = ['전력전', '통솔전', '일기토', '설전'];
|
const typeNames = ['전력전', '통솔전', '일기토', '설전'];
|
||||||
const typeStatNames = ['종합', '통솔', '무력', '지력'];
|
const typeStatNames = ['종합', '통솔', '무력', '지력'];
|
||||||
@@ -63,6 +64,12 @@ const myBetTotals = computed(() => betting.value?.myTotals as Record<number, num
|
|||||||
const isParticipant = computed(() =>
|
const isParticipant = computed(() =>
|
||||||
(snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value)
|
(snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value)
|
||||||
);
|
);
|
||||||
|
const sectionVisibility = computed(() =>
|
||||||
|
resolveTournamentSectionVisibility(snapshot.value?.state?.stage ?? 0, snapshot.value?.state?.winnerId)
|
||||||
|
);
|
||||||
|
const preliminaryGroupIdOf = (participant: Snapshot['participants'][number]): number | undefined =>
|
||||||
|
participant.preliminaryGroupId ??
|
||||||
|
(participant.groupId !== undefined && participant.groupId < 8 ? participant.groupId : undefined);
|
||||||
const groups = computed(() =>
|
const groups = computed(() =>
|
||||||
Array.from({ length: 8 }, (_, index) =>
|
Array.from({ length: 8 }, (_, index) =>
|
||||||
(snapshot.value?.participants ?? [])
|
(snapshot.value?.participants ?? [])
|
||||||
@@ -74,10 +81,7 @@ const preliminaryGroups = computed(() =>
|
|||||||
Array.from({ length: 8 }, (_, index) =>
|
Array.from({ length: 8 }, (_, index) =>
|
||||||
(snapshot.value?.participants ?? [])
|
(snapshot.value?.participants ?? [])
|
||||||
.filter((participant) => {
|
.filter((participant) => {
|
||||||
const groupId =
|
return preliminaryGroupIdOf(participant) === index;
|
||||||
participant.preliminaryGroupId ??
|
|
||||||
(participant.groupId !== undefined && participant.groupId < 8 ? participant.groupId : undefined);
|
|
||||||
return groupId === index;
|
|
||||||
})
|
})
|
||||||
.map((participant) => ({
|
.map((participant) => ({
|
||||||
...participant,
|
...participant,
|
||||||
@@ -113,15 +117,38 @@ const currentMatch = computed(() => {
|
|||||||
return matchesAt(state.stage).find((match) => !match.winnerId) ?? matchesAt(state.stage)[state.phase] ?? null;
|
return matchesAt(state.stage).find((match) => !match.winnerId) ?? matchesAt(state.stage)[state.phase] ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const revealMyPreliminaryGroup = async (): Promise<number | undefined> => {
|
||||||
|
const participant = (snapshot.value?.participants ?? []).find((entry) => entry.id === myGeneralId.value);
|
||||||
|
if (!participant) return undefined;
|
||||||
|
|
||||||
|
const groupId = preliminaryGroupIdOf(participant);
|
||||||
|
if (groupId === undefined || groupId < 0 || groupId >= groupNames.length) return undefined;
|
||||||
|
|
||||||
|
activePreliminaryGroup.value = groupId;
|
||||||
|
await nextTick();
|
||||||
|
tournamentContainer.value
|
||||||
|
?.querySelector<HTMLElement>(`[data-preliminary-group="${groupId}"]`)
|
||||||
|
?.scrollIntoView({ block: 'center' });
|
||||||
|
return groupId;
|
||||||
|
};
|
||||||
|
|
||||||
const join = async () => {
|
const join = async () => {
|
||||||
actionMessage.value = null;
|
actionMessage.value = null;
|
||||||
|
let joined = false;
|
||||||
try {
|
try {
|
||||||
await trpc.tournament.join.mutate();
|
await trpc.tournament.join.mutate();
|
||||||
actionMessage.value = '참가 신청이 반영되었습니다.';
|
joined = true;
|
||||||
} catch (value) {
|
} catch (value) {
|
||||||
actionMessage.value = errorText(value);
|
actionMessage.value = errorText(value);
|
||||||
} finally {
|
} finally {
|
||||||
await load();
|
await load();
|
||||||
|
if (joined) {
|
||||||
|
const groupId = await revealMyPreliminaryGroup();
|
||||||
|
actionMessage.value =
|
||||||
|
groupId === undefined
|
||||||
|
? '참가 신청이 반영되었습니다.'
|
||||||
|
: `참가 신청이 반영되었습니다. ${groupNames[groupId]}조에 배정되었습니다.`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -159,7 +186,7 @@ const start = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main id="tournament-container" class="legacy-page">
|
<main id="tournament-container" ref="tournamentContainer" class="legacy-page">
|
||||||
<TournamentPageHeader class="bg0" active-page="tournament" title="삼모전 토너먼트" />
|
<TournamentPageHeader class="bg0" active-page="tournament" title="삼모전 토너먼트" />
|
||||||
|
|
||||||
<section class="toolbar bg0">
|
<section class="toolbar bg0">
|
||||||
@@ -183,145 +210,152 @@ const start = async () => {
|
|||||||
({{ resolveTournamentStageName(snapshot?.state?.stage ?? 0) }}, 개막시간 {{ openingTime }}, 경기당
|
({{ resolveTournamentStageName(snapshot?.state?.stage ?? 0) }}, 개막시간 {{ openingTime }}, 경기당
|
||||||
{{ snapshot?.state?.termSeconds ?? '-' }}초)
|
{{ snapshot?.state?.termSeconds ?? '-' }}초)
|
||||||
</section>
|
</section>
|
||||||
<section class="section-title bg2">16강 승자전</section>
|
<template v-if="sectionVisibility.knockout">
|
||||||
|
<section class="section-title bg2">16강 승자전</section>
|
||||||
|
|
||||||
<TournamentBracket
|
<TournamentBracket
|
||||||
class="bg0"
|
class="bg0"
|
||||||
:participants="snapshot?.participants ?? []"
|
:participants="snapshot?.participants ?? []"
|
||||||
:matches="snapshot?.matches ?? []"
|
:matches="snapshot?.matches ?? []"
|
||||||
:winner-id="snapshot?.state?.winnerId"
|
:winner-id="snapshot?.state?.winnerId"
|
||||||
:bet-totals="betTotals"
|
:bet-totals="betTotals"
|
||||||
:my-bet-totals="myBetTotals"
|
:my-bet-totals="myBetTotals"
|
||||||
:total-bet="totalBet"
|
:total-bet="totalBet"
|
||||||
:tournament-type="snapshot?.state?.type ?? 0"
|
:tournament-type="snapshot?.state?.type ?? 0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section v-if="currentMatch" class="fight bg0">
|
<section v-if="currentMatch" class="fight bg0">
|
||||||
<h2>{{ nameOf(currentMatch.attackerId) }} vs {{ nameOf(currentMatch.defenderId) }}</h2>
|
<h2>{{ nameOf(currentMatch.attackerId) }} vs {{ nameOf(currentMatch.defenderId) }}</h2>
|
||||||
<p v-for="(line, index) in currentMatch.log ?? []" :key="index">{{ line }}</p>
|
<p v-for="(line, index) in currentMatch.log ?? []" :key="index">{{ line }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<section class="section-title groups-title bg2">조별 본선 순위</section>
|
<template v-if="sectionVisibility.final">
|
||||||
<div class="group-tabs bg0" role="tablist" aria-label="본선 조 선택">
|
<section class="section-title groups-title bg2">조별 본선 순위</section>
|
||||||
<button
|
<div class="group-tabs bg0" role="tablist" aria-label="본선 조 선택">
|
||||||
v-for="(groupName, groupIndex) in groupNames"
|
<button
|
||||||
:key="`final-tab-${groupName}`"
|
v-for="(groupName, groupIndex) in groupNames"
|
||||||
type="button"
|
:key="`final-tab-${groupName}`"
|
||||||
role="tab"
|
type="button"
|
||||||
:aria-selected="activeFinalGroup === groupIndex"
|
role="tab"
|
||||||
:class="{ active: activeFinalGroup === groupIndex }"
|
:aria-selected="activeFinalGroup === groupIndex"
|
||||||
@click="activeFinalGroup = groupIndex"
|
:class="{ active: activeFinalGroup === groupIndex }"
|
||||||
>
|
@click="activeFinalGroup = groupIndex"
|
||||||
{{ groupName }}조
|
>
|
||||||
</button>
|
{{ groupName }}조
|
||||||
</div>
|
</button>
|
||||||
<section class="group-grid bg0">
|
</div>
|
||||||
<table
|
<section class="group-grid bg0">
|
||||||
v-for="(group, groupIndex) in groups"
|
<table
|
||||||
:key="groupIndex"
|
v-for="(group, groupIndex) in groups"
|
||||||
:class="{ 'mobile-active': activeFinalGroup === groupIndex }"
|
:key="groupIndex"
|
||||||
>
|
:class="{ 'mobile-active': activeFinalGroup === groupIndex }"
|
||||||
<caption>
|
>
|
||||||
{{
|
<caption>
|
||||||
groupNames[groupIndex]
|
{{
|
||||||
}}조
|
groupNames[groupIndex]
|
||||||
</caption>
|
}}조
|
||||||
<thead>
|
</caption>
|
||||||
<tr>
|
<thead>
|
||||||
<th>순</th>
|
<tr>
|
||||||
<th>장수</th>
|
<th>순</th>
|
||||||
<th>{{ typeStatNames[snapshot?.state?.type ?? 0] }}</th>
|
<th>장수</th>
|
||||||
<th>경</th>
|
<th>{{ typeStatNames[snapshot?.state?.type ?? 0] }}</th>
|
||||||
<th>승</th>
|
<th>경</th>
|
||||||
<th>무</th>
|
<th>승</th>
|
||||||
<th>패</th>
|
<th>무</th>
|
||||||
<th>점</th>
|
<th>패</th>
|
||||||
<th>득</th>
|
<th>점</th>
|
||||||
</tr>
|
<th>득</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr v-for="rowIndex in 4" :key="rowIndex">
|
<tbody>
|
||||||
<td>{{ rowIndex }}</td>
|
<tr v-for="rowIndex in 4" :key="rowIndex">
|
||||||
<td class="general-cell">
|
<td>{{ rowIndex }}</td>
|
||||||
<GeneralIdentity
|
<td class="general-cell">
|
||||||
v-if="group[rowIndex - 1]"
|
<GeneralIdentity
|
||||||
:name="group[rowIndex - 1]!.name"
|
v-if="group[rowIndex - 1]"
|
||||||
:picture="group[rowIndex - 1]!.picture"
|
:name="group[rowIndex - 1]!.name"
|
||||||
:image-server="group[rowIndex - 1]!.imageServer"
|
:picture="group[rowIndex - 1]!.picture"
|
||||||
/>
|
:image-server="group[rowIndex - 1]!.imageServer"
|
||||||
</td>
|
/>
|
||||||
<td>{{ statOf(group[rowIndex - 1]) }}</td>
|
</td>
|
||||||
<td>{{ gamesOf(group[rowIndex - 1]) }}</td>
|
<td>{{ statOf(group[rowIndex - 1]) }}</td>
|
||||||
<td>{{ group[rowIndex - 1]?.win ?? '' }}</td>
|
<td>{{ gamesOf(group[rowIndex - 1]) }}</td>
|
||||||
<td>{{ group[rowIndex - 1]?.draw ?? '' }}</td>
|
<td>{{ group[rowIndex - 1]?.win ?? '' }}</td>
|
||||||
<td>{{ group[rowIndex - 1]?.lose ?? '' }}</td>
|
<td>{{ group[rowIndex - 1]?.draw ?? '' }}</td>
|
||||||
<td>{{ pointsOf(group[rowIndex - 1]) }}</td>
|
<td>{{ group[rowIndex - 1]?.lose ?? '' }}</td>
|
||||||
<td>{{ group[rowIndex - 1]?.gl ?? '' }}</td>
|
<td>{{ pointsOf(group[rowIndex - 1]) }}</td>
|
||||||
</tr>
|
<td>{{ group[rowIndex - 1]?.gl ?? '' }}</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
</section>
|
</table>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<section class="section-title groups-title bg2">조별 예선 순위</section>
|
<template v-if="sectionVisibility.preliminary">
|
||||||
<div class="group-tabs bg0" role="tablist" aria-label="예선 조 선택">
|
<section class="section-title groups-title bg2">조별 예선 순위</section>
|
||||||
<button
|
<div class="group-tabs bg0" role="tablist" aria-label="예선 조 선택">
|
||||||
v-for="(groupName, groupIndex) in groupNames"
|
<button
|
||||||
:key="`preliminary-tab-${groupName}`"
|
v-for="(groupName, groupIndex) in groupNames"
|
||||||
type="button"
|
:key="`preliminary-tab-${groupName}`"
|
||||||
role="tab"
|
type="button"
|
||||||
:aria-selected="activePreliminaryGroup === groupIndex"
|
role="tab"
|
||||||
:class="{ active: activePreliminaryGroup === groupIndex }"
|
:aria-selected="activePreliminaryGroup === groupIndex"
|
||||||
@click="activePreliminaryGroup = groupIndex"
|
:class="{ active: activePreliminaryGroup === groupIndex }"
|
||||||
>
|
@click="activePreliminaryGroup = groupIndex"
|
||||||
{{ groupName }}조
|
>
|
||||||
</button>
|
{{ groupName }}조
|
||||||
</div>
|
</button>
|
||||||
<section class="group-grid preliminary-grid bg0">
|
</div>
|
||||||
<table
|
<section class="group-grid preliminary-grid bg0">
|
||||||
v-for="(group, groupIndex) in preliminaryGroups"
|
<table
|
||||||
:key="`preliminary-${groupIndex}`"
|
v-for="(group, groupIndex) in preliminaryGroups"
|
||||||
:class="{ 'mobile-active': activePreliminaryGroup === groupIndex }"
|
:key="`preliminary-${groupIndex}`"
|
||||||
>
|
:data-preliminary-group="groupIndex"
|
||||||
<caption>
|
:class="{ 'mobile-active': activePreliminaryGroup === groupIndex }"
|
||||||
{{
|
>
|
||||||
groupNames[groupIndex]
|
<caption>
|
||||||
}}조
|
{{
|
||||||
</caption>
|
groupNames[groupIndex]
|
||||||
<thead>
|
}}조
|
||||||
<tr>
|
</caption>
|
||||||
<th>순</th>
|
<thead>
|
||||||
<th>장수</th>
|
<tr>
|
||||||
<th>{{ typeStatNames[snapshot?.state?.type ?? 0] }}</th>
|
<th>순</th>
|
||||||
<th>경</th>
|
<th>장수</th>
|
||||||
<th>승</th>
|
<th>{{ typeStatNames[snapshot?.state?.type ?? 0] }}</th>
|
||||||
<th>무</th>
|
<th>경</th>
|
||||||
<th>패</th>
|
<th>승</th>
|
||||||
<th>점</th>
|
<th>무</th>
|
||||||
<th>득</th>
|
<th>패</th>
|
||||||
</tr>
|
<th>점</th>
|
||||||
</thead>
|
<th>득</th>
|
||||||
<tbody>
|
</tr>
|
||||||
<tr v-for="rowIndex in 8" :key="rowIndex">
|
</thead>
|
||||||
<td>{{ rowIndex }}</td>
|
<tbody>
|
||||||
<td class="general-cell">
|
<tr v-for="rowIndex in 8" :key="rowIndex">
|
||||||
<GeneralIdentity
|
<td>{{ rowIndex }}</td>
|
||||||
v-if="group[rowIndex - 1]"
|
<td class="general-cell">
|
||||||
:name="group[rowIndex - 1]!.name"
|
<GeneralIdentity
|
||||||
:picture="group[rowIndex - 1]!.picture"
|
v-if="group[rowIndex - 1]"
|
||||||
:image-server="group[rowIndex - 1]!.imageServer"
|
:name="group[rowIndex - 1]!.name"
|
||||||
/>
|
:picture="group[rowIndex - 1]!.picture"
|
||||||
</td>
|
:image-server="group[rowIndex - 1]!.imageServer"
|
||||||
<td>{{ statOf(group[rowIndex - 1]) }}</td>
|
/>
|
||||||
<td>{{ gamesOf(group[rowIndex - 1]) }}</td>
|
</td>
|
||||||
<td>{{ group[rowIndex - 1]?.win ?? '' }}</td>
|
<td>{{ statOf(group[rowIndex - 1]) }}</td>
|
||||||
<td>{{ group[rowIndex - 1]?.draw ?? '' }}</td>
|
<td>{{ gamesOf(group[rowIndex - 1]) }}</td>
|
||||||
<td>{{ group[rowIndex - 1]?.lose ?? '' }}</td>
|
<td>{{ group[rowIndex - 1]?.win ?? '' }}</td>
|
||||||
<td>{{ pointsOf(group[rowIndex - 1]) }}</td>
|
<td>{{ group[rowIndex - 1]?.draw ?? '' }}</td>
|
||||||
<td>{{ group[rowIndex - 1]?.gl ?? '' }}</td>
|
<td>{{ group[rowIndex - 1]?.lose ?? '' }}</td>
|
||||||
</tr>
|
<td>{{ pointsOf(group[rowIndex - 1]) }}</td>
|
||||||
</tbody>
|
<td>{{ group[rowIndex - 1]?.gl ?? '' }}</td>
|
||||||
</table>
|
</tr>
|
||||||
</section>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="legacy-bracket-table-signature" hidden>
|
<div class="legacy-bracket-table-signature" hidden>
|
||||||
<table v-for="(rowCount, tableIndex) in [11, 11, 11, 10]" :key="tableIndex">
|
<table v-for="(rowCount, tableIndex) in [11, 11, 11, 10]" :key="tableIndex">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
import { resolveTournamentStageName } from '../src/utils/tournamentStatus.ts';
|
import { resolveTournamentSectionVisibility, resolveTournamentStageName } from '../src/utils/tournamentStatus.ts';
|
||||||
|
|
||||||
void describe('tournament status labels', () => {
|
void describe('tournament status labels', () => {
|
||||||
void it('describes inactive and active tournament stages', () => {
|
void it('describes inactive and active tournament stages', () => {
|
||||||
@@ -14,4 +14,32 @@ void describe('tournament status labels', () => {
|
|||||||
assert.equal(resolveTournamentStageName(11), '상태 확인 중');
|
assert.equal(resolveTournamentStageName(11), '상태 확인 중');
|
||||||
assert.equal(resolveTournamentStageName(-1), '상태 확인 중');
|
assert.equal(resolveTournamentStageName(-1), '상태 확인 중');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void it('reveals tournament sections only after their stage begins and retains completed results', () => {
|
||||||
|
assert.deepEqual(resolveTournamentSectionVisibility(0), {
|
||||||
|
preliminary: false,
|
||||||
|
final: false,
|
||||||
|
knockout: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(resolveTournamentSectionVisibility(1), {
|
||||||
|
preliminary: true,
|
||||||
|
final: false,
|
||||||
|
knockout: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(resolveTournamentSectionVisibility(3), {
|
||||||
|
preliminary: true,
|
||||||
|
final: true,
|
||||||
|
knockout: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(resolveTournamentSectionVisibility(5), {
|
||||||
|
preliminary: true,
|
||||||
|
final: true,
|
||||||
|
knockout: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(resolveTournamentSectionVisibility(0, 7), {
|
||||||
|
preliminary: true,
|
||||||
|
final: true,
|
||||||
|
knockout: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user