From ed07e95c9eb76ecb4ef065d5e705550e870321af Mon Sep 17 00:00:00 2001 From: "Jaeeun.Cho" Date: Mon, 17 Nov 2025 14:54:34 -0500 Subject: [PATCH] Map: add cluster feature --- public_html/doc/map.php | 227 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 216 insertions(+), 11 deletions(-) diff --git a/public_html/doc/map.php b/public_html/doc/map.php index 8ac5967..8777bc3 100644 --- a/public_html/doc/map.php +++ b/public_html/doc/map.php @@ -98,8 +98,8 @@ + - @@ -1246,15 +1246,16 @@ function popup(){ var rstInqPickupQty = function(json) { jQuery("#pickupquantity").text(json.curPickupQty); } - + var rstSaveInput = function(json) { - $('#map-modal-input .close').trigger('click'); - jQuery(".modal").removeClass("show");; - jQuery(".modal-backdrop").remove(); + $('#map-modal-input').modal('hide'); // Bootstrap 정상 닫기 시도 + $('#map-modal-input').removeClass('show in').css('display', 'none').attr('aria-hidden', 'true'); // 남아 있을 수 있는 클래스 제거 + $('.modal-backdrop').remove(); // backdrop 제거 + showPopupMessage(json.msg); //drawPoint(); updatePickupQty(); - } +} function replaceAll(str, searchStr, replaceStr) { @@ -1593,7 +1594,17 @@ function popup(){ var marker = []; var markerwindow = []; var overlay = []; + + var clusterer = null; + var clusterInfoGlobal = null; + var typeOrder = { "#FF0000": 1, "#800080": 2, "#FF80FF": 3, "#7B7A7A": 4 }; // Request > Scheduled > Normal > Finished + var rstInqPoint = function(json) { + if (clusterInfoGlobal) { + clusterInfoGlobal.close(); + clusterInfoGlobal = null; + } + for(let j=0; j < marker.length; j++) { marker[j].setMap(null); marker[j] = null; @@ -1602,6 +1613,12 @@ function popup(){ overlay[j] = null; } } + + if (clusterer) { + clusterer.setMap(null); + clusterer = null; + } + marker = []; overlay = []; let pointGeocoder = new google.maps.Geocoder(); @@ -1678,10 +1695,11 @@ function popup(){ position: location, map: map, icon: icon, - animation: (property.rnote && /\S/.test(property.rnote))? google.maps.Animation.BOUNCE : null // google.maps.Animation.DROP + animation: (property.rnote && /\S/.test(property.rnote) && property.color !== "#7B7A7A") ? google.maps.Animation.BOUNCE : null }); marker[i].customType = property.type; //'flag' 또는 'map-marker' + marker[i].property = property; // Add an info window with company information markerwindow[i] = new google.maps.InfoWindow({ @@ -1692,10 +1710,13 @@ function popup(){ markerwindow[i].open(map, marker[i]); }); - google.maps.event.addListener(markerwindow[i], 'domready', function() { - jQuery(".gm-ui-hover-effect").css("display","none"); + google.maps.event.addListener(markerwindow[i], 'domready', function() { + // InfoWindow의 닫기 버튼만 숨기기 + const iwOuter = jQuery(".gm-style-iw").last(); + const closeBtn = iwOuter.parent().find(".gm-ui-hover-effect"); + closeBtn.css("display","none"); }); - + marker[i].addListener('mouseout', () => { markerwindow[i].close(); }); @@ -1711,9 +1732,193 @@ function popup(){ } } - + + // 클러스터 생성 + if (marker.length > 0) { + // flag 타입만 필터링 + const flagMarkers = marker.filter(m => m.customType === 'flag'); + + if (flagMarkers.length > 0) { + // 사이즈 줄이기 + const algorithm = new markerClusterer.GridAlgorithm({ + gridSize: 5, + maxDistance: 50, + }); + + clusterer = new markerClusterer.MarkerClusterer({ + map, + markers: flagMarkers, + algorithm, + renderer: { + render: ({ count, position, markers }) => { + // 클러스터 안에 rnote 있는 flag 마커가 한 개라도 포함됐는지 확인 + const hasRnoteMarker = markers.some(m => + m.property && m.property.rnote && /\S/.test(m.property.rnote) + ); + + markers.sort((a, b) => { + const t1 = a.property.color || "#FF80FF"; + const t2 = b.property.color || "#FF80FF"; + return typeOrder[t1] - typeOrder[t2]; + }); + + const topColor = markers[0]?.property?.color || "#4285f4"; + const size = Math.min(50, 30 + Math.log(count) * 10); + + return new google.maps.Marker({ + position, + icon: { + url: + "data:image/svg+xml;charset=UTF-8," + + encodeURIComponent(` + + + ${count} + + `), + scaledSize: new google.maps.Size(40, 40), + anchor: new google.maps.Point(10, 10), + }, + zIndex: Number(google.maps.Marker.MAX_ZINDEX) + count, + + animation: ( + // 클러스터 안에 rnote가 있으면서 finished가 아닌 마커가 1개라도 있는 경우만 Bounce + markers.some(m => + m.property && + m.property.rnote && /\S/.test(m.property.rnote) && + m.property.color !== "#7B7A7A" + ) + ) ? google.maps.Animation.BOUNCE : null, + }); + }, + }, + + onClusterClick: (...args) => { + let clusterObj = null; + + if (args.length === 1 && args[0]) { + const a = args[0]; + clusterObj = a.cluster || a; + } else if (args.length >= 2) { + clusterObj = args[1]; + } + + if (!clusterObj) return false; + + const markersInCluster = + clusterObj.markers || + (typeof clusterObj.getMarkers === "function" + ? clusterObj.getMarkers() + : []) || + []; + + const clusterCenter = + clusterObj.position || + (typeof clusterObj.getCenter === "function" + ? clusterObj.getCenter() + : null); + + if (!markersInCluster.length || !clusterCenter) return false; + + if (clusterInfoGlobal) { + clusterInfoGlobal.close(); + } + + markersInCluster.sort((a, b) => { + const t1 = a.property.color || "#FF80FF"; + const t2 = b.property.color || "#FF80FF"; + return typeOrder[t1] - typeOrder[t2]; + }); + + let html = + "
"; + markersInCluster.forEach((m) => { + const p = m.property || {}; + const hasRnote = p.rnote && /\S/.test(p.rnote); + + html += ` +
+ + ${p.store ?? "—"} + ${ + hasRnote + ? ` + notice + ` + : "" + } +
+
+ Est. Quantity : ${p.estqty ?? "-"} +
+
+ `; + }); + html += "
"; + + const clusterInfo = new google.maps.InfoWindow({ + content: html, + position: clusterCenter, + pixelOffset: new google.maps.Size(0, -25), + }); + + clusterInfoGlobal = clusterInfo; + clusterInfo.open(map); + + google.maps.event.addListenerOnce( + clusterInfo, + "domready", + function () { + document.querySelectorAll(".cluster-item").forEach((el) => { + el.addEventListener("click", () => { + const idx = el.getAttribute("data-marker-index"); + if (idx !== null && marker[idx]) { + google.maps.event.trigger(marker[idx], "click"); + } + clusterInfo.close(); + }); + }); + } + ); + return false; + }, + }); + } + + // 클러스터링이 끝났을 때 실행되는 이벤트 + clusterer.addListener('clusteringend', () => { + marker.forEach(m => { + if (!m.getMap()) return; + if (m.property?.rnote && /\S/.test(m.property.rnote) && m.property.color !== "#7B7A7A") { + m.setAnimation(google.maps.Animation.BOUNCE); + } else { + m.setAnimation(null); + } + }); + }); + } } + function showInfoWindow(position, title, quantity, infoType) { + const showClose = infoType === 'cluster'; // 클러스터일 때만 X 표시 + + const infoContent = ` +
+ ${showClose ? '' : ''} +
+ ${title}
+ Est. Quantity: ${quantity} +
+
+ `; + + infoWindow.setContent(infoContent); + infoWindow.setPosition(position); + infoWindow.open(map); + } + function addDragEvent(customOverlay) { let div = customOverlay.div;