merge: 최신 main을 레거시 DB 재이관에 통합

# Conflicts:
#	app/gateway-api/test/releaseManifest.test.ts
#	release-manifest.json
This commit is contained in:
2026-08-17 15:58:39 +00:00
28 changed files with 1228 additions and 128 deletions
@@ -153,3 +153,18 @@
border-color: var(--legacy-button-border);
background: var(--legacy-button-bg);
}
/*
* Split controls share one outer silhouette. Keep the main control's right
* border as the subtle divider while removing every inner corner and the
* toggle's overlapping left border. These rules intentionally follow the
* Lumen family so its border shorthand cannot restore the inner rounding.
*/
.legacy-split-button > .main-menu-link {
border-radius: 5.25px 0 0 5.25px;
}
.legacy-split-button > .legacy-split-button__toggle {
border-left-width: 0;
border-radius: 0 5.25px 5.25px 0;
}
@@ -69,7 +69,7 @@ const updateAmount = (event: Event) => {
};
const submit = async (crewType?: RecruitmentCrewType) => {
if (crewType) selectCrewType(crewType);
if (crewType && crewType.id !== selectedCrewTypeId.value) selectCrewType(crewType);
await nextTick();
if (valid.value) emit('submit');
};
@@ -59,7 +59,7 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
</template>
</ul>
</div>
<div v-else class="main-menu-split">
<div v-else class="main-menu-split legacy-split-button">
<MainNavigationLink
:link="entry.main"
:enabled="isNavigationConfigured(entry.main)"
@@ -67,7 +67,7 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
lumen-variant="navigation"
/>
<button
class="main-menu-button main-menu-split__toggle legacy-button legacy-button--navigation"
class="main-menu-button main-menu-split__toggle legacy-split-button__toggle legacy-button legacy-button--navigation"
type="button"
:data-menu-id="entry.id"
:aria-label="`${entry.main.label} 하위 메뉴`"
@@ -131,12 +131,6 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
min-width: 28px;
width: 28px;
padding: 0;
border-left-width: 0;
border-radius: 0 5.25px 5.25px 0;
}
.main-menu-split > :deep(.main-menu-link) {
border-radius: 5.25px 0 0 5.25px;
}
.menu-caret {
@@ -226,7 +226,6 @@ const onQuick = (item: QuickNavigationItem) => {
data-bottom-menu="manual-refresh"
aria-label="직접 갱신"
title="직접 갱신"
:disabled="refreshing"
:aria-busy="refreshing"
@click="emit('refresh')"
>
@@ -306,12 +305,6 @@ const onQuick = (item: QuickNavigationItem) => {
line-height: 1;
}
.manual-refresh-trigger:disabled {
cursor: wait;
filter: grayscale(0.6);
opacity: 0.55;
}
.bottom-trigger.legacy-button:not(:disabled, [aria-disabled='true']):hover,
.bottom-trigger.legacy-button:not(:disabled, [aria-disabled='true'])[aria-expanded='true'] {
height: 44px;
@@ -35,7 +35,7 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props
:active="isActive(entry)"
lumen-variant="lumen"
/>
<div v-else-if="entry.kind === 'split'" class="nation-menu-split">
<div v-else-if="entry.kind === 'split'" class="nation-menu-split legacy-split-button">
<MainNavigationLink
:link="entry.main"
:enabled="isNationNavigationEnabled(entry.main, access)"
@@ -43,7 +43,7 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props
lumen-variant="lumen"
/>
<button
class="main-menu-button nation-menu-split__toggle legacy-button legacy-button--lumen"
class="main-menu-button nation-menu-split__toggle legacy-split-button__toggle legacy-button legacy-button--lumen"
type="button"
:data-menu-id="entry.id"
:aria-label="`${entry.main.label} 하위 메뉴`"
@@ -108,12 +108,6 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props
min-width: 28px;
width: 28px;
padding: 0;
border-left-width: 0;
border-radius: 0 5.25px 5.25px 0;
}
.nation-menu-split > :deep(.main-menu-link) {
border-radius: 5.25px 0 0 5.25px;
}
.menu-caret {
@@ -12,6 +12,7 @@ const props = defineProps<{
matches: TournamentBracketMatch[];
winnerId?: number;
betTotals?: Record<number, number>;
myBetTotals?: Record<number, number>;
totalBet: number;
showLegend?: boolean;
}>();
@@ -66,6 +67,7 @@ const odds = (id: number | null) => {
if (!amount) return '∞';
return (props.totalBet / amount).toFixed(2);
};
const myBet = (id: number | null) => (id === null ? 0 : (props.myBetTotals?.[id] ?? 0));
const mobilePairs = computed(() => {
const column = roundColumns.value[activeMobileRound.value] ?? [];
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" />
<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>
</template>
</div>
@@ -146,7 +151,10 @@ const mobilePairs = computed(() => {
:data-general-id="slot.id ?? undefined"
>
<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>
<strong v-if="pair.length === 2" class="versus" aria-hidden="true">VS</strong>
</article>
@@ -220,12 +228,26 @@ const mobilePairs = computed(() => {
width: 100%;
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;
min-width: 0;
color: skyblue;
font-size: 11px;
line-height: 12px;
text-align: right;
}
.bracket-my-bet {
overflow: hidden;
color: orange;
text-overflow: ellipsis;
}
.mobile-bracket {
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 { computed, onMounted, ref } from 'vue';
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
import TournamentPageHeader from '../components/tournament/TournamentPageHeader.vue';
import GeneralIdentity from '../components/ui/GeneralIdentity.vue';
import { trpc } from '../utils/trpc';
@@ -72,6 +73,7 @@ const candidates = computed(() =>
const totalAmount = computed(() => summary.value?.totalAmount ?? 0);
const myAmount = computed(() => summary.value?.myAmount ?? 0);
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 totals = summary.value?.totals as Record<number, number> | undefined;
const amount = totals?.[id] ?? 0;
@@ -110,12 +112,7 @@ const placeBet = async (targetId: number) => {
<template>
<main id="tournament-betting-container" class="betting-page">
<section class="title bg0">
<br />
<RouterLink v-slot="{ navigate }" custom to="/">
<button class="close-button" type="button" @click="navigate"> 닫기</button>
</RouterLink>
</section>
<TournamentPageHeader class="bg0" active-page="betting" title="베 팅 장" />
<section class="toolbar bg0">
<button type="button" @click="load">갱신</button>
<span v-if="loading">불러오는 중...</span>
@@ -138,6 +135,7 @@ const placeBet = async (targetId: number) => {
:matches="snapshot?.matches ?? []"
:winner-id="snapshot?.state?.winnerId"
:bet-totals="betTotals"
:my-bet-totals="myBetTotals"
:total-bet="totalAmount"
:show-legend="false"
/>
@@ -303,25 +301,6 @@ const placeBet = async (targetId: number) => {
.bg2 {
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 {
min-height: 46px;
padding: 1px;
@@ -417,10 +396,6 @@ button:hover,
button:focus {
filter: brightness(1.25);
}
.close-button:hover,
.close-button:focus {
filter: brightness(1.2);
}
button:focus-visible,
select:focus-visible {
outline: 2px solid #f39c12;
@@ -507,10 +482,6 @@ select:disabled {
max-width: 100%;
font-size: 13px;
}
.title {
height: auto;
min-height: 55px;
}
.state {
font-size: 18px;
}
+12 -3
View File
@@ -20,11 +20,13 @@ import type { QuickNavigationItem } from '../components/main/mainNavigation';
import { formatLog } from '../utils/formatLog';
import { useSessionStore } from '../stores/session';
import { useMainDashboardStore } from '../stores/mainDashboard';
import { useGameFeedback } from '../composables/useGameFeedback';
import { trpc } from '../utils/trpc';
import type { CommandPatternEntry } from '../components/command/types';
const session = useSessionStore();
const dashboard = useMainDashboardStore();
const { info: showInfoToast } = useGameFeedback();
const isMobile = useMediaQuery('(max-width: 939.98px)');
const npcMode = ref(0);
@@ -115,6 +117,14 @@ const loadMainData = async () => {
npcMode.value = worldState?.config.npcMode ?? 0;
};
const requestManualRefresh = () => {
if (refreshing.value) {
showInfoToast('이미 정보를 갱신하고 있습니다.');
return;
}
void loadMainData();
};
const moveLobby = () => {
window.location.replace(import.meta.env.VITE_GATEWAY_WEB_URL?.trim() || '/gateway/');
};
@@ -154,9 +164,8 @@ watch(
<button
class="game-shell__action legacy-button legacy-button--navigation"
type="button"
:disabled="refreshing"
:aria-busy="refreshing"
@click="loadMainData"
@click="requestManualRefresh"
>
</button>
@@ -451,7 +460,7 @@ watch(
:npc-mode="npcMode"
:realtime-enabled="realtimeEnabled"
:refreshing="refreshing"
@refresh="loadMainData"
@refresh="requestManualRefresh"
@toggle-realtime="dashboard.setRealtimeEnabled(!realtimeEnabled)"
@lobby="moveLobby"
@quick="moveQuick"
+23 -35
View File
@@ -2,6 +2,7 @@
import { formatServerDateTime } from '@sammo-ts/common';
import { computed, onMounted, ref } from 'vue';
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
import TournamentPageHeader from '../components/tournament/TournamentPageHeader.vue';
import GeneralIdentity from '../components/ui/GeneralIdentity.vue';
import { trpc } from '../utils/trpc';
import { resolveTournamentStageName } from '../utils/tournamentStatus';
@@ -58,6 +59,7 @@ const openingTime = computed(() =>
formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' })
);
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(() =>
(snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value)
);
@@ -71,8 +73,25 @@ const groups = computed(() =>
const preliminaryGroups = computed(() =>
Array.from({ length: 8 }, (_, index) =>
(snapshot.value?.participants ?? [])
.filter((participant) => participant.groupId === index)
.sort((a, b) => (a.seedRank ?? 99) - (b.seedRank ?? 99) || (a.groupNo ?? 99) - (b.groupNo ?? 99))
.filter((participant) => {
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 = ['一', '二', '三', '四', '五', '六', '七', '八'];
@@ -141,12 +160,7 @@ const start = async () => {
<template>
<main id="tournament-container" class="legacy-page">
<section class="legacy-title bg0">
<div>삼모전 토너먼트</div>
<RouterLink v-slot="{ navigate }" custom to="/">
<button class="close-button" type="button" @click="navigate"> 닫기</button>
</RouterLink>
</section>
<TournamentPageHeader class="bg0" active-page="tournament" title="삼모전 토너먼트" />
<section class="toolbar bg0">
<button type="button" @click="load">갱신</button>
@@ -177,6 +191,7 @@ const start = async () => {
:matches="snapshot?.matches ?? []"
:winner-id="snapshot?.state?.winnerId"
:bet-totals="betTotals"
:my-bet-totals="myBetTotals"
:total-bet="totalBet"
/>
@@ -384,25 +399,6 @@ const start = async () => {
.bg2 {
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 {
min-height: 46px;
padding: 1px;
@@ -433,10 +429,6 @@ button:hover,
button:focus {
filter: brightness(1.25);
}
.close-button:hover,
.close-button:focus {
filter: brightness(1.2);
}
button:focus-visible {
outline: 2px solid #f39c12;
outline-offset: 1px;
@@ -532,10 +524,6 @@ td {
max-width: 100%;
font-size: 13px;
}
.legacy-title {
height: auto;
min-height: 55px;
}
.state-row {
font-size: 18px;
}