feat: 지도 모바일 두 번 탭 이동을 복원

Ref처럼 첫 탭은 도시 툴팁만 표시하고 같은 도시의 두 번째 탭에서 이동하도록 한다. 한 번 탭 모드와 localStorage 보존, PC hover/click 회귀를 Chromium 테스트로 검증한다.
This commit is contained in:
2026-08-21 00:58:57 +00:00
parent 7b14585f0d
commit b621381109
5 changed files with 208 additions and 1 deletions
@@ -28,6 +28,8 @@ const props = defineProps<{
const emit = defineEmits<{
(event: 'hover', cityId: number): void;
(event: 'leave'): void;
(event: 'touch', cityId: number, touchEvent: TouchEvent): void;
(event: 'touchleave'): void;
(event: 'select', cityId: number): void;
}>();
@@ -37,6 +39,25 @@ const stateOffset = computed(() => -6 * props.mapScale);
const selectCity = () => {
if (!props.readonly) emit('select', props.city.id);
};
let touchOnTrack = false;
const touchstart = () => {
touchOnTrack = true;
};
const touchmove = () => {
touchOnTrack = false;
};
const touchend = (event: TouchEvent) => {
if (touchOnTrack) {
event.stopPropagation();
emit('touch', props.city.id, event);
return;
}
emit('touchleave');
};
</script>
<template>
@@ -59,6 +80,9 @@ const selectCity = () => {
:style="{ left: `${props.city.x}px`, top: `${props.city.y}px` }"
@mouseenter="emit('hover', props.city.id)"
@mouseleave="emit('leave')"
@touchstart="touchstart"
@touchmove="touchmove"
@touchend="touchend"
@click.stop="selectCity"
>
<div class="city-dot" :style="{ backgroundColor: props.city.color, width: `${size}px`, height: `${size}px` }">
@@ -54,6 +54,8 @@ const props = defineProps<{
const emit = defineEmits<{
(event: 'hover', cityId: number): void;
(event: 'leave'): void;
(event: 'touch', cityId: number, touchEvent: TouchEvent): void;
(event: 'touchleave'): void;
(event: 'select', cityId: number): void;
}>();
@@ -148,6 +150,25 @@ const selectCity = () => {
if (!props.readonly) emit('select', props.city.id);
};
let touchOnTrack = false;
const touchstart = () => {
touchOnTrack = true;
};
const touchmove = () => {
touchOnTrack = false;
};
const touchend = (event: TouchEvent) => {
if (touchOnTrack) {
event.stopPropagation();
emit('touch', props.city.id, event);
return;
}
emit('touchleave');
};
const cityStateStyle = computed(() => ({
width: `${12 * props.mapScale}px`,
height: `${12 * props.mapScale}px`,
@@ -174,6 +195,9 @@ const cityStateStyle = computed(() => ({
:style="cityBaseStyle"
@mouseenter="emit('hover', props.city.id)"
@mouseleave="emit('leave')"
@touchstart="touchstart"
@touchmove="touchmove"
@touchend="touchend"
@click.stop="selectCity"
>
<div v-if="cityBgStyle" class="city-bg" :style="[cityBgWrapperStyle, cityBgStyle]" />
@@ -94,9 +94,11 @@ const mapStore = useMapViewerStore();
const {
showCityName,
detailMode: storeDetailMode,
singleTapNavigation,
hoveredCityId,
selectedCityId: storeSelectedCityId,
} = storeToRefs(mapStore);
const hasTouchInput = useMediaQuery('(any-pointer: coarse)');
const mapArea = ref<HTMLElement | null>(null);
const mapBody = ref<HTMLElement | null>(null);
@@ -404,6 +406,28 @@ const setHoveredCity = (cityId: number | null) => {
mapStore.setHoveredCity(cityId);
};
const touchPreviewCityId = ref<number | null>(null);
const clearTouchPreview = () => {
touchPreviewCityId.value = null;
setHoveredCity(null);
};
const touchCity = (cityId: number, event: TouchEvent) => {
if (touchPreviewCityId.value !== cityId) {
touchPreviewCityId.value = cityId;
setHoveredCity(cityId);
if (!singleTapNavigation.value) {
event.preventDefault();
}
}
};
const toggleSingleTapNavigation = () => {
clearTouchPreview();
mapStore.toggleSingleTapNavigation();
};
const selectCity = (cityId: number) => {
if (props.readonly) return;
emit('select-city', cityId);
@@ -433,6 +457,7 @@ const selectCity = (cityId: number) => {
class="map-area"
:class="[mapThemeClass, mapSeasonClass]"
:style="{ width: mapWidth, height: mapHeight }"
@click="clearTouchPreview"
>
<div class="map-layer map-bglayer1" :style="mapBackgroundStyle" />
<div class="map-layer map-bglayer2" />
@@ -449,6 +474,8 @@ const selectCity = (cityId: number) => {
v-bind="detailProps"
@hover="setHoveredCity"
@leave="setHoveredCity(null)"
@touch="touchCity"
@touchleave="clearTouchPreview"
@select="selectCity"
/>
<div
@@ -466,9 +493,18 @@ const selectCity = (cityId: number) => {
<div class="tooltip-body">{{ hoveredCity.nationId > 0 ? hoveredCity.nationName : '' }}</div>
</div>
<div class="map-controls">
<button class="map-toggle" :class="{ active: showCityName }" @click="mapStore.toggleCityName">
<button class="map-toggle" :class="{ active: showCityName }" @click.stop="mapStore.toggleCityName">
도시명 표기 {{ showCityName ? '끄기' : '켜기' }}
</button>
<button
v-if="hasTouchInput"
class="map-toggle map-toggle-single-tap"
:class="{ active: singleTapNavigation }"
:aria-pressed="singleTapNavigation"
@click.stop="toggleSingleTapNavigation"
>
두번 도시 이동 {{ singleTapNavigation ? '켜기' : '끄기' }}
</button>
</div>
</div>
</div>
@@ -555,6 +591,8 @@ const selectCity = (cityId: number) => {
right: 4px;
bottom: 4px;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.map-toggle {
+15
View File
@@ -3,14 +3,23 @@ import { defineStore } from 'pinia';
interface MapViewerState {
showCityName: boolean;
detailMode: boolean;
singleTapNavigation: boolean;
hoveredCityId: number | null;
selectedCityId: number | null;
}
const SINGLE_TAP_STORAGE_KEY = 'sam.toggleSingleTap';
const loadSingleTapNavigation = (): boolean => {
if (typeof window === 'undefined') return false;
return window.localStorage.getItem(SINGLE_TAP_STORAGE_KEY) === 'yes';
};
export const useMapViewerStore = defineStore('mapViewer', {
state: (): MapViewerState => ({
showCityName: true,
detailMode: true,
singleTapNavigation: loadSingleTapNavigation(),
hoveredCityId: null,
selectedCityId: null,
}),
@@ -21,6 +30,12 @@ export const useMapViewerStore = defineStore('mapViewer', {
toggleDetailMode() {
this.detailMode = !this.detailMode;
},
toggleSingleTapNavigation() {
this.singleTapNavigation = !this.singleTapNavigation;
if (typeof window !== 'undefined') {
window.localStorage.setItem(SINGLE_TAP_STORAGE_KEY, this.singleTapNavigation ? 'yes' : 'no');
}
},
setHoveredCity(cityId: number | null) {
this.hoveredCityId = cityId;
},