diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 7654e9ec..0ca54a9e 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -20,6 +20,7 @@ type NavigationFixture = { generalMeCalls: number; operations: string[]; generalName?: string; + generalTurnTime?: string; cityDefence?: number; cityState?: number; nationRate?: number; @@ -239,7 +240,7 @@ const generalContext = (state: NavigationFixture) => ({ dex: [350, 100_000, 500_000, 1_000_000, 1_275_975], }, items: { horse: null, weapon: null, book: null, item: null }, - turnTime: '0185-01-01T00:00:00.000Z', + turnTime: state.generalTurnTime ?? '0185-01-01T00:00:00.000Z', }, city: { id: 1, @@ -666,6 +667,66 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`); }); +test('main general card renders the next turn in the Seoul server timezone', async ({ page }) => { + const state: NavigationFixture = { + officerLevel: 0, + permission: 0, + nationLevel: 0, + stage: 0, + npcMode: 1, + generalMeCalls: 0, + operations: [], + generalName: 'Administrator', + generalTurnTime: '2026-08-13T00:07:06.713Z', + currentYear: 179, + currentMonth: 8, + }; + await installFixture(page, state); + await page.setViewportSize({ width: 1200, height: 900 }); + await waitForMain(page); + + const title = page.locator('[data-main-target="general"] .general-title').first(); + await expect(title).toContainText('Administrator'); + await expect(title).toContainText('다음 턴 09:07'); + await expect(title).not.toContainText('00:07'); + + const desktopGeometry = await title.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + width: rect.width, + height: rect.height, + fontSize: style.fontSize, + lineHeight: style.lineHeight, + overflow: style.overflow, + }; + }); + expect(desktopGeometry.width).toBeGreaterThan(0); + expect(desktopGeometry.height).toBeGreaterThan(0); + if (artifactRoot) { + const target = resolve(artifactRoot); + await mkdir(target, { recursive: true }); + await Promise.all([ + page.screenshot({ path: resolve(target, 'main-turn-time-seoul-desktop-1200.png'), fullPage: true }), + writeFile( + resolve(target, 'main-turn-time-seoul-desktop-1200.json'), + `${JSON.stringify(desktopGeometry, null, 2)}\n` + ), + ]); + } + + await page.setViewportSize({ width: 500, height: 900 }); + const mobileTitle = page.locator('[data-main-target="general"] .general-title').first(); + await expect(mobileTitle).toContainText('다음 턴 09:07'); + expect(await mobileTitle.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(0); + if (artifactRoot) { + await page.screenshot({ + path: resolve(artifactRoot, 'main-turn-time-seoul-mobile-500.png'), + fullPage: true, + }); + } +}); + test('pure NPC message senders are not rendered as reply targets', async ({ page }) => { const target = (generalId: number, generalName: string) => ({ generalId, diff --git a/app/game-frontend/src/components/main/GeneralBasicCard.vue b/app/game-frontend/src/components/main/GeneralBasicCard.vue index 919cb411..9588e162 100644 --- a/app/game-frontend/src/components/main/GeneralBasicCard.vue +++ b/app/game-frontend/src/components/main/GeneralBasicCard.vue @@ -2,6 +2,7 @@ import { computed } from 'vue'; import SkeletonLines from '../ui/SkeletonLines.vue'; import LegacyProgressBar from '../ui/LegacyProgressBar.vue'; +import { formatSeoulHourMinute } from '../../utils/legacyDateTime'; import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress'; interface GeneralStats { @@ -80,7 +81,7 @@ const experiencePercent = computed(() =>
{{ props.general.name }} · 관직 {{ props.general.officerLevel }} · {{ props.general.age ?? '-' }}세 · 다음 턴 - {{ props.general.turnTime?.slice(11, 16) ?? '-' }} + {{ props.general.turnTime ? formatSeoulHourMinute(props.general.turnTime) : '-' }}
diff --git a/app/game-frontend/src/utils/legacyDateTime.ts b/app/game-frontend/src/utils/legacyDateTime.ts index 2435c0c0..3b89fd7e 100644 --- a/app/game-frontend/src/utils/legacyDateTime.ts +++ b/app/game-frontend/src/utils/legacyDateTime.ts @@ -20,3 +20,5 @@ export const formatSeoulDateTime = (value: string | Date): string => { koreaTime.getUTCSeconds() )}`; }; + +export const formatSeoulHourMinute = (value: string | Date): string => formatSeoulDateTime(value).slice(11, 16); diff --git a/app/game-frontend/test/legacyDateTime.test.ts b/app/game-frontend/test/legacyDateTime.test.ts new file mode 100644 index 00000000..6a6cef91 --- /dev/null +++ b/app/game-frontend/test/legacyDateTime.test.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { formatSeoulDateTime, formatSeoulHourMinute } from '../src/utils/legacyDateTime.ts'; + +void test('formats API UTC timestamps in the server Seoul timezone', () => { + assert.equal(formatSeoulDateTime('2026-08-13T00:07:06.713Z'), '2026-08-13 09:07:06'); + assert.equal(formatSeoulHourMinute('2026-08-13T00:07:06.713Z'), '09:07'); +}); + +void test('keeps legacy timezone-less server timestamps unchanged', () => { + assert.equal(formatSeoulDateTime('2026-08-13 09:07:06'), '2026-08-13 09:07:06'); + assert.equal(formatSeoulHourMinute('2026-08-13 09:07:06'), '09:07'); +});