From 991e4028e90928d1ddbdce849010df350e742a62 Mon Sep 17 00:00:00 2001 From: hided62 Date: Thu, 13 Aug 2026 17:38:12 +0000 Subject: [PATCH] fix(game-ui): keep current city visible in command maps --- .../e2e/commandArguments.spec.ts | 45 ++++++++++++ .../components/main/CommandArgumentForm.vue | 69 +++++++++++++++++++ .../src/components/main/MapViewer.vue | 57 +++++++++++++++ 3 files changed, 171 insertions(+) diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index e8d3265c..6b7e3ace 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -465,6 +465,7 @@ test('enters general and nation command arguments and sends exact values', async const requests = await install(page); await page.setViewportSize({ width: 1200, height: 900 }); await page.goto('/'); + await expect(page.getByTestId('current-city-marker')).toHaveCount(0); await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click(); await page.getByTestId('command-picker').getByRole('button', { name: /화계/ }).click(); @@ -474,6 +475,12 @@ test('enters general and nation command arguments and sends exact values', async await expect(form.getByTestId('command-argument-guidance')).toContainText('선택한 도시에 화계를 실행합니다.'); await expect(form.getByTestId('command-map-target-summary')).toContainText('현재 도시에서 0칸'); const commandMap = form.getByTestId('command-argument-map'); + const currentCityMarker = commandMap.getByTestId('current-city-marker'); + const selectionStatus = form.getByTestId('command-map-selection-status'); + await expect(currentCityMarker).toHaveText('현재'); + await expect(currentCityMarker).toHaveAttribute('aria-label', '현재 도시 업'); + await expect(selectionStatus).toContainText('현재 도시업'); + await expect(selectionStatus).toContainText('선택 도시업'); const mapCities = commandMap.locator('.city-base'); await expect(mapCities).toHaveCount(2); await expect(commandMap.locator('.city-bg')).toHaveCount(2); @@ -515,11 +522,17 @@ test('enters general and nation command arguments and sends exact values', async await mapCities.nth(1).click(); await expect(form.locator('select')).toHaveValue('2'); await expect(form.getByTestId('command-map-target-summary')).toContainText('현재 도시에서 1칸'); + await expect(selectionStatus).toContainText('현재 도시업'); + await expect(selectionStatus).toContainText('선택 도시허창'); + await expect(currentCityMarker).toHaveAttribute('aria-label', '현재 도시 업'); + await expect(mapCities.nth(0)).toHaveClass(/mine/); + await expect(mapCities.nth(1)).toHaveClass(/selected/); await mapCities.nth(1).hover(); expect(await mapCities.nth(1).evaluate((element) => getComputedStyle(element).cursor)).toBe('pointer'); await mapCities.nth(1).focus(); await expect(mapCities.nth(1)).toBeFocused(); await expect(page).toHaveURL(/\/$/); + await commandMap.screenshot({ path: test.info().outputPath('main-city-current-marker-desktop.png') }); const mapGeometry = await form.getByTestId('command-argument-map').evaluate((element) => { const area = element.querySelector('.map-area')!; const rect = area.getBoundingClientRect(); @@ -685,6 +698,9 @@ test('uses the map to choose a nation target in the chief command window', async await expect(form.getByTestId('command-argument-guidance')).toContainText('초반 제한'); await form.getByTestId('command-argument-map').locator('.city-base').nth(1).click(); await expect(form.locator('select')).toHaveValue('2'); + await expect(form.getByTestId('command-map-selection-status')).toContainText('현재 도시업'); + await expect(form.getByTestId('command-map-selection-status')).toContainText('선택 국가적국'); + await expect(form.getByTestId('current-city-marker')).toHaveAttribute('aria-label', '현재 도시 업'); await expect(form.getByTestId('command-map-target-summary')).toContainText('수도 허창 · 도시 1개'); await expect(page).toHaveURL(/\/che\/chief-center$/); await page.screenshot({ path: test.info().outputPath('chief-nation-map-option.png'), fullPage: true }); @@ -697,6 +713,16 @@ test('fits the city map option window inside the Ref-compatible 500px mobile pag await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click(); const picker = page.getByTestId('command-picker'); await picker.getByRole('button', { name: /화계/ }).click(); + const mobileForm = picker.getByTestId('command-argument-form'); + const mobileMap = mobileForm.getByTestId('command-argument-map'); + const mobileCities = mobileMap.locator('.city-base'); + await mobileCities.nth(1).click(); + await expect(mobileForm.locator('select')).toHaveValue('2'); + await expect(mobileForm.getByTestId('command-map-selection-status')).toContainText('현재 도시업'); + await expect(mobileForm.getByTestId('command-map-selection-status')).toContainText('선택 도시허창'); + await expect(mobileMap.getByTestId('current-city-marker')).toHaveAttribute('aria-label', '현재 도시 업'); + await expect(mobileCities.nth(0)).toHaveClass(/mine/); + await expect(mobileCities.nth(1)).toHaveClass(/selected/); const geometry = await picker.evaluate((element) => { const map = element.querySelector('[data-testid="command-argument-map"] .map-area')!; const pickerRect = element.getBoundingClientRect(); @@ -708,6 +734,19 @@ test('fits the city map option window inside the Ref-compatible 500px mobile pag pickerScrollWidth: element.scrollWidth, mapWidth: mapRect.width, mapHeight: mapRect.height, + marker: (() => { + const marker = element.querySelector('[data-testid="current-city-marker"]')!; + const rect = marker.getBoundingClientRect(); + const style = getComputedStyle(marker); + return { + left: rect.left, + right: rect.right, + top: rect.top, + bottom: rect.bottom, + pointerEvents: style.pointerEvents, + borderColor: style.borderColor, + }; + })(), }; }); expect(geometry.pickerX).toBeGreaterThanOrEqual(0); @@ -716,6 +755,12 @@ test('fits the city map option window inside the Ref-compatible 500px mobile pag expect(geometry.pickerScrollWidth).toBeLessThanOrEqual(geometry.pickerWidth); expect(geometry.mapWidth).toBeGreaterThan(470); expect(geometry.mapHeight / geometry.mapWidth).toBeCloseTo(5 / 7, 2); + expect(geometry.marker.left).toBeGreaterThanOrEqual(0); + expect(geometry.marker.right).toBeLessThanOrEqual(500); + expect(geometry.marker.top).toBeGreaterThanOrEqual(0); + expect(geometry.marker.bottom).toBeLessThanOrEqual(900); + expect(geometry.marker.pointerEvents).toBe('none'); + expect(geometry.marker.borderColor).toBe('rgb(130, 207, 255)'); await page.screenshot({ path: test.info().outputPath('main-city-map-option-mobile.png'), fullPage: true }); }); diff --git a/app/game-frontend/src/components/main/CommandArgumentForm.vue b/app/game-frontend/src/components/main/CommandArgumentForm.vue index 8e17a473..8a6c78f8 100644 --- a/app/game-frontend/src/components/main/CommandArgumentForm.vue +++ b/app/game-frontend/src/components/main/CommandArgumentForm.vue @@ -109,6 +109,26 @@ const mapSelectedCityId = computed(() => { return null; }); +const currentCityName = computed(() => { + const cityId = props.mapData?.myCity; + if (!cityId) return '-'; + return props.mapLayout?.cityList.find((city) => city.id === cityId)?.name ?? '-'; +}); + +const selectedMapTargetName = computed(() => { + if (presentation.value.mapTarget === 'city') { + const cityId = mapSelectedCityId.value; + if (!cityId) return '-'; + return props.mapLayout?.cityList.find((city) => city.id === cityId)?.name ?? '-'; + } + if (presentation.value.mapTarget === 'nation' && nationTargetField.value) { + const nationId = values[nationTargetField.value.key]; + if (typeof nationId !== 'number') return '-'; + return props.mapData?.nationList.find((nation) => nation[0] === nationId)?.[1] ?? '-'; + } + return '-'; +}); + const distanceFromMyCity = (destination: number): number | null => { const start = props.mapData?.myCity; if (!start || !props.mapLayout) return null; @@ -260,9 +280,23 @@ watch( :selected-city-id="mapSelectedCityId" :detail-mode="true" :fit-container="true" + :show-current-city-marker="true" @select-city="selectMapCity" /> 지도에서 도시를 클릭하거나 아래 목록에서 대상을 선택하세요. +
+ + 현재 도시 + {{ currentCityName }} + + + + {{ + presentation.mapTarget === 'nation' ? '선택 국가' : '선택 도시' + }} + {{ selectedMapTargetName }} + +
{{ mapTargetSummary }}
@@ -376,6 +410,41 @@ watch( line-height: 1.35; } +.map-selection-status { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 7px; + padding: 0 8px 5px; + color: rgba(232, 221, 196, 0.82); + line-height: 18px; +} + +.map-selection-status > span:not([aria-hidden='true']) { + display: inline-flex; + gap: 4px; + align-items: center; +} + +.status-key { + border-radius: 2px; + padding: 0 4px; + font-size: 10px; + font-weight: 700; +} + +.current-city-status .status-key { + border: 1px solid #82cfff; + background: rgba(5, 27, 43, 0.92); + color: #d9f3ff; +} + +.selected-target-status .status-key { + border: 1px solid rgba(255, 235, 150, 0.9); + background: rgba(201, 164, 90, 0.18); + color: #ffe996; +} + .command-guidance { display: grid; gap: 3px; diff --git a/app/game-frontend/src/components/main/MapViewer.vue b/app/game-frontend/src/components/main/MapViewer.vue index 0ba580a8..2b6bdab5 100644 --- a/app/game-frontend/src/components/main/MapViewer.vue +++ b/app/game-frontend/src/components/main/MapViewer.vue @@ -71,6 +71,7 @@ const props = withDefaults( selectedCityId?: number | null; detailMode?: boolean; fitContainer?: boolean; + showCurrentCityMarker?: boolean; }>(), { // Vue casts an absent Boolean prop to false unless undefined is an explicit default. @@ -214,6 +215,20 @@ const cityViews = computed(() => { }); }); +const currentCityMarker = computed(() => + props.showCurrentCityMarker ? (cityViews.value.find((city) => city.isMyCity) ?? null) : null +); + +const currentCityMarkerStyle = computed(() => { + const city = currentCityMarker.value; + if (!city) return undefined; + const verticalOffset = (effectiveDetailMode.value ? 22 : 14) * mapScale.value; + return { + left: `${city.x}px`, + top: `${city.y - verticalOffset}px`, + }; +}); + const mapSeason = computed(() => { if (!props.mapData) { return 'spring'; @@ -433,6 +448,16 @@ const selectCity = (cityId: number) => { @leave="setHoveredCity(null)" @select="selectCity" /> +
+ 현재 +
{{ hoveredCityTitle }}
{{ hoveredCity.nationId > 0 ? hoveredCity.nationName : '' }}
@@ -583,6 +608,38 @@ const selectCity = (cityId: number) => { white-space: nowrap; } +.current-city-marker { + position: absolute; + z-index: 12; + box-sizing: border-box; + min-width: 30px; + border: 1px solid #82cfff; + border-radius: 2px; + padding: 1px 4px; + background: rgba(5, 27, 43, 0.92); + color: #d9f3ff; + font-size: 10px; + font-weight: 700; + line-height: 14px; + pointer-events: none; + text-align: center; + text-shadow: 0 1px #000; + transform: translate(-50%, -100%); + white-space: nowrap; +} + +.current-city-marker::after { + position: absolute; + top: 100%; + left: 50%; + width: 0; + height: 0; + border: 4px solid transparent; + border-top-color: #82cfff; + content: ''; + transform: translateX(-50%); +} + .tooltip-title { height: 15px; }