diff --git a/app/game-frontend/e2e/tournamentBracket.spec.ts b/app/game-frontend/e2e/tournamentBracket.spec.ts index aafff128..84782d31 100644 --- a/app/game-frontend/e2e/tournamentBracket.spec.ts +++ b/app/game-frontend/e2e/tournamentBracket.spec.ts @@ -126,7 +126,7 @@ const persistScreenshot = async (page: Page, name: string, fallbackPath: string) 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; await page.addInitScript((profile) => { window.localStorage.setItem('sammo-game-token', 'ga_tournament_bracket_playwright'); @@ -152,7 +152,7 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean } state: { stage: options.applicationOpen ? 1 : 0, phase: 0, - type: 0, + type: options.tournamentType ?? 0, auto: false, openYear: 184, 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 oddsContainment = await firstSlot.evaluate((slot) => { const card = slot.getBoundingClientRect(); + const stat = slot.querySelector('.bracket-core-stat')!.getBoundingClientRect(); const odds = slot.querySelector('.bracket-odds')!.getBoundingClientRect(); return { cardTop: card.top, cardBottom: card.bottom, + statTop: stat.top, + statBottom: stat.bottom, oddsTop: odds.top, oddsBottom: odds.bottom, cardHeight: card.height, }; }); 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.oddsBottom).toBeLessThanOrEqual(oddsContainment.cardBottom); 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 mobileOddsContainment = await firstMobileSlot.evaluate((slot) => { const card = slot.getBoundingClientRect(); + const stat = slot.querySelector('.bracket-core-stat')!.getBoundingClientRect(); const odds = slot.querySelector('.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.statTop).toBeGreaterThanOrEqual(mobileOddsContainment.cardTop); + expect(mobileOddsContainment.statBottom).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(); @@ -469,6 +484,7 @@ test('mobile betting rankings use tabs and keep dedicated icons beside general n await page.goto('betting'); 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.getByRole('tablist', { name: '토너먼트 랭킹 종목 선택' })).toBeVisible(); 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')); }); +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 ({ page, }, testInfo) => { diff --git a/app/game-frontend/src/components/tournament/TournamentBracket.vue b/app/game-frontend/src/components/tournament/TournamentBracket.vue index e4e6387d..6146404b 100644 --- a/app/game-frontend/src/components/tournament/TournamentBracket.vue +++ b/app/game-frontend/src/components/tournament/TournamentBracket.vue @@ -3,6 +3,7 @@ import { computed, ref } from 'vue'; import GeneralIdentity from '../ui/GeneralIdentity.vue'; import { buildTournamentBracket, + resolveTournamentCoreStat, type TournamentBracketMatch, type TournamentBracketParticipant, } from '../../utils/tournamentBracket'; @@ -14,6 +15,7 @@ const props = defineProps<{ betTotals?: Record; myBetTotals?: Record; totalBet: number; + tournamentType?: number; showLegend?: boolean; }>(); @@ -68,6 +70,8 @@ const odds = (id: number | null) => { return (props.totalBet / amount).toFixed(2); }; 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 column = roundColumns.value[activeMobileRound.value] ?? []; if (activeMobileRound.value === roundColumns.value.length - 1) return column.map((slot) => [slot]); @@ -119,6 +123,9 @@ const mobilePairs = computed(() => { >
+ + {{ coreStat(slot)?.label }} {{ coreStat(slot)?.value }} + 배당 {{ odds(slot.id) }} 내 투자 금{{ myBet(slot.id) }}
@@ -152,6 +159,9 @@ const mobilePairs = computed(() => { >
+ + {{ coreStat(slot)?.label }} {{ coreStat(slot)?.value }} + 배당 {{ odds(slot.id) }} 내 투자 금{{ myBet(slot.id) }}
@@ -236,6 +246,7 @@ const mobilePairs = computed(() => { gap: 4px; white-space: nowrap; } +.bracket-core-stat, .bracket-odds, .bracket-my-bet { display: block; @@ -244,6 +255,9 @@ const mobilePairs = computed(() => { font-size: 11px; line-height: 12px; } +.bracket-core-stat { + color: #fff; +} .bracket-my-bet { overflow: hidden; color: orange; diff --git a/app/game-frontend/src/utils/tournamentBracket.ts b/app/game-frontend/src/utils/tournamentBracket.ts index 7303a57f..430ced48 100644 --- a/app/game-frontend/src/utils/tournamentBracket.ts +++ b/app/game-frontend/src/utils/tournamentBracket.ts @@ -3,6 +3,9 @@ export interface TournamentBracketParticipant { name: string; picture?: string | null; imageServer?: number | null; + leadership?: number; + strength?: number; + intel?: number; } export interface TournamentBracketMatch { @@ -20,6 +23,14 @@ export interface TournamentBracketSlot { picture: string | null; imageServer: number; advanced: boolean; + leadership?: number; + strength?: number; + intel?: number; +} + +export interface TournamentCoreStat { + label: '종합' | '통솔' | '무력' | '지력'; + value: number; } export interface TournamentBracketRound { @@ -43,6 +54,18 @@ const emptySlot = (): TournamentBracketSlot => ({ advanced: false, }); +export const resolveTournamentCoreStat = ( + participant: Pick, + 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 = ( participants: TournamentBracketParticipant[], matches: TournamentBracketMatch[], @@ -65,6 +88,9 @@ export const buildTournamentBracket = ( picture: participant?.picture ?? null, imageServer: participant?.imageServer ?? 0, 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, imageServer: participantOf(resolvedWinnerId)?.imageServer ?? 0, advanced: resolvedWinnerId !== null, + leadership: participantOf(resolvedWinnerId)?.leadership, + strength: participantOf(resolvedWinnerId)?.strength, + intel: participantOf(resolvedWinnerId)?.intel, }, final, semi: buildRound(9, 4), diff --git a/app/game-frontend/src/views/BettingView.vue b/app/game-frontend/src/views/BettingView.vue index 92325d23..26a442d3 100644 --- a/app/game-frontend/src/views/BettingView.vue +++ b/app/game-frontend/src/views/BettingView.vue @@ -137,6 +137,7 @@ const placeBet = async (targetId: number) => { :bet-totals="betTotals" :my-bet-totals="myBetTotals" :total-bet="totalAmount" + :tournament-type="snapshot?.state?.type ?? 0" :show-legend="false" /> diff --git a/app/game-frontend/src/views/TournamentView.vue b/app/game-frontend/src/views/TournamentView.vue index 4c7b75fe..4b14178e 100644 --- a/app/game-frontend/src/views/TournamentView.vue +++ b/app/game-frontend/src/views/TournamentView.vue @@ -193,6 +193,7 @@ const start = async () => { :bet-totals="betTotals" :my-bet-totals="myBetTotals" :total-bet="totalBet" + :tournament-type="snapshot?.state?.type ?? 0" />
diff --git a/app/game-frontend/test/tournamentBracket.test.ts b/app/game-frontend/test/tournamentBracket.test.ts index 8bfe6df7..c9cfbbee 100644 --- a/app/game-frontend/test/tournamentBracket.test.ts +++ b/app/game-frontend/test/tournamentBracket.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; 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) => ({ id: index + 1, @@ -38,6 +38,15 @@ const matches = [ ]; 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', () => { const bracket = buildTournamentBracket(participants, matches, 1);