fix: 설문과 장수 선택 기한 및 토너먼트 개최를 게임 시각으로 통일

This commit is contained in:
2026-09-16 01:38:21 +00:00
parent a51574d20c
commit f4aabec1fa
9 changed files with 388 additions and 56 deletions
+16 -22
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import { useGameFeedback } from '../composables/useGameFeedback';
import PanelCard from '../components/ui/PanelCard.vue';
@@ -11,7 +11,7 @@ import { useSessionStore } from '../stores/session';
import { cityLevelMap, formatOfficerLevelText, regionMap } from '../utils/nationFormat';
import { getNpcColor } from '../utils/npcColor';
import { useClockDisplay } from '../composables/useClockDisplay';
const { formatTime: formatSeoulDateTime } = useClockDisplay();
const { formatTime: formatSeoulDateTime, gameTime } = useClockDisplay();
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
import { abilityLeadint, abilityLeadpow, abilityPowint, abilityRand, type GeneralStats } from '../utils/generalStats';
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
@@ -185,8 +185,6 @@ const npcReservation = ref<PossessReservation | null>(null);
const npcLoading = ref(false);
const npcError = ref<string | null>(null);
const keptNpcIds = ref<number[]>([]);
const nowMs = ref(Date.now());
const npcPickMoreAvailableAtMs = ref(0);
const pendingPossessAction = ref<PendingPossessAction | null>(null);
const npcGeneralList = ref<NpcGeneralList | null>(null);
const npcGeneralListLoading = ref(false);
@@ -202,17 +200,22 @@ const publicGeneralsLoaded = ref(false);
const publicGeneralsLoading = ref(false);
const publicGeneralsError = ref('');
const publicGeneralFilter = ref('');
let npcTimer: number | null = null;
const npcCandidates = computed<PossessCandidate[]>(() => npcReservation.value?.candidates ?? []);
const npcValidUntilMs = computed(() => {
const value = npcReservation.value?.validUntil;
return value ? new Date(value).getTime() : 0;
});
const npcExpired = computed(() => npcValidUntilMs.value > 0 && npcValidUntilMs.value < nowMs.value);
const npcPickMoreSeconds = computed(() =>
Math.max(0, Math.ceil((npcPickMoreAvailableAtMs.value - nowMs.value) / 1000))
const npcExpired = computed(
() => npcValidUntilMs.value > 0 && gameTime.value !== null && npcValidUntilMs.value < gameTime.value.getTime()
);
const npcPickMoreSeconds = computed(() => {
const reservation = npcReservation.value;
if (!reservation) return 0;
if (!gameTime.value) return reservation.pickMoreSeconds;
// pickMoreFrom은 GAME 기한이므로 복구 배속과 정지 상태도 같은 시계를 따른다.
return Math.max(0, Math.ceil((new Date(reservation.pickMoreFrom).getTime() - gameTime.value.getTime()) / 1000));
});
const hasPendingPossession = computed(
() => pendingPossessAction.value !== null && pendingPossessAction.value.ownerUserId === joinConfig.value?.user.id
);
@@ -261,7 +264,8 @@ const filteredPublicGenerals = computed(() => {
return sortGeneralsByTypeThenName(filtered);
});
const npcValidColor = computed(() => {
const remaining = npcValidUntilMs.value - nowMs.value;
if (!gameTime.value) return '#ffffff';
const remaining = npcValidUntilMs.value - gameTime.value.getTime();
if (remaining > 30_000) return '#ffffff';
const channel = Math.max(0, Math.min(255, Math.round((remaining / 30_000) * 255)));
return `rgb(255, ${channel}, ${channel})`;
@@ -465,9 +469,6 @@ const loadNpcCandidates = async (refresh = false) => {
});
npcReservation.value = reservation;
keptNpcIds.value = [];
const receivedAt = Date.now();
nowMs.value = receivedAt;
npcPickMoreAvailableAtMs.value = receivedAt + reservation.pickMoreSeconds * 1000;
} catch (err) {
npcError.value = err instanceof Error ? err.message : 'npc_list_failed';
if (refresh) {
@@ -648,17 +649,8 @@ watch(contextTab, (value) => {
});
onMounted(() => {
npcTimer = window.setInterval(() => {
nowMs.value = Date.now();
}, 250);
void loadConfig();
});
onUnmounted(() => {
if (npcTimer !== null) {
window.clearInterval(npcTimer);
}
});
</script>
<template>
@@ -1119,7 +1111,9 @@ onUnmounted(() => {
id="btn-pick-more"
class="legacy-button legacy-button--secondary"
type="button"
:disabled="npcLoading || npcPickMoreSeconds > 0 || submitting || hasPendingPossession"
:disabled="
npcLoading || !gameTime || npcPickMoreSeconds > 0 || submitting || hasPendingPossession
"
@click="loadNpcCandidates(true)"
>
다른 장수 보기<span v-if="npcPickMoreSeconds > 0">({{ npcPickMoreSeconds }})</span>
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { usePageExit } from '../composables/usePageExit';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useGameFeedback } from '../composables/useGameFeedback';
import { useSessionStore } from '../stores/session';
import { useClockDisplay } from '../composables/useClockDisplay';
const { formatTime: formatSeoulDateTime } = useClockDisplay();
const { formatTime: formatSeoulDateTime, gameTime } = useClockDisplay();
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
import { trpc } from '../utils/trpc';
@@ -38,8 +38,6 @@ const selectedIconId = ref('');
const loading = ref(true);
const submitting = ref(false);
const error = ref('');
const now = ref(Date.now());
let timer: number | null = null;
const pendingActionStorageKey = 'sammo-select-pool-pending-action';
const candidates = computed(() => reservation.value?.candidates ?? []);
@@ -54,9 +52,12 @@ const validUntil = computed(() => {
const value = reservation.value?.validUntil;
return value ? new Date(value).getTime() : 0;
});
const expired = computed(() => validUntil.value > 0 && now.value > validUntil.value);
const expired = computed(
() => validUntil.value > 0 && gameTime.value !== null && gameTime.value.getTime() > validUntil.value
);
const validUntilColor = computed(() => {
const remaining = validUntil.value - now.value;
if (!gameTime.value) return '#fff';
const remaining = validUntil.value - gameTime.value.getTime();
if (remaining <= 0 || remaining > 30_000) {
return '#fff';
}
@@ -227,7 +228,6 @@ async function loadPage(): Promise<void> {
return;
}
reservation.value = await trpc.join.getSelectionPool.mutate();
now.value = Date.now();
} catch (cause) {
console.error(cause);
error.value = errorText(cause);
@@ -250,17 +250,8 @@ const goBack = (): void => {
};
onMounted(() => {
timer = window.setInterval(() => {
now.value = Date.now();
}, 1_000);
void loadPage();
});
onBeforeUnmount(() => {
if (timer !== null) {
window.clearInterval(timer);
}
});
</script>
<template>
+8 -2
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useClockDisplay } from '../composables/useClockDisplay';
import { usePageExit } from '../composables/usePageExit';
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
@@ -7,6 +8,7 @@ import { computed, onMounted, ref } from 'vue';
import { useGameFeedback } from '../composables/useGameFeedback';
import { trpc } from '../utils/trpc';
const { gameTime } = useClockDisplay();
const { pageExitLabel, exitPage } = usePageExit();
type VoteListResponse = Awaited<ReturnType<typeof trpc.vote.getVoteList.query>>;
@@ -41,11 +43,15 @@ const isEnded = (poll: { endAt: string | null; closedAt: string | null }): boole
if (poll.closedAt) {
return true;
}
return poll.endAt ? new Date(poll.endAt).getTime() < Date.now() : false;
return Boolean(poll.endAt && gameTime.value && new Date(poll.endAt).getTime() < gameTime.value.getTime());
};
const canVote = computed(
() => Boolean(currentVote.value) && !currentVote.value?.myVote && !isEnded(currentVote.value!.voteInfo)
() =>
Boolean(currentVote.value) &&
!currentVote.value?.myVote &&
(!currentVote.value?.voteInfo.endAt || gameTime.value !== null) &&
!isEnded(currentVote.value!.voteInfo)
);
const voteTotal = computed(() => (currentVote.value?.votes ?? []).reduce((total, vote) => total + vote.count, 0));
+1 -13
View File
@@ -172,20 +172,8 @@ const cancel = async () => {
};
const start = async () => {
const now = new Date();
try {
await trpc.tournament.setState.mutate({
stage: 1,
phase: 0,
type: 0,
auto: true,
openYear: snapshot.value?.state?.openYear ?? now.getUTCFullYear(),
openMonth: snapshot.value?.state?.openMonth ?? now.getUTCMonth() + 1,
termSeconds: snapshot.value?.state?.termSeconds ?? 60,
nextAt: new Date(Date.now() + 60_000).toISOString(),
bettingSettled: false,
rewardSettled: false,
});
await trpc.tournament.start.mutate();
showSuccessToast('토너먼트를 개최했습니다.');
await load();
} catch (value) {