베팅장 장수칸에서 금액 입력과 개별 즉시 베팅 지원

This commit is contained in:
2026-09-12 01:20:35 +00:00
parent a31c2711c7
commit 5041fc365e
5 changed files with 445 additions and 463 deletions
@@ -47,9 +47,9 @@ const roundColumns = computed(() => [
bracket.value.final.slots,
[bracket.value.champion],
]);
const desktopX = [110, 355, 600, 845, 1090];
const desktopX = computed(() => (props.bettingMode ? [180, 480, 690, 895, 1100] : [110, 355, 600, 845, 1090]));
const cardWidth = 190;
const desktopSlotHeight = computed(() => (props.bettingMode ? 100 : 88));
const desktopSlotHeight = computed(() => (props.bettingMode ? 154 : 88));
const desktopCanvasHeight = computed(() => desktopSlotHeight.value * 16);
const slotY = (columnIndex: number, slotIndex: number) => {
const slotHeight = desktopSlotHeight.value * 2 ** columnIndex;
@@ -57,8 +57,8 @@ const slotY = (columnIndex: number, slotIndex: number) => {
};
const connections = computed(() =>
roundColumns.value.slice(0, -1).flatMap((column, columnIndex) => {
const sourceX = desktopX[columnIndex]! + cardWidth / 2;
const targetX = desktopX[columnIndex + 1]! - cardWidth / 2;
const sourceX = desktopX.value[columnIndex]! + (props.bettingMode && columnIndex === 0 ? 170 : cardWidth / 2);
const targetX = desktopX.value[columnIndex + 1]! - cardWidth / 2;
const jointX = (sourceX + targetX) / 2;
return Array.from({ length: column.length / 2 }, (_, pairIndex) => {
const upper = column[pairIndex * 2]!;
@@ -95,11 +95,6 @@ const myExpectedReturn = (id: number | null): number | null => {
if (invested <= 0 || targetTotal <= 0) return null;
return Math.round((invested * props.totalBet) / targetTotal);
};
const myExpectedReturnDescription = (id: number | null): string | null => {
const expectedReturn = myExpectedReturn(id);
if (id === null || expectedReturn === null) return null;
return `현재 배당 ${odds(id)} × 내 투자 금${myBet(id).toLocaleString('ko-KR')} = 금${expectedReturn.toLocaleString('ko-KR')}`;
};
const coreStat = (slot: (typeof bracket.value.top16.slots)[number]) =>
resolveTournamentCoreStat(slot, props.tournamentType ?? 0);
const requestBet = (slot: TournamentBracketSlot) => {
@@ -114,7 +109,12 @@ const mobilePairs = computed(() => {
</script>
<template>
<section class="tournament-bracket" aria-label="토너먼트 대진표" tabindex="0">
<section
class="tournament-bracket"
:class="{ 'inline-betting-bracket': bettingMode }"
aria-label="토너먼트 대진표"
tabindex="0"
>
<div class="desktop-bracket">
<div class="desktop-round-labels" aria-hidden="true">
<strong v-for="label in roundLabels" :key="label">{{ label }}</strong>
@@ -182,34 +182,30 @@ const mobilePairs = computed(() => {
:image-server="slot.imageServer"
:npc-state="slot.npcState"
/>
<button
v-if="columnIndex === 0 && bettingOpen && slot.id !== null"
type="button"
class="bracket-bet-button"
:aria-label="`${slot.name}에게 베팅하기`"
@click="requestBet(slot)"
>
베팅하기
</button>
<div v-if="columnIndex === 0 && bettingMode" 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>
<span class="bracket-my-bet-tooltip">
<RichTooltip
title="현재 배당 기준 예상 환수금"
:description="myExpectedReturnDescription(slot.id)"
placement="bottom"
:max-width="260"
:test-id="slot.id === null ? undefined : `candidate-return-${slot.id}`"
>
<small class="bracket-my-bet"
> 투자 {{ myBet(slot.id).toLocaleString('ko-KR') }}</small
>
</RichTooltip>
</span>
<small class="bracket-my-bet"> 투자 {{ myBet(slot.id).toLocaleString('ko-KR') }}</small>
<small class="bracket-return"
>예상 환수 {{ (myExpectedReturn(slot.id) ?? 0).toLocaleString('ko-KR') }}</small
>
</div>
<slot
v-if="columnIndex === 0 && bettingOpen && slot.id !== null"
name="bet-controls"
:candidate="slot"
>
<button
type="button"
class="bracket-bet-button"
:aria-label="`${slot.name}에게 베팅하기`"
@click="requestBet(slot)"
>
베팅
</button>
</slot>
</article>
</template>
</div>
@@ -269,34 +265,30 @@ const mobilePairs = computed(() => {
:image-server="slot.imageServer"
:npc-state="slot.npcState"
/>
<button
v-if="activeMobileRound === 0 && bettingOpen && slot.id !== null"
type="button"
class="bracket-bet-button"
:aria-label="`${slot.name}에게 베팅하기`"
@click="requestBet(slot)"
>
베팅하기
</button>
<div v-if="activeMobileRound === 0 && bettingMode" 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>
<span class="bracket-my-bet-tooltip">
<RichTooltip
title="현재 배당 기준 예상 환수금"
:description="myExpectedReturnDescription(slot.id)"
placement="bottom"
:max-width="260"
:test-id="slot.id === null ? undefined : `mobile-candidate-return-${slot.id}`"
>
<small class="bracket-my-bet"
> 투자 {{ myBet(slot.id).toLocaleString('ko-KR') }}</small
>
</RichTooltip>
</span>
<small class="bracket-my-bet"> 투자 {{ myBet(slot.id).toLocaleString('ko-KR') }}</small>
<small class="bracket-return"
>예상 환수 {{ (myExpectedReturn(slot.id) ?? 0).toLocaleString('ko-KR') }}</small
>
</div>
<slot
v-if="activeMobileRound === 0 && bettingOpen && slot.id !== null"
name="bet-controls"
:candidate="slot"
>
<button
type="button"
class="bracket-bet-button"
:aria-label="`${slot.name}에게 베팅하기`"
@click="requestBet(slot)"
>
베팅
</button>
</slot>
</div>
<strong v-if="pair.length === 2" class="versus" aria-hidden="true">VS</strong>
</article>
@@ -361,8 +353,8 @@ const mobilePairs = computed(() => {
padding: 1px 3px;
}
.desktop-bracket-name.betting-candidate {
width: clamp(150px, 18vw, 216px);
min-height: 92px;
width: 28.3333%;
min-height: 140px;
align-content: center;
gap: 5px;
padding: 5px 6px;
@@ -412,7 +404,7 @@ const mobilePairs = computed(() => {
min-width: 0;
min-height: 24px;
align-items: center;
padding-right: 62px;
padding-right: 0;
text-align: left;
}
.bracket-candidate-identity :deep(.rich-tooltip-trigger) {
@@ -449,16 +441,40 @@ const mobilePairs = computed(() => {
.bracket-odds {
text-align: right;
}
.bracket-my-bet-tooltip {
grid-column: 1 / -1;
min-width: 0;
color: orange;
text-align: left;
}
.bracket-my-bet {
grid-column: 1 / -1;
text-align: left;
color: orange;
font-variant-numeric: tabular-nums;
}
.bracket-return {
grid-column: 1 / -1;
text-align: left;
color: cyan;
font-size: 11px;
line-height: 14px;
white-space: normal;
overflow-wrap: anywhere;
}
.inline-betting-bracket .desktop-round-labels {
grid-template-columns: 30% 20% 20% 16.6667% 13.3333%;
}
.inline-betting-bracket .mobile-bracket-name.betting-candidate {
display: flex;
flex-direction: column;
gap: 5px;
}
.inline-betting-bracket .mobile-bracket-name.betting-candidate > :last-child {
margin-top: auto;
}
@media (max-width: 1100px) {
.inline-betting-bracket .desktop-bracket {
display: none;
}
.inline-betting-bracket .mobile-bracket {
display: block;
}
}
.mobile-bracket {
display: none;
}
@@ -505,7 +521,7 @@ const mobilePairs = computed(() => {
padding: 1px 3px;
}
.mobile-bracket-name.betting-candidate {
min-height: 92px;
min-height: 140px;
padding: 5px 6px;
}
.versus {