merge: 최신 main을 분할 버튼 이음새 보정에 통합

This commit is contained in:
2026-08-17 15:37:44 +00:00
11 changed files with 283 additions and 88 deletions
@@ -71,6 +71,13 @@ const zParticipant = z.object({
gl: z.number().int().optional(), gl: z.number().int().optional(),
seedRank: z.number().int().optional(), seedRank: z.number().int().optional(),
finalRank: z.number().int().optional(), finalRank: z.number().int().optional(),
preliminaryGroupId: z.number().int().min(0).max(7).optional(),
preliminaryGroupNo: z.number().int().min(0).max(7).optional(),
preliminaryRank: z.number().int().min(1).max(8).optional(),
preliminaryWin: z.number().int().min(0).optional(),
preliminaryDraw: z.number().int().min(0).optional(),
preliminaryLose: z.number().int().min(0).optional(),
preliminaryGl: z.number().int().optional(),
}); });
const zMatch = z.object({ const zMatch = z.object({
+7
View File
@@ -34,6 +34,13 @@ export interface TournamentParticipantEntry {
gl?: number; gl?: number;
seedRank?: number; seedRank?: number;
finalRank?: number; finalRank?: number;
preliminaryGroupId?: number;
preliminaryGroupNo?: number;
preliminaryRank?: number;
preliminaryWin?: number;
preliminaryDraw?: number;
preliminaryLose?: number;
preliminaryGl?: number;
} }
export interface TournamentMatchEntry { export interface TournamentMatchEntry {
+11 -2
View File
@@ -186,6 +186,8 @@ export const applyPreBattleStage = async (
gl: 0, gl: 0,
seedRank: 0, seedRank: 0,
finalRank: 0, finalRank: 0,
preliminaryGroupId: entry.groupId,
preliminaryGroupNo: entry.groupNo,
})); }));
await store.setParticipants(grouped); await store.setParticipants(grouped);
const nextState: TournamentState = { const nextState: TournamentState = {
@@ -244,10 +246,17 @@ export const applyPreBattleStage = async (
for (let groupId = 0; groupId < 8; groupId += 1) { for (let groupId = 0; groupId < 8; groupId += 1) {
const groupEntries = ranked.filter((entry) => entry.groupId === groupId); const groupEntries = ranked.filter((entry) => entry.groupId === groupId);
const ordered = sortByRanking(groupEntries); const ordered = sortByRanking(groupEntries);
ordered.slice(0, 4).forEach((entry, idx) => { ordered.forEach((entry, idx) => {
const target = ranked.find((item) => item.id === entry.id); const target = ranked.find((item) => item.id === entry.id);
if (target) { if (target) {
target.seedRank = idx + 1; target.preliminaryGroupId = groupId;
target.preliminaryGroupNo = entry.groupNo;
target.preliminaryRank = idx + 1;
target.preliminaryWin = entry.win ?? 0;
target.preliminaryDraw = entry.draw ?? 0;
target.preliminaryLose = entry.lose ?? 0;
target.preliminaryGl = entry.gl ?? 0;
target.seedRank = idx < 4 ? idx + 1 : 0;
} }
}); });
} }
+4 -1
View File
@@ -195,17 +195,20 @@ export const assignManualApplicantGroup = (options: {
extraSeed: `manual-group:${options.current.map((entry) => entry.id).join('-')}:${openGroupIds.join('-')}`, extraSeed: `manual-group:${options.current.map((entry) => entry.id).join('-')}:${openGroupIds.join('-')}`,
}); });
const groupId = rng.choice(openGroupIds); const groupId = rng.choice(openGroupIds);
const groupNo = groupCounts[groupId] ?? 0;
return { return {
...options.applicant, ...options.applicant,
groupId, groupId,
groupNo: groupCounts[groupId] ?? 0, groupNo,
win: 0, win: 0,
draw: 0, draw: 0,
lose: 0, lose: 0,
gl: 0, gl: 0,
seedRank: 0, seedRank: 0,
finalRank: 0, finalRank: 0,
preliminaryGroupId: groupId,
preliminaryGroupNo: groupNo,
}; };
}; };
@@ -249,6 +249,7 @@ describe('tournament router permissions and mutations', () => {
expect(results.filter((result) => result.status === 'rejected')).toHaveLength(1); expect(results.filter((result) => result.status === 'rejected')).toHaveLength(1);
const summary = await caller.tournament.getBettingSummary(); const summary = await caller.tournament.getBettingSummary();
expect(summary.myAmount).toBe(600); expect(summary.myAmount).toBe(600);
expect(Object.values(summary.myTotals)).toEqual([600]);
expect(summary.totalAmount).toBe(600); expect(summary.totalAmount).toBe(600);
expect(transport.gold.get(general.id)).toBe(2_400); expect(transport.gold.get(general.id)).toBe(2_400);
}); });
+33 -6
View File
@@ -246,8 +246,9 @@ describe('tournament worker schedule compatibility', () => {
groupNo, groupNo,
}; };
}); });
const groupCounts = Array.from({ length: 8 }, (_, groupId) => const groupCounts = Array.from(
current.filter((entry) => entry.groupId === groupId).length { length: 8 },
(_, groupId) => current.filter((entry) => entry.groupId === groupId).length
); );
const openGroupId = groupCounts.findIndex((count) => count === 7); const openGroupId = groupCounts.findIndex((count) => count === 7);
expect(openGroupId).toBeGreaterThanOrEqual(0); expect(openGroupId).toBeGreaterThanOrEqual(0);
@@ -267,7 +268,16 @@ describe('tournament worker schedule compatibility', () => {
}, },
}); });
expect(applicant).toMatchObject({ groupId: openGroupId, groupNo: 7, win: 0, draw: 0, lose: 0, gl: 0 }); expect(applicant).toMatchObject({
groupId: openGroupId,
groupNo: 7,
preliminaryGroupId: openGroupId,
preliminaryGroupNo: 7,
win: 0,
draw: 0,
lose: 0,
gl: 0,
});
}); });
it('catches up from the stored schedule instead of discarding elapsed legacy phases', () => { it('catches up from the stored schedule instead of discarding elapsed legacy phases', () => {
@@ -334,6 +344,7 @@ describe('tournament worker (in-memory)', () => {
expect(entries.map((entry) => entry.groupNo).sort((a, b) => Number(a) - Number(b))).toEqual([ expect(entries.map((entry) => entry.groupNo).sort((a, b) => Number(a) - Number(b))).toEqual([
0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7,
]); ]);
expect(entries.every((entry) => entry.preliminaryGroupId === groupId)).toBe(true);
} }
}); });
@@ -648,14 +659,30 @@ describe('tournament worker (in-memory)', () => {
expect(participants.find((entry) => entry.id === 1)).toMatchObject({ groupId: expect.any(Number) }); expect(participants.find((entry) => entry.id === 1)).toMatchObject({ groupId: expect.any(Number) });
expect(participants.find((entry) => entry.id === 1001)).toMatchObject({ groupId: expect.any(Number) }); expect(participants.find((entry) => entry.id === 1001)).toMatchObject({ groupId: expect.any(Number) });
expect( expect(
Array.from({ length: 8 }, (_, groupId) => Array.from({ length: 8 }, (_, groupId) => participants.filter((entry) => entry.groupId === groupId).length)
participants.filter((entry) => entry.groupId === groupId).length
)
).toEqual(Array.from({ length: 8 }, () => 8)); ).toEqual(Array.from({ length: 8 }, () => 8));
await store.setState(afterJoin); await store.setState(afterJoin);
const finalState = await runTournamentToCompletion({ store, prisma, baseSeed: 'seed' }); const finalState = await runTournamentToCompletion({ store, prisma, baseSeed: 'seed' });
const finalParticipants = await store.getParticipants();
expect(finalState.stage).toBe(0); expect(finalState.stage).toBe(0);
expect(finalState.winnerId).toBeDefined(); expect(finalState.winnerId).toBeDefined();
for (let groupId = 0; groupId < 8; groupId += 1) {
const preliminaryEntries = finalParticipants
.filter((entry) => entry.preliminaryGroupId === groupId)
.sort((lhs, rhs) => (lhs.preliminaryRank ?? 99) - (rhs.preliminaryRank ?? 99));
expect(preliminaryEntries).toHaveLength(8);
expect(preliminaryEntries.map((entry) => entry.preliminaryRank)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
expect(
preliminaryEntries.every(
(entry) =>
entry.preliminaryGroupNo !== undefined &&
entry.preliminaryWin !== undefined &&
entry.preliminaryDraw !== undefined &&
entry.preliminaryLose !== undefined &&
entry.preliminaryGl !== undefined
)
).toBe(true);
}
}); });
}); });
@@ -35,6 +35,7 @@ const names = [
'허저', '허저',
'주태', '주태',
longGeneralName, longGeneralName,
...Array.from({ length: 48 }, (_, index) => `예선장수${index + 17}`),
]; ];
const participants = names.map((name, index) => ({ const participants = names.map((name, index) => ({
id: index + 1, id: index + 1,
@@ -45,13 +46,20 @@ const participants = names.map((name, index) => ({
level: 10, level: 10,
picture: 'default.jpg', picture: 'default.jpg',
imageServer: 0, imageServer: 0,
groupId: 10 + (index % 8), groupId: Math.floor(index / 8) < 4 ? 10 + (index % 8) : index % 8,
groupNo: Math.floor(index / 8), groupNo: Math.floor(index / 8),
win: 3 - (index % 2), win: Math.floor(index / 8) < 4 ? 3 - (index % 2) : 7 - Math.floor(index / 8),
draw: index % 2, draw: index % 2,
lose: 0, lose: Math.floor(index / 8) < 4 ? 0 : Math.floor(index / 8),
gl: 12 - index, gl: 64 - index,
finalRank: Math.floor(index / 8) + 1, finalRank: Math.floor(index / 8) + 1,
preliminaryGroupId: index % 8,
preliminaryGroupNo: Math.floor(index / 8),
preliminaryRank: Math.floor(index / 8) + 1,
preliminaryWin: 7 - Math.floor(index / 8),
preliminaryDraw: index % 2,
preliminaryLose: Math.floor(index / 8),
preliminaryGl: 64 - index,
})); }));
const matches = [ const matches = [
...Array.from({ length: 8 }, (_, index) => ({ ...Array.from({ length: 8 }, (_, index) => ({
@@ -181,11 +189,11 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean }
if (operation === 'tournament.getBettingSummary') { if (operation === 'tournament.getBettingSummary') {
return response({ return response({
totals: Object.fromEntries( totals: Object.fromEntries(
participants.map((participant, index) => [participant.id, 100 + index * 10]) participants.slice(0, 16).map((participant, index) => [participant.id, 100 + index * 10])
), ),
myTotals: {}, myTotals: { 1: 120, 2: 40 },
totalAmount: 2800, totalAmount: 2800,
myAmount: 0, myAmount: 160,
}); });
} }
if (operation === 'tournament.getRankings') { if (operation === 'tournament.getRankings') {
@@ -297,6 +305,14 @@ test('desktop bracket connects every real general slot to the next round', async
expect(oddsContainment.cardHeight).toBeGreaterThanOrEqual(82); expect(oddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
expect(oddsContainment.oddsTop).toBeGreaterThanOrEqual(oddsContainment.cardTop); expect(oddsContainment.oddsTop).toBeGreaterThanOrEqual(oddsContainment.cardTop);
expect(oddsContainment.oddsBottom).toBeLessThanOrEqual(oddsContainment.cardBottom); expect(oddsContainment.oddsBottom).toBeLessThanOrEqual(oddsContainment.cardBottom);
await expect(firstSlot.locator('.bracket-my-bet')).toHaveText('내 투자 금120');
const preliminaryTables = page.locator('.preliminary-grid table');
await expect(preliminaryTables).toHaveCount(8);
for (let groupIndex = 0; groupIndex < 8; groupIndex += 1) {
await expect(preliminaryTables.nth(groupIndex).locator('tbody tr')).toHaveCount(8);
await expect(preliminaryTables.nth(groupIndex).locator('.general-identity')).toHaveCount(8);
}
await persistScreenshot(page, 'tournament-desktop', testInfo.outputPath('tournament-bracket-desktop.webp')); await persistScreenshot(page, 'tournament-desktop', testInfo.outputPath('tournament-bracket-desktop.webp'));
}); });
@@ -413,6 +429,7 @@ test('mobile bracket exposes every round through tabs with standard horizontal i
}); });
expect(mobileOddsContainment.cardHeight).toBeGreaterThanOrEqual(82); expect(mobileOddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
expect(mobileOddsContainment.oddsBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom); expect(mobileOddsContainment.oddsBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom);
await expect(firstMobileSlot.locator('.bracket-my-bet')).toHaveText('내 투자 금120');
await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible(); await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible();
await page.getByRole('tab', { name: '二조' }).first().click(); await page.getByRole('tab', { name: '二조' }).first().click();
await expect(page.getByRole('tab', { name: '二조' }).first()).toHaveAttribute('aria-selected', 'true'); await expect(page.getByRole('tab', { name: '二조' }).first()).toHaveAttribute('aria-selected', 'true');
@@ -420,12 +437,39 @@ test('mobile bracket exposes every round through tabs with standard horizontal i
await persistScreenshot(page, 'tournament-mobile', testInfo.outputPath('tournament-bracket-mobile.webp')); await persistScreenshot(page, 'tournament-mobile', testInfo.outputPath('tournament-bracket-mobile.webp'));
}); });
test('tournament and betting pages expose same-row navigation tabs beside close', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await openTournament(page);
const navigation = page.getByRole('tablist', { name: '토너먼트와 베팅장 이동' });
const tournamentTab = navigation.getByRole('tab', { name: '토너먼트' });
const bettingTab = navigation.getByRole('tab', { name: '베팅장' });
const close = page.getByRole('button', { name: '창 닫기' }).first();
await expect(tournamentTab).toHaveAttribute('aria-selected', 'true');
const headerCenters = await Promise.all(
[tournamentTab, bettingTab, close].map(async (control) => {
const box = await control.boundingBox();
return box ? box.y + box.height / 2 : -1;
})
);
expect(Math.max(...headerCenters) - Math.min(...headerCenters)).toBeLessThan(1);
await bettingTab.click();
await expect(page).toHaveURL(/\/betting$/);
await expect(page.getByRole('tab', { name: '베팅장' })).toHaveAttribute('aria-selected', 'true');
await page.getByRole('tab', { name: '토너먼트' }).click();
await expect(page).toHaveURL(/\/tournament$/);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
});
test('mobile betting rankings use tabs and keep dedicated icons beside general names', async ({ page }, testInfo) => { test('mobile betting rankings use tabs and keep dedicated icons beside general names', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 }); await page.setViewportSize({ width: 390, height: 844 });
await installFixture(page); await installFixture(page);
await page.goto('betting'); await page.goto('betting');
await expect(page.locator('.candidate-card')).toHaveCount(16); await expect(page.locator('.candidate-card')).toHaveCount(16);
await expect(page.locator('.betting-bracket .bracket-my-bet').first()).toHaveText('내 투자 금120');
await expect(page.getByRole('tablist', { name: '토너먼트 랭킹 종목 선택' })).toBeVisible(); await expect(page.getByRole('tablist', { name: '토너먼트 랭킹 종목 선택' })).toBeVisible();
await expect(page.locator('.ranking-table:visible')).toHaveCount(1); await expect(page.locator('.ranking-table:visible')).toHaveCount(1);
await page.getByRole('tab', { name: '통솔전' }).click(); await page.getByRole('tab', { name: '통솔전' }).click();
@@ -12,6 +12,7 @@ const props = defineProps<{
matches: TournamentBracketMatch[]; matches: TournamentBracketMatch[];
winnerId?: number; winnerId?: number;
betTotals?: Record<number, number>; betTotals?: Record<number, number>;
myBetTotals?: Record<number, number>;
totalBet: number; totalBet: number;
showLegend?: boolean; showLegend?: boolean;
}>(); }>();
@@ -66,6 +67,7 @@ const odds = (id: number | null) => {
if (!amount) return '∞'; if (!amount) return '∞';
return (props.totalBet / amount).toFixed(2); return (props.totalBet / amount).toFixed(2);
}; };
const myBet = (id: number | null) => (id === null ? 0 : (props.myBetTotals?.[id] ?? 0));
const mobilePairs = computed(() => { const mobilePairs = computed(() => {
const column = roundColumns.value[activeMobileRound.value] ?? []; const column = roundColumns.value[activeMobileRound.value] ?? [];
if (activeMobileRound.value === roundColumns.value.length - 1) return column.map((slot) => [slot]); if (activeMobileRound.value === roundColumns.value.length - 1) return column.map((slot) => [slot]);
@@ -116,7 +118,10 @@ const mobilePairs = computed(() => {
}" }"
> >
<GeneralIdentity :name="slot.name" :picture="slot.picture" :image-server="slot.imageServer" /> <GeneralIdentity :name="slot.name" :picture="slot.picture" :image-server="slot.imageServer" />
<small v-if="columnIndex === 0" class="bracket-odds">배당 {{ odds(slot.id) }}</small> <div v-if="columnIndex === 0" class="bracket-bet-summary">
<small class="bracket-odds">배당 {{ odds(slot.id) }}</small>
<small class="bracket-my-bet"> 투자 {{ myBet(slot.id) }}</small>
</div>
</article> </article>
</template> </template>
</div> </div>
@@ -146,7 +151,10 @@ const mobilePairs = computed(() => {
:data-general-id="slot.id ?? undefined" :data-general-id="slot.id ?? undefined"
> >
<GeneralIdentity :name="slot.name" :picture="slot.picture" :image-server="slot.imageServer" /> <GeneralIdentity :name="slot.name" :picture="slot.picture" :image-server="slot.imageServer" />
<small v-if="activeMobileRound === 0" class="bracket-odds">배당 {{ odds(slot.id) }}</small> <div v-if="activeMobileRound === 0" class="bracket-bet-summary">
<small class="bracket-odds">배당 {{ odds(slot.id) }}</small>
<small class="bracket-my-bet"> 투자 {{ myBet(slot.id) }}</small>
</div>
</div> </div>
<strong v-if="pair.length === 2" class="versus" aria-hidden="true">VS</strong> <strong v-if="pair.length === 2" class="versus" aria-hidden="true">VS</strong>
</article> </article>
@@ -220,12 +228,26 @@ const mobilePairs = computed(() => {
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
} }
.bracket-odds { .bracket-bet-summary {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 4px;
white-space: nowrap;
}
.bracket-odds,
.bracket-my-bet {
display: block; display: block;
min-width: 0;
color: skyblue; color: skyblue;
font-size: 11px; font-size: 11px;
line-height: 12px; line-height: 12px;
text-align: right; }
.bracket-my-bet {
overflow: hidden;
color: orange;
text-overflow: ellipsis;
} }
.mobile-bracket { .mobile-bracket {
display: none; display: none;
@@ -0,0 +1,116 @@
<script setup lang="ts">
defineProps<{
activePage: 'tournament' | 'betting';
title: string;
}>();
</script>
<template>
<header class="tournament-page-header">
<strong class="tournament-page-title">{{ title }}</strong>
<div class="tournament-page-actions">
<nav class="tournament-page-tabs" role="tablist" aria-label="토너먼트와 베팅장 이동">
<RouterLink v-slot="{ navigate }" custom to="/tournament">
<button
type="button"
role="tab"
:aria-selected="activePage === 'tournament'"
:class="{ active: activePage === 'tournament' }"
@click="navigate"
>
토너먼트
</button>
</RouterLink>
<RouterLink v-slot="{ navigate }" custom to="/betting">
<button
type="button"
role="tab"
:aria-selected="activePage === 'betting'"
:class="{ active: activePage === 'betting' }"
@click="navigate"
>
베팅장
</button>
</RouterLink>
</nav>
<RouterLink v-slot="{ navigate }" custom to="/">
<button class="close-button" type="button" @click="navigate"> 닫기</button>
</RouterLink>
</div>
</header>
</template>
<style scoped>
.tournament-page-header {
display: flex;
min-height: 48px;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 2px 4px;
}
.tournament-page-title {
min-width: 0;
overflow: hidden;
font-size: 16px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
}
.tournament-page-actions,
.tournament-page-tabs {
display: flex;
align-items: center;
gap: 4px;
}
button {
height: 44px;
margin: 0;
border: 1px solid #666;
border-radius: 5.25px;
background: #444;
color: #fff;
font-size: 14px;
line-height: 18px;
cursor: pointer;
}
.tournament-page-tabs button {
min-width: 72px;
padding: 10px 12px;
}
.tournament-page-tabs button.active {
border-color: #f39c12;
background: #8a5b13;
}
.close-button {
width: 88px;
padding: 10px 16px;
border-color: #375a7f;
background: #375a7f;
}
button:hover,
button:focus {
filter: brightness(1.25);
}
button:focus-visible {
outline: 2px solid #f39c12;
outline-offset: 1px;
}
@media (max-width: 800px) {
.tournament-page-header {
gap: 4px;
padding-inline: 2px;
}
.tournament-page-title {
font-size: 13px;
}
.tournament-page-actions,
.tournament-page-tabs {
gap: 2px;
}
.tournament-page-tabs button {
min-width: 68px;
padding-inline: 8px;
}
}
</style>
+4 -33
View File
@@ -2,6 +2,7 @@
import { formatServerDateTime } from '@sammo-ts/common'; import { formatServerDateTime } from '@sammo-ts/common';
import { computed, onMounted, ref } from 'vue'; import { computed, 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 GeneralIdentity from '../components/ui/GeneralIdentity.vue'; import GeneralIdentity from '../components/ui/GeneralIdentity.vue';
import { trpc } from '../utils/trpc'; import { trpc } from '../utils/trpc';
@@ -72,6 +73,7 @@ const candidates = computed(() =>
const totalAmount = computed(() => summary.value?.totalAmount ?? 0); const totalAmount = computed(() => summary.value?.totalAmount ?? 0);
const myAmount = computed(() => summary.value?.myAmount ?? 0); const myAmount = computed(() => summary.value?.myAmount ?? 0);
const betTotals = computed(() => summary.value?.totals as Record<number, number> | undefined); const betTotals = computed(() => summary.value?.totals as Record<number, number> | undefined);
const myBetTotals = computed(() => summary.value?.myTotals as Record<number, number> | undefined);
const ratio = (id: number) => { const ratio = (id: number) => {
const totals = summary.value?.totals as Record<number, number> | undefined; const totals = summary.value?.totals as Record<number, number> | undefined;
const amount = totals?.[id] ?? 0; const amount = totals?.[id] ?? 0;
@@ -110,12 +112,7 @@ const placeBet = async (targetId: number) => {
<template> <template>
<main id="tournament-betting-container" class="betting-page"> <main id="tournament-betting-container" class="betting-page">
<section class="title bg0"> <TournamentPageHeader class="bg0" active-page="betting" title="베 팅 장" />
<br />
<RouterLink v-slot="{ navigate }" custom to="/">
<button class="close-button" type="button" @click="navigate"> 닫기</button>
</RouterLink>
</section>
<section class="toolbar bg0"> <section class="toolbar bg0">
<button type="button" @click="load">갱신</button> <button type="button" @click="load">갱신</button>
<span v-if="loading">불러오는 중...</span> <span v-if="loading">불러오는 중...</span>
@@ -138,6 +135,7 @@ const placeBet = async (targetId: number) => {
: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"
:total-bet="totalAmount" :total-bet="totalAmount"
:show-legend="false" :show-legend="false"
/> />
@@ -303,25 +301,6 @@ const placeBet = async (targetId: number) => {
.bg2 { .bg2 {
background: #142b42 var(--sammo-texture-blue); background: #142b42 var(--sammo-texture-blue);
} }
.title {
min-height: 68px;
padding: 0;
font-size: 14px;
line-height: 19.1875px;
}
.close-button {
display: block;
width: 88px;
height: 44px;
padding: 10px 16px;
border: 1px solid #375a7f;
border-radius: 5.25px;
background: #375a7f;
color: #fff;
font-size: 14px;
line-height: 18px;
text-decoration: none;
}
.toolbar { .toolbar {
min-height: 46px; min-height: 46px;
padding: 1px; padding: 1px;
@@ -417,10 +396,6 @@ button:hover,
button:focus { button:focus {
filter: brightness(1.25); filter: brightness(1.25);
} }
.close-button:hover,
.close-button:focus {
filter: brightness(1.2);
}
button:focus-visible, button:focus-visible,
select:focus-visible { select:focus-visible {
outline: 2px solid #f39c12; outline: 2px solid #f39c12;
@@ -507,10 +482,6 @@ select:disabled {
max-width: 100%; max-width: 100%;
font-size: 13px; font-size: 13px;
} }
.title {
height: auto;
min-height: 55px;
}
.state { .state {
font-size: 18px; font-size: 18px;
} }
+23 -35
View File
@@ -2,6 +2,7 @@
import { formatServerDateTime } from '@sammo-ts/common'; import { formatServerDateTime } from '@sammo-ts/common';
import { computed, onMounted, ref } from 'vue'; import { computed, 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 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 { resolveTournamentStageName } from '../utils/tournamentStatus';
@@ -58,6 +59,7 @@ const openingTime = computed(() =>
formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' }) formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' })
); );
const betTotals = computed(() => betting.value?.totals as Record<number, number> | undefined); const betTotals = computed(() => betting.value?.totals as Record<number, number> | undefined);
const myBetTotals = computed(() => betting.value?.myTotals as Record<number, number> | undefined);
const isParticipant = computed(() => const isParticipant = computed(() =>
(snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value) (snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value)
); );
@@ -71,8 +73,25 @@ const groups = computed(() =>
const preliminaryGroups = computed(() => const preliminaryGroups = computed(() =>
Array.from({ length: 8 }, (_, index) => Array.from({ length: 8 }, (_, index) =>
(snapshot.value?.participants ?? []) (snapshot.value?.participants ?? [])
.filter((participant) => participant.groupId === index) .filter((participant) => {
.sort((a, b) => (a.seedRank ?? 99) - (b.seedRank ?? 99) || (a.groupNo ?? 99) - (b.groupNo ?? 99)) const groupId =
participant.preliminaryGroupId ??
(participant.groupId !== undefined && participant.groupId < 8 ? participant.groupId : undefined);
return groupId === index;
})
.map((participant) => ({
...participant,
groupNo: participant.preliminaryGroupNo ?? participant.groupNo,
win: participant.preliminaryWin ?? participant.win,
draw: participant.preliminaryDraw ?? participant.draw,
lose: participant.preliminaryLose ?? participant.lose,
gl: participant.preliminaryGl ?? participant.gl,
}))
.sort(
(a, b) =>
(a.preliminaryRank ?? a.seedRank ?? 99) - (b.preliminaryRank ?? b.seedRank ?? 99) ||
(a.groupNo ?? 99) - (b.groupNo ?? 99)
)
) )
); );
const groupNames = ['一', '二', '三', '四', '五', '六', '七', '八']; const groupNames = ['一', '二', '三', '四', '五', '六', '七', '八'];
@@ -141,12 +160,7 @@ const start = async () => {
<template> <template>
<main id="tournament-container" class="legacy-page"> <main id="tournament-container" class="legacy-page">
<section class="legacy-title bg0"> <TournamentPageHeader class="bg0" active-page="tournament" title="삼모전 토너먼트" />
<div>삼모전 토너먼트</div>
<RouterLink v-slot="{ navigate }" custom to="/">
<button class="close-button" type="button" @click="navigate"> 닫기</button>
</RouterLink>
</section>
<section class="toolbar bg0"> <section class="toolbar bg0">
<button type="button" @click="load">갱신</button> <button type="button" @click="load">갱신</button>
@@ -177,6 +191,7 @@ const start = async () => {
: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"
:total-bet="totalBet" :total-bet="totalBet"
/> />
@@ -384,25 +399,6 @@ const start = async () => {
.bg2 { .bg2 {
background: #142b42 var(--sammo-texture-blue); background: #142b42 var(--sammo-texture-blue);
} }
.legacy-title {
min-height: 68px;
padding: 0;
font-size: 14px;
line-height: 19.1875px;
}
.close-button {
display: block;
width: 88px;
height: 44px;
padding: 10px 16px;
border: 1px solid #375a7f;
border-radius: 5.25px;
background: #375a7f;
color: #fff;
font-size: 14px;
line-height: 18px;
text-decoration: none;
}
.toolbar { .toolbar {
min-height: 46px; min-height: 46px;
padding: 1px; padding: 1px;
@@ -433,10 +429,6 @@ button:hover,
button:focus { button:focus {
filter: brightness(1.25); filter: brightness(1.25);
} }
.close-button:hover,
.close-button:focus {
filter: brightness(1.2);
}
button:focus-visible { button:focus-visible {
outline: 2px solid #f39c12; outline: 2px solid #f39c12;
outline-offset: 1px; outline-offset: 1px;
@@ -532,10 +524,6 @@ td {
max-width: 100%; max-width: 100%;
font-size: 13px; font-size: 13px;
} }
.legacy-title {
height: auto;
min-height: 55px;
}
.state-row { .state-row {
font-size: 18px; font-size: 18px;
} }