From 4c221a153149d10fb2bcfe26548c3a96928b4b92 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 15 Aug 2026 18:13:03 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9E=A5=EC=88=98=20=EB=8F=99=ED=96=A5?= =?UTF-8?q?=20=ED=96=89=20=EA=B0=84=EA=B2=A9=EA=B3=BC=20=EC=9E=84=EA=B4=80?= =?UTF-8?q?=20=EC=9B=94=20=ED=91=9C=EA=B8=B0=EB=A5=BC=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 출병형 기록을 포함한 메인 동향 행을 21px 리듬으로 고정한다. 세 임관 명령의 전체 기록은 레거시와 같은 월 표제를 사용하고 단위·Chromium 회귀 검증을 추가한다. --- app/game-frontend/e2e/inGameMenus.spec.ts | 51 +++++++- app/game-frontend/src/views/MainView.vue | 10 ++ .../src/actions/turn/general/che_랜덤임관.ts | 2 +- .../src/actions/turn/general/che_임관.ts | 2 +- .../actions/turn/general/che_장수대상임관.ts | 1 + .../actions/turn/appointmentLogFormat.test.ts | 120 ++++++++++++++++++ 6 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 packages/logic/test/actions/turn/appointmentLogFormat.test.ts diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index 1a241beb..05503f87 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -656,17 +656,32 @@ test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명 await expect(page.locator('.main-page')).not.toContainText('che_'); }); -test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄에 표시한다', async ({ page }) => { +test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을 유지한다', async ({ page }) => { const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [], recentRecords: { - global: [], - general: [ + global: [ + { + id: 18611, + text: + '●9월:Administrator가 낙양으로 ' + + '진격합니다.(전투시드: 0123456789abcdef)', + }, + { + id: 18610, + text: '●9월:Administrator가 임관했습니다.', + }, { id: 18609, + text: '●9월:뇌동의 기병이 퇴각했습니다.', + }, + ], + general: [ + { + id: 18608, text: '◆186년 9월:
' + '귀병 ' + @@ -686,6 +701,31 @@ test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄 await page.setViewportSize({ width: 1200, height: 900 }); await page.goto(''); + const inspectGlobalRhythm = async (selector: string) => { + const lines = page.locator(selector); + await expect(lines).toHaveCount(3); + return lines.evaluateAll((elements) => + elements.map((element) => { + const rect = element.getBoundingClientRect(); + return { + top: rect.top, + height: rect.height, + lineHeight: getComputedStyle(element).lineHeight, + }; + }) + ); + }; + const assertUniformGlobalRhythm = (geometry: Awaited>) => { + expect(geometry.map((line) => line.height)).toEqual([21, 21, 21]); + expect(geometry.map((line) => line.lineHeight)).toEqual(['21px', '21px', '21px']); + expect(geometry[1]!.top - geometry[0]!.top).toBe(21); + expect(geometry[2]!.top - geometry[1]!.top).toBe(21); + }; + + const desktopGlobalGeometry = await inspectGlobalRhythm('.record-zone [data-record-bucket="global"] .record-line'); + assertUniformGlobalRhythm(desktopGlobalGeometry); + await persistParityArtifact(page, 'core-main-trend-log-rhythm-desktop', desktopGlobalGeometry); + const expectedText = '◆186년 9월:귀병 【Administrator】 0(-2209) ← 1361(-5539) 기병 【ⓝ뇌동】 12:54'; const inspect = async (line: Locator) => { await expect(line).toContainText(expectedText); @@ -723,6 +763,11 @@ test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄 page.locator('.record-zone-mobile [data-record-bucket="general"] .record-line').first() ); assertSingleLine(mobileGeometry); + const mobileGlobalGeometry = await inspectGlobalRhythm( + '.record-zone-mobile [data-record-bucket="global"] .record-line' + ); + assertUniformGlobalRhythm(mobileGlobalGeometry); + await persistParityArtifact(page, 'core-main-trend-log-rhythm-mobile', mobileGlobalGeometry); await persistParityArtifact(page, 'core-main-personal-battle-log-inline-mobile', mobileGeometry); }); diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index 13dc1681..2327fc33 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -666,11 +666,21 @@ button { } .record-line { + box-sizing: border-box; + height: 21px; + margin: 0; overflow: hidden; overflow-wrap: normal; + line-height: 21px; white-space: nowrap; } +.record-line :deep(.small_war_log) { + height: 21px; + line-height: 21px; + vertical-align: top; +} + .record-line :deep(.hidden_but_copyable) { color: transparent !important; font-size: 0; diff --git a/packages/logic/src/actions/turn/general/che_랜덤임관.ts b/packages/logic/src/actions/turn/general/che_랜덤임관.ts index 521cc407..5c5c19fb 100644 --- a/packages/logic/src/actions/turn/general/che_랜덤임관.ts +++ b/packages/logic/src/actions/turn/general/che_랜덤임관.ts @@ -272,7 +272,7 @@ export class ActionDefinition< { scope: LogScope.SYSTEM, category: LogCategory.SUMMARY, - format: LogFormat.PLAIN, + format: LogFormat.MONTH, } ) ); diff --git a/packages/logic/src/actions/turn/general/che_임관.ts b/packages/logic/src/actions/turn/general/che_임관.ts index 081dc9c7..371dd059 100644 --- a/packages/logic/src/actions/turn/general/che_임관.ts +++ b/packages/logic/src/actions/turn/general/che_임관.ts @@ -104,7 +104,7 @@ export class ActionDefinition< context.addLog(`${context.general.name}${josaYi} ${destNationName}임관했습니다.`, { scope: LogScope.SYSTEM, category: LogCategory.SUMMARY, - format: LogFormat.RAWTEXT, + format: LogFormat.MONTH, }); tryApplyUniqueLottery(context, { diff --git a/packages/logic/src/actions/turn/general/che_장수대상임관.ts b/packages/logic/src/actions/turn/general/che_장수대상임관.ts index 956e48cc..b2247af4 100644 --- a/packages/logic/src/actions/turn/general/che_장수대상임관.ts +++ b/packages/logic/src/actions/turn/general/che_장수대상임관.ts @@ -183,6 +183,7 @@ export class ActionDefinition< context.addLog(`${general.name}${josaYi} ${destNation.name}임관했습니다.`, { scope: LogScope.SYSTEM, category: LogCategory.SUMMARY, + format: LogFormat.MONTH, }); const initialNationGenLimit = context.initialNationGenLimit ?? 10; diff --git a/packages/logic/test/actions/turn/appointmentLogFormat.test.ts b/packages/logic/test/actions/turn/appointmentLogFormat.test.ts new file mode 100644 index 00000000..7418b217 --- /dev/null +++ b/packages/logic/test/actions/turn/appointmentLogFormat.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest'; + +import type { General, Nation } from '../../../src/domain/entities.js'; +import type { GeneralActionResolveContext } from '../../../src/actions/engine.js'; +import { finalizeLogEntry } from '../../../src/logging/entries.js'; +import { LogCategory, type LogEntryDraft, LogFormat, LogScope } from '../../../src/logging/types.js'; +import type { TurnCommandEnv } from '../../../src/actions/turn/commandEnv.js'; +import { ActionDefinition as AppointmentAction } from '../../../src/actions/turn/general/che_임관.js'; +import { ActionDefinition as RandomAppointmentAction } from '../../../src/actions/turn/general/che_랜덤임관.js'; +import { ActionDefinition as FollowAppointmentAction } from '../../../src/actions/turn/general/che_장수대상임관.js'; + +const actor = { + id: 1, + name: '검증장수', + nationId: 0, + cityId: 1, + npcState: 0, + officerLevel: 1, + experience: 100, + stats: { leadership: 50, strength: 50, intelligence: 50 }, + role: { + personality: null, + specialDomestic: null, + specialWar: null, + items: { horse: null, weapon: null, book: null, item: null }, + }, + meta: {}, +} as General; + +const nation = { + id: 2, + name: '검증국', + capitalCityId: 2, + meta: { gennum: 1 }, +} as unknown as Nation; + +const createLogSink = + (logs: LogEntryDraft[]): GeneralActionResolveContext['addLog'] => + (text, options = {}) => { + logs.push({ + scope: options.scope ?? LogScope.GENERAL, + category: options.category ?? LogCategory.ACTION, + text, + format: options.format ?? LogFormat.MONTH, + }); + }; + +const expectMonthlySummary = (logs: LogEntryDraft[]): void => { + const summary = logs.find((entry) => entry.scope === LogScope.SYSTEM && entry.category === LogCategory.SUMMARY); + expect(summary).toBeDefined(); + expect(summary?.format).toBe(LogFormat.MONTH); + expect(finalizeLogEntry(summary!, { year: 186, month: 9 })?.text).toMatch(/^●<\/>9월:/u); +}; + +describe('appointment global summary log format', () => { + it('adds the legacy month prefix to direct appointment', () => { + const logs: LogEntryDraft[] = []; + const action = new AppointmentAction({} as TurnCommandEnv); + + action.resolve( + { + general: actor, + destNation: nation, + destNationGeneralCount: 1, + destCityId: 2, + addLog: createLogSink(logs), + } as unknown as Parameters[0], + { destNationId: nation.id } + ); + + expectMonthlySummary(logs); + }); + + it('adds the legacy month prefix to random appointment', () => { + const action = new RandomAppointmentAction({} as TurnCommandEnv); + const result = action.resolve( + { + general: actor, + rng: { + nextFloat1: () => 0, + nextInt: () => 0, + }, + candidateNations: [ + { + nation, + generals: [], + generalCount: 1, + monarchCityId: 2, + monarchAffinity: 0, + }, + ], + relYear: 1, + initialNationGenLimit: 10, + historicalNpcAffinityMode: false, + } as unknown as Parameters[0], + {} + ); + const logs = result.effects.flatMap((effect) => (effect.type === 'log' ? [effect.entry] : [])); + + expectMonthlySummary(logs); + }); + + it('keeps the same month prefix when following another general', () => { + const logs: LogEntryDraft[] = []; + const action = new FollowAppointmentAction(); + + action.resolve( + { + general: actor, + destNation: nation, + destNationGeneralCount: 1, + initialNationGenLimit: 10, + addLog: createLogSink(logs), + } as unknown as Parameters[0], + { destGeneralID: 9 } + ); + + expectMonthlySummary(logs); + }); +});