feat: 참가자 잠금 기능 추가 및 관리자 상태 조회 기능 구현
This commit is contained in:
@@ -37,6 +37,7 @@ const zTournamentState = z.object({
|
|||||||
winnerId: z.number().int().optional(),
|
winnerId: z.number().int().optional(),
|
||||||
bettingSettled: z.boolean().optional(),
|
bettingSettled: z.boolean().optional(),
|
||||||
rewardSettled: z.boolean().optional(),
|
rewardSettled: z.boolean().optional(),
|
||||||
|
participantsLockedAt: z.string().optional(),
|
||||||
lastError: z.string().optional(),
|
lastError: z.string().optional(),
|
||||||
lastErrorAt: z.string().optional(),
|
lastErrorAt: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -95,6 +96,7 @@ export const tournamentRouter = router({
|
|||||||
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
return store.getState();
|
return store.getState();
|
||||||
}),
|
}),
|
||||||
|
getAdminStatus: adminProcedure.query(async () => ({ ok: true })),
|
||||||
getSnapshot: procedure.query(async ({ ctx }) => {
|
getSnapshot: procedure.query(async ({ ctx }) => {
|
||||||
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
const [state, participants, matches, bets] = await Promise.all([
|
const [state, participants, matches, bets] = await Promise.all([
|
||||||
@@ -230,7 +232,7 @@ export const tournamentRouter = router({
|
|||||||
}
|
}
|
||||||
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
const state = await store.getState();
|
const state = await store.getState();
|
||||||
if (!state || state.stage !== 1) {
|
if (!state || state.stage !== 1 || state.participantsLockedAt) {
|
||||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '참가 신청 기간이 아닙니다.' });
|
throw new TRPCError({ code: 'BAD_REQUEST', message: '참가 신청 기간이 아닙니다.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface TournamentState {
|
|||||||
winnerId?: number;
|
winnerId?: number;
|
||||||
bettingSettled?: boolean;
|
bettingSettled?: boolean;
|
||||||
rewardSettled?: boolean;
|
rewardSettled?: boolean;
|
||||||
|
participantsLockedAt?: string;
|
||||||
lastError?: string;
|
lastError?: string;
|
||||||
lastErrorAt?: string;
|
lastErrorAt?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ const applyPreBattleStage = async (
|
|||||||
...state,
|
...state,
|
||||||
stage: 2,
|
stage: 2,
|
||||||
phase: 0,
|
phase: 0,
|
||||||
|
participantsLockedAt: new Date().toISOString(),
|
||||||
nextAt: resolveNextAt(state),
|
nextAt: resolveNextAt(state),
|
||||||
};
|
};
|
||||||
await store.setState(nextState);
|
await store.setState(nextState);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const snapshot = ref<TournamentSnapshot | null>(null);
|
|||||||
const bettingSummary = ref<TournamentBettingSummary | null>(null);
|
const bettingSummary = ref<TournamentBettingSummary | null>(null);
|
||||||
const myGeneral = ref<GeneralMe | null>(null);
|
const myGeneral = ref<GeneralMe | null>(null);
|
||||||
const lastActionMessage = ref<string | null>(null);
|
const lastActionMessage = ref<string | null>(null);
|
||||||
|
const adminEnabled = ref(false);
|
||||||
|
|
||||||
const mobileTabs = [
|
const mobileTabs = [
|
||||||
{ key: 'status', label: '상태' },
|
{ key: 'status', label: '상태' },
|
||||||
@@ -46,14 +47,16 @@ const loadTournament = async () => {
|
|||||||
lastActionMessage.value = null;
|
lastActionMessage.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [snapshotResult, bettingResult, generalResult] = await Promise.all([
|
const [snapshotResult, bettingResult, generalResult, adminResult] = await Promise.all([
|
||||||
trpc.tournament.getSnapshot.query(),
|
trpc.tournament.getSnapshot.query(),
|
||||||
trpc.tournament.getBettingSummary.query(),
|
trpc.tournament.getBettingSummary.query(),
|
||||||
trpc.general.me.query(),
|
trpc.general.me.query(),
|
||||||
|
trpc.tournament.getAdminStatus.query().catch(() => null),
|
||||||
]);
|
]);
|
||||||
snapshot.value = snapshotResult;
|
snapshot.value = snapshotResult;
|
||||||
bettingSummary.value = bettingResult;
|
bettingSummary.value = bettingResult;
|
||||||
myGeneral.value = generalResult;
|
myGeneral.value = generalResult;
|
||||||
|
adminEnabled.value = !!adminResult?.ok;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = resolveErrorMessage(err);
|
error.value = resolveErrorMessage(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -223,6 +226,7 @@ const isParticipant = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showJoinButton = computed(() => snapshot.value?.state?.stage === 1);
|
const showJoinButton = computed(() => snapshot.value?.state?.stage === 1);
|
||||||
|
const joinLockedAt = computed(() => snapshot.value?.state?.participantsLockedAt ?? null);
|
||||||
|
|
||||||
const betTargetId = ref<number>(0);
|
const betTargetId = ref<number>(0);
|
||||||
const betAmount = ref<number>(0);
|
const betAmount = ref<number>(0);
|
||||||
@@ -267,6 +271,64 @@ const bettingTotals = computed<Record<number, number>>(() => bettingSummary.valu
|
|||||||
const bettingMine = computed<Record<number, number>>(() => bettingSummary.value?.myTotals ?? {});
|
const bettingMine = computed<Record<number, number>>(() => bettingSummary.value?.myTotals ?? {});
|
||||||
const totalBetAmount = computed(() => bettingSummary.value?.totalAmount ?? 0);
|
const totalBetAmount = computed(() => bettingSummary.value?.totalAmount ?? 0);
|
||||||
const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
||||||
|
|
||||||
|
const progressSummary = computed(() => {
|
||||||
|
return {
|
||||||
|
participants: snapshot.value?.participants?.length ?? 0,
|
||||||
|
matches: snapshot.value?.matches?.length ?? 0,
|
||||||
|
bets: snapshot.value?.bets?.length ?? 0,
|
||||||
|
nextAt: snapshot.value?.state?.nextAt ?? null,
|
||||||
|
lockedAt: snapshot.value?.state?.participantsLockedAt ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminMessage = ref<string | null>(null);
|
||||||
|
|
||||||
|
const adminStopTournament = async () => {
|
||||||
|
if (!snapshot.value?.state) {
|
||||||
|
adminMessage.value = '토너먼트 상태를 찾을 수 없습니다.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await trpc.tournament.patchState.mutate({ auto: false });
|
||||||
|
adminMessage.value = '자동 진행이 중지되었습니다.';
|
||||||
|
await loadTournament();
|
||||||
|
} catch (err) {
|
||||||
|
adminMessage.value = resolveErrorMessage(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminStartTournament = async () => {
|
||||||
|
const current = snapshot.value?.state;
|
||||||
|
if (!current) {
|
||||||
|
adminMessage.value = '토너먼트 상태를 찾을 수 없습니다.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
trpc.tournament.setParticipants.mutate([]),
|
||||||
|
trpc.tournament.setMatches.mutate([]),
|
||||||
|
trpc.tournament.setBettingEntries.mutate([]),
|
||||||
|
]);
|
||||||
|
await trpc.tournament.setState.mutate({
|
||||||
|
...current,
|
||||||
|
stage: 1,
|
||||||
|
phase: 0,
|
||||||
|
auto: true,
|
||||||
|
nextAt: new Date().toISOString(),
|
||||||
|
bettingId: undefined,
|
||||||
|
bettingCloseAt: undefined,
|
||||||
|
winnerId: undefined,
|
||||||
|
bettingSettled: false,
|
||||||
|
rewardSettled: false,
|
||||||
|
participantsLockedAt: undefined,
|
||||||
|
});
|
||||||
|
adminMessage.value = '임의 개최가 시작되었습니다.';
|
||||||
|
await loadTournament();
|
||||||
|
} catch (err) {
|
||||||
|
adminMessage.value = resolveErrorMessage(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -315,6 +377,24 @@ const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
|||||||
{{ isParticipant ? '참가 완료' : '참가 신청' }}
|
{{ isParticipant ? '참가 완료' : '참가 신청' }}
|
||||||
</button>
|
</button>
|
||||||
<span v-else class="muted">참가 신청 기간이 아닙니다.</span>
|
<span v-else class="muted">참가 신청 기간이 아닙니다.</span>
|
||||||
|
<span v-if="joinLockedAt" class="muted">
|
||||||
|
접수 종료: {{ new Date(joinLockedAt).toLocaleString('ko-KR') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</PanelCard>
|
||||||
|
|
||||||
|
<PanelCard v-if="adminEnabled" title="관리자 진행 상황">
|
||||||
|
<div class="status-grid">
|
||||||
|
<div>참가자: {{ progressSummary.participants }}</div>
|
||||||
|
<div>대진: {{ progressSummary.matches }}</div>
|
||||||
|
<div>베팅: {{ progressSummary.bets }}</div>
|
||||||
|
<div>다음 진행: {{ progressSummary.nextAt ? new Date(progressSummary.nextAt).toLocaleString('ko-KR') : '-' }}</div>
|
||||||
|
<div>접수 종료: {{ progressSummary.lockedAt ? new Date(progressSummary.lockedAt).toLocaleString('ko-KR') : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-actions">
|
||||||
|
<button class="ghost" @click="adminStopTournament">중지</button>
|
||||||
|
<button class="ghost" @click="adminStartTournament">임의 개최</button>
|
||||||
|
<span v-if="adminMessage" class="muted">{{ adminMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
</PanelCard>
|
</PanelCard>
|
||||||
|
|
||||||
@@ -471,6 +551,24 @@ const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
|||||||
{{ isParticipant ? '참가 완료' : '참가 신청' }}
|
{{ isParticipant ? '참가 완료' : '참가 신청' }}
|
||||||
</button>
|
</button>
|
||||||
<span v-else class="muted">참가 신청 기간이 아닙니다.</span>
|
<span v-else class="muted">참가 신청 기간이 아닙니다.</span>
|
||||||
|
<span v-if="joinLockedAt" class="muted">
|
||||||
|
접수 종료: {{ new Date(joinLockedAt).toLocaleString('ko-KR') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</PanelCard>
|
||||||
|
|
||||||
|
<PanelCard v-if="adminEnabled" title="관리자 진행 상황">
|
||||||
|
<div class="status-grid">
|
||||||
|
<div>참가자: {{ progressSummary.participants }}</div>
|
||||||
|
<div>대진: {{ progressSummary.matches }}</div>
|
||||||
|
<div>베팅: {{ progressSummary.bets }}</div>
|
||||||
|
<div>다음 진행: {{ progressSummary.nextAt ? new Date(progressSummary.nextAt).toLocaleString('ko-KR') : '-' }}</div>
|
||||||
|
<div>접수 종료: {{ progressSummary.lockedAt ? new Date(progressSummary.lockedAt).toLocaleString('ko-KR') : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-actions">
|
||||||
|
<button class="ghost" @click="adminStopTournament">중지</button>
|
||||||
|
<button class="ghost" @click="adminStartTournament">임의 개최</button>
|
||||||
|
<span v-if="adminMessage" class="muted">{{ adminMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
</PanelCard>
|
</PanelCard>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user