fix: 베팅 후보에 종목별 핵심 능력치를 표시

전력전은 종합, 통솔전은 통솔, 일기토는 무력, 설전은 지력을 배당과 현재 투자금 옆에 표시한다. 네 종목 매핑과 desktop/mobile Chromium containment를 회귀 검사한다.
This commit is contained in:
2026-08-19 13:32:08 +00:00
parent cc1a7d8bbf
commit 8dae24a376
6 changed files with 85 additions and 4 deletions
@@ -126,7 +126,7 @@ 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 } = {}) => { const installFixture = async (page: Page, options: { applicationOpen?: boolean; tournamentType?: 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');
@@ -152,7 +152,7 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean }
state: { state: {
stage: options.applicationOpen ? 1 : 0, stage: options.applicationOpen ? 1 : 0,
phase: 0, phase: 0,
type: 0, type: options.tournamentType ?? 0,
auto: false, auto: false,
openYear: 184, openYear: 184,
openMonth: 1, openMonth: 1,
@@ -293,16 +293,21 @@ test('desktop bracket connects every real general slot to the next round', async
const firstSlot = page.locator('.desktop-bracket-name').first(); const firstSlot = page.locator('.desktop-bracket-name').first();
const oddsContainment = await firstSlot.evaluate((slot) => { const oddsContainment = await firstSlot.evaluate((slot) => {
const card = slot.getBoundingClientRect(); const card = slot.getBoundingClientRect();
const stat = slot.querySelector<HTMLElement>('.bracket-core-stat')!.getBoundingClientRect();
const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect(); const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect();
return { return {
cardTop: card.top, cardTop: card.top,
cardBottom: card.bottom, cardBottom: card.bottom,
statTop: stat.top,
statBottom: stat.bottom,
oddsTop: odds.top, oddsTop: odds.top,
oddsBottom: odds.bottom, oddsBottom: odds.bottom,
cardHeight: card.height, cardHeight: card.height,
}; };
}); });
expect(oddsContainment.cardHeight).toBeGreaterThanOrEqual(82); expect(oddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
expect(oddsContainment.statTop).toBeGreaterThanOrEqual(oddsContainment.cardTop);
expect(oddsContainment.statBottom).toBeLessThanOrEqual(oddsContainment.cardBottom);
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'); await expect(firstSlot.locator('.bracket-my-bet')).toHaveText('내 투자 금120');
@@ -424,10 +429,20 @@ test('mobile bracket exposes every round through tabs with standard horizontal i
const firstMobileSlot = bracket.locator('.mobile-bracket-name').first(); const firstMobileSlot = bracket.locator('.mobile-bracket-name').first();
const mobileOddsContainment = await firstMobileSlot.evaluate((slot) => { const mobileOddsContainment = await firstMobileSlot.evaluate((slot) => {
const card = slot.getBoundingClientRect(); const card = slot.getBoundingClientRect();
const stat = slot.querySelector<HTMLElement>('.bracket-core-stat')!.getBoundingClientRect();
const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect(); const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect();
return { cardBottom: card.bottom, oddsBottom: odds.bottom, cardHeight: card.height }; return {
cardTop: card.top,
cardBottom: card.bottom,
statTop: stat.top,
statBottom: stat.bottom,
oddsBottom: odds.bottom,
cardHeight: card.height,
};
}); });
expect(mobileOddsContainment.cardHeight).toBeGreaterThanOrEqual(82); expect(mobileOddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
expect(mobileOddsContainment.statTop).toBeGreaterThanOrEqual(mobileOddsContainment.cardTop);
expect(mobileOddsContainment.statBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom);
expect(mobileOddsContainment.oddsBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom); expect(mobileOddsContainment.oddsBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom);
await expect(firstMobileSlot.locator('.bracket-my-bet')).toHaveText('내 투자 금120'); await expect(firstMobileSlot.locator('.bracket-my-bet')).toHaveText('내 투자 금120');
await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible(); await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible();
@@ -469,6 +484,7 @@ test('mobile betting rankings use tabs and keep dedicated icons beside general n
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-core-stat').first()).toHaveText('종합 240');
await expect(page.locator('.betting-bracket .bracket-my-bet').first()).toHaveText('내 투자 금120'); 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);
@@ -491,6 +507,17 @@ test('mobile betting rankings use tabs and keep dedicated icons beside general n
await persistScreenshot(page, 'tournament-ranking-mobile', testInfo.outputPath('tournament-ranking-mobile.webp')); await persistScreenshot(page, 'tournament-ranking-mobile', testInfo.outputPath('tournament-ranking-mobile.webp'));
}); });
test('betting bracket shows intelligence for debate tournament candidates', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await installFixture(page, { tournamentType: 3 });
await page.goto('betting');
await expect(page.locator('.betting-bracket .bracket-core-stat').first()).toHaveText('지력 80');
await expect(page.locator('.betting-bracket .bracket-odds').first()).toHaveText('배당 28.00');
await expect(page.locator('.betting-bracket .bracket-my-bet').first()).toHaveText('내 투자 금120');
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
});
test('desktop betting presents icon-and-name cards and all four rankings without document overflow', async ({ test('desktop betting presents icon-and-name cards and all four rankings without document overflow', async ({
page, page,
}, testInfo) => { }, testInfo) => {
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue';
import GeneralIdentity from '../ui/GeneralIdentity.vue'; import GeneralIdentity from '../ui/GeneralIdentity.vue';
import { import {
buildTournamentBracket, buildTournamentBracket,
resolveTournamentCoreStat,
type TournamentBracketMatch, type TournamentBracketMatch,
type TournamentBracketParticipant, type TournamentBracketParticipant,
} from '../../utils/tournamentBracket'; } from '../../utils/tournamentBracket';
@@ -14,6 +15,7 @@ const props = defineProps<{
betTotals?: Record<number, number>; betTotals?: Record<number, number>;
myBetTotals?: Record<number, number>; myBetTotals?: Record<number, number>;
totalBet: number; totalBet: number;
tournamentType?: number;
showLegend?: boolean; showLegend?: boolean;
}>(); }>();
@@ -68,6 +70,8 @@ const odds = (id: number | null) => {
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 myBet = (id: number | null) => (id === null ? 0 : (props.myBetTotals?.[id] ?? 0));
const coreStat = (slot: (typeof bracket.value.top16.slots)[number]) =>
resolveTournamentCoreStat(slot, props.tournamentType ?? 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]);
@@ -119,6 +123,9 @@ 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" />
<div v-if="columnIndex === 0" class="bracket-bet-summary"> <div v-if="columnIndex === 0" class="bracket-bet-summary">
<small v-if="coreStat(slot)" class="bracket-core-stat">
{{ coreStat(slot)?.label }} {{ coreStat(slot)?.value }}
</small>
<small class="bracket-odds">배당 {{ odds(slot.id) }}</small> <small class="bracket-odds">배당 {{ odds(slot.id) }}</small>
<small class="bracket-my-bet"> 투자 {{ myBet(slot.id) }}</small> <small class="bracket-my-bet"> 투자 {{ myBet(slot.id) }}</small>
</div> </div>
@@ -152,6 +159,9 @@ 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" />
<div v-if="activeMobileRound === 0" class="bracket-bet-summary"> <div v-if="activeMobileRound === 0" class="bracket-bet-summary">
<small v-if="coreStat(slot)" class="bracket-core-stat">
{{ coreStat(slot)?.label }} {{ coreStat(slot)?.value }}
</small>
<small class="bracket-odds">배당 {{ odds(slot.id) }}</small> <small class="bracket-odds">배당 {{ odds(slot.id) }}</small>
<small class="bracket-my-bet"> 투자 {{ myBet(slot.id) }}</small> <small class="bracket-my-bet"> 투자 {{ myBet(slot.id) }}</small>
</div> </div>
@@ -236,6 +246,7 @@ const mobilePairs = computed(() => {
gap: 4px; gap: 4px;
white-space: nowrap; white-space: nowrap;
} }
.bracket-core-stat,
.bracket-odds, .bracket-odds,
.bracket-my-bet { .bracket-my-bet {
display: block; display: block;
@@ -244,6 +255,9 @@ const mobilePairs = computed(() => {
font-size: 11px; font-size: 11px;
line-height: 12px; line-height: 12px;
} }
.bracket-core-stat {
color: #fff;
}
.bracket-my-bet { .bracket-my-bet {
overflow: hidden; overflow: hidden;
color: orange; color: orange;
@@ -3,6 +3,9 @@ export interface TournamentBracketParticipant {
name: string; name: string;
picture?: string | null; picture?: string | null;
imageServer?: number | null; imageServer?: number | null;
leadership?: number;
strength?: number;
intel?: number;
} }
export interface TournamentBracketMatch { export interface TournamentBracketMatch {
@@ -20,6 +23,14 @@ export interface TournamentBracketSlot {
picture: string | null; picture: string | null;
imageServer: number; imageServer: number;
advanced: boolean; advanced: boolean;
leadership?: number;
strength?: number;
intel?: number;
}
export interface TournamentCoreStat {
label: '종합' | '통솔' | '무력' | '지력';
value: number;
} }
export interface TournamentBracketRound { export interface TournamentBracketRound {
@@ -43,6 +54,18 @@ const emptySlot = (): TournamentBracketSlot => ({
advanced: false, advanced: false,
}); });
export const resolveTournamentCoreStat = (
participant: Pick<TournamentBracketParticipant, 'leadership' | 'strength' | 'intel'>,
tournamentType: number
): TournamentCoreStat | null => {
const { leadership, strength, intel } = participant;
if (leadership === undefined || strength === undefined || intel === undefined) return null;
if (tournamentType === 0) return { label: '종합', value: leadership + strength + intel };
if (tournamentType === 1) return { label: '통솔', value: leadership };
if (tournamentType === 2) return { label: '무력', value: strength };
return { label: '지력', value: intel };
};
export const buildTournamentBracket = ( export const buildTournamentBracket = (
participants: TournamentBracketParticipant[], participants: TournamentBracketParticipant[],
matches: TournamentBracketMatch[], matches: TournamentBracketMatch[],
@@ -65,6 +88,9 @@ export const buildTournamentBracket = (
picture: participant?.picture ?? null, picture: participant?.picture ?? null,
imageServer: participant?.imageServer ?? 0, imageServer: participant?.imageServer ?? 0,
advanced: match.winnerId === id, advanced: match.winnerId === id,
leadership: participant?.leadership,
strength: participant?.strength,
intel: participant?.intel,
}; };
}) })
); );
@@ -84,6 +110,9 @@ export const buildTournamentBracket = (
picture: participantOf(resolvedWinnerId)?.picture ?? null, picture: participantOf(resolvedWinnerId)?.picture ?? null,
imageServer: participantOf(resolvedWinnerId)?.imageServer ?? 0, imageServer: participantOf(resolvedWinnerId)?.imageServer ?? 0,
advanced: resolvedWinnerId !== null, advanced: resolvedWinnerId !== null,
leadership: participantOf(resolvedWinnerId)?.leadership,
strength: participantOf(resolvedWinnerId)?.strength,
intel: participantOf(resolvedWinnerId)?.intel,
}, },
final, final,
semi: buildRound(9, 4), semi: buildRound(9, 4),
@@ -137,6 +137,7 @@ const placeBet = async (targetId: number) => {
:bet-totals="betTotals" :bet-totals="betTotals"
:my-bet-totals="myBetTotals" :my-bet-totals="myBetTotals"
:total-bet="totalAmount" :total-bet="totalAmount"
:tournament-type="snapshot?.state?.type ?? 0"
:show-legend="false" :show-legend="false"
/> />
@@ -193,6 +193,7 @@ const start = async () => {
: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"
/> />
<section v-if="currentMatch" class="fight bg0"> <section v-if="currentMatch" class="fight bg0">
@@ -1,7 +1,7 @@
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 { buildTournamentBracket } from '../src/utils/tournamentBracket.ts'; import { buildTournamentBracket, resolveTournamentCoreStat } from '../src/utils/tournamentBracket.ts';
const participants = Array.from({ length: 16 }, (_, index) => ({ const participants = Array.from({ length: 16 }, (_, index) => ({
id: index + 1, id: index + 1,
@@ -38,6 +38,15 @@ const matches = [
]; ];
void describe('tournament bracket', () => { void describe('tournament bracket', () => {
void it('maps every tournament type to the legacy core stat', () => {
const participant = { leadership: 81, strength: 72, intel: 93 };
assert.deepEqual(resolveTournamentCoreStat(participant, 0), { label: '종합', value: 246 });
assert.deepEqual(resolveTournamentCoreStat(participant, 1), { label: '통솔', value: 81 });
assert.deepEqual(resolveTournamentCoreStat(participant, 2), { label: '무력', value: 72 });
assert.deepEqual(resolveTournamentCoreStat(participant, 3), { label: '지력', value: 93 });
});
void it('keeps every general in the worker roundIndex order and marks the actual winner path', () => { void it('keeps every general in the worker roundIndex order and marks the actual winner path', () => {
const bracket = buildTournamentBracket(participants, matches, 1); const bracket = buildTournamentBracket(participants, matches, 1);