diff --git a/app/game-api/src/maps/mapLayout.ts b/app/game-api/src/maps/mapLayout.ts index 33d116d8..93fd9314 100644 --- a/app/game-api/src/maps/mapLayout.ts +++ b/app/game-api/src/maps/mapLayout.ts @@ -66,8 +66,10 @@ const resolveMapName = async ( } }; -export const loadMapLayout = async (scenario: string, options: MapLayoutLoaderOptions = {}): Promise => { - const mapName = await resolveMapName(scenario, options.loadScenario ?? loadScenarioDefinitionById); +export const loadMapLayoutByName = async ( + mapName: string, + options: MapLayoutLoaderOptions = {} +): Promise => { const useCache = !options.loadScenario && !options.loadMap && !options.loadRegionMap; const cached = useCache ? layoutCache.get(mapName) : undefined; if (cached) { @@ -98,3 +100,8 @@ export const loadMapLayout = async (scenario: string, options: MapLayoutLoaderOp } return layout; }; + +export const loadMapLayout = async (scenario: string, options: MapLayoutLoaderOptions = {}): Promise => { + const mapName = await resolveMapName(scenario, options.loadScenario ?? loadScenarioDefinitionById); + return loadMapLayoutByName(mapName, options); +}; diff --git a/app/game-api/src/router/yearbook/index.ts b/app/game-api/src/router/yearbook/index.ts index 746e5c29..f386226b 100644 --- a/app/game-api/src/router/yearbook/index.ts +++ b/app/game-api/src/router/yearbook/index.ts @@ -6,6 +6,7 @@ import { asRecord, isRecord } from '@sammo-ts/common'; import { LogCategory, LogScope } from '@sammo-ts/logic'; import type { GameApiContext } from '../../context.js'; +import { loadMapLayout, loadMapLayoutByName } from '../../maps/mapLayout.js'; import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js'; import { formatGeneralAccessLimitMessage, @@ -339,11 +340,23 @@ export const yearbookRouter = router({ const firstYearMonth = firstRow ? joinYearMonth(firstRow.year, firstRow.month) : fallbackYearMonth; const lastYearMonth = lastRow ? joinYearMonth(lastRow.year, lastRow.month) : fallbackYearMonth; const selectedYearMonth = target.isCurrentProfile ? currentYearMonth : lastYearMonth; + // Ref v_history.php resolves an archived generation's map from ng_games.map. + const mapLayout = target.isCurrentProfile + ? await loadMapLayout(ctx.profile.scenario) + : await loadMapLayoutByName( + ( + await ctx.db.gameHistory.findUnique({ + where: { serverId: target.archiveKey }, + select: { map: true }, + }) + )?.map || 'che' + ); return { firstYearMonth, lastYearMonth, currentYearMonth: selectedYearMonth, + mapLayout, }; }), getHistory: authedProcedure diff --git a/app/game-api/test/yearbookArchiveRouter.test.ts b/app/game-api/test/yearbookArchiveRouter.test.ts index c5bf25fa..16932414 100644 --- a/app/game-api/test/yearbookArchiveRouter.test.ts +++ b/app/game-api/test/yearbookArchiveRouter.test.ts @@ -142,6 +142,10 @@ const buildContext = ( meta: options.worldMeta ?? { serverId: currentServerId }, }), }, + gameHistory: { + findUnique: async ({ where }: { where: { serverId: string } }) => + where.serverId === archiveServerId ? { map: 'miniche' } : null, + }, yearbookHistory: { findFirst: async (args: { where: { profileName: string; year?: number; month?: number }; @@ -187,11 +191,13 @@ describe('historical yearbook access from dynasty', () => { const caller = appRouter.createCaller(buildContext(authFor('owner-a'))); const result = await caller.yearbook.getRange({ serverID: archiveServerId }); - expect(result).toEqual({ + expect(result).toMatchObject({ firstYearMonth: 200 * 12, lastYearMonth: 200 * 12 + 1, currentYearMonth: 200 * 12 + 1, + mapLayout: { mapName: 'miniche' }, }); + expect(result.mapLayout.cityList.length).toBeGreaterThan(0); }); it('returns the same archived public history for distinct general owners', async () => { @@ -249,20 +255,22 @@ describe('historical yearbook access from dynasty', () => { expect(canonical).toEqual(alias); expect(alias).toEqual(omitted); - expect(canonical).toEqual({ + expect(canonical).toMatchObject({ firstYearMonth: 219 * 12 + 11, lastYearMonth: 219 * 12 + 11, currentYearMonth: 220 * 12, + mapLayout: { mapName: 'che' }, }); }); it('reads imported history under the short profile ID when world metadata has no server ID', async () => { const caller = appRouter.createCaller(buildContext(authFor('owner-a'), { worldMeta: {} })); - await expect(caller.yearbook.getRange()).resolves.toEqual({ + await expect(caller.yearbook.getRange()).resolves.toMatchObject({ firstYearMonth: 219 * 12 + 10, lastYearMonth: 219 * 12 + 10, currentYearMonth: 220 * 12, + mapLayout: { mapName: 'che' }, }); }); diff --git a/app/game-frontend/e2e/legacyArchiveViews.spec.ts b/app/game-frontend/e2e/legacyArchiveViews.spec.ts index f7fbeb2a..aa309388 100644 --- a/app/game-frontend/e2e/legacyArchiveViews.spec.ts +++ b/app/game-frontend/e2e/legacyArchiveViews.spec.ts @@ -1,3 +1,4 @@ +import { writeFile } from 'node:fs/promises'; import { expect, test, type Page, type Route } from '@playwright/test'; import { gameProfile, gameTrpcRoute } from './gameTestPaths.js'; @@ -156,7 +157,12 @@ const installArchiveViews = async (page: Page) => { }); } if (operation === 'yearbook.getRange') { - return response({ firstYearMonth: 22001, lastYearMonth: 22001, currentYearMonth: 22001 }); + return response({ + firstYearMonth: 22001, + lastYearMonth: 22001, + currentYearMonth: 22001, + mapLayout: { mapName: 'miniche', cityList: [], regionMap: {}, levelMap: {} }, + }); } if (operation === 'public.getMapLayout') { return response({ mapName: 'che', cityList: [], regionMap: {}, levelMap: {} }); @@ -293,3 +299,48 @@ test('연감 국가 라벨은 밝은 배경에 검정, 어두운 배경에 흰 await expect(labels.filter({ hasText: '명국' })).toHaveCSS('color', 'rgb(0, 0, 0)'); await page.screenshot({ path: testInfo.outputPath('yearbook-nation-contrast-mobile.png'), fullPage: true }); }); + +test('왕조의 역사 보기는 해당 기수의 지도 레이아웃으로 연감을 렌더한다', async ({ page }, testInfo) => { + await installArchiveViews(page); + await page.setViewportSize({ width: 1200, height: 800 }); + await page.goto('dynasty'); + const archivedHistory = page.getByRole('link', { name: '역사 보기' }).last(); + await expect(archivedHistory).toHaveAttribute('href', /yearbook\?serverID=che-current-1$/); + await archivedHistory.click(); + + await expect(page.locator('.history-grid')).toBeVisible(); + await expect(page.locator('.map-area')).toHaveClass(/map-theme-miniche/); + await expect(page.locator('.map-bgroad')).toHaveCSS('background-image', /miniche_road\.png/); + await expect(page.locator('.nation-position tbody tr')).toHaveCount(2); + const mapEvidence = await page.locator('.map-area').evaluate((area) => { + const road = area.querySelector('.map-bgroad'); + const background = area.querySelector('.map-background-image'); + return { + mapClass: area.className, + mapRect: area.getBoundingClientRect().toJSON(), + backgroundRect: background?.getBoundingClientRect().toJSON(), + backgroundNaturalSize: background + ? { width: background.naturalWidth, height: background.naturalHeight } + : null, + backgroundObjectFit: background ? getComputedStyle(background).objectFit : null, + roadBackground: road ? getComputedStyle(road).backgroundImage : null, + }; + }); + expect(mapEvidence.mapRect.width / mapEvidence.mapRect.height).toBeCloseTo(7 / 5, 2); + expect(mapEvidence.backgroundNaturalSize?.width).toBeGreaterThan(0); + await page.screenshot({ path: testInfo.outputPath('dynasty-history-map-desktop.png'), fullPage: true }); + + await page.setViewportSize({ width: 500, height: 800 }); + await expect(page.locator('.map-area')).toHaveClass(/map-theme-miniche/); + const mobileEvidence = await page.locator('.map-area').evaluate((area) => ({ + mapClass: area.className, + mapRect: area.getBoundingClientRect().toJSON(), + roadBackground: getComputedStyle(area.querySelector('.map-bgroad')!).backgroundImage, + })); + expect(mobileEvidence.mapRect.width / mobileEvidence.mapRect.height).toBeCloseTo(7 / 5, 2); + await writeFile( + testInfo.outputPath('dynasty-history-map-geometry.json'), + JSON.stringify({ desktop: mapEvidence, mobile: mobileEvidence }, null, 2) + ); + await page.screenshot({ path: testInfo.outputPath('dynasty-history-map-mobile.png'), fullPage: true }); +}); diff --git a/app/game-frontend/src/views/YearbookView.vue b/app/game-frontend/src/views/YearbookView.vue index dd03af8c..2900c0af 100644 --- a/app/game-frontend/src/views/YearbookView.vue +++ b/app/game-frontend/src/views/YearbookView.vue @@ -12,7 +12,7 @@ import { trpc } from '../utils/trpc'; const { pageExitLabel, exitPage } = usePageExit(); type YearbookRange = Awaited>; -type MapLayout = Awaited>; +type MapLayout = YearbookRange['mapLayout']; type HistoryData = { year: number; month: number; @@ -114,12 +114,11 @@ watch(selectedYearMonth, () => { onMounted(async () => { loading.value = true; try { - const [loadedRange, loadedLayout] = await Promise.all([ - trpc.yearbook.getRange.query(serverID.value ? { serverID: serverID.value } : undefined), - trpc.public.getMapLayout.query(), - ]); + const loadedRange = await trpc.yearbook.getRange.query( + serverID.value ? { serverID: serverID.value } : undefined + ); range.value = loadedRange; - mapLayout.value = loadedLayout; + mapLayout.value = loadedRange.mapLayout; selectedYearMonth.value = loadedRange.currentYearMonth; } catch (error) { errorMessage.value = error instanceof Error ? error.message : '연감 범위를 불러오지 못했습니다.';