Skip to content

Commit 0184e2a

Browse files
authored
refactor: 지도 기능 복구 및 alert를 모달로 대체 (#85)
* feat: 계획에 추가된 장소 마커 사이즈를 더 크게 조정 및 폴리라인 추가 * feat: 폴리라인이 마커보다 상단에 오게 수정 * feat: 여행 계획 페이지 alert 대신 모달 사용
1 parent 6316295 commit 0184e2a

5 files changed

Lines changed: 273 additions & 99 deletions

File tree

src/components/common/KakaoMap.vue

Lines changed: 118 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ const renderMarkers = (markers: MarkerOption[]) => {
8282
const kakao = (window as any).kakao
8383
clearOverlays()
8484
85+
86+
8587
if (!markers || markers.length === 0) return
8688
8789
const bounds = new kakao.maps.LatLngBounds()
@@ -99,26 +101,23 @@ const renderMarkers = (markers: MarkerOption[]) => {
99101
const pos = new kakao.maps.LatLng(m.lat, m.lng)
100102
const isSelected = props.selectedMarkerId && String(m.id) === String(props.selectedMarkerId)
101103
102-
if (m.type === 'plan') {
103-
const content = document.createElement('div')
104-
const bgColor = m.color ? '' : 'bg-[#9BCCC4]'
105-
content.className = `
106-
flex items-center justify-center w-8 h-8 rounded-full font-black text-black border-[2px] border-[#2C2C2C] text-xs
107-
transition-all duration-200 ease-in-out
108-
${isSelected ? 'bg-[#FF8A00] scale-125' : bgColor}
109-
`
110-
if (m.color && !isSelected) {
111-
content.style.backgroundColor = m.color;
112-
}
113-
content.innerHTML = String(m.order || '')
114-
115-
const customOverlay = new kakao.maps.CustomOverlay({
116-
position: pos,
117-
content: content,
118-
map: rawMap,
119-
yAnchor: 0.8, // Adjust yAnchor for smaller size
120-
zIndex: isSelected ? 999 : 50,
121-
})
104+
if (m.type === 'plan') {
105+
const content = document.createElement('div')
106+
content.className = `
107+
flex items-center justify-center w-12 h-12 rounded-full font-black text-black border-[2px] border-[#2C2C2C]
108+
transition-all duration-200 ease-in-out
109+
${isSelected ? 'bg-[#FF8A00] scale-110' : 'bg-[#9BCCC4]'}
110+
`
111+
content.innerHTML = String(m.order || '')
112+
113+
const customOverlay = new kakao.maps.CustomOverlay({
114+
position: pos,
115+
content: content,
116+
map: rawMap,
117+
yAnchor: 0.5,
118+
zIndex: isSelected ? 999 : 100, // Increased zIndex for plan markers
119+
})
120+
122121
content.onclick = () => {
123122
if (m.id !== undefined && m.id !== null) {
124123
emits('marker-click', m.id)
@@ -128,14 +127,13 @@ const renderMarkers = (markers: MarkerOption[]) => {
128127
customOverlay.customId = m.id
129128
customOverlay.customType = m.type
130129
customOverlay.customOrder = m.order
131-
customOverlay.customColor = m.color
132130
kakaoOverlays.push(customOverlay)
133131
} else {
134132
const marker = new kakao.maps.Marker({
135133
position: pos,
136134
map: rawMap,
137135
image: isSelected ? selectedImage : normalImage,
138-
zIndex: isSelected ? 999 : 1,
136+
zIndex: isSelected ? 999 : 1, // Search markers stay low
139137
clickable: true,
140138
})
141139
@@ -152,11 +150,99 @@ const renderMarkers = (markers: MarkerOption[]) => {
152150
bounds.extend(pos)
153151
})
154152
153+
// Draw polyline for plan markers
154+
// Draw polyline for plan markers
155+
renderPlanPolylineAsOverlay(markers)
156+
155157
if (!props.selectedMarkerId) {
156158
rawMap.setBounds(bounds)
157159
}
158160
}
159161
162+
let kakaoPlanPolylineOverlay: any = null
163+
164+
const renderPlanPolylineAsOverlay = (markers: MarkerOption[]) => {
165+
const rawMap = toRaw(map.value)
166+
if (!rawMap) return
167+
const kakao = (window as any).kakao
168+
169+
// Clear existing overlay
170+
if (kakaoPlanPolylineOverlay) {
171+
kakaoPlanPolylineOverlay.setMap(null)
172+
kakaoPlanPolylineOverlay = null
173+
}
174+
175+
const linePath = markers
176+
.filter((m) => m.type === 'plan')
177+
.sort((a, b) => (a.order || 0) - (b.order || 0))
178+
.map((m) => ({ lat: m.lat, lng: m.lng }))
179+
180+
if (linePath.length < 2) return
181+
182+
// SVG로 라인을 그릴 컨테이너 생성
183+
const svgNS = "http://www.w3.org/2000/svg"
184+
const svg = document.createElementNS(svgNS, "svg")
185+
186+
// Use a large canvas approach to ensure visibility and avoid clipping issues
187+
// Centered on the anchor point.
188+
const CANVAS_SIZE = 4000
189+
const OFFSET = CANVAS_SIZE / 2
190+
191+
svg.style.position = 'absolute'
192+
svg.style.pointerEvents = 'none'
193+
svg.style.width = `${CANVAS_SIZE}px`
194+
svg.style.height = `${CANVAS_SIZE}px`
195+
svg.style.overflow = 'visible'
196+
197+
// Center the SVG on the anchor point (which will be at top-left 0,0 locally)
198+
// so we shift it back by OFFSET
199+
svg.style.transform = `translate(-${OFFSET}px, -${OFFSET}px)`
200+
201+
// 지도 투영 변환을 사용하여 좌표 변환
202+
const projection = rawMap.getProjection()
203+
204+
const startPos = projection.pointFromCoords(new kakao.maps.LatLng(linePath[0].lat, linePath[0].lng))
205+
206+
linePath.forEach((point, i) => {
207+
if (i === 0) return
208+
209+
const prevPoint = linePath[i - 1]
210+
const fromPos = projection.pointFromCoords(new kakao.maps.LatLng(prevPoint.lat, prevPoint.lng))
211+
const toPos = projection.pointFromCoords(new kakao.maps.LatLng(point.lat, point.lng))
212+
213+
const line = document.createElementNS(svgNS, "line")
214+
215+
// Coordinates relative to anchor, plus OFFSET to center in SVG
216+
const x1 = (fromPos.x - startPos.x) + OFFSET
217+
const y1 = (fromPos.y - startPos.y) + OFFSET
218+
const x2 = (toPos.x - startPos.x) + OFFSET
219+
const y2 = (toPos.y - startPos.y) + OFFSET
220+
221+
line.setAttribute("x1", String(x1))
222+
line.setAttribute("y1", String(y1))
223+
line.setAttribute("x2", String(x2))
224+
line.setAttribute("y2", String(y2))
225+
line.setAttribute("stroke", "#000000")
226+
line.setAttribute("stroke-width", "3")
227+
line.setAttribute("stroke-opacity", "0.9")
228+
line.setAttribute("stroke-linecap", "round") // Smooth joints
229+
230+
svg.appendChild(line)
231+
})
232+
233+
// CustomOverlay로 추가
234+
kakaoPlanPolylineOverlay = new kakao.maps.CustomOverlay({
235+
position: new kakao.maps.LatLng(linePath[0].lat, linePath[0].lng),
236+
content: svg,
237+
map: rawMap,
238+
zIndex: 50, // Between Search(10) and Plan(100)
239+
xAnchor: 0,
240+
yAnchor: 0
241+
})
242+
243+
// kakaoOverlays.push(kakaoPlanPolylineOverlay) - Do not push to avoid array growth on zoom re-renders
244+
}
245+
160246
const renderPolylines = (polylines: PolylineOption[]) => {
161247
const rawMap = toRaw(map.value)
162248
if (!rawMap) return
@@ -192,10 +278,8 @@ const panTo = (lat: number, lng: number) => {
192278
watch(
193279
() => props.selectedMarkerId,
194280
(newId) => {
195-
if (!mapLoaded.value) return // Add guard clause
196-
197281
const kakao = (window as any).kakao
198-
if (!kakao || !kakao.maps) return
282+
if (!kakao) return
199283
200284
const normalImage = new kakao.maps.MarkerImage(
201285
DEFAULT_MARKER_SRC,
@@ -213,17 +297,12 @@ watch(
213297
const content = overlay.getContent()
214298
if (isTarget) {
215299
content.classList.remove('bg-[#9BCCC4]')
216-
content.style.backgroundColor = '';
217300
content.classList.add('bg-[#FF8A00]', 'scale-110')
218301
} else {
219302
content.classList.remove('bg-[#FF8A00]', 'scale-110')
220-
if (overlay.customColor) {
221-
content.style.backgroundColor = overlay.customColor;
222-
} else {
223-
content.classList.add('bg-[#9BCCC4]')
224-
}
303+
content.classList.add('bg-[#9BCCC4]')
225304
}
226-
overlay.setZIndex(isTarget ? 999 : 50)
305+
overlay.setZIndex(isTarget ? 999 : 100)
227306
} else {
228307
// This is a kakao.maps.Marker
229308
overlay.setImage(isTarget ? selectedImage : normalImage)
@@ -251,6 +330,10 @@ const initMap = () => {
251330
})
252331
kakao.maps.event.addListener(mapInstance, 'zoom_changed', () => {
253332
emits('map-move')
333+
// Re-render polyline overlay to adjust line paths for new zoom level
334+
if (props.markers && props.markers.length > 0) {
335+
renderPlanPolylineAsOverlay(props.markers)
336+
}
254337
})
255338
256339
map.value = markRaw(mapInstance)
@@ -304,6 +387,9 @@ watch(() => props.level, (newLevel) => {
304387
onUnmounted(() => {
305388
clearOverlays()
306389
clearPolylines()
390+
if (kakaoPlanPolylineOverlay) {
391+
kakaoPlanPolylineOverlay.setMap(null)
392+
}
307393
map.value = null
308394
})
309395

src/components/trip/TripSearchListPanel.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
:key="place.id"
2929
:ref="
3030
(el) => {
31-
if (el && place.id) itemRefs[place.id] = el as HTMLElement
31+
if (el && place.kakaoPlaceId) itemRefs[place.kakaoPlaceId] = el as HTMLElement
3232
}
3333
"
3434
class="mb-3 last:mb-0"
@@ -37,7 +37,7 @@
3737
@click="$emit('click-item', place)"
3838
class="p-3 border-[2px] border-[#2C2C2C] rounded-xl hover:shadow-[4px_4px_0px_0px_rgba(44,44,44,0.1)] transition-all bg-white group cursor-pointer"
3939
:class="[
40-
String(place.id) === String(selectedId)
40+
String(place.kakaoPlaceId) === String(selectedId)
4141
? 'border-[#9BCCC4] bg-[#F0FAF9] shadow-[4px_4px_0px_0px_rgba(155,204,196,0.6)]'
4242
: 'border-[#2C2C2C] bg-white hover:border-[#9BCCC4] hover:shadow-[4px_4px_0px_0px_rgba(44,44,44,0.1)]',
4343
]"

src/composables/trip/useMapInteraction.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,27 @@ export function useMapInteraction({
6969
lng?: number,
7070
options?: { panWithOffset?: boolean }
7171
) => {
72-
selectedMarkerId.value = id
73-
74-
// 좌표가 인자로 안 들어왔으면(마커 클릭 시), 리스트에서 찾음
72+
// 1. 타겟 마커 찾기 (ID 또는 KakaoID 매칭)
7573
let targetLat = lat
7674
let targetLng = lng
75+
let resolvedId = id
76+
77+
const target = markerPositions.value.find(
78+
(m) => String(m.id) === String(id) || String(m.kakaoPlaceId) === String(id)
79+
)
7780

78-
if (targetLat === undefined || targetLng === undefined) {
79-
// id가 tripItemId일 수도, kakaoPlaceId일 수도 있으므로 둘 다 비교
80-
const target = markerPositions.value.find(
81-
(m) => String(m.id) === String(id) || String(m.kakaoPlaceId) === String(id)
82-
)
83-
if (target) {
81+
if (target) {
82+
// 마커가 존재하면, 마커의 ID(markerPositions에서 설정한 ID)를 선택된 ID로 설정해야 함
83+
// (검색 결과의 경우 place.id(DB ID)가 들어와도 마커는 kakaoPlaceId를 쓰고 있을 수 있음)
84+
resolvedId = target.id
85+
if (targetLat === undefined || targetLng === undefined) {
8486
targetLat = target.lat
8587
targetLng = target.lng
8688
}
8789
}
8890

91+
selectedMarkerId.value = resolvedId
92+
8993
if (targetLat !== undefined && targetLng !== undefined && kakaoMapRef.value?.panTo) {
9094
let finalLat = targetLat
9195

@@ -117,8 +121,13 @@ export function useMapInteraction({
117121

118122
// 2. 리스트에서 카드 클릭 핸들러
119123
const handlePlaceClick = (place: Place, options?: { panWithOffset?: boolean }) => {
120-
// place.id (tripItemId)가 있으면 그걸 사용하고, 없으면 kakaoPlaceId 사용
121-
selectAndPanToPlace(place.id || place.kakaoPlaceId, place.lat, place.lng, options)
124+
// kakaoPlaceId를 우선 사용 (검색 결과 마커는 kakaoPlaceId를 ID로 가짐)
125+
// Plan에 있는 마커라도 markerPositions 찾을 때 kakaoPlaceId로도 찾으므로 안전함
126+
// ID가 반드시 존재한다고 가정 (Place 모델 상)
127+
const targetId = place.kakaoPlaceId || place.id
128+
if (targetId) {
129+
selectAndPanToPlace(targetId, place.lat, place.lng, options)
130+
}
122131
}
123132

124133
// 3. 지도 마커 클릭 핸들러

0 commit comments

Comments
 (0)