merge: 최신 main을 도시정보 명령 요약에 통합한다
This commit is contained in:
@@ -517,6 +517,27 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or
|
|||||||
await page.setViewportSize({ width: 1200, height: 900 });
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
await go(page, 'global-info');
|
await go(page, 'global-info');
|
||||||
|
|
||||||
|
const desktopOptionsTrigger = page.getByRole('button', { name: '지도 옵션' });
|
||||||
|
await expect(desktopOptionsTrigger).toBeVisible();
|
||||||
|
await expect(desktopOptionsTrigger).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
await expect(desktopOptionsTrigger).toHaveCSS('background-color', 'rgb(52, 92, 133)');
|
||||||
|
await desktopOptionsTrigger.hover();
|
||||||
|
await expect(desktopOptionsTrigger).toHaveCSS('background-color', 'rgb(40, 73, 105)');
|
||||||
|
await expect(page.getByRole('button', { name: '도시명 표기 끄기' })).not.toBeVisible();
|
||||||
|
await desktopOptionsTrigger.click();
|
||||||
|
const cityNameToggle = page.getByRole('button', { name: '도시명 표기 끄기' });
|
||||||
|
await expect(cityNameToggle).toBeVisible();
|
||||||
|
await expect(desktopOptionsTrigger).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
await expect(page.locator('.map-options-menu .map-toggle')).toHaveCount(1);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await desktopOptionsTrigger.focus();
|
||||||
|
await expect(desktopOptionsTrigger).toHaveCSS('outline-style', 'solid');
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('desktop-map-options-open.png'), fullPage: true });
|
||||||
|
await cityNameToggle.click();
|
||||||
|
await expect(page.locator('.map-area .city-name')).toHaveCount(0);
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await expect(desktopOptionsTrigger).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
const desktopCity = page.locator('.city-base').first();
|
const desktopCity = page.locator('.city-base').first();
|
||||||
await expect(page.locator('.map-toggle-single-tap')).toHaveCount(0);
|
await expect(page.locator('.map-toggle-single-tap')).toHaveCount(0);
|
||||||
await desktopCity.hover();
|
await desktopCity.hover();
|
||||||
@@ -550,20 +571,48 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or
|
|||||||
await install(mobilePage);
|
await install(mobilePage);
|
||||||
await go(mobilePage, 'global-info');
|
await go(mobilePage, 'global-info');
|
||||||
|
|
||||||
|
const mobileOptionsTrigger = mobilePage.getByRole('button', { name: '지도 옵션' });
|
||||||
|
await expect(mobileOptionsTrigger).toBeVisible();
|
||||||
|
await expect(mobileOptionsTrigger).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
await mobileOptionsTrigger.tap();
|
||||||
|
await expect(mobileOptionsTrigger).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
await expect(mobilePage.locator('.map-options-menu .map-toggle')).toHaveCount(2);
|
||||||
|
|
||||||
const twoTapButton = mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 끄기' });
|
const twoTapButton = mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 끄기' });
|
||||||
await expect(twoTapButton).toBeVisible();
|
await expect(twoTapButton).toBeVisible();
|
||||||
await expect(twoTapButton).toHaveAttribute('aria-pressed', 'false');
|
await expect(twoTapButton).toHaveAttribute('aria-pressed', 'false');
|
||||||
const controlGeometry = await twoTapButton.evaluate((element) => {
|
const controlGeometry = await mobilePage.locator('.map-controls').evaluate((element) => {
|
||||||
const rect = element.getBoundingClientRect();
|
const triggerRect = element.querySelector('.map-options-trigger')!.getBoundingClientRect();
|
||||||
|
const menuRect = element.querySelector('.map-options-menu')!.getBoundingClientRect();
|
||||||
|
const optionRects = Array.from(element.querySelectorAll('.map-options-menu .map-toggle')).map((option) => {
|
||||||
|
const rect = option.getBoundingClientRect();
|
||||||
|
return { top: rect.top, bottom: rect.bottom };
|
||||||
|
});
|
||||||
const mapRect = element.closest('.map-area')?.getBoundingClientRect();
|
const mapRect = element.closest('.map-area')?.getBoundingClientRect();
|
||||||
const style = getComputedStyle(element);
|
const optionStyle = getComputedStyle(element.querySelector('.map-toggle')!);
|
||||||
return {
|
return {
|
||||||
right: rect.right,
|
trigger: {
|
||||||
bottom: rect.bottom,
|
right: triggerRect.right,
|
||||||
mapRight: mapRect?.right,
|
bottom: triggerRect.bottom,
|
||||||
mapBottom: mapRect?.bottom,
|
top: triggerRect.top,
|
||||||
fontSize: style.fontSize,
|
},
|
||||||
lineHeight: style.lineHeight,
|
menu: {
|
||||||
|
left: menuRect.left,
|
||||||
|
right: menuRect.right,
|
||||||
|
top: menuRect.top,
|
||||||
|
bottom: menuRect.bottom,
|
||||||
|
},
|
||||||
|
optionRects,
|
||||||
|
map: mapRect
|
||||||
|
? {
|
||||||
|
left: mapRect.left,
|
||||||
|
right: mapRect.right,
|
||||||
|
top: mapRect.top,
|
||||||
|
bottom: mapRect.bottom,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
fontSize: optionStyle.fontSize,
|
||||||
|
lineHeight: optionStyle.lineHeight,
|
||||||
documentWidth: document.documentElement.scrollWidth,
|
documentWidth: document.documentElement.scrollWidth,
|
||||||
viewportWidth: document.documentElement.clientWidth,
|
viewportWidth: document.documentElement.clientWidth,
|
||||||
overflowing: Array.from(document.querySelectorAll<HTMLElement>('body *'))
|
overflowing: Array.from(document.querySelectorAll<HTMLElement>('body *'))
|
||||||
@@ -585,8 +634,20 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or
|
|||||||
overflowing: [],
|
overflowing: [],
|
||||||
});
|
});
|
||||||
expect(controlGeometry.documentWidth).toBeLessThanOrEqual(controlGeometry.viewportWidth + 1);
|
expect(controlGeometry.documentWidth).toBeLessThanOrEqual(controlGeometry.viewportWidth + 1);
|
||||||
expect(controlGeometry.mapRight! - controlGeometry.right).toBeCloseTo(4, 1);
|
expect(controlGeometry.map).not.toBeNull();
|
||||||
expect(controlGeometry.mapBottom! - controlGeometry.bottom).toBeCloseTo(4, 1);
|
expect(controlGeometry.map!.right - controlGeometry.trigger.right).toBeCloseTo(4, 1);
|
||||||
|
expect(controlGeometry.map!.bottom - controlGeometry.trigger.bottom).toBeCloseTo(4, 1);
|
||||||
|
expect(controlGeometry.menu.left).toBeGreaterThanOrEqual(controlGeometry.map!.left);
|
||||||
|
expect(controlGeometry.menu.right).toBeLessThanOrEqual(controlGeometry.map!.right);
|
||||||
|
expect(controlGeometry.menu.top).toBeGreaterThanOrEqual(controlGeometry.map!.top);
|
||||||
|
expect(controlGeometry.menu.bottom).toBeLessThan(controlGeometry.trigger.top);
|
||||||
|
expect(controlGeometry.optionRects).toHaveLength(2);
|
||||||
|
expect(controlGeometry.optionRects[0]!.bottom).toBeLessThanOrEqual(controlGeometry.optionRects[1]!.top);
|
||||||
|
expect(controlGeometry.optionRects[1]!.bottom).toBeLessThanOrEqual(controlGeometry.menu.bottom);
|
||||||
|
await mobilePage.screenshot({ path: testInfo.outputPath('mobile-map-options-open.png'), fullPage: true });
|
||||||
|
|
||||||
|
await mobilePage.locator('.map-area').tap({ position: { x: 10, y: 10 } });
|
||||||
|
await expect(mobileOptionsTrigger).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
const mobileCities = mobilePage.locator('.city-base');
|
const mobileCities = mobilePage.locator('.city-base');
|
||||||
await mobileCities.nth(0).tap();
|
await mobileCities.nth(0).tap();
|
||||||
@@ -602,17 +663,19 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or
|
|||||||
await expect(mobilePage).toHaveURL(/\/current-city\?cityId=2$/u);
|
await expect(mobilePage).toHaveURL(/\/current-city\?cityId=2$/u);
|
||||||
|
|
||||||
await go(mobilePage, 'global-info');
|
await go(mobilePage, 'global-info');
|
||||||
|
await mobilePage.getByRole('button', { name: '지도 옵션' }).tap();
|
||||||
await mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 끄기' }).click();
|
await mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 끄기' }).click();
|
||||||
const singleTapButton = mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 켜기' });
|
const singleTapButton = mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 켜기' });
|
||||||
await expect(singleTapButton).toHaveAttribute('aria-pressed', 'true');
|
await expect(singleTapButton).toHaveAttribute('aria-pressed', 'true');
|
||||||
expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('yes');
|
expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('yes');
|
||||||
|
|
||||||
await mobilePage.reload();
|
await mobilePage.reload();
|
||||||
await expect(singleTapButton).toBeVisible();
|
await expect(singleTapButton).not.toBeVisible();
|
||||||
await mobilePage.locator('.city-base').nth(2).tap();
|
await mobilePage.locator('.city-base').nth(2).tap();
|
||||||
await expect(mobilePage).toHaveURL(/\/current-city\?cityId=3$/u);
|
await expect(mobilePage).toHaveURL(/\/current-city\?cityId=3$/u);
|
||||||
|
|
||||||
await go(mobilePage, 'global-info');
|
await go(mobilePage, 'global-info');
|
||||||
|
await mobilePage.getByRole('button', { name: '지도 옵션' }).tap();
|
||||||
await mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 켜기' }).click();
|
await mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 켜기' }).click();
|
||||||
expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('no');
|
expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('no');
|
||||||
await mobilePage.locator('.city-base').first().tap();
|
await mobilePage.locator('.city-base').first().tap();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, useId } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useElementSize, useMediaQuery, useMouseInElement } from '@vueuse/core';
|
import { onClickOutside, useElementSize, useMediaQuery, useMouseInElement } from '@vueuse/core';
|
||||||
import SkeletonLines from '../ui/SkeletonLines.vue';
|
import SkeletonLines from '../ui/SkeletonLines.vue';
|
||||||
import MapCityBasic from './MapCityBasic.vue';
|
import MapCityBasic from './MapCityBasic.vue';
|
||||||
import MapCityDetail from './MapCityDetail.vue';
|
import MapCityDetail from './MapCityDetail.vue';
|
||||||
@@ -104,6 +104,9 @@ const hasTouchInput = useMediaQuery('(any-pointer: coarse)');
|
|||||||
|
|
||||||
const mapArea = ref<HTMLElement | null>(null);
|
const mapArea = ref<HTMLElement | null>(null);
|
||||||
const mapBody = ref<HTMLElement | null>(null);
|
const mapBody = ref<HTMLElement | null>(null);
|
||||||
|
const mapControls = ref<HTMLElement | null>(null);
|
||||||
|
const mapOptionsOpen = ref(false);
|
||||||
|
const mapOptionsMenuId = `map-options-${useId()}`;
|
||||||
const { width: mapBodyWidth } = useElementSize(mapBody);
|
const { width: mapBodyWidth } = useElementSize(mapBody);
|
||||||
const { elementX, elementY } = useMouseInElement(mapArea);
|
const { elementX, elementY } = useMouseInElement(mapArea);
|
||||||
|
|
||||||
@@ -416,6 +419,16 @@ const clearTouchPreview = () => {
|
|||||||
setHoveredCity(null);
|
setHoveredCity(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeMapOptions = () => {
|
||||||
|
mapOptionsOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMapOptions = () => {
|
||||||
|
mapOptionsOpen.value = !mapOptionsOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
onClickOutside(mapControls, closeMapOptions);
|
||||||
|
|
||||||
const touchCity = (cityId: number, event: TouchEvent) => {
|
const touchCity = (cityId: number, event: TouchEvent) => {
|
||||||
if (touchPreviewCityId.value !== cityId) {
|
if (touchPreviewCityId.value !== cityId) {
|
||||||
touchPreviewCityId.value = cityId;
|
touchPreviewCityId.value = cityId;
|
||||||
@@ -460,7 +473,10 @@ const selectCity = (cityId: number) => {
|
|||||||
class="map-area"
|
class="map-area"
|
||||||
:class="[mapThemeClass, mapSeasonClass]"
|
:class="[mapThemeClass, mapSeasonClass]"
|
||||||
:style="{ width: mapWidth, height: mapHeight }"
|
:style="{ width: mapWidth, height: mapHeight }"
|
||||||
@click="clearTouchPreview"
|
@click="
|
||||||
|
clearTouchPreview();
|
||||||
|
closeMapOptions();
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="map-layer map-bglayer1" :style="mapBackgroundStyle" />
|
<div class="map-layer map-bglayer1" :style="mapBackgroundStyle" />
|
||||||
<div class="map-layer map-bglayer2" />
|
<div class="map-layer map-bglayer2" />
|
||||||
@@ -495,8 +511,21 @@ const selectCity = (cityId: number) => {
|
|||||||
<div class="tooltip-title">{{ hoveredCityTitle }}</div>
|
<div class="tooltip-title">{{ hoveredCityTitle }}</div>
|
||||||
<div class="tooltip-body">{{ hoveredCity.nationId > 0 ? hoveredCity.nationName : '' }}</div>
|
<div class="tooltip-body">{{ hoveredCity.nationId > 0 ? hoveredCity.nationName : '' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-controls">
|
<div ref="mapControls" class="map-controls" @keydown.esc.stop="closeMapOptions">
|
||||||
<button class="map-toggle" :class="{ active: showCityName }" @click.stop="mapStore.toggleCityName">
|
<div
|
||||||
|
v-show="mapOptionsOpen"
|
||||||
|
:id="mapOptionsMenuId"
|
||||||
|
class="map-options-menu"
|
||||||
|
role="group"
|
||||||
|
aria-label="지도 옵션 메뉴"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="map-toggle"
|
||||||
|
:class="{ active: showCityName }"
|
||||||
|
:aria-pressed="showCityName"
|
||||||
|
@click="mapStore.toggleCityName"
|
||||||
|
>
|
||||||
도시명 표기 {{ showCityName ? '끄기' : '켜기' }}
|
도시명 표기 {{ showCityName ? '끄기' : '켜기' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -504,11 +533,23 @@ const selectCity = (cityId: number) => {
|
|||||||
class="map-toggle map-toggle-single-tap"
|
class="map-toggle map-toggle-single-tap"
|
||||||
:class="{ active: singleTapNavigation }"
|
:class="{ active: singleTapNavigation }"
|
||||||
:aria-pressed="singleTapNavigation"
|
:aria-pressed="singleTapNavigation"
|
||||||
@click.stop="toggleSingleTapNavigation"
|
@click="toggleSingleTapNavigation"
|
||||||
>
|
>
|
||||||
두번 탭 해 도시 이동 {{ singleTapNavigation ? '켜기' : '끄기' }}
|
두번 탭 해 도시 이동 {{ singleTapNavigation ? '켜기' : '끄기' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="map-options-trigger"
|
||||||
|
aria-label="지도 옵션"
|
||||||
|
title="지도 옵션"
|
||||||
|
:aria-controls="mapOptionsMenuId"
|
||||||
|
:aria-expanded="mapOptionsOpen"
|
||||||
|
@click.stop="toggleMapOptions"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">⚙</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -591,14 +632,68 @@ const selectCity = (cityId: number) => {
|
|||||||
.map-controls {
|
.map-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
right: 4px;
|
inset: 4px;
|
||||||
bottom: 4px;
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-options-trigger {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: grid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 30px;
|
||||||
|
height: 28px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #6c757d;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0;
|
||||||
|
background: #345c85;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-options-trigger:hover,
|
||||||
|
.map-options-trigger:focus-visible,
|
||||||
|
.map-options-trigger[aria-expanded='true'] {
|
||||||
|
border-color: #b7cadc;
|
||||||
|
background: #284969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-options-trigger:active {
|
||||||
|
background: #1f3a54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-options-trigger:focus-visible {
|
||||||
|
outline: 2px solid #fff;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-options-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(100% - 32px);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: stretch;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #6c757d;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 3px;
|
||||||
|
background: rgba(11, 11, 11, 0.94);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.55);
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-toggle {
|
.map-toggle {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
border: 1px solid #6c757d;
|
border: 1px solid #6c757d;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
@@ -607,6 +702,12 @@ const selectCity = (cityId: number) => {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-toggle + .map-toggle {
|
||||||
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-toggle.active {
|
.map-toggle.active {
|
||||||
|
|||||||
Reference in New Issue
Block a user