From b8e9e6b5137e7e4c05eb075309b122e2b9edec69 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 22 Aug 2026 05:03:25 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=B9=88=20=ED=8A=B9=EA=B8=B0=EC=9D=98?= =?UTF-8?q?=20=ED=9A=8D=EB=93=9D=20=EC=98=88=EC=A0=95=20=EB=82=98=EC=9D=B4?= =?UTF-8?q?=EB=A5=BC=20=ED=91=9C=EC=8B=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메인 장수 read model에 내정·전투 특기 예정 나이를 투영하고, Ref와 같은 현재 나이 다음 해 하한을 적용한다. API 회귀와 Chromium 데스크톱·모바일 검증을 추가한다. --- app/game-api/src/router/general/index.ts | 4 ++ .../test/inGameMenuPermissions.test.ts | 30 +++++++++++++ app/game-frontend/e2e/inGameMenus.spec.ts | 43 +++++++++++++++++++ .../src/components/main/GeneralBasicCard.vue | 23 +++++++--- 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/app/game-api/src/router/general/index.ts b/app/game-api/src/router/general/index.ts index 23e6d3c1..a09371d8 100644 --- a/app/game-api/src/router/general/index.ts +++ b/app/game-api/src/router/general/index.ts @@ -553,6 +553,10 @@ export const getGeneralContext = async (ctx: GameApiContext) => { specialDomestic: resolveTraitDisplayName(general.specialCode, domesticNames), specialWar: resolveTraitDisplayName(general.special2Code, warNames), }, + traitAges: { + specialDomestic: readNumber(metaRecord.specage, 0), + specialWar: readNumber(metaRecord.specage2, 0), + }, traitInfo: { personal: personalityNames.get(general.personalCode)?.info ?? '', specialDomestic: domesticNames.get(general.specialCode)?.info ?? '', diff --git a/app/game-api/test/inGameMenuPermissions.test.ts b/app/game-api/test/inGameMenuPermissions.test.ts index 9cd5752f..32cad590 100644 --- a/app/game-api/test/inGameMenuPermissions.test.ts +++ b/app/game-api/test/inGameMenuPermissions.test.ts @@ -510,6 +510,7 @@ describe('in-game my information ownership', () => { personalCode: 'che_안전', specialCode: 'che_상재', special2Code: 'che_신산', + meta: { specage: 31, specage2: 35 }, }), }); @@ -520,6 +521,10 @@ describe('in-game my information ownership', () => { specialDomestic: '상재', specialWar: '신산', }, + traitAges: { + specialDomestic: 31, + specialWar: 35, + }, traitInfo: { personal: '사기 -5, 징·모병 비용 -20%', specialDomestic: '[내정] 상업 투자 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%', @@ -543,6 +548,31 @@ describe('in-game my information ownership', () => { expect(fixture.db.nation.findUnique).not.toHaveBeenCalled(); }); + it('returns the scheduled acquisition ages when the owned general has no domestic or war trait', async () => { + const fixture = createContext({ + me: buildGeneral({ + age: 30, + specialCode: 'None', + special2Code: 'None', + meta: { specage: 35, specage2: 29 }, + }), + }); + + await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({ + general: { + age: 30, + traits: { + specialDomestic: '-', + specialWar: '-', + }, + traitAges: { + specialDomestic: 35, + specialWar: 29, + }, + }, + }); + }); + it('reads legacy top-level settings and dispatches only the session-owned general', async () => { const requestCommand = vi.fn(async () => ({ type: 'setMySetting', ok: true, generalId: 7 })); const fixture = createContext({ requestCommand }); diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index 7b51451d..2f2f0c37 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -77,6 +77,7 @@ type FixtureState = { joinConfig?: Record; createGeneralInputs?: Array>; mainTraits?: { personal: string; specialDomestic: string; specialWar: string }; + mainTraitAges?: { specialDomestic: number; specialWar: number }; richMyInfo?: boolean; hiddenSeedLogText?: string; recentRecords?: { @@ -130,6 +131,7 @@ const myGeneral = (state: FixtureState) => ({ } : null, traits: state.mainTraits ?? { personal: '-', specialDomestic: '-', specialWar: '-' }, + traitAges: state.mainTraitAges ?? { specialDomestic: 31, specialWar: 31 }, traitInfo: state.richMyInfo ? { personal: '부상당할 확률이 감소합니다.', @@ -725,6 +727,47 @@ test('재야 메인은 국가 틀과 성격·특기 표기명을 Chromium에 표 await persistParityArtifact(page, 'main-neutral-trait-display', geometry); }); +test('메인 장수 정보는 없는 내정·전투 특기의 Ref 획득 나이를 Chromium에 표시한다', async ({ page }) => { + const state: FixtureState = { + permission: 'member', + myset: 0, + mainTraits: { personal: '안전', specialDomestic: '-', specialWar: '-' }, + mainTraitAges: { specialDomestic: 35, specialWar: 29 }, + settingMutations: [], + accessPages: [], + }; + await install(page, state); + + for (const viewport of [ + { width: 1000, height: 900 }, + { width: 390, height: 844 }, + ]) { + await page.setViewportSize(viewport); + await page.goto(''); + + const specialValue = page.locator('.general-card .special-value'); + await expect(specialValue).toHaveText(/35세\s*\/\s*31세/u); + await expect(specialValue).toHaveAttribute('aria-label', '35세 / 31세'); + const geometry = await specialValue.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const documentWidth = document.documentElement.scrollWidth; + return { + text: element.textContent?.replace(/\s+/gu, ' ').trim(), + left: rect.left, + right: rect.right, + width: rect.width, + documentWidth, + viewportWidth: window.innerWidth, + }; + }); + expect(geometry.width).toBeGreaterThan(0); + expect(geometry.left).toBeGreaterThanOrEqual(0); + expect(geometry.right).toBeLessThanOrEqual(geometry.documentWidth); + expect(geometry.documentWidth).toBe(Math.max(viewport.width, 500)); + await persistParityArtifact(page, `main-speciality-age-${viewport.width}`, geometry); + } +}); + test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명으로 표시된다', async ({ page }) => { const state: FixtureState = { permission: 'head', diff --git a/app/game-frontend/src/components/main/GeneralBasicCard.vue b/app/game-frontend/src/components/main/GeneralBasicCard.vue index 4fa8125e..64ff9d7c 100644 --- a/app/game-frontend/src/components/main/GeneralBasicCard.vue +++ b/app/game-frontend/src/components/main/GeneralBasicCard.vue @@ -99,6 +99,7 @@ export interface GeneralBasicCardData { crewTypeName?: string; crewTypeInfo?: CrewTypeDisplayInfo | null; traits?: { personal: string; specialWar: string; specialDomestic: string }; + traitAges?: { specialWar: number; specialDomestic: number }; traitInfo?: { personal: string; specialWar: string; specialDomestic: string }; progression?: GeneralProgression; itemNames?: ItemDisplayNames; @@ -226,9 +227,19 @@ const displayDefence = computed(() => { }); const displayKillTurn = computed(() => props.general?.killTurn ?? props.killTurn); const displayRemainingMinutes = computed(() => props.general?.remainingMinutes ?? props.remainingMinutes); +const resolveSpecialDisplayName = (kind: 'specialDomestic' | 'specialWar') => { + const general = props.general; + if (!general) return '-'; + const traitName = general.traits?.[kind]; + if (traitName && traitName !== '-') return traitName; + const scheduledAge = general.traitAges?.[kind]; + if (general.age === undefined || scheduledAge === undefined) return '-'; + return `${Math.max(general.age + 1, scheduledAge)}세`; +}; +const specialDomesticText = computed(() => resolveSpecialDisplayName('specialDomestic')); +const specialWarText = computed(() => resolveSpecialDisplayName('specialWar')); const specialText = computed(() => { - const traits = props.general?.traits; - return traits ? `${traits.specialDomestic || '-'} / ${traits.specialWar || '-'}` : '-'; + return `${specialDomesticText.value} / ${specialWarText.value}`; }); @@ -379,19 +390,19 @@ const specialText = computed(() => { 특기 - {{ props.general.traits?.specialDomestic ?? '-' }} + {{ specialDomesticText }} / - {{ props.general.traits?.specialWar ?? '-' }} + {{ specialWarText }}