마커가 겹쳐도 선택되도록 지도 클릭 지점에서 가장 가까운 스팟을 선택. 현재위치(Geolocation) 버튼은 스팟 선택 간섭 소지로 현 단계에서 비활성. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-63
@@ -2,19 +2,6 @@
|
|||||||
<div class="map-wrap">
|
<div class="map-wrap">
|
||||||
<div ref="mapEl" class="map-canvas"></div>
|
<div ref="mapEl" class="map-canvas"></div>
|
||||||
|
|
||||||
<!-- 현재위치로 이동 -->
|
|
||||||
<q-btn
|
|
||||||
v-if="!errorMsg"
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
color="white"
|
|
||||||
text-color="primary"
|
|
||||||
icon="my_location"
|
|
||||||
class="locate-btn"
|
|
||||||
:loading="locating"
|
|
||||||
@click="locate"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- SDK 미로드/키 미설정 시 폴백 -->
|
<!-- SDK 미로드/키 미설정 시 폴백 -->
|
||||||
<div v-if="errorMsg" class="map-fallback flex flex-center column text-grey-6">
|
<div v-if="errorMsg" class="map-fallback flex flex-center column text-grey-6">
|
||||||
<q-icon name="map" size="40px" color="secondary" />
|
<q-icon name="map" size="40px" color="secondary" />
|
||||||
@@ -25,8 +12,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useQuasar } from 'quasar'
|
|
||||||
import { Geolocation } from '@capacitor/geolocation'
|
|
||||||
import { loadKakaoMaps } from '@/lib/kakaoMap'
|
import { loadKakaoMaps } from '@/lib/kakaoMap'
|
||||||
import type { Spot } from '@/api/spots'
|
import type { Spot } from '@/api/spots'
|
||||||
|
|
||||||
@@ -36,18 +21,14 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{ (e: 'select', id: number): void }>()
|
const emit = defineEmits<{ (e: 'select', id: number): void }>()
|
||||||
|
|
||||||
const $q = useQuasar()
|
|
||||||
|
|
||||||
// 기본 중심: 서울시청
|
// 기본 중심: 서울시청
|
||||||
const DEFAULT_CENTER = { lat: 37.5666, lng: 126.9784 }
|
const DEFAULT_CENTER = { lat: 37.5666, lng: 126.9784 }
|
||||||
|
|
||||||
const mapEl = ref<HTMLElement | null>(null)
|
const mapEl = ref<HTMLElement | null>(null)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
const locating = ref(false)
|
|
||||||
|
|
||||||
let map: kakao.maps.Map | null = null
|
let map: kakao.maps.Map | null = null
|
||||||
let infoWindow: kakao.maps.InfoWindow | null = null
|
let infoWindow: kakao.maps.InfoWindow | null = null
|
||||||
let myCircle: kakao.maps.Circle | null = null
|
|
||||||
const markers = new Map<number, kakao.maps.Marker>()
|
const markers = new Map<number, kakao.maps.Marker>()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -63,6 +44,12 @@ onMounted(async () => {
|
|||||||
// 확대/축소 +/- 컨트롤 (핀치 줌도 함께 동작)
|
// 확대/축소 +/- 컨트롤 (핀치 줌도 함께 동작)
|
||||||
map.addControl(new kk.maps.ZoomControl(), kk.maps.ControlPosition.RIGHT)
|
map.addControl(new kk.maps.ZoomControl(), kk.maps.ControlPosition.RIGHT)
|
||||||
infoWindow = new kk.maps.InfoWindow({ removable: false })
|
infoWindow = new kk.maps.InfoWindow({ removable: false })
|
||||||
|
// 지도 아무 곳이나 탭하면 가장 가까운 스팟 선택 (마커가 겹쳐도 선택 가능)
|
||||||
|
kk.maps.event.addListener(map, 'click', (e) => {
|
||||||
|
if (!e) return
|
||||||
|
const spot = nearestSpot(e.latLng.getLat(), e.latLng.getLng())
|
||||||
|
if (spot) emit('select', spot.id)
|
||||||
|
})
|
||||||
renderMarkers(kk)
|
renderMarkers(kk)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg.value = `${(e as Error).message} 지도 키(VITE_KAKAO_MAP_KEY)를 설정하세요.`
|
errorMsg.value = `${(e as Error).message} 지도 키(VITE_KAKAO_MAP_KEY)를 설정하세요.`
|
||||||
@@ -72,45 +59,8 @@ onMounted(async () => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
markers.forEach((m) => m.setMap(null))
|
markers.forEach((m) => m.setMap(null))
|
||||||
markers.clear()
|
markers.clear()
|
||||||
myCircle?.setMap(null)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 현재 위치로 지도 이동 + 파란 점 표시. (iOS 위치 권한 필요) */
|
|
||||||
async function locate() {
|
|
||||||
if (!map) return
|
|
||||||
locating.value = true
|
|
||||||
try {
|
|
||||||
const pos = await Geolocation.getCurrentPosition({ enableHighAccuracy: true, timeout: 10000 })
|
|
||||||
const kk = window.kakao
|
|
||||||
const here = new kk.maps.LatLng(pos.coords.latitude, pos.coords.longitude)
|
|
||||||
map.setLevel(4)
|
|
||||||
map.panTo(here)
|
|
||||||
if (!myCircle) {
|
|
||||||
myCircle = new kk.maps.Circle({
|
|
||||||
center: here,
|
|
||||||
radius: 30,
|
|
||||||
strokeWeight: 2,
|
|
||||||
strokeColor: '#3E92CC',
|
|
||||||
strokeOpacity: 0.9,
|
|
||||||
fillColor: '#3E92CC',
|
|
||||||
fillOpacity: 0.35,
|
|
||||||
})
|
|
||||||
myCircle.setMap(map)
|
|
||||||
} else {
|
|
||||||
myCircle.setPosition(here)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
$q.notify({
|
|
||||||
color: 'warning',
|
|
||||||
message: '현재 위치를 가져오지 못했어요. 위치 권한을 확인하세요.',
|
|
||||||
icon: 'location_off',
|
|
||||||
position: 'top',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
locating.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 스팟 목록이 갱신되면 마커 다시 그림
|
// 스팟 목록이 갱신되면 마커 다시 그림
|
||||||
watch(
|
watch(
|
||||||
() => props.spots,
|
() => props.spots,
|
||||||
@@ -138,6 +88,23 @@ function firstCenter() {
|
|||||||
return withCoord ? { lat: withCoord.latitude!, lng: withCoord.longitude! } : DEFAULT_CENTER
|
return withCoord ? { lat: withCoord.latitude!, lng: withCoord.longitude! } : DEFAULT_CENTER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 클릭 좌표에서 가장 가까운 스팟(위경도 제곱거리 최소). */
|
||||||
|
function nearestSpot(lat: number, lng: number): Spot | null {
|
||||||
|
let best: Spot | null = null
|
||||||
|
let bestDist = Infinity
|
||||||
|
for (const s of props.spots) {
|
||||||
|
if (s.latitude == null || s.longitude == null) continue
|
||||||
|
const dLat = s.latitude - lat
|
||||||
|
const dLng = s.longitude - lng
|
||||||
|
const dist = dLat * dLat + dLng * dLng
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist
|
||||||
|
best = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
function renderMarkers(kk: typeof kakao) {
|
function renderMarkers(kk: typeof kakao) {
|
||||||
markers.forEach((m) => m.setMap(null))
|
markers.forEach((m) => m.setMap(null))
|
||||||
markers.clear()
|
markers.clear()
|
||||||
@@ -177,13 +144,6 @@ function openInfo(spot: Spot, marker: kakao.maps.Marker) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.locate-btn {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
bottom: 10px;
|
|
||||||
z-index: 2;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
.map-fallback {
|
.map-fallback {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
Vendored
+10
-1
@@ -58,8 +58,17 @@ declare namespace kakao.maps {
|
|||||||
close(): void
|
close(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 지도 클릭 등 좌표를 포함하는 이벤트
|
||||||
|
interface MouseEvent {
|
||||||
|
latLng: LatLng
|
||||||
|
}
|
||||||
|
|
||||||
namespace event {
|
namespace event {
|
||||||
function addListener(target: unknown, type: string, handler: () => void): void
|
function addListener(
|
||||||
|
target: unknown,
|
||||||
|
type: string,
|
||||||
|
handler: (event?: MouseEvent) => void,
|
||||||
|
): void
|
||||||
}
|
}
|
||||||
|
|
||||||
// autoload=false 로 로드한 뒤 SDK 초기화 콜백.
|
// autoload=false 로 로드한 뒤 SDK 초기화 콜백.
|
||||||
|
|||||||
Reference in New Issue
Block a user