merge: 베팅 후보 핵심 능력치 표시를 통합
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user