From 3587befa2c40657abddba742d970a6ac9476ed79 Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 17 Aug 2026 10:22:13 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A0=84=ED=88=AC=20=EC=8B=9C=EB=AE=AC?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=84=B0=20=EA=B2=B0=EA=B3=BC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=95=88=EC=A0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-api/test/battleSimProcessor.test.ts | 3 ++ app/game-frontend/e2e/battleSimulator.spec.ts | 44 ++++++++++++++++++- .../src/views/BattleSimulatorView.vue | 25 +++++------ .../logic/src/battleSimulator/processor.ts | 1 + packages/logic/src/battleSimulator/types.ts | 2 + 5 files changed, 59 insertions(+), 16 deletions(-) diff --git a/app/game-api/test/battleSimProcessor.test.ts b/app/game-api/test/battleSimProcessor.test.ts index 233b3d51..d569422a 100644 --- a/app/game-api/test/battleSimProcessor.test.ts +++ b/app/game-api/test/battleSimProcessor.test.ts @@ -230,12 +230,14 @@ const buildPayload = (action: BattleSimJobPayload['action']): BattleSimJobPayloa describe('battle sim processor', () => { it('returns the fixed-seed battle summary instead of only a successful shape', () => { const payload = buildPayload('battle'); + payload.repeatCnt = 1000; const result = processBattleSimJob(payload); expect(result).toMatchObject({ result: true, reason: 'success', datetime: '2026-01-01 00:00:00', + repeatCnt: 1, avgWar: 1, phase: 2, killed: 626, @@ -277,6 +279,7 @@ describe('battle sim processor', () => { const second = processBattleSimJob(secondPayload); expect(first).toEqual(second); + expect(first.repeatCnt).toBe(2); expect(observedSeeds).toEqual(['server-repeat-0', 'server-repeat-1']); }); diff --git a/app/game-frontend/e2e/battleSimulator.spec.ts b/app/game-frontend/e2e/battleSimulator.spec.ts index ebd67c11..60b9d3ec 100644 --- a/app/game-frontend/e2e/battleSimulator.spec.ts +++ b/app/game-frontend/e2e/battleSimulator.spec.ts @@ -202,6 +202,7 @@ const importedGeneral = { type Fixture = { hasGeneral: boolean; failNextSimulation?: boolean; + prepareDelayMs?: number; requests: string[]; preparedPayloads: BattleSimJobPayload[]; serverResults: BattleSimResultPayload[]; @@ -256,6 +257,9 @@ const installApi = async (page: Page, fixture: Fixture) => { }, gameProfile); await page.route(gameTrpcRoute, async (route) => { const operations = operationNames(route); + if (fixture.prepareDelayMs && operations.includes('battle.prepareSimulation')) { + await new Promise((resolveDelay) => setTimeout(resolveDelay, fixture.prepareDelayMs)); + } const rawRequestBody: unknown = route.request().postData() ? route.request().postDataJSON() : {}; const requestBody = rawRequestBody && typeof rawRequestBody === 'object' ? (rawRequestBody as Record) : {}; @@ -446,8 +450,21 @@ test('keeps simulation available without a game general and preserves input afte await expect(page.getByText('시뮬레이터 입력 오류')).toBeVisible(); await expect(page.getByLabel('시드')).toHaveValue('keep-this-seed'); + fixture.prepareDelayMs = 1_500; await page.getByRole('button', { name: '전투', exact: true }).click(); - await expect(page.getByText('전투를 진행 중입니다.')).toHaveCount(0); + const progressToast = page.getByTestId('game-toast').filter({ hasText: '전투를 진행 중입니다.' }); + await expect(progressToast).toBeVisible(); + const progressToastRect = await progressToast.boundingBox(); + expect(progressToastRect?.x).toBeGreaterThanOrEqual(0); + expect((progressToastRect?.x ?? 0) + (progressToastRect?.width ?? 0)).toBeLessThanOrEqual(500); + if (artifactRoot) { + await page.screenshot({ + path: resolve(artifactRoot, 'battle-simulator-progress-mobile.png'), + fullPage: true, + animations: 'disabled', + }); + } + await expect(progressToast).toHaveCount(0); await expect(page.getByText('시뮬레이터 입력 오류')).toHaveCount(0); expect(await readBrowserWorkerResult(page, 0)).toEqual(fixture.serverResults[0]); @@ -468,6 +485,7 @@ test('runs 1000 battles in the Chromium worker and matches the Node processor ex test.setTimeout(60_000); const fixture: Fixture = { hasGeneral: false, + prepareDelayMs: 500, requests: [], preparedPayloads: [], serverResults: [], @@ -478,13 +496,35 @@ test('runs 1000 battles in the Chromium worker and matches the Node processor ex await page.getByLabel('반복 횟수').selectOption('1000'); await page.getByLabel('시드').fill(''); + const worldSettings = page.locator('[data-parity-id="world-settings"]'); + const worldSettingsTop = (await worldSettings.boundingBox())?.y; await page.getByRole('button', { name: '전투', exact: true }).click(); - await expect(page.getByText('전투를 진행 중입니다.')).toHaveCount(0, { timeout: 30_000 }); + + const progressToast = page.getByTestId('game-toast').filter({ hasText: '전투를 진행 중입니다.' }); + await expect(progressToast).toBeVisible(); + expect(await page.locator('.game-toast-viewport').evaluate((element) => getComputedStyle(element).position)).toBe( + 'fixed' + ); + expect((await worldSettings.boundingBox())?.y).toBe(worldSettingsTop); + if (artifactRoot) { + await page.screenshot({ + path: resolve(artifactRoot, 'battle-simulator-progress-desktop.png'), + fullPage: true, + animations: 'disabled', + }); + } + await expect(progressToast).toHaveCount(0, { timeout: 30_000 }); expect(fixture.preparedPayloads).toHaveLength(1); expect(fixture.preparedPayloads[0]?.seeds).toHaveLength(1000); expect(new Set(fixture.preparedPayloads[0]?.seeds).size).toBe(1000); expect(await readBrowserWorkerResult(page, 0)).toEqual(fixture.serverResults[0]); + const battleSummary = page.locator('[data-parity-id="battle-summary"]'); + await expect(battleSummary.locator('tr').filter({ hasText: '전투 횟수' }).locator('td')).toHaveText('1,000'); + await expect(battleSummary.locator('tr').filter({ hasText: '전투 일시' }).locator('td')).toHaveText( + /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/u + ); + expect(fixture.preparedPayloads[0]?.attackerGeneral.turntime).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/u); expect(fixture.requests).not.toContain('battle.simulate'); expect(fixture.requests).not.toContain('battle.getSimulation'); const workerUrls = await page.evaluate(() => { diff --git a/app/game-frontend/src/views/BattleSimulatorView.vue b/app/game-frontend/src/views/BattleSimulatorView.vue index 3f49c0cf..f42c6663 100644 --- a/app/game-frontend/src/views/BattleSimulatorView.vue +++ b/app/game-frontend/src/views/BattleSimulatorView.vue @@ -4,10 +4,12 @@ import type { BattleSimRequestPayload, BattleSimResultPayload } from '@sammo-ts/ import PanelCard from '../components/ui/PanelCard.vue'; import SkeletonLines from '../components/ui/SkeletonLines.vue'; import BattleGeneralCard from '../components/battle/BattleGeneralCard.vue'; +import { useGameFeedback } from '../composables/useGameFeedback'; import { trpc } from '../utils/trpc'; import { getNpcColor } from '../utils/npcColor'; import type { BattleSimOptions, GeneralDraft, InheritBuff } from '../utils/battleSimulatorTypes'; import { BattleSimulatorWorkerClient } from '../utils/battleSimulatorWorkerClient'; +import { formatSeoulDateTime } from '../utils/legacyDateTime'; type GeneralExport = Omit; @@ -67,8 +69,8 @@ const attackerGeneral = ref(null); const defenders = ref([]); const isSimulating = ref(false); -const statusMessage = ref(null); const simulationWorker = new BattleSimulatorWorkerClient(); +const { info: showInfoToast, dismissToast } = useGameFeedback(); onBeforeUnmount(() => { simulationWorker.dispose(); @@ -428,13 +430,6 @@ const normalizeGeneralExport = (raw: Record): GeneralExport => inheritBuff: normalizeInheritBuff(raw.inheritBuff), }); -const formatBattleTime = (date: Date): string => { - const pad = (value: number) => String(value).padStart(2, '0'); - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad( - date.getMinutes() - )}`; -}; - const buildGeneralPayload = ( general: GeneralDraft, nationId: number, @@ -492,7 +487,7 @@ const buildBattlePayload = (action: BattleSimRequestPayload['action']): BattleSi if (!attackerGeneral.value) { throw new Error('attacker_general_missing'); } - const now = formatBattleTime(new Date()); + const now = formatSeoulDateTime(new Date()); const attackerNationPayload = { nation: 1, @@ -583,7 +578,10 @@ const runSimulation = async (action: BattleSimRequestPayload['action']) => { if (action === 'battle') { battleResult.value = null; } - statusMessage.value = action === 'battle' ? '전투를 진행 중입니다.' : '수비자 순서를 계산 중입니다.'; + const progressToastId = showInfoToast( + action === 'battle' ? '전투를 진행 중입니다.' : '수비자 순서를 계산 중입니다.', + 0 + ); try { const payload = buildBattlePayload(action); @@ -605,7 +603,7 @@ const runSimulation = async (action: BattleSimRequestPayload['action']) => { error.value = resolveErrorMessage(err); } finally { isSimulating.value = false; - statusMessage.value = null; + dismissToast(progressToastId); } }; @@ -952,8 +950,8 @@ const summaryRows = computed(() => { ]; } return [ - { label: '전투 일시', value: battleResult.value.datetime ?? '-' }, - { label: '전투 횟수', value: formatNumber(battleResult.value.avgWar) }, + { label: '전투 일시', value: formatSeoulDateTime(battleResult.value.datetime ?? '') || '-' }, + { label: '전투 횟수', value: formatNumber(battleResult.value.repeatCnt) }, { label: '전투 페이즈', value: formatNumber(battleResult.value.phase) }, { label: '준 피해', @@ -1001,7 +999,6 @@ const shouldShowUI = computed(() => !loading.value && !!options.value);
{{ error }}
-
{{ statusMessage }}
diff --git a/packages/logic/src/battleSimulator/processor.ts b/packages/logic/src/battleSimulator/processor.ts index 23e8d314..cdfa22e4 100644 --- a/packages/logic/src/battleSimulator/processor.ts +++ b/packages/logic/src/battleSimulator/processor.ts @@ -481,6 +481,7 @@ export const processBattleSimJob = ( result: true, reason: 'success', datetime: payload.attackerGeneral.turntime, + repeatCnt, lastWarLog: logBuckets, avgWar, phase: avgPhase, diff --git a/packages/logic/src/battleSimulator/types.ts b/packages/logic/src/battleSimulator/types.ts index deb11fac..3d45bc3e 100644 --- a/packages/logic/src/battleSimulator/types.ts +++ b/packages/logic/src/battleSimulator/types.ts @@ -121,6 +121,8 @@ export interface BattleSimResultPayload { result: boolean; reason: string; datetime?: string; + /** Number of battles actually evaluated. A fixed seed intentionally collapses a repeated request to one run. */ + repeatCnt?: number; lastWarLog?: BattleSimLogBuckets; avgWar?: number; phase?: number;