merge: 최신 main을 토너먼트 카드 베팅에 반영한다

# Conflicts:
#	app/game-frontend/e2e/tournamentBracket.spec.ts
This commit is contained in:
2026-08-21 15:59:32 +00:00
14 changed files with 1004 additions and 189 deletions
@@ -3,6 +3,7 @@ import { computed } from 'vue';
import SkeletonLines from '../ui/SkeletonLines.vue';
import LegacyProgressBar from '../ui/LegacyProgressBar.vue';
import RichTooltip from '../ui/RichTooltip.vue';
import { formatLocalTimeSeconds } from '../../utils/legacyDateTime';
import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress';
import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconBackgroundImage } from '../../utils/generalIcon';
@@ -30,6 +31,28 @@ interface ItemDisplayNames {
item?: string | null;
}
interface ItemDisplayInfo {
horse?: string | null;
weapon?: string | null;
book?: string | null;
item?: string | null;
}
interface CrewTypeDisplayInfo {
name: string;
info: string[];
requirements: string[];
stats: {
attack: number;
defence: number;
speed: number;
avoid: number;
magicCoef: number;
cost: number;
rice: number;
};
}
interface GeneralTroopDisplay {
name: string;
status: 'inactive' | 'present' | 'away';
@@ -73,9 +96,12 @@ interface GeneralInfo {
refreshScore?: GeneralRefreshScore;
crewTypeId?: number;
crewTypeName?: string;
crewTypeInfo?: CrewTypeDisplayInfo | null;
traits?: { personal: string; specialWar: string; specialDomestic: string };
traitInfo?: { personal: string; specialWar: string; specialDomestic: string };
progression?: GeneralProgression;
itemNames?: ItemDisplayNames;
itemInfo?: ItemDisplayInfo;
equipmentNames?: ItemDisplayNames;
}
@@ -243,9 +269,36 @@ const specialText = computed(() => {
</strong>
</template>
<span class="cell-label">명마</span><strong>{{ itemNames.horse ?? '-' }}</strong>
<span class="cell-label">무기</span><strong>{{ itemNames.weapon ?? '-' }}</strong>
<span class="cell-label">서적</span><strong>{{ itemNames.book ?? '-' }}</strong>
<span class="cell-label">명마</span>
<strong>
<RichTooltip
:title="itemNames.horse ?? ''"
:description="props.general.itemInfo?.horse"
test-id="horse"
>
{{ itemNames.horse ?? '-' }}
</RichTooltip>
</strong>
<span class="cell-label">무기</span>
<strong>
<RichTooltip
:title="itemNames.weapon ?? ''"
:description="props.general.itemInfo?.weapon"
test-id="weapon"
>
{{ itemNames.weapon ?? '-' }}
</RichTooltip>
</strong>
<span class="cell-label">서적</span>
<strong>
<RichTooltip
:title="itemNames.book ?? ''"
:description="props.general.itemInfo?.book"
test-id="book"
>
{{ itemNames.book ?? '-' }}
</RichTooltip>
</strong>
<span
class="general-image general-crew-type-icon"
@@ -255,15 +308,91 @@ const specialText = computed(() => {
/>
<span class="cell-label">자금</span><strong>{{ props.general.gold.toLocaleString('ko-KR') }}</strong>
<span class="cell-label">군량</span><strong>{{ props.general.rice.toLocaleString('ko-KR') }}</strong>
<span class="cell-label">도구</span><strong>{{ itemNames.item ?? '-' }}</strong>
<span class="cell-label">도구</span>
<strong>
<RichTooltip
:title="itemNames.item ?? ''"
:description="props.general.itemInfo?.item"
test-id="item"
>
{{ itemNames.item ?? '-' }}
</RichTooltip>
</strong>
<span class="cell-label">병종</span><strong>{{ props.general.crewTypeName ?? '-' }}</strong>
<span class="cell-label">병종</span>
<strong>
<RichTooltip
:title="props.general.crewTypeName ?? ''"
:description="props.general.crewTypeInfo?.info"
test-id="crew-type"
>
{{ props.general.crewTypeName ?? '-' }}
<template v-if="props.general.crewTypeInfo" #content>
<span class="rich-tooltip-content__title">{{ props.general.crewTypeInfo.name }}</span>
<span
v-for="(line, index) in props.general.crewTypeInfo.info"
:key="`crew-info:${index}`"
class="rich-tooltip-content__line"
>
{{ line }}
</span>
<span class="rich-tooltip-content__section">전투 정보</span>
<span class="rich-tooltip-content__meta">
공격 {{ props.general.crewTypeInfo.stats.attack }} · 방어
{{ props.general.crewTypeInfo.stats.defence }} · 속도
{{ props.general.crewTypeInfo.stats.speed }} · 회피
{{ props.general.crewTypeInfo.stats.avoid }}% · 계략
{{ props.general.crewTypeInfo.stats.magicCoef }}%
</span>
<span class="rich-tooltip-content__meta">
병사 100 기준 {{ props.general.crewTypeInfo.stats.cost }} ·
{{ props.general.crewTypeInfo.stats.rice }}
</span>
<template v-if="props.general.crewTypeInfo.requirements.length">
<span class="rich-tooltip-content__section">생성 조건</span>
<span
v-for="(requirement, index) in props.general.crewTypeInfo.requirements"
:key="`crew-requirement:${index}`"
class="rich-tooltip-content__line"
>
{{ requirement }}
</span>
</template>
</template>
</RichTooltip>
</strong>
<span class="cell-label">병사</span><strong>{{ props.general.crew.toLocaleString('ko-KR') }}</strong>
<span class="cell-label">성격</span><strong>{{ props.general.traits?.personal ?? '-' }}</strong>
<span class="cell-label">성격</span>
<strong>
<RichTooltip
:title="props.general.traits?.personal ?? ''"
:description="props.general.traitInfo?.personal"
test-id="personality"
>
{{ props.general.traits?.personal ?? '-' }}
</RichTooltip>
</strong>
<span class="cell-label">훈련</span><strong>{{ props.general.train }}</strong>
<span class="cell-label">사기</span><strong>{{ props.general.atmos }}</strong>
<span class="cell-label">특기</span><strong :title="specialText">{{ specialText }}</strong>
<span class="cell-label">특기</span>
<strong class="special-value" :aria-label="specialText">
<RichTooltip
:title="`내정특기 · ${props.general.traits?.specialDomestic ?? '-'}`"
:description="props.general.traitInfo?.specialDomestic"
test-id="special-domestic"
>
{{ props.general.traits?.specialDomestic ?? '-' }}
</RichTooltip>
/
<RichTooltip
:title="`전투특기 · ${props.general.traits?.specialWar ?? '-'}`"
:description="props.general.traitInfo?.specialWar"
test-id="special-war"
>
{{ props.general.traits?.specialWar ?? '-' }}
</RichTooltip>
</strong>
<span class="cell-label level-label">Lv</span>
<strong class="level-value">{{ props.general.progression?.experienceLevel ?? 0 }}</strong>
@@ -1,5 +1,6 @@
<script setup lang="ts">
import SkeletonLines from '../ui/SkeletonLines.vue';
import RichTooltip from '../ui/RichTooltip.vue';
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor';
import { formatOfficerLevelText } from '../../utils/nationFormat';
import { getNpcColor } from '../../utils/npcColor';
@@ -19,6 +20,7 @@ interface NationInfo {
rice: number;
tech: number;
typeName: string;
typeInfo?: string;
typePros: string;
typeCons: string;
population: { cityCount: number; current: number; max: number };
@@ -67,9 +69,37 @@ const displayChiefName = (chief: NationChief | undefined): string => {
<span class="head">성향</span>
<strong class="body type-body">
{{ props.nation.typeName }} (<span class="pros">{{ props.nation.typePros }}</span>
<span class="cons">{{ props.nation.typeCons }}</span
>)
<RichTooltip
v-if="props.nation.typeInfo"
:title="`국가 성향 · ${props.nation.typeName}`"
:description="props.nation.typeInfo"
test-id="nation-type"
>
{{ props.nation.typeName }} (<span class="pros">{{ props.nation.typePros }}</span>
<span class="cons">{{ props.nation.typeCons }}</span
>)
<template #content="{ descriptionLines }">
<span class="rich-tooltip-content__title">국가 성향 · {{ props.nation.typeName }}</span>
<span
v-for="(line, index) in descriptionLines"
:key="`nation-type-info:${index}`"
class="rich-tooltip-content__line"
>
{{ line }}
</span>
<span class="rich-tooltip-content__line rich-tooltip-content__pros">
장점 {{ props.nation.typePros || '-' }}
</span>
<span class="rich-tooltip-content__line rich-tooltip-content__cons">
단점 {{ props.nation.typeCons || '-' }}
</span>
</template>
</RichTooltip>
<template v-else>
{{ props.nation.typeName }} (<span class="pros">{{ props.nation.typePros }}</span>
<span class="cons">{{ props.nation.typeCons }}</span
>)
</template>
</strong>
<span class="head">{{ formatOfficerLevelText(12, props.nation.level) }}</span>
@@ -0,0 +1,179 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue';
import tippy, { type Instance, type Placement } from 'tippy.js';
import 'tippy.js/dist/tippy.css';
const props = withDefaults(
defineProps<{
title?: string;
description?: string | readonly string[] | null;
placement?: Placement;
maxWidth?: number;
testId?: string;
}>(),
{
title: '',
description: null,
placement: 'top',
maxWidth: 360,
testId: undefined,
}
);
const slots = useSlots();
const triggerElement = ref<HTMLElement | null>(null);
const contentElement = ref<HTMLElement | null>(null);
let instance: Instance | null = null;
const descriptionLines = computed(() => {
const source = Array.isArray(props.description) ? props.description : [props.description ?? ''];
return source
.flatMap((line) => line.split(/<br\s*\/?\s*>|\r?\n/giu))
.map((line) => line.trim())
.filter(Boolean);
});
const hasContent = computed(() => Boolean(slots.content) || descriptionLines.value.length > 0);
const destroyTooltip = () => {
instance?.destroy();
instance = null;
};
const installTooltip = async () => {
destroyTooltip();
await nextTick();
if (!hasContent.value || !triggerElement.value || !contentElement.value) return;
instance = tippy(triggerElement.value, {
allowHTML: true,
appendTo: () => document.body,
content: () => contentElement.value?.innerHTML ?? '',
maxWidth: props.maxWidth,
placement: props.placement,
theme: 'sammo-rich',
trigger: 'mouseenter focus',
});
};
onMounted(() => void installTooltip());
onBeforeUnmount(destroyTooltip);
watch(
() => [props.title, props.description, props.placement, props.maxWidth],
() => void installTooltip(),
{ deep: true }
);
</script>
<template>
<span
ref="triggerElement"
class="rich-tooltip-trigger"
:class="{ 'rich-tooltip-trigger--enabled': hasContent }"
:tabindex="hasContent ? 0 : undefined"
:data-rich-tooltip="props.testId"
>
<slot />
</span>
<span ref="contentElement" class="rich-tooltip-template" hidden aria-hidden="true">
<slot name="content" :description-lines="descriptionLines">
<span v-if="props.title" class="rich-tooltip-content__title">{{ props.title }}</span>
<span
v-for="(line, index) in descriptionLines"
:key="`${index}:${line}`"
class="rich-tooltip-content__line"
>
{{ line }}
</span>
</slot>
</span>
</template>
<style>
.rich-tooltip-trigger {
display: inline;
min-width: 0;
}
.rich-tooltip-trigger--enabled {
cursor: help;
text-decoration: underline dotted rgb(150 210 255 / 85%);
text-underline-offset: 2px;
}
.rich-tooltip-trigger--enabled:focus-visible {
border-radius: 2px;
outline: 1px solid #6fc7ff;
outline-offset: 1px;
}
.tippy-box[data-theme~='sammo-rich'] {
border: 1px solid #8c8c8c;
border-radius: 3px;
background: #101010;
box-shadow: 0 3px 12px rgb(0 0 0 / 65%);
color: #f5f5f5;
font-family: Pretendard, sans-serif;
font-size: 12.5px;
line-height: 1.45;
text-align: left;
}
.tippy-box[data-theme~='sammo-rich'][data-placement^='top'] > .tippy-arrow::before {
border-top-color: #101010;
}
.tippy-box[data-theme~='sammo-rich'][data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: #101010;
}
.tippy-box[data-theme~='sammo-rich'][data-placement^='left'] > .tippy-arrow::before {
border-left-color: #101010;
}
.tippy-box[data-theme~='sammo-rich'][data-placement^='right'] > .tippy-arrow::before {
border-right-color: #101010;
}
.tippy-box[data-theme~='sammo-rich'] .tippy-content {
padding: 7px 9px;
}
.rich-tooltip-content__title,
.rich-tooltip-content__line,
.rich-tooltip-content__section,
.rich-tooltip-content__meta {
display: block;
}
.rich-tooltip-content__title {
margin-bottom: 4px;
color: #7fd4ff;
font-size: 13px;
font-weight: 700;
}
.rich-tooltip-content__line + .rich-tooltip-content__line {
margin-top: 2px;
}
.rich-tooltip-content__section {
margin-top: 5px;
border-top: 1px solid #4d4d4d;
padding-top: 4px;
color: #ffdc76;
font-weight: 700;
}
.rich-tooltip-content__meta {
color: #d5d5d5;
}
.rich-tooltip-content__pros {
color: cyan;
}
.rich-tooltip-content__cons {
color: magenta;
}
</style>
@@ -13,3 +13,18 @@ export const tournamentStageNames = [
] as const;
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,
};
};
+177 -143
View File
@@ -1,11 +1,11 @@
<script setup lang="ts">
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 TournamentPageHeader from '../components/tournament/TournamentPageHeader.vue';
import GeneralIdentity from '../components/ui/GeneralIdentity.vue';
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>>;
@@ -18,6 +18,7 @@ const actionMessage = ref<string | null>(null);
const adminEnabled = ref(false);
const activeFinalGroup = ref(0);
const activePreliminaryGroup = ref(0);
const tournamentContainer = ref<HTMLElement | null>(null);
const typeNames = ['전력전', '통솔전', '일기토', '설전'];
const typeStatNames = ['종합', '통솔', '무력', '지력'];
@@ -63,6 +64,12 @@ const myBetTotals = computed(() => betting.value?.myTotals as Record<number, num
const isParticipant = computed(() =>
(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(() =>
Array.from({ length: 8 }, (_, index) =>
(snapshot.value?.participants ?? [])
@@ -74,10 +81,7 @@ const preliminaryGroups = computed(() =>
Array.from({ length: 8 }, (_, index) =>
(snapshot.value?.participants ?? [])
.filter((participant) => {
const groupId =
participant.preliminaryGroupId ??
(participant.groupId !== undefined && participant.groupId < 8 ? participant.groupId : undefined);
return groupId === index;
return preliminaryGroupIdOf(participant) === index;
})
.map((participant) => ({
...participant,
@@ -113,15 +117,38 @@ const currentMatch = computed(() => {
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 () => {
actionMessage.value = null;
let joined = false;
try {
await trpc.tournament.join.mutate();
actionMessage.value = '참가 신청이 반영되었습니다.';
joined = true;
} catch (value) {
actionMessage.value = errorText(value);
} finally {
await load();
if (joined) {
const groupId = await revealMyPreliminaryGroup();
actionMessage.value =
groupId === undefined
? '참가 신청이 반영되었습니다.'
: `참가 신청이 반영되었습니다. ${groupNames[groupId]}조에 배정되었습니다.`;
}
}
};
@@ -159,7 +186,7 @@ const start = async () => {
</script>
<template>
<main id="tournament-container" class="legacy-page">
<main id="tournament-container" ref="tournamentContainer" class="legacy-page">
<TournamentPageHeader class="bg0" active-page="tournament" title="삼모전 토너먼트" />
<section class="toolbar bg0">
@@ -183,145 +210,152 @@ const start = async () => {
({{ resolveTournamentStageName(snapshot?.state?.stage ?? 0) }}, 개막시간 {{ openingTime }}, 경기당
{{ snapshot?.state?.termSeconds ?? '-' }})
</section>
<section class="section-title bg2">16 승자전</section>
<template v-if="sectionVisibility.knockout">
<section class="section-title bg2">16 승자전</section>
<TournamentBracket
class="bg0"
:participants="snapshot?.participants ?? []"
:matches="snapshot?.matches ?? []"
:winner-id="snapshot?.state?.winnerId"
:bet-totals="betTotals"
:my-bet-totals="myBetTotals"
:total-bet="totalBet"
:tournament-type="snapshot?.state?.type ?? 0"
/>
<TournamentBracket
class="bg0"
:participants="snapshot?.participants ?? []"
:matches="snapshot?.matches ?? []"
:winner-id="snapshot?.state?.winnerId"
:bet-totals="betTotals"
:my-bet-totals="myBetTotals"
:total-bet="totalBet"
:tournament-type="snapshot?.state?.type ?? 0"
/>
<section v-if="currentMatch" class="fight bg0">
<h2>{{ nameOf(currentMatch.attackerId) }} vs {{ nameOf(currentMatch.defenderId) }}</h2>
<p v-for="(line, index) in currentMatch.log ?? []" :key="index">{{ line }}</p>
</section>
<section v-if="currentMatch" class="fight bg0">
<h2>{{ nameOf(currentMatch.attackerId) }} vs {{ nameOf(currentMatch.defenderId) }}</h2>
<p v-for="(line, index) in currentMatch.log ?? []" :key="index">{{ line }}</p>
</section>
</template>
<section class="section-title groups-title bg2">조별 본선 순위</section>
<div class="group-tabs bg0" role="tablist" aria-label="본선 선택">
<button
v-for="(groupName, groupIndex) in groupNames"
:key="`final-tab-${groupName}`"
type="button"
role="tab"
:aria-selected="activeFinalGroup === groupIndex"
:class="{ active: activeFinalGroup === groupIndex }"
@click="activeFinalGroup = groupIndex"
>
{{ groupName }}
</button>
</div>
<section class="group-grid bg0">
<table
v-for="(group, groupIndex) in groups"
:key="groupIndex"
:class="{ 'mobile-active': activeFinalGroup === groupIndex }"
>
<caption>
{{
groupNames[groupIndex]
}}
</caption>
<thead>
<tr>
<th></th>
<th>장수</th>
<th>{{ typeStatNames[snapshot?.state?.type ?? 0] }}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="rowIndex in 4" :key="rowIndex">
<td>{{ rowIndex }}</td>
<td class="general-cell">
<GeneralIdentity
v-if="group[rowIndex - 1]"
:name="group[rowIndex - 1]!.name"
:picture="group[rowIndex - 1]!.picture"
:image-server="group[rowIndex - 1]!.imageServer"
/>
</td>
<td>{{ statOf(group[rowIndex - 1]) }}</td>
<td>{{ gamesOf(group[rowIndex - 1]) }}</td>
<td>{{ group[rowIndex - 1]?.win ?? '' }}</td>
<td>{{ group[rowIndex - 1]?.draw ?? '' }}</td>
<td>{{ group[rowIndex - 1]?.lose ?? '' }}</td>
<td>{{ pointsOf(group[rowIndex - 1]) }}</td>
<td>{{ group[rowIndex - 1]?.gl ?? '' }}</td>
</tr>
</tbody>
</table>
</section>
<template v-if="sectionVisibility.final">
<section class="section-title groups-title bg2">조별 본선 순위</section>
<div class="group-tabs bg0" role="tablist" aria-label="본선 선택">
<button
v-for="(groupName, groupIndex) in groupNames"
:key="`final-tab-${groupName}`"
type="button"
role="tab"
:aria-selected="activeFinalGroup === groupIndex"
:class="{ active: activeFinalGroup === groupIndex }"
@click="activeFinalGroup = groupIndex"
>
{{ groupName }}
</button>
</div>
<section class="group-grid bg0">
<table
v-for="(group, groupIndex) in groups"
:key="groupIndex"
:class="{ 'mobile-active': activeFinalGroup === groupIndex }"
>
<caption>
{{
groupNames[groupIndex]
}}
</caption>
<thead>
<tr>
<th></th>
<th>장수</th>
<th>{{ typeStatNames[snapshot?.state?.type ?? 0] }}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="rowIndex in 4" :key="rowIndex">
<td>{{ rowIndex }}</td>
<td class="general-cell">
<GeneralIdentity
v-if="group[rowIndex - 1]"
:name="group[rowIndex - 1]!.name"
:picture="group[rowIndex - 1]!.picture"
:image-server="group[rowIndex - 1]!.imageServer"
/>
</td>
<td>{{ statOf(group[rowIndex - 1]) }}</td>
<td>{{ gamesOf(group[rowIndex - 1]) }}</td>
<td>{{ group[rowIndex - 1]?.win ?? '' }}</td>
<td>{{ group[rowIndex - 1]?.draw ?? '' }}</td>
<td>{{ group[rowIndex - 1]?.lose ?? '' }}</td>
<td>{{ pointsOf(group[rowIndex - 1]) }}</td>
<td>{{ group[rowIndex - 1]?.gl ?? '' }}</td>
</tr>
</tbody>
</table>
</section>
</template>
<section class="section-title groups-title bg2">조별 예선 순위</section>
<div class="group-tabs bg0" role="tablist" aria-label="예선 선택">
<button
v-for="(groupName, groupIndex) in groupNames"
:key="`preliminary-tab-${groupName}`"
type="button"
role="tab"
:aria-selected="activePreliminaryGroup === groupIndex"
:class="{ active: activePreliminaryGroup === groupIndex }"
@click="activePreliminaryGroup = groupIndex"
>
{{ groupName }}
</button>
</div>
<section class="group-grid preliminary-grid bg0">
<table
v-for="(group, groupIndex) in preliminaryGroups"
:key="`preliminary-${groupIndex}`"
:class="{ 'mobile-active': activePreliminaryGroup === groupIndex }"
>
<caption>
{{
groupNames[groupIndex]
}}
</caption>
<thead>
<tr>
<th></th>
<th>장수</th>
<th>{{ typeStatNames[snapshot?.state?.type ?? 0] }}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="rowIndex in 8" :key="rowIndex">
<td>{{ rowIndex }}</td>
<td class="general-cell">
<GeneralIdentity
v-if="group[rowIndex - 1]"
:name="group[rowIndex - 1]!.name"
:picture="group[rowIndex - 1]!.picture"
:image-server="group[rowIndex - 1]!.imageServer"
/>
</td>
<td>{{ statOf(group[rowIndex - 1]) }}</td>
<td>{{ gamesOf(group[rowIndex - 1]) }}</td>
<td>{{ group[rowIndex - 1]?.win ?? '' }}</td>
<td>{{ group[rowIndex - 1]?.draw ?? '' }}</td>
<td>{{ group[rowIndex - 1]?.lose ?? '' }}</td>
<td>{{ pointsOf(group[rowIndex - 1]) }}</td>
<td>{{ group[rowIndex - 1]?.gl ?? '' }}</td>
</tr>
</tbody>
</table>
</section>
<template v-if="sectionVisibility.preliminary">
<section class="section-title groups-title bg2">조별 예선 순위</section>
<div class="group-tabs bg0" role="tablist" aria-label="예선 선택">
<button
v-for="(groupName, groupIndex) in groupNames"
:key="`preliminary-tab-${groupName}`"
type="button"
role="tab"
:aria-selected="activePreliminaryGroup === groupIndex"
:class="{ active: activePreliminaryGroup === groupIndex }"
@click="activePreliminaryGroup = groupIndex"
>
{{ groupName }}
</button>
</div>
<section class="group-grid preliminary-grid bg0">
<table
v-for="(group, groupIndex) in preliminaryGroups"
:key="`preliminary-${groupIndex}`"
:data-preliminary-group="groupIndex"
:class="{ 'mobile-active': activePreliminaryGroup === groupIndex }"
>
<caption>
{{
groupNames[groupIndex]
}}
</caption>
<thead>
<tr>
<th></th>
<th>장수</th>
<th>{{ typeStatNames[snapshot?.state?.type ?? 0] }}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="rowIndex in 8" :key="rowIndex">
<td>{{ rowIndex }}</td>
<td class="general-cell">
<GeneralIdentity
v-if="group[rowIndex - 1]"
:name="group[rowIndex - 1]!.name"
:picture="group[rowIndex - 1]!.picture"
:image-server="group[rowIndex - 1]!.imageServer"
/>
</td>
<td>{{ statOf(group[rowIndex - 1]) }}</td>
<td>{{ gamesOf(group[rowIndex - 1]) }}</td>
<td>{{ group[rowIndex - 1]?.win ?? '' }}</td>
<td>{{ group[rowIndex - 1]?.draw ?? '' }}</td>
<td>{{ group[rowIndex - 1]?.lose ?? '' }}</td>
<td>{{ pointsOf(group[rowIndex - 1]) }}</td>
<td>{{ group[rowIndex - 1]?.gl ?? '' }}</td>
</tr>
</tbody>
</table>
</section>
</template>
<div class="legacy-bracket-table-signature" hidden>
<table v-for="(rowCount, tableIndex) in [11, 11, 11, 10]" :key="tableIndex">