홈 지도를 Kakao Maps JS SDK 로 교체. ZoomControl(확대/축소)과 현재위치 버튼(@capacitor/geolocation → 파란 점 표시) 추가. naverMap.ts / NaverMap.vue 는 미사용으로 보존. 시뮬레이터에서 렌더·현재위치 확인. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="map-wrap">
|
||||
<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 미로드/키 미설정 시 폴백 -->
|
||||
<div v-if="errorMsg" class="map-fallback flex flex-center column text-grey-6">
|
||||
<q-icon name="map" size="40px" color="secondary" />
|
||||
<div class="q-mt-sm text-caption text-center">{{ errorMsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { Geolocation } from '@capacitor/geolocation'
|
||||
import { loadKakaoMaps } from '@/lib/kakaoMap'
|
||||
import type { Spot } from '@/api/spots'
|
||||
|
||||
const props = defineProps<{
|
||||
spots: Spot[]
|
||||
selectedId: number | null
|
||||
}>()
|
||||
const emit = defineEmits<{ (e: 'select', id: number): void }>()
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
// 기본 중심: 서울시청
|
||||
const DEFAULT_CENTER = { lat: 37.5666, lng: 126.9784 }
|
||||
|
||||
const mapEl = ref<HTMLElement | null>(null)
|
||||
const errorMsg = ref('')
|
||||
const locating = ref(false)
|
||||
|
||||
let map: kakao.maps.Map | null = null
|
||||
let infoWindow: kakao.maps.InfoWindow | null = null
|
||||
let myCircle: kakao.maps.Circle | null = null
|
||||
const markers = new Map<number, kakao.maps.Marker>()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const kk = await loadKakaoMaps()
|
||||
if (!mapEl.value) return
|
||||
|
||||
const center = firstCenter()
|
||||
map = new kk.maps.Map(mapEl.value, {
|
||||
center: new kk.maps.LatLng(center.lat, center.lng),
|
||||
level: 5, // 네이버 zoom 14 ≈ 카카오 level 5
|
||||
})
|
||||
// 확대/축소 +/- 컨트롤 (핀치 줌도 함께 동작)
|
||||
map.addControl(new kk.maps.ZoomControl(), kk.maps.ControlPosition.RIGHT)
|
||||
infoWindow = new kk.maps.InfoWindow({ removable: false })
|
||||
renderMarkers(kk)
|
||||
} catch (e) {
|
||||
errorMsg.value = `${(e as Error).message} 지도 키(VITE_KAKAO_MAP_KEY)를 설정하세요.`
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
markers.forEach((m) => m.setMap(null))
|
||||
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(
|
||||
() => props.spots,
|
||||
() => {
|
||||
if (map && window.kakao?.maps) renderMarkers(window.kakao)
|
||||
},
|
||||
)
|
||||
|
||||
// 외부에서 선택된 스팟이 바뀌면 지도 중심 이동 + 정보창
|
||||
watch(
|
||||
() => props.selectedId,
|
||||
(id) => {
|
||||
if (!map || id == null) return
|
||||
const spot = props.spots.find((s) => s.id === id)
|
||||
const marker = markers.get(id)
|
||||
if (spot?.latitude != null && spot?.longitude != null) {
|
||||
map.panTo(new window.kakao.maps.LatLng(spot.latitude, spot.longitude))
|
||||
}
|
||||
if (spot && marker) openInfo(spot, marker)
|
||||
},
|
||||
)
|
||||
|
||||
function firstCenter() {
|
||||
const withCoord = props.spots.find((s) => s.latitude != null && s.longitude != null)
|
||||
return withCoord ? { lat: withCoord.latitude!, lng: withCoord.longitude! } : DEFAULT_CENTER
|
||||
}
|
||||
|
||||
function renderMarkers(kk: typeof kakao) {
|
||||
markers.forEach((m) => m.setMap(null))
|
||||
markers.clear()
|
||||
|
||||
for (const spot of props.spots) {
|
||||
if (spot.latitude == null || spot.longitude == null) continue
|
||||
const marker = new kk.maps.Marker({
|
||||
position: new kk.maps.LatLng(spot.latitude, spot.longitude),
|
||||
map: map!,
|
||||
title: spot.name,
|
||||
})
|
||||
kk.maps.event.addListener(marker, 'click', () => {
|
||||
openInfo(spot, marker)
|
||||
emit('select', spot.id)
|
||||
})
|
||||
markers.set(spot.id, marker)
|
||||
}
|
||||
}
|
||||
|
||||
function openInfo(spot: Spot, marker: kakao.maps.Marker) {
|
||||
if (!infoWindow || !map) return
|
||||
infoWindow.setContent(
|
||||
`<div style="padding:6px 12px;font-size:13px;font-weight:600;color:#3E92CC;white-space:nowrap;">📍 ${spot.name}</div>`,
|
||||
)
|
||||
infoWindow.open(map, marker)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-wrap {
|
||||
position: relative;
|
||||
height: 240px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.map-canvas {
|
||||
width: 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 {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(160deg, #eaf5fe 0%, #d7ecfb 100%);
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user