fix: 설문과 장수 선택 기한 및 토너먼트 개최를 게임 시각으로 통일
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user