From 60f3f6fc2e05f111e9f18d4375af83c36cc90ad4 Mon Sep 17 00:00:00 2001 From: parkdu7 Date: Sat, 30 Aug 2025 14:04:12 +0900 Subject: [PATCH] =?UTF-8?q?fix=20:=20POI=20=EC=9C=84=EC=B9=98=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95,=20=EC=B6=A9=EB=8F=8C=EC=8B=9C=20=EB=A7=88=EC=BB=A4?= =?UTF-8?q?=EA=B0=80=20=EC=9B=80=EC=A7=81=EC=9D=B4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/map/ClusterManagerInitializer.kt | 9 +- .../presentation/ui/map/MapClusterManager.kt | 259 +++++++++++++++++- .../presentation/ui/map/POIMarkerManager.kt | 199 ++------------ .../presentation/ui/screens/FullMapScreen.kt | 24 +- 4 files changed, 291 insertions(+), 200 deletions(-) diff --git a/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/ClusterManagerInitializer.kt b/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/ClusterManagerInitializer.kt index ef2f98c..6e3d1ac 100644 --- a/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/ClusterManagerInitializer.kt +++ b/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/ClusterManagerInitializer.kt @@ -17,7 +17,8 @@ class ClusterManagerInitializer( fun createClusterManager( naverMap: NaverMap, mapContainer: ViewGroup? = null, - onHighlightedContentChanged: (MapContent?) -> Unit + onHighlightedContentChanged: (MapContent?) -> Unit, + poiMarkerManager: POIMarkerManager? = null ): MapClusterManager { return MapClusterManager(context, naverMap, mapContainer).also { manager -> manager.setupClustering() @@ -85,6 +86,12 @@ class ClusterManagerInitializer( Log.d("ClusterManagerInitializer", "๐ŸŽฏ ํ†ตํ•ฉ ํด๋Ÿฌ์Šคํ„ฐ ํด๋ฆญ: ${mixedClusterItems.size}๊ฐœ ์•„์ดํ…œ") mapViewModel.selectMixedCluster(mixedClusterItems) } + + // POI ๋งค๋‹ˆ์ €์™€ ์—ฐ๊ฒฐ (์ถฉ๋Œ ๊ฐ์ง€์šฉ) + if (poiMarkerManager != null) { + manager.poiMarkerManager = poiMarkerManager + Log.d("ClusterManagerInitializer", "๐Ÿช POI ๋งค๋‹ˆ์ €์™€ ๋งˆ์ปค ๋งค๋‹ˆ์ € ์—ฐ๊ฒฐ ์™„๋ฃŒ") + } } } } \ No newline at end of file diff --git a/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/MapClusterManager.kt b/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/MapClusterManager.kt index 4a893aa..2099ea9 100644 --- a/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/MapClusterManager.kt +++ b/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/MapClusterManager.kt @@ -57,6 +57,12 @@ class MapClusterManager( // POI ์ถฉ๋Œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋งˆ์ปค ์œ„์น˜ ์—…๋ฐ์ดํŠธ ์ฝœ๋ฐฑ var onMarkerPositionsUpdated: ((List, Double) -> Unit)? = null + + // POI ๋งค๋‹ˆ์ € ์ฐธ์กฐ (POI ์œ„์น˜ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•จ) + var poiMarkerManager: POIMarkerManager? = null + + // ์นด๋ฉ”๋ผ ๋ณ€๊ฒฝ ์‹œ ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ ์ฝœ๋ฐฑ + var onCameraChangeForMarkerRepositioning: (() -> Unit)? = null // ์„ ํƒ๋œ ๋งˆ์ปค ์ƒํƒœ ๊ด€๋ฆฌ var selectedMarker: Marker? = null @@ -93,6 +99,10 @@ class MapClusterManager( private val MARKER_SIZE get() = MarkerConfig.BASE_MARKER_SIZE private val SELECTED_MARKER_SCALE get() = MarkerConfig.SELECTED_SCALE private val HIGHLIGHTED_MARKER_SCALE get() = MarkerConfig.HIGHLIGHTED_SCALE + + // ๋งˆ์ปค ์ถฉ๋Œ ๊ฐ์ง€ ์„ค์ • + private const val COLLISION_DETECTION_MIN_ZOOM = 19.0 // ์คŒ 19 ์ด์ƒ์—์„œ๋งŒ ์ถฉ๋Œ ๊ฐ์ง€ + private const val MARKER_COLLISION_RADIUS_PX = 60 // ๋งˆ์ปค ์ถฉ๋Œ ๋ฐ˜๊ฒฝ (ํ”ฝ์…€) } // ์•„์ด์ฝ˜ ์บ์‹ฑ ์‹œ์Šคํ…œ @@ -438,6 +448,12 @@ class MapClusterManager( // POI ๋งค๋‹ˆ์ €์— ํ˜„์žฌ ๋งˆ์ปค ์œ„์น˜๋“ค ์ „๋‹ฌ (์ถฉ๋Œ ๋ฐฉ์ง€์šฉ) notifyMarkerPositions() + + // ์คŒ ๋ ˆ๋ฒจ์ด ๋†’๊ณ  POI๊ฐ€ ์žˆ์œผ๋ฉด ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ (POI ๋ณ€๊ฒฝ ์‹œ ๋Œ€์‘) + if (currentZoom >= COLLISION_DETECTION_MIN_ZOOM && poiMarkerManager != null) { + Log.d("MapClusterManager", "๐Ÿ”„ POI ์—…๋ฐ์ดํŠธ ๊ฐ์ง€ - ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ ๊ฒ€ํ† ") + redistributeMarkersAvoidingPOI() + } // ํด๋Ÿฌ์Šคํ„ฐ๋ง ์™„๋ฃŒ ์ฝœ๋ฐฑ ํ˜ธ์ถœ onComplete?.invoke() @@ -530,11 +546,22 @@ class MapClusterManager( // ๋‹จ์ผ ๋งˆ์ปค val content = cluster[0] Log.d("MapClusterManager", "๐Ÿ“ [MARKER] ๋‹จ์ผ ๋งˆ์ปค ์ƒ์„ฑ: ${content.title} (ID: ${content.contentId})") - Log.d("MapClusterManager", "๐Ÿ“ [MARKER] ์œ„์น˜: (${content.location.latitude}, ${content.location.longitude})") + Log.d("MapClusterManager", "๐Ÿ“ [MARKER] ์›๋ž˜ ์œ„์น˜: (${content.location.latitude}, ${content.location.longitude})") Log.d("MapClusterManager", "๐Ÿ”— [MARKER] ๋งˆ์ปค ์ƒ์„ฑ ์‹œ์  onMarkerClick: ${onMarkerClick}") + // POI์™€์˜ ์ถฉ๋Œ์„ ํ”ผํ•œ ์ตœ์  ์œ„์น˜ ๊ณ„์‚ฐ + val originalPosition = LatLng(content.location.latitude, content.location.longitude) + val optimalPosition = calculateOptimalMarkerPosition(originalPosition, content) + + if (optimalPosition != originalPosition) { + Log.e("MapClusterManager", "๐Ÿ•ท๏ธ [MARKER] Spider ์ ์šฉ๋จ: ${content.title}") + Log.e("MapClusterManager", "๐Ÿ•ท๏ธ [MARKER] ์ตœ์ข… ์œ„์น˜: (${optimalPosition.latitude}, ${optimalPosition.longitude})") + } else { + Log.d("MapClusterManager", "๐Ÿ“ [MARKER] ์ถฉ๋Œ ์—†์Œ - ์›์œ„์น˜ ์‚ฌ์šฉ: ${content.title}") + } + val marker = Marker().apply { - position = LatLng(content.location.latitude, content.location.longitude) + position = optimalPosition icon = getNormalMarkerIcon(content.postType) map = naverMap tag = content // MapContent ์ €์žฅ @@ -551,7 +578,7 @@ class MapClusterManager( onMarkerClick?.invoke(content) } else { Log.d("MapClusterManager", "๐ŸŽฏ [CLICK] ์ƒˆ ๋งˆ์ปค ์„ ํƒ - selectMarker ํ˜ธ์ถœ") - // ์ƒˆ๋กœ์šด ๋งˆ์ปค ์„ ํƒ ๋ฐ ์นด๋ฉ”๋ผ ์ด๋™ (์คŒ๋ ˆ๋ฒจ ์œ ์ง€) + // ์ƒˆ๋กœ์šด ๋งˆ์ปค ์„ ํƒ ๋ฐ ์นด๋ฉ”๋ผ ์ด๋™ (์›๋ž˜ ์œ„์น˜๋กœ ์ด๋™) selectMarker(content) // ๋งˆ์ปค ํด๋ฆญ ์ด๋™ ํ”Œ๋ž˜๊ทธ ์„ค์ • @@ -876,7 +903,7 @@ class MapClusterManager( when (item) { is MapContentItem -> { - val marker = createContentMarker(item.content) + val marker = createContentMarkerWithCollisionDetection(item.content) markers.add(marker) Log.d("MapClusterManager", "โœ… [MIXED] Content ๋งˆ์ปค ์ถ”๊ฐ€ ์™„๋ฃŒ") } @@ -966,7 +993,52 @@ class MapClusterManager( } /** - * Content ๋งˆ์ปค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ + * Content ๋งˆ์ปค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ (์ถฉ๋Œ ๊ฐ์ง€ ์ ์šฉ) + */ + private fun createContentMarkerWithCollisionDetection(content: MapContent): Marker { + // POI์™€์˜ ์ถฉ๋Œ์„ ํ”ผํ•œ ์ตœ์  ์œ„์น˜ ๊ณ„์‚ฐ + val originalPosition = LatLng(content.location.latitude, content.location.longitude) + val optimalPosition = calculateOptimalMarkerPosition(originalPosition, content) + + if (optimalPosition != originalPosition) { + Log.e("MapClusterManager", "๐Ÿ•ท๏ธ [MIXED CONTENT] Spider ์ ์šฉ๋จ: ${content.title}") + } + + return Marker().apply { + position = optimalPosition + icon = getNormalMarkerIcon(content.postType) + map = naverMap + tag = content + + setOnClickListener { + Log.e("MapClusterManager", "๐ŸŽฏ๐ŸŽฏ๐ŸŽฏ [MIXED CONTENT] ๊ฐœ๋ณ„ Content ๋งˆ์ปค ํด๋ฆญ!!!") + + if (selectedContent?.contentId == content.contentId) { + clearSelection() + onMarkerClick?.invoke(content) + } else { + selectMarker(content) + + isClusterMoving = true + + naverMap.moveCamera( + CameraUpdate.scrollTo(LatLng(content.location.latitude, content.location.longitude)) + .animate(CameraAnimation.Easing) + ) + + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + isClusterMoving = false + }, 1000) + + onMarkerClick?.invoke(content) + } + true + } + } + } + + /** + * Content ๋งˆ์ปค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ (๊ธฐ์กด ๋ฐฉ์‹ - ํ˜ธํ™˜์„ฑ ์œ ์ง€) */ private fun createContentMarker(content: MapContent): Marker { return Marker().apply { @@ -1705,4 +1777,181 @@ class MapClusterManager( // POI ๋งค๋‹ˆ์ €์— ์ฝœ๋ฐฑ์œผ๋กœ ์ „๋‹ฌ onMarkerPositionsUpdated?.invoke(allPositions, currentZoom) } + + // ===== ๐Ÿ†• ๋งˆ์ปค ์ถฉ๋Œ ๊ฐ์ง€ ๋ฐ Spider ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‹œ์Šคํ…œ ===== + + /** + * POI์™€์˜ ์ถฉ๋Œ์„ ํ”ผํ•ด ๋งˆ์ปค ์ตœ์  ์œ„์น˜ ๊ณ„์‚ฐ (ํ™”๋ฉด ์ขŒํ‘œ ๊ธฐ๋ฐ˜) + */ + private fun calculateOptimalMarkerPosition(originalPosition: LatLng, content: MapContent): LatLng { + val currentZoom = naverMap.cameraPosition.zoom + + // ์คŒ ๋ ˆ๋ฒจ์ด ์ถฉ๋Œ ๊ฐ์ง€ ์ž„๊ณ„๊ฐ’๋ณด๋‹ค ๋‚ฎ์œผ๋ฉด ์›์œ„์น˜ ์‚ฌ์šฉ + if (currentZoom < COLLISION_DETECTION_MIN_ZOOM) { + return originalPosition + } + + // POI ์œ„์น˜๋“ค ๊ฐ€์ ธ์˜ค๊ธฐ + val poiPositions = poiMarkerManager?.getPOIPositions() ?: emptyList() + + if (poiPositions.isEmpty()) { + return originalPosition + } + + Log.d("MapClusterManager", "๐ŸŽฏ ๋งˆ์ปค ์ถฉ๋Œ ๊ฐ์ง€: ${content.title} - POI ${poiPositions.size}๊ฐœ์™€ ๋น„๊ต") + + // 1. ๋งˆ์ปค ์›๋ž˜ ์œ„์น˜๋ฅผ ํ™”๋ฉด ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ + val markerScreenPoint = naverMap.projection.toScreenLocation(originalPosition) + + // 2. POI๋“ค์˜ ํ™”๋ฉด ์ขŒํ‘œ ๊ณ„์‚ฐ + val poiScreenPoints = poiPositions.map { position -> + naverMap.projection.toScreenLocation(position) + } + + // 3. ํ™”๋ฉด์ƒ ์ถฉ๋Œ ๊ฐ์ง€ + var hasCollision = false + poiScreenPoints.forEachIndexed { index, screenPoint -> + val pixelDistance = sqrt( + (markerScreenPoint.x - screenPoint.x).toDouble().pow(2) + + (markerScreenPoint.y - screenPoint.y).toDouble().pow(2) + ) + + if (pixelDistance <= MARKER_COLLISION_RADIUS_PX) { + hasCollision = true + Log.e("MapClusterManager", "๐Ÿšจ ๋งˆ์ปค-POI ์ถฉ๋Œ ๊ฐ์ง€! POI[$index]: ${pixelDistance.toInt()}px โ‰ค ${MARKER_COLLISION_RADIUS_PX}px") + } + } + + if (!hasCollision) { + Log.d("MapClusterManager", "โœ… ๋งˆ์ปค ์ถฉ๋Œ ์—†์Œ - ์›์œ„์น˜ ์‚ฌ์šฉ: ${content.title}") + return originalPosition + } + + // 4. ์ถฉ๋Œ ์‹œ Spider ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ 8๋ฐฉํ–ฅ ์˜คํ”„์…‹ ์ ์šฉ + Log.e("MapClusterManager", "๐Ÿ•ท๏ธ Spider ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‹œ์ž‘: ${content.title}") + + val offsetDistance = MARKER_COLLISION_RADIUS_PX + 20 // ์—ฌ์œ  ๊ณต๊ฐ„ + val angles = listOf(0, 45, 90, 135, 180, 225, 270, 315) // 8๋ฐฉํ–ฅ + + for (angle in angles) { + val radians = Math.toRadians(angle.toDouble()) + val offsetX = cos(radians) * offsetDistance + val offsetY = sin(radians) * offsetDistance + + val offsetScreenPoint = android.graphics.PointF( + (markerScreenPoint.x + offsetX).toFloat(), + (markerScreenPoint.y + offsetY).toFloat() + ) + + // ์˜คํ”„์…‹๋œ ํ™”๋ฉด ์ขŒํ‘œ๋ฅผ ์ง€๋ฆฌ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ + val offsetPosition = naverMap.projection.fromScreenLocation(offsetScreenPoint) + + // ์˜คํ”„์…‹ ์œ„์น˜์—์„œ๋„ ์ถฉ๋Œ ์ฒดํฌ + var offsetHasCollision = false + poiScreenPoints.forEach { screenPoint -> + val pixelDistance = sqrt( + (offsetScreenPoint.x - screenPoint.x).toDouble().pow(2) + + (offsetScreenPoint.y - screenPoint.y).toDouble().pow(2) + ) + if (pixelDistance <= MARKER_COLLISION_RADIUS_PX) { + offsetHasCollision = true + } + } + + if (!offsetHasCollision) { + Log.e("MapClusterManager", "โœ… Spider: ์ตœ์  ์œ„์น˜ ์ฐพ์Œ ${angle}๋„ - ${content.title}") + Log.e("MapClusterManager", "โœ… ์›๋ž˜: (${originalPosition.latitude}, ${originalPosition.longitude})") + Log.e("MapClusterManager", "โœ… ์ตœ์ข…: (${offsetPosition.latitude}, ${offsetPosition.longitude})") + return offsetPosition + } + } + + // ๋ชจ๋“  ๋ฐฉํ–ฅ์—์„œ ์ถฉ๋Œํ•˜๋ฉด ์›์œ„์น˜ ๊ฐ•์ œ ์‚ฌ์šฉ + Log.e("MapClusterManager", "โŒ Spider: ๋ชจ๋“  ๋ฐฉํ–ฅ ์ถฉ๋Œ - ์›์œ„์น˜ ๊ฐ•์ œ ์‚ฌ์šฉ: ${content.title}") + return originalPosition + } + + /** + * ๋งˆ์ปค๋“ค์„ POI ์ถฉ๋Œ์„ ํ”ผํ•ด ์žฌ๋ฐฐ์น˜ + */ + fun redistributeMarkersAvoidingPOI() { + val currentZoom = naverMap.cameraPosition.zoom + + if (currentZoom < COLLISION_DETECTION_MIN_ZOOM) { + Log.d("MapClusterManager", "์คŒ ๋ ˆ๋ฒจ ๋‚ฎ์Œ ($currentZoom) - ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ ์Šคํ‚ต") + return + } + + Log.e("MapClusterManager", "๐Ÿ”„ === ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ ์‹œ์ž‘ ===") + Log.e("MapClusterManager", "๐Ÿ”„ ๋Œ€์ƒ ๋งˆ์ปค: ${markers.size}๊ฐœ") + + var repositionedCount = 0 + + markers.forEach { marker -> + val content = marker.tag as? MapContent ?: return@forEach + val originalPosition = LatLng(content.location.latitude, content.location.longitude) + val newPosition = calculateOptimalMarkerPosition(originalPosition, content) + + if (newPosition != originalPosition) { + marker.position = newPosition + repositionedCount++ + Log.e("MapClusterManager", "๐Ÿ”„ ๋งˆ์ปค ์žฌ๋ฐฐ์น˜๋จ: ${content.title}") + } + } + + Log.e("MapClusterManager", "๐Ÿ”„ ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ ์™„๋ฃŒ: ${repositionedCount}/${markers.size}๊ฐœ") + } + + /** + * ๋งˆ์ปค๋“ค์„ ์›๋ž˜ ์œ„์น˜๋กœ ๋ณต์› (์คŒ ์•„์›ƒ ์‹œ) + */ + fun restoreMarkersToOriginalPositions() { + Log.e("MapClusterManager", "๐Ÿ”„ === ๋งˆ์ปค ์›์œ„์น˜ ๋ณต์› ===") + + var restoredCount = 0 + + markers.forEach { marker -> + val content = marker.tag as? MapContent ?: return@forEach + val originalPosition = LatLng(content.location.latitude, content.location.longitude) + + if (marker.position != originalPosition) { + marker.position = originalPosition + restoredCount++ + Log.e("MapClusterManager", "๐Ÿ”„ ๋งˆ์ปค ์›์œ„์น˜ ๋ณต์›: ${content.title}") + } + } + + Log.e("MapClusterManager", "๐Ÿ”„ ์›์œ„์น˜ ๋ณต์› ์™„๋ฃŒ: ${restoredCount}/${markers.size}๊ฐœ") + } + + /** + * ์นด๋ฉ”๋ผ ๋ณ€๊ฒฝ ์‹œ ํ˜ธ์ถœ๋˜๋Š” ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ + */ + fun onCameraChanged(zoom: Double) { + val wasCollisionActive = zoom >= COLLISION_DETECTION_MIN_ZOOM + val prevZoom = naverMap.cameraPosition.zoom + val wasPrevCollisionActive = prevZoom >= COLLISION_DETECTION_MIN_ZOOM + + Log.d("MapClusterManager", "๐Ÿ“ท ์นด๋ฉ”๋ผ ๋ณ€๊ฒฝ: $prevZoom โ†’ $zoom") + + // ์ถฉ๋Œ ๊ฐ์ง€ ์ž„๊ณ„๊ฐ’์„ ๋„˜๋‚˜๋“ค ๋•Œ๋งŒ ์žฌ๋ฐฐ์น˜ + when { + !wasPrevCollisionActive && wasCollisionActive -> { + // ์คŒ ์ธ: ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ ์‹œ์ž‘ + Log.e("MapClusterManager", "๐Ÿ” ์คŒ ์ธ: ๋งˆ์ปค ์žฌ๋ฐฐ์น˜ ์‹œ์ž‘ ($zoom โ‰ฅ $COLLISION_DETECTION_MIN_ZOOM)") + redistributeMarkersAvoidingPOI() + } + wasPrevCollisionActive && !wasCollisionActive -> { + // ์คŒ ์•„์›ƒ: ๋งˆ์ปค ์›์œ„์น˜ ๋ณต์› + Log.e("MapClusterManager", "๐Ÿ” ์คŒ ์•„์›ƒ: ๋งˆ์ปค ์›์œ„์น˜ ๋ณต์› ($zoom < $COLLISION_DETECTION_MIN_ZOOM)") + restoreMarkersToOriginalPositions() + } + wasCollisionActive && wasPrevCollisionActive -> { + // ๊ณ ์คŒ์—์„œ ์ด๋™/์คŒ: ์‹ค์‹œ๊ฐ„ ์žฌ๋ฐฐ์น˜ + Log.d("MapClusterManager", "๐Ÿ” ๊ณ ์คŒ ์ƒํƒœ ๋ณ€๊ฒฝ: ์‹ค์‹œ๊ฐ„ ์žฌ๋ฐฐ์น˜") + redistributeMarkersAvoidingPOI() + } + // else: ์ €์คŒ ์ƒํƒœ ์œ ์ง€ - ์•„๋ฌด ์ž‘์—… ์•ˆํ•จ + } + } } \ No newline at end of file diff --git a/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/POIMarkerManager.kt b/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/POIMarkerManager.kt index 1df4aad..e4abc1f 100644 --- a/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/POIMarkerManager.kt +++ b/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/map/POIMarkerManager.kt @@ -45,44 +45,34 @@ class POIMarkerManager( // POI ํ‘œ์‹œ ์—ฌ๋ถ€ ์ƒํƒœ private var isPOIVisible = true // ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๊ธฐ๋ณธ๊ฐ’์„ true๋กœ ๋ณ€๊ฒฝ - // ๋งˆ์ปค/ํด๋Ÿฌ์Šคํ„ฐ ์ถฉ๋Œ ๊ฐ์ง€๋ฅผ ์œ„ํ•œ ์œ„์น˜ ์ €์žฅ + // ๊ธฐ์กด ๋งˆ์ปค ์œ„์น˜ ์ €์žฅ (ํ˜ธํ™˜์„ฑ ์œ ์ง€) private var existingMarkerPositions = mutableListOf() private var currentZoomLevel = 16.0 - // ํ˜„์žฌ ํ‘œ์‹œ ์ค‘์ธ POI ๋ฐ์ดํ„ฐ ์ €์žฅ (์žฌ๋ฐฐ์น˜์šฉ) + // ํ˜„์žฌ ํ‘œ์‹œ ์ค‘์ธ POI ๋ฐ์ดํ„ฐ ์ €์žฅ (์œ„์น˜ ์ •๋ณด ์ œ๊ณต์šฉ) private var currentPOIData = mutableListOf() - // ํ™”๋ฉด ๊ธฐ๋ฐ˜ ์ถฉ๋Œ ๊ฐ์ง€ ์„ค์ • - private val COLLISION_DETECTION_MIN_ZOOM = 19.0 // ์คŒ 19 ์ด์ƒ์—์„œ๋งŒ ์ถฉ๋Œ ๊ฐ์ง€ - private val MARKER_COLLISION_RADIUS_PX = 60 // ๋งˆ์ปค ์ถฉ๋Œ ๋ฐ˜๊ฒฝ (ํ”ฝ์…€) - init { Log.d("POIMarkerManager", "POI ๋งˆ์ปค ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™” ์™„๋ฃŒ") } /** - * ๊ธฐ์กด ๋งˆ์ปค/ํด๋Ÿฌ์Šคํ„ฐ ์œ„์น˜ ์—…๋ฐ์ดํŠธ + ์‹ค์‹œ๊ฐ„ POI ์žฌ๋ฐฐ์น˜ + * POI ์œ„์น˜ ์ •๋ณด ์ œ๊ณต (๋งˆ์ปค ์ถฉ๋Œ ๊ฐ์ง€๋ฅผ ์œ„ํ•ด) + */ + fun getPOIPositions(): List { + return poiMarkers.map { it.position } + } + + /** + * ๊ธฐ์กด ๋งˆ์ปค ์œ„์น˜ ์ •๋ณด ์ €์žฅ (ํ˜ธํ™˜์„ฑ ์œ ์ง€) */ fun updateExistingMarkerPositions(positions: List, zoomLevel: Double) { - val wasCollisionActive = currentZoomLevel >= COLLISION_DETECTION_MIN_ZOOM - existingMarkerPositions.clear() existingMarkerPositions.addAll(positions) currentZoomLevel = zoomLevel - val isCollisionActive = currentZoomLevel >= COLLISION_DETECTION_MIN_ZOOM - Log.d("POIMarkerManager", "๐Ÿช ๊ธฐ์กด ๋งˆ์ปค ์œ„์น˜ ์—…๋ฐ์ดํŠธ: ${positions.size}๊ฐœ, ์คŒ: $zoomLevel") - Log.d("POIMarkerManager", "๐Ÿช ์ถฉ๋Œ ๊ฐ์ง€ ํ™œ์„ฑํ™”: $isCollisionActive (์คŒ $COLLISION_DETECTION_MIN_ZOOM ์ด์ƒ)") - - // ์คŒ ๋ ˆ๋ฒจ์ด ์ถฉ๋Œ ๊ฐ์ง€ ์ž„๊ณ„๊ฐ’์„ ๋„˜๋‚˜๋“ค๊ฑฐ๋‚˜, ์ด๋ฏธ ํ™œ์„ฑํ™” ์ƒํƒœ์—์„œ ๋งˆ์ปค ์œ„์น˜๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ POI ์žฌ๋ฐฐ์น˜ - if (isCollisionActive && currentPOIData.isNotEmpty()) { - Log.w("POIMarkerManager", "๐Ÿช ๐Ÿ”„ ์‹ค์‹œ๊ฐ„ POI ์žฌ๋ฐฐ์น˜ ์‹œ์ž‘ (์คŒ: $zoomLevel)") - redistributePOIMarkersRealtime() - } else if (!isCollisionActive && wasCollisionActive && currentPOIData.isNotEmpty()) { - Log.w("POIMarkerManager", "๐Ÿช ๐Ÿ”„ ์คŒ ์•„์›ƒ์œผ๋กœ POI ์›์œ„์น˜ ๋ณต์›") - restorePOIToOriginalPositions() - } + Log.d("POIMarkerManager", "๐Ÿช POI๋Š” ๊ณ ์ • ์œ„์น˜ ์œ ์ง€ - ์žฌ๋ฐฐ์น˜ ์—†์Œ") // ๋””๋ฒ„๊น…์„ ์œ„ํ•ด ์ฒซ 3๊ฐœ ์œ„์น˜ ์ถœ๋ ฅ positions.take(3).forEachIndexed { index, position -> @@ -91,13 +81,12 @@ class POIMarkerManager( } /** - * POI ๋งˆ์ปค๋“ค์„ ์ง€๋„์— ํ‘œ์‹œ (ํ™”๋ฉด ์ขŒํ‘œ ๊ธฐ๋ฐ˜ ์ถฉ๋Œ ๊ฐ์ง€) + * POI ๋งˆ์ปค๋“ค์„ ์ง€๋„์— ํ‘œ์‹œ (๊ณ ์ • ์œ„์น˜ - ์ถฉ๋Œ ๊ฐ์ง€ ์ œ๊ฑฐ) */ fun showPOIMarkers(pois: List) { Log.w("POIMarkerManager", "๐Ÿช === showPOIMarkers ํ˜ธ์ถœ๋จ ===") Log.w("POIMarkerManager", "๐Ÿช POI ๊ฐ€์‹œ์„ฑ: $isPOIVisible") Log.w("POIMarkerManager", "๐Ÿช ์š”์ฒญ๋œ POI: ${pois.size}๊ฐœ") - Log.w("POIMarkerManager", "๐Ÿช ํ˜„์žฌ ์คŒ ๋ ˆ๋ฒจ: $currentZoomLevel") if (!isPOIVisible) { Log.e("POIMarkerManager", "๐Ÿช POI ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ - ๋งˆ์ปค ํ‘œ์‹œ ์Šคํ‚ต") @@ -106,16 +95,12 @@ class POIMarkerManager( clearPOIMarkers() - // ํ˜„์žฌ POI ๋ฐ์ดํ„ฐ ์ €์žฅ (์‹ค์‹œ๊ฐ„ ์žฌ๋ฐฐ์น˜์šฉ) + // ํ˜„์žฌ POI ๋ฐ์ดํ„ฐ ์ €์žฅ (์œ„์น˜ ์ •๋ณด ์ œ๊ณต์šฉ) currentPOIData.clear() currentPOIData.addAll(pois) var validCount = 0 var skippedCount = 0 - var offsetCount = 0 - - val isCollisionDetectionActive = currentZoomLevel >= COLLISION_DETECTION_MIN_ZOOM - Log.w("POIMarkerManager", "๐Ÿช ์ถฉ๋Œ ๊ฐ์ง€ ํ™œ์„ฑํ™”: $isCollisionDetectionActive (์คŒ $COLLISION_DETECTION_MIN_ZOOM ์ด์ƒ)") pois.forEach { poi -> // ์ธ๋„ค์ผ URL์ด ์—†์œผ๋ฉด ๋งˆ์ปค๋ฅผ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ @@ -128,24 +113,9 @@ class POIMarkerManager( val originalPosition = LatLng(poi.latitude, poi.longitude) Log.d("POIMarkerManager", "๐Ÿช [DEBUG] POI ์ฒ˜๋ฆฌ ์‹œ์ž‘: ${poi.name} - ์œ„์น˜: (${poi.latitude}, ${poi.longitude})") - // ํ™”๋ฉด ์ขŒํ‘œ ๊ธฐ๋ฐ˜ ์ถฉ๋Œ ๊ฐ์ง€ (์คŒ 19 ์ด์ƒ์—์„œ๋งŒ) - val finalPosition = if (isCollisionDetectionActive) { - calculateOptimalPositionScreenBased(originalPosition, poi.name) - } else { - originalPosition // ์คŒ์ด ๋‚ฎ์œผ๋ฉด ์›์œ„์น˜ - } - - if (finalPosition != originalPosition) { - offsetCount++ - Log.e("POIMarkerManager", "๐Ÿช โœจ POI ์˜คํ”„์…‹ ์ ์šฉ๋จ: ${poi.name}") - Log.e("POIMarkerManager", "๐Ÿช โœจ ์›๋ž˜: (${poi.latitude}, ${poi.longitude})") - Log.e("POIMarkerManager", "๐Ÿช โœจ ์ตœ์ข…: (${finalPosition.latitude}, ${finalPosition.longitude})") - } else { - Log.d("POIMarkerManager", "๐Ÿช POI ์˜คํ”„์…‹ ์—†์Œ: ${poi.name}") - } - + // POI๋Š” ํ•ญ์ƒ ์›๋ž˜ ์œ„์น˜์— ๊ณ ์ • ํ‘œ์‹œ val marker = Marker().apply { - position = finalPosition + position = originalPosition map = naverMap tag = poi zIndex = 500 // ์ผ๋ฐ˜ ๋งˆ์ปค๋ณด๋‹ค ๋‚ฎ๊ฒŒ ์„ค์ •ํ•˜์—ฌ ๊ฒน์น˜์ง€ ์•Š๋„๋ก @@ -166,7 +136,7 @@ class POIMarkerManager( loadPOIImage(marker, poi.thumbnailUrl!!, poi.category) } - Log.d("POIMarkerManager", "๐Ÿช POI ๋งˆ์ปค ์ฒ˜๋ฆฌ ์™„๋ฃŒ - ์œ ํšจ: ${validCount}๊ฐœ, ์Šคํ‚ต: ${skippedCount}๊ฐœ, ์˜คํ”„์…‹: ${offsetCount}๊ฐœ") + Log.d("POIMarkerManager", "๐Ÿช POI ๋งˆ์ปค ์ฒ˜๋ฆฌ ์™„๋ฃŒ - ์œ ํšจ: ${validCount}๊ฐœ, ์Šคํ‚ต: ${skippedCount}๊ฐœ (๊ณ ์ • ์œ„์น˜)") } /** @@ -599,141 +569,4 @@ class POIMarkerManager( return earthRadius * c } - // ===== ๐Ÿ†• ํ™”๋ฉด ์ขŒํ‘œ ๊ธฐ๋ฐ˜ ์ถฉ๋Œ ๊ฐ์ง€ ์‹œ์Šคํ…œ ===== - - /** - * ํ™”๋ฉด ์ขŒํ‘œ ๊ธฐ๋ฐ˜ POI ์ตœ์  ์œ„์น˜ ๊ณ„์‚ฐ (์คŒ 19 ์ด์ƒ) - */ - private fun calculateOptimalPositionScreenBased(originalPosition: LatLng, poiName: String): LatLng { - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ–ผ๏ธ === ํ™”๋ฉด ๊ธฐ๋ฐ˜ ์œ„์น˜ ๊ณ„์‚ฐ ์‹œ์ž‘ ===") - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ–ผ๏ธ POI: $poiName") - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ–ผ๏ธ ์คŒ: $currentZoomLevel (์ž„๊ณ„๊ฐ’: $COLLISION_DETECTION_MIN_ZOOM)") - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ–ผ๏ธ ๊ธฐ์กด ๋งˆ์ปค: ${existingMarkerPositions.size}๊ฐœ") - - if (existingMarkerPositions.isEmpty()) { - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ–ผ๏ธ ๊ธฐ์กด ๋งˆ์ปค ์—†์Œ - ์›์œ„์น˜ ์‚ฌ์šฉ") - return originalPosition - } - - // 1. POI ์›๋ž˜ ์œ„์น˜๋ฅผ ํ™”๋ฉด ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ - val poiScreenPoint = naverMap.projection.toScreenLocation(originalPosition) - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ–ผ๏ธ POI ํ™”๋ฉด ์ขŒํ‘œ: (${poiScreenPoint.x}, ${poiScreenPoint.y})") - - // 2. ๊ธฐ์กด ๋งˆ์ปค๋“ค์˜ ํ™”๋ฉด ์ขŒํ‘œ ๊ณ„์‚ฐ - val existingScreenPoints = existingMarkerPositions.map { position -> - naverMap.projection.toScreenLocation(position) - } - - // 3. ํ™”๋ฉด์ƒ ์ถฉ๋Œ ๊ฐ์ง€ - var hasCollision = false - existingScreenPoints.forEachIndexed { index, screenPoint -> - val pixelDistance = sqrt( - (poiScreenPoint.x - screenPoint.x).toDouble().pow(2) + - (poiScreenPoint.y - screenPoint.y).toDouble().pow(2) - ) - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ–ผ๏ธ ๋งˆ์ปค[$index] ํ”ฝ์…€ ๊ฑฐ๋ฆฌ: ${pixelDistance.toInt()}px (์ถฉ๋Œ ๋ฐ˜๊ฒฝ: ${MARKER_COLLISION_RADIUS_PX}px)") - - if (pixelDistance <= MARKER_COLLISION_RADIUS_PX) { - hasCollision = true - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿšจ ํ™”๋ฉด์ƒ ์ถฉ๋Œ ๊ฐ์ง€! ๋งˆ์ปค[$index]: ${pixelDistance.toInt()}px โ‰ค ${MARKER_COLLISION_RADIUS_PX}px") - } - } - - if (!hasCollision) { - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ–ผ๏ธ ์ถฉ๋Œ ์—†์Œ - ์›์œ„์น˜ ์‚ฌ์šฉ") - return originalPosition - } - - // 4. ์ถฉ๋Œ ์‹œ ์˜คํ”„์…‹ ์ ์šฉ - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿšจ ์ถฉ๋Œ ๊ฐ์ง€๋จ! ํ™”๋ฉด ๊ธฐ๋ฐ˜ ์˜คํ”„์…‹ ๊ณ„์‚ฐ") - - val offsetDistance = MARKER_COLLISION_RADIUS_PX + 20 // ์—ฌ์œ  ๊ณต๊ฐ„ - val angles = listOf(0, 45, 90, 135, 180, 225, 270, 315) // 8๋ฐฉํ–ฅ - - for (angle in angles) { - val radians = Math.toRadians(angle.toDouble()) - val offsetX = cos(radians) * offsetDistance - val offsetY = sin(radians) * offsetDistance - - val offsetScreenPoint = android.graphics.PointF( - (poiScreenPoint.x + offsetX).toFloat(), - (poiScreenPoint.y + offsetY).toFloat() - ) - - // ์˜คํ”„์…‹๋œ ํ™”๋ฉด ์ขŒํ‘œ๋ฅผ ์ง€๋ฆฌ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ - val offsetPosition = naverMap.projection.fromScreenLocation(offsetScreenPoint) - - // ์˜คํ”„์…‹ ์œ„์น˜์—์„œ๋„ ์ถฉ๋Œ ์ฒดํฌ - var offsetHasCollision = false - existingScreenPoints.forEach { screenPoint -> - val pixelDistance = sqrt( - (offsetScreenPoint.x - screenPoint.x).toDouble().pow(2) + - (offsetScreenPoint.y - screenPoint.y).toDouble().pow(2) - ) - if (pixelDistance <= MARKER_COLLISION_RADIUS_PX) { - offsetHasCollision = true - } - } - - if (!offsetHasCollision) { - Log.e("POIMarkerManager", "๐Ÿช โœ… ํ™”๋ฉด ๊ธฐ๋ฐ˜ ์˜คํ”„์…‹ ์œ„์น˜ ์ฐพ์Œ: ${angle}๋„") - Log.e("POIMarkerManager", "๐Ÿช โœ… ํ™”๋ฉด ์ขŒํ‘œ: (${offsetScreenPoint.x}, ${offsetScreenPoint.y})") - Log.e("POIMarkerManager", "๐Ÿช โœ… ์ง€๋ฆฌ ์ขŒํ‘œ: (${offsetPosition.latitude}, ${offsetPosition.longitude})") - return offsetPosition - } - } - - Log.e("POIMarkerManager", "๐Ÿช โŒ ๋ชจ๋“  ์˜คํ”„์…‹ ์œ„์น˜์— ์ถฉ๋Œ - ์›์œ„์น˜ ๊ฐ•์ œ ์‚ฌ์šฉ") - return originalPosition - } - - /** - * ์‹ค์‹œ๊ฐ„ POI ์žฌ๋ฐฐ์น˜ (์คŒ/์ด๋™ ์‹œ ํ˜ธ์ถœ) - */ - private fun redistributePOIMarkersRealtime() { - if (currentPOIData.isEmpty()) return - - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ”„ === ์‹ค์‹œ๊ฐ„ POI ์žฌ๋ฐฐ์น˜ ===") - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ”„ ๋Œ€์ƒ POI: ${currentPOIData.size}๊ฐœ") - - var repositionedCount = 0 - - poiMarkers.forEachIndexed { index, marker -> - val poi = marker.tag as? POIData ?: return@forEachIndexed - val originalPosition = LatLng(poi.latitude, poi.longitude) - val newPosition = calculateOptimalPositionScreenBased(originalPosition, poi.name) - - if (newPosition != originalPosition) { - marker.position = newPosition - repositionedCount++ - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ”„ POI ์žฌ๋ฐฐ์น˜: ${poi.name}") - } - } - - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ”„ ์žฌ๋ฐฐ์น˜ ์™„๋ฃŒ: ${repositionedCount}/${poiMarkers.size}๊ฐœ") - } - - /** - * POI๋ฅผ ์›๋ž˜ ์œ„์น˜๋กœ ๋ณต์› (์คŒ ์•„์›ƒ ์‹œ) - */ - private fun restorePOIToOriginalPositions() { - if (currentPOIData.isEmpty()) return - - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ”„ === POI ์›์œ„์น˜ ๋ณต์› ===") - - var restoredCount = 0 - - poiMarkers.forEach { marker -> - val poi = marker.tag as? POIData ?: return@forEach - val originalPosition = LatLng(poi.latitude, poi.longitude) - - if (marker.position != originalPosition) { - marker.position = originalPosition - restoredCount++ - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ”„ POI ์›์œ„์น˜ ๋ณต์›: ${poi.name}") - } - } - - Log.e("POIMarkerManager", "๐Ÿช ๐Ÿ”„ ์›์œ„์น˜ ๋ณต์› ์™„๋ฃŒ: ${restoredCount}/${poiMarkers.size}๊ฐœ") - } } \ No newline at end of file diff --git a/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/screens/FullMapScreen.kt b/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/screens/FullMapScreen.kt index 627c837..6302b5f 100644 --- a/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/screens/FullMapScreen.kt +++ b/android/campung/app/src/main/java/com/shinhan/campung/presentation/ui/screens/FullMapScreen.kt @@ -695,28 +695,30 @@ fun FullMapScreen( naverMapRef = map mapInitializer.setupMapUI(map) - android.util.Log.d("FullMapScreen", "๐Ÿš€ [INIT] ClusterManager ์ƒ์„ฑ ์‹œ์ž‘") - clusterManager = - clusterManagerInitializer.createClusterManager(map) { centerContent -> - highlightedContent = centerContent - } - android.util.Log.d("FullMapScreen", "โœ… [INIT] ClusterManager ์ƒ์„ฑ ์™„๋ฃŒ") - android.util.Log.d("FullMapScreen", "๐Ÿ”— [INIT] clusterManager.onMarkerClick: ${clusterManager?.onMarkerClick}") - - // POI ๋งˆ์ปค ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™” + // POI ๋งˆ์ปค ๋งค๋‹ˆ์ € ๋จผ์ € ์ดˆ๊ธฐํ™” + android.util.Log.d("FullMapScreen", "๐Ÿช [INIT] POI ๋งˆ์ปค ๋งค๋‹ˆ์ € ์ƒ์„ฑ ์‹œ์ž‘") poiMarkerManager = POIMarkerManager(context, map, coroutineScope).apply { onPOIClick = { poi -> android.util.Log.d("FullMapScreen", "๐Ÿช POI ๋งˆ์ปค ํด๋ฆญ๋จ: ${poi.name}") mapViewModel.onPOIClick(poi) } } + android.util.Log.d("FullMapScreen", "โœ… [INIT] POI ๋งˆ์ปค ๋งค๋‹ˆ์ € ์ƒ์„ฑ ์™„๋ฃŒ") + + android.util.Log.d("FullMapScreen", "๐Ÿš€ [INIT] ClusterManager ์ƒ์„ฑ ์‹œ์ž‘") + clusterManager = + clusterManagerInitializer.createClusterManager(map, null, { centerContent -> + highlightedContent = centerContent + }, poiMarkerManager) + android.util.Log.d("FullMapScreen", "โœ… [INIT] ClusterManager ์ƒ์„ฑ ์™„๋ฃŒ") + android.util.Log.d("FullMapScreen", "๐Ÿ”— [INIT] clusterManager.onMarkerClick: ${clusterManager?.onMarkerClick}") - // ํด๋Ÿฌ์Šคํ„ฐ ๋งค๋‹ˆ์ €์™€ POI ๋งค๋‹ˆ์ € ์—ฐ๊ฒฐ (๋งˆ์ปค ์œ„์น˜ ๋™๊ธฐํ™”) + // ํด๋Ÿฌ์Šคํ„ฐ ๋งค๋‹ˆ์ €์™€ POI ๋งค๋‹ˆ์ € ์—ฐ๊ฒฐ (๋งˆ์ปค ์œ„์น˜ ๋™๊ธฐํ™” - ํ˜ธํ™˜์„ฑ ์œ ์ง€) clusterManager?.onMarkerPositionsUpdated = { positions, zoomLevel -> android.util.Log.d("FullMapScreen", "๐ŸŽฏ ํด๋Ÿฌ์Šคํ„ฐ โ†’ POI ์œ„์น˜ ๋™๊ธฐํ™”: ${positions.size}๊ฐœ, ์คŒ: $zoomLevel") poiMarkerManager?.updateExistingMarkerPositions(positions, zoomLevel) } - android.util.Log.d("FullMapScreen", "๐Ÿช POI ๋งˆ์ปค ๋งค๋‹ˆ์ € ์ดˆ๊ธฐํ™” ์™„๋ฃŒ") + android.util.Log.d("FullMapScreen", "๐Ÿชโœ… POI ๋งค๋‹ˆ์ €์™€ ํด๋Ÿฌ์Šคํ„ฐ ๋งค๋‹ˆ์ € ์—ฐ๊ฒฐ ์™„๋ฃŒ") // ๐Ÿ‘‡ ์นด๋ฉ”๋ผ๊ฐ€ '์›€์ง์ด๋Š” ๋™์•ˆ' ๊ณ„์† ํ˜ธ์ถœ๋จ: ์•„์ด์ฝ˜ ์‹ค์‹œ๊ฐ„ ๊ฐฑ์‹  map.addOnCameraChangeListener { _, _ ->