fix: 토너먼트 단계를 순차 노출하고 참가 조를 표시한다
예선, 본선, 16강 영역을 실제 진행 단계부터 누적해서 보여준다. 참가 신청이 성공하면 배정된 예선 조를 선택하고 해당 표로 이동한다.
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