From 73e1743a3d919ce3b3903578d6193c27d929b60f Mon Sep 17 00:00:00 2001 From: hided62 Date: Thu, 10 Sep 2026 04:22:18 +0000 Subject: [PATCH] =?UTF-8?q?=EB=B3=B5=EA=B5=AC=202=EB=B0=B0=EC=86=8D?= =?UTF-8?q?=EC=9D=98=20=EA=B2=8C=EC=9E=84=C2=B7=EC=8B=A4=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=ED=91=9C=EC=8B=9C=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EA=B3=BC=20=EC=8B=9C=EA=B3=84=20=EB=B9=A0=EB=A5=B8=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../e2e/commandArguments.spec.ts | 162 +++++++++++++++++- app/game-frontend/src/App.vue | 2 + .../components/chief/ChiefCommandEditor.vue | 6 +- .../command/ReservedCommandEditor.vue | 24 ++- .../src/components/main/CommandListPanel.vue | 65 +------ .../src/components/main/GeneralBasicCard.vue | 4 +- .../src/components/main/MainFrontStatus.vue | 129 ++++++-------- .../src/composables/useClockDisplay.ts | 98 +++++++++++ .../src/composables/useClockDisplayRefresh.ts | 37 ++++ app/game-frontend/src/stores/mainDashboard.ts | 2 + .../src/utils/serverClockProjection.ts | 24 +++ app/game-frontend/src/utils/trpc.ts | 21 +++ app/game-frontend/src/views/AuctionView.vue | 8 +- .../src/views/BattleCenterView.vue | 4 +- app/game-frontend/src/views/BettingView.vue | 5 +- .../src/views/ChiefCenterView.vue | 7 +- app/game-frontend/src/views/JoinView.vue | 3 +- app/game-frontend/src/views/MyPageView.vue | 3 +- .../src/views/MySettingsView.vue | 14 ++ .../src/views/NationCitiesView.vue | 5 +- .../src/views/NationGeneralsView.vue | 4 +- .../src/views/NationSecretView.vue | 5 +- .../src/views/SelectGeneralView.vue | 3 +- .../src/views/TournamentView.vue | 5 +- app/game-frontend/src/views/TroopView.vue | 5 +- .../test/serverClockProjection.test.ts | 57 ++++++ 26 files changed, 537 insertions(+), 165 deletions(-) create mode 100644 app/game-frontend/src/composables/useClockDisplay.ts create mode 100644 app/game-frontend/src/composables/useClockDisplayRefresh.ts diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index ba38de0f..d656281c 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -840,12 +840,23 @@ const install = async ( page: Page, rejectGeneral = false, commandTableResponse: unknown = commandTable, - generalId = 1 + generalId = 1, + recoveryClock?: { + serverTime: string; + serverWallTime: string; + clockRunning: boolean; + clockRecovery: { startsAt: string; endsAt: string } | null; + turnEngineRunning: boolean; + } ) => { const requests: unknown[] = []; const currentGeneralContext = { ...generalContext, - general: { ...generalContext.general, id: generalId }, + general: { + ...generalContext.general, + id: generalId, + ...(recoveryClock ? { turnTime: '2026-09-10T01:20:00Z' } : {}), + }, }; const generalTurns = turns(30); const nationTurns = turns(12); @@ -929,7 +940,8 @@ const install = async ( myGeneral: { id: generalId, name: '장수' }, year: 200, month: 1, - turnTerm: 10, + turnTerm: recoveryClock ? 60 : 10, + ...recoveryClock, userCnt: 1, maxUserCnt: 100, npcCnt: 0, @@ -959,7 +971,19 @@ const install = async ( }); } if (name === 'turns.getCommandTable') return response(commandTableResponse); - if (name === 'nation.getChiefCenter') return response(chiefCenter); + if (name === 'nation.getChiefCenter') + return response( + recoveryClock + ? { + ...chiefCenter, + turnTermMinutes: 60, + chiefs: chiefCenter.chiefs.map((chief) => ({ + ...chief, + turnTime: '2026-09-10T01:20:00Z', + })), + } + : chiefCenter + ); if (name === 'turns.reserved.getGeneral') return response({ turns: generalTurns, revision: generalRevision, autorunLimit: 2403 }); if (name === 'turns.reserved.getNation') return response({ turns: nationTurns, revision: nationRevision }); @@ -3341,3 +3365,133 @@ test('keeps the chief footer return button the same size as the top return butto const mobile = await measure(); expect(mobile.bottom).toEqual(mobile.top); }); + +const captureRecoveryControl = async (page: Page, selector: string, name: string) => { + const control = page.locator(selector).first(); + await control.scrollIntoViewIfNeeded(); + const read = () => + control.evaluate((el) => { + const rect = el.getBoundingClientRect(); + const style = getComputedStyle(el); + return { + rect: rect.toJSON(), + color: style.color, + background: style.backgroundColor, + font: style.font, + outline: style.outline, + cursor: style.cursor, + html: el.outerHTML, + overflow: el.scrollWidth > el.clientWidth, + }; + }); + const normal = await read(); + expect(normal.rect.width).toBeGreaterThan(0); + expect(normal.rect.x).toBeGreaterThanOrEqual(0); + expect(normal.rect.right).toBeLessThanOrEqual(page.viewportSize()!.width + 1); + await control.hover(); + const hover = await read(); + await control.focus(); + const focus = await read(); + await page.mouse.down(); + const active = await read(); + await page.mouse.move(0, 0); + await page.mouse.up(); + await writeFile(test.info().outputPath(`${name}.json`), JSON.stringify({ normal, hover, focus, active }, null, 2)); +}; + +for (const width of [1200, 500]) { + test(`recovery clock preference and quick switches at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 1000 }); + await page.clock.setFixedTime(new Date('2026-09-10T02:00:00Z')); + const recoveryClock: NonNullable[4]> = { + serverTime: '2026-09-10T01:00:00Z', + serverWallTime: '2026-09-10T02:00:00Z', + clockRunning: true, + turnEngineRunning: true, + clockRecovery: { startsAt: '2026-09-10T02:00:00Z', endsAt: '2026-09-10T03:00:00Z' }, + }; + await install(page, false, commandTable, 1, recoveryClock); + await page.goto(gamePath('/')); + const top = page.locator('.execution-status'); + const clock = page.locator('[data-command-current-time]:visible').first(); + await expect(top).toContainText('게임 시간'); + await expect(top).toHaveCSS('color', 'rgb(255, 209, 128)'); + const gameText = await clock.textContent(); + await top.click(); + await expect(top).toContainText('실제 시간'); + await expect(top).toHaveCSS('color', 'rgb(165, 214, 167)'); + await expect(clock).not.toHaveText(gameText!); + await expect(page.locator('[data-general-turn-time]:visible').first()).toContainText('02:10:00'); + const editor = page.locator('[data-command-scope="general"]:visible').first(); + await expect(editor).toContainText('02:40'); + await expect(editor).toContainText('03:20'); + await clock.focus(); + await page.keyboard.press('Enter'); + await expect(top).toContainText('게임 시간'); + await editor.getByRole('button', { name: '고급 모드', exact: true }).click(); + await clock.click(); + await expect(top).toContainText('실제 시간'); + await page.reload(); + await expect(top).toContainText('실제 시간'); + await page.evaluate(() => document.fonts.ready); + await page.screenshot({ path: test.info().outputPath(`recovery-main-${width}.png`), fullPage: true }); + await writeFile( + test.info().outputPath(`recovery-main-${width}.json`), + JSON.stringify( + await top.evaluate((el) => { + const rect = el.getBoundingClientRect(); + const style = getComputedStyle(el); + return { + text: el.textContent, + rect: rect.toJSON(), + color: style.color, + font: style.font, + html: el.outerHTML, + }; + }), + null, + 2 + ) + ); + await captureRecoveryControl(page, '.execution-status', `recovery-main-states-${width}`); + await page.goto(gamePath('/chief-center')); + const chiefClock = page.locator('[data-command-current-time]:visible').first(); + await expect(chiefClock).toHaveText('11:00:00'); + await chiefClock.click(); + await expect(chiefClock).toHaveText('10:00:00'); + const chief = page.locator('[data-command-scope="nation"]:visible').first(); + await chief.getByRole('button', { name: '고급 모드', exact: true }).click(); + await chiefClock.click(); + await expect(chiefClock).toHaveText('11:00:00'); + await page.screenshot({ path: test.info().outputPath(`recovery-chief-${width}.png`), fullPage: true }); + await captureRecoveryControl(page, '[data-command-current-time]:visible', `recovery-chief-states-${width}`); + // At the end boundary, 1x clicks cannot change the preference. + recoveryClock.clockRecovery = null; + recoveryClock.serverTime = '2026-09-10T03:00:00Z'; + recoveryClock.serverWallTime = '2026-09-10T03:00:00Z'; + await page.clock.setFixedTime(new Date('2026-09-10T03:00:00Z')); + await expect(chiefClock).toBeDisabled(); + await expect(chiefClock).toHaveText('12:00:00'); + await chiefClock.evaluate((element: HTMLButtonElement) => element.click()); + await expect(chiefClock).toHaveAttribute('title', '실제 시간 기준'); + await page.goto(gamePath('/my-settings')); + const setting = page.getByRole('radiogroup', { name: '가속 시 시간 표시 기준' }); + await expect(setting.getByRole('radio', { name: '실제 시간 기준', exact: true })).toBeChecked(); + await setting.getByRole('radio', { name: '게임 시간 기준', exact: true }).check(); + await page.screenshot({ path: test.info().outputPath(`recovery-settings-${width}.png`), fullPage: true }); + await captureRecoveryControl( + page, + '[aria-label="가속 시 시간 표시 기준"]', + `recovery-settings-states-${width}` + ); + await page.reload(); + await expect(setting.getByRole('radio', { name: '게임 시간 기준', exact: true })).toBeChecked(); + await page.goto(gamePath('/')); + await expect(top).toHaveCSS('color', 'rgb(0, 255, 255)'); + await expect(top).toBeDisabled(); + recoveryClock.turnEngineRunning = false; + await page.reload(); + await expect(top).toHaveCSS('color', 'rgb(255, 0, 255)'); + await expect(clock).toBeDisabled(); + }); +} diff --git a/app/game-frontend/src/App.vue b/app/game-frontend/src/App.vue index 488f243e..7e2d6e19 100644 --- a/app/game-frontend/src/App.vue +++ b/app/game-frontend/src/App.vue @@ -1,10 +1,12 @@