feat: port pre-start general deletion lifecycle

This commit is contained in:
2026-07-31 05:44:36 +00:00
parent f1929be3fe
commit 71ec02d091
24 changed files with 1257 additions and 160 deletions
+4
View File
@@ -99,6 +99,10 @@ export const useSessionStore = defineStore('session', {
this.setGameToken(null);
this.status = 'public';
},
leaveGame() {
this.setGameToken(null);
this.status = this.sessionToken ? 'authed' : 'public';
},
async refreshGeneralStatus() {
if (!this.gameToken) {
this.status = this.sessionToken ? 'authed' : 'public';
+39 -9
View File
@@ -4,14 +4,17 @@ import { trpc } from '../utils/trpc';
import { formatLog } from '../utils/formatLog';
import { formatSeoulDateTime } from '../utils/legacyDateTime';
import { isDefenceTrainPenaltyWaivedByScenarioEffect } from '@sammo-ts/logic';
import { useSessionStore } from '../stores/session';
const SCREEN_MODE_KEY = 'sam.screenMode';
const CUSTOM_CSS_KEY = 'sam_customCSS';
const PENDING_DIE_ON_PRESTART_KEY = 'sam.pending.dieOnPrestart';
type ScreenMode = 'auto' | '500px' | '1000px';
type LogType = 'generalHistory' | 'battleDetail' | 'battleResult' | 'generalAction';
type ItemSlotKey = 'horse' | 'weapon' | 'book' | 'item';
type MyGeneralResponse = Awaited<ReturnType<typeof trpc.general.me.query>>;
type SelectionPoolStatus = Awaited<ReturnType<typeof trpc.join.getConfig.query>>['selectionPool'];
type DieOnPrestartStatus = Awaited<ReturnType<typeof trpc.general.ensureDieOnPrestartStatus.mutate>>;
type WorldSnapshot = {
currentYear: number;
@@ -31,13 +34,20 @@ type SettingForm = {
const data = ref<MyGeneralResponse | null>(null);
const world = ref<WorldSnapshot>(null);
const selectionPoolStatus = ref<SelectionPoolStatus | null>(null);
const dieOnPrestartStatus = ref<DieOnPrestartStatus | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const screenMode = ref<ScreenMode>('auto');
const customCss = ref('');
const cssSaving = ref(false);
const session = useSessionStore();
let cssTimer: number | null = null;
const readPendingDieOnPrestartId = (): string => {
const stored = window.sessionStorage.getItem(PENDING_DIE_ON_PRESTART_KEY);
return stored && /^[0-9a-f-]{36}$/iu.test(stored) ? stored : crypto.randomUUID();
};
const immediateActionRequestIds = reactive({
dieOnPrestart: readPendingDieOnPrestartId(),
buildNationCandidate: crypto.randomUUID(),
instantRetreat: crypto.randomUUID(),
});
@@ -136,12 +146,16 @@ const actionAvailability = computed(() => {
const preopen = Boolean(turnTime && openTime && turnTime.getTime() <= openTime.getTime());
const npcMode = numberValue(config.npcMode ?? config.npcmode, 0);
return {
dieOnPrestart: Boolean(preopen && general?.npcState === 0 && general.nationId === 0),
dieOnPrestart: Boolean(dieOnPrestartStatus.value?.show),
buildNationCandidate: Boolean(preopen && general?.nationId === 0),
instantRetreat: Boolean(availableInstantAction.instantRetreat),
selectOtherGeneral: Boolean(npcMode === 2 && general?.npcState === 0),
};
});
const formatDieOnPrestartAvailableAt = computed(() => {
const value = dieOnPrestartStatus.value?.availableAt;
return value ? formatSeoulDateTime(value) : '';
});
const formatSelectionAvailableAt = computed(() => {
const value = selectionPoolStatus.value?.nextChangeAt;
if (!value) return '';
@@ -178,14 +192,16 @@ const loadPage = async () => {
loading.value = true;
error.value = null;
try {
const [general, state, joinConfig] = await Promise.all([
const [general, state, joinConfig, prestartStatus] = await Promise.all([
trpc.general.me.query(),
trpc.world.getState.query() as Promise<WorldSnapshot>,
trpc.join.getConfig.query(),
trpc.general.ensureDieOnPrestartStatus.mutate(),
]);
data.value = general;
world.value = state;
selectionPoolStatus.value = joinConfig.selectionPool;
dieOnPrestartStatus.value = prestartStatus;
if (general) {
Object.assign(form, general.settings);
}
@@ -218,6 +234,25 @@ const confirmMutation = async (message: string, mutation: () => Promise<unknown>
}
};
const dieOnPrestart = async () => {
if (!confirm('정말로 삭제하시겠습니까?')) return;
const clientRequestId = immediateActionRequestIds.dieOnPrestart;
window.sessionStorage.setItem(PENDING_DIE_ON_PRESTART_KEY, clientRequestId);
try {
await trpc.general.dieOnPrestart.mutate({ clientRequestId });
window.sessionStorage.removeItem(PENDING_DIE_ON_PRESTART_KEY);
session.leaveGame();
window.location.replace(import.meta.env.VITE_GATEWAY_WEB_URL?.trim() || '/gateway/');
} catch (cause) {
const code = asRecord(asRecord(cause).data).code;
if (code !== 'TIMEOUT') {
window.sessionStorage.removeItem(PENDING_DIE_ON_PRESTART_KEY);
}
alert(`실패했습니다: ${errorText(cause)}`);
window.location.reload();
}
};
const dropItem = (item: { key: ItemSlotKey; name: string; code: string | null }) =>
confirmMutation(`${item.code ?? item.name}을(를) 버리시겠습니까?`, () =>
trpc.general.dropItem.mutate({ itemType: item.key })
@@ -387,13 +422,8 @@ onMounted(() => {
</button>
</div>
<div v-if="actionAvailability.dieOnPrestart" class="action-line">
가오픈 기간 장수 삭제<br />
<button
class="action-button"
@click="confirmMutation('정말로 삭제하시겠습니까?', () => trpc.general.dieOnPrestart.mutate())"
>
장수 삭제
</button>
가오픈 기간 장수 삭제 ({{ formatDieOnPrestartAvailableAt }} 부터)<br />
<button class="action-button" @click="dieOnPrestart">장수 삭제</button>
</div>
<div v-if="actionAvailability.buildNationCandidate" class="action-line">
서버 개시 이전 거병(2턴부터 건국 가능)<br />