@mint-ui/map 1.2.0-test.4 → 1.2.0-test.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/components/mint-map/core/MintMapCore.js +5 -6
  2. package/dist/components/mint-map/core/advanced/index.d.ts +2 -1
  3. package/dist/components/mint-map/core/advanced/shared/context.d.ts +39 -0
  4. package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/context.js +62 -79
  5. package/dist/components/mint-map/core/advanced/shared/helpers.d.ts +20 -0
  6. package/dist/components/mint-map/core/advanced/shared/helpers.js +40 -0
  7. package/dist/components/mint-map/core/advanced/shared/hooks.d.ts +74 -0
  8. package/dist/components/mint-map/core/advanced/shared/hooks.js +189 -0
  9. package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/index.d.ts +3 -0
  10. package/dist/components/mint-map/core/advanced/shared/performance.d.ts +77 -0
  11. package/dist/components/mint-map/core/advanced/shared/performance.js +262 -0
  12. package/dist/components/mint-map/core/advanced/shared/types.d.ts +111 -0
  13. package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/types.js +0 -1
  14. package/dist/components/mint-map/core/advanced/shared/utils.d.ts +62 -0
  15. package/dist/components/mint-map/core/advanced/shared/utils.js +221 -0
  16. package/dist/components/mint-map/core/advanced/shared/viewport.d.ts +42 -0
  17. package/dist/components/mint-map/core/advanced/shared/viewport.js +51 -0
  18. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.d.ts +47 -0
  19. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.js +620 -0
  20. package/dist/components/mint-map/core/advanced/woongCanvasMarker/index.d.ts +3 -0
  21. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.d.ts +61 -0
  22. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.js +582 -0
  23. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/index.d.ts +3 -0
  24. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.d.ts +120 -0
  25. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.js +295 -0
  26. package/dist/components/mint-map/google/GoogleMintMapController.js +5 -4
  27. package/dist/components/mint-map/kakao/KakaoMintMapController.js +5 -4
  28. package/dist/components/mint-map/naver/NaverMintMapController.js +5 -4
  29. package/dist/index.es.js +1711 -1056
  30. package/dist/index.js +23 -8
  31. package/dist/index.umd.js +1723 -1057
  32. package/package.json +1 -1
  33. package/dist/components/mint-map/core/advanced/woongCanvas/ClusterMarker.d.ts +0 -11
  34. package/dist/components/mint-map/core/advanced/woongCanvas/WoongKonvaMarker.d.ts +0 -50
  35. package/dist/components/mint-map/core/advanced/woongCanvas/WoongKonvaMarker.js +0 -1065
  36. package/dist/components/mint-map/core/advanced/woongCanvas/index.d.ts +0 -3
  37. package/dist/components/mint-map/core/advanced/woongCanvas/shared/context.d.ts +0 -31
  38. package/dist/components/mint-map/core/advanced/woongCanvas/shared/performance.d.ts +0 -161
  39. package/dist/components/mint-map/core/advanced/woongCanvas/shared/performance.js +0 -343
  40. package/dist/components/mint-map/core/advanced/woongCanvas/shared/types.d.ts +0 -131
  41. package/dist/components/mint-map/core/advanced/woongCanvas/shared/utils.d.ts +0 -23
  42. package/dist/components/mint-map/core/advanced/woongCanvas/shared/utils.js +0 -115
package/dist/index.umd.js CHANGED
@@ -656,7 +656,6 @@
656
656
 
657
657
  /**
658
658
  * 캔버스 데이터 타입 Enum
659
- * 마커인지 폴리곤인지 구분하는 상수
660
659
  */
661
660
  exports.CanvasDataType = void 0;
662
661
 
@@ -799,7 +798,11 @@
799
798
  }();
800
799
 
801
800
  /**
802
- * 폴리곤 offset 계산
801
+ * 폴리곤 좌표 변환 (위경도 → 화면 좌표)
802
+ *
803
+ * @param polygonData 폴리곤 데이터
804
+ * @param controller MintMapController 인스턴스
805
+ * @returns 변환된 화면 좌표 배열 (4차원 배열) 또는 null
803
806
  */
804
807
 
805
808
  var computePolygonOffsets = function (polygonData, controller) {
@@ -809,7 +812,7 @@
809
812
  return null;
810
813
  }
811
814
 
812
- var result = [];
815
+ var result = []; // GeoJSON MultiPolygon 구조: [MultiPolygon][PolygonGroup][Coordinate][lng, lat]
813
816
 
814
817
  for (var _i = 0, _a = paths.coordinates; _i < _a.length; _i++) {
815
818
  var multiPolygon = _a[_i];
@@ -820,7 +823,8 @@
820
823
  var polygonOffsets = [];
821
824
 
822
825
  for (var _c = 0, polygonGroup_1 = polygonGroup; _c < polygonGroup_1.length; _c++) {
823
- var coord = polygonGroup_1[_c];
826
+ var coord = polygonGroup_1[_c]; // GeoJSON은 [lng, lat] 순서이지만 Position은 [lat, lng] 순서
827
+
824
828
  var pos = new Position(coord[1], coord[0]);
825
829
  var offset = controller.positionToOffset(pos);
826
830
  polygonOffsets.push([offset.x, offset.y]);
@@ -835,47 +839,97 @@
835
839
  return result;
836
840
  };
837
841
  /**
838
- * 마커 offset 계산
842
+ * 마커 좌표 변환 (위경도 → 화면 좌표)
843
+ *
844
+ * @param markerData 마커 데이터
845
+ * @param controller MintMapController 인스턴스
846
+ * @returns 변환된 화면 좌표 또는 null
839
847
  */
840
848
 
841
849
  var computeMarkerOffset = function (markerData, controller) {
842
- if (!markerData.position) {
843
- return null;
844
- }
845
-
850
+ if (!markerData.position) return null;
846
851
  return controller.positionToOffset(markerData.position);
847
852
  };
848
853
  /**
849
- * Point-in-Polygon 알고리즘
854
+ * Point-in-Polygon 알고리즘 (Ray Casting)
855
+ *
856
+ * @param point 확인할 점의 좌표
857
+ * @param polygon 폴리곤 좌표 배열
858
+ * @returns 점이 폴리곤 내부에 있으면 true
850
859
  */
851
860
 
852
861
  var isPointInPolygon = function (point, polygon) {
862
+ // Ray Casting 알고리즘: 점에서 오른쪽으로 무한히 뻗은 선과 폴리곤 변의 교차 횟수로 판단
853
863
  var inside = false;
854
864
 
855
865
  for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
856
866
  var xi = polygon[i][0],
857
867
  yi = polygon[i][1];
858
868
  var xj = polygon[j][0],
859
- yj = polygon[j][1];
869
+ yj = polygon[j][1]; // 점의 y 좌표가 변의 양 끝점 사이에 있고, 교차점의 x 좌표가 점의 x 좌표보다 큰지 확인
870
+
860
871
  var intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
861
- if (intersect) inside = !inside;
872
+ if (intersect) inside = !inside; // 교차할 때마다 inside 상태 토글
862
873
  }
863
874
 
864
875
  return inside;
865
876
  };
866
877
  /**
867
- * 폴리곤 히트 테스트
878
+ * 폴리곤 히트 테스트 (도넛 폴리곤 지원)
879
+ *
880
+ * @param clickedOffset 클릭/마우스 위치 좌표
881
+ * @param polygonData 폴리곤 데이터
882
+ * @param getPolygonOffsets 폴리곤 좌표 변환 함수
883
+ * @returns 점이 폴리곤 내부에 있으면 true
868
884
  */
869
885
 
870
886
  var isPointInPolygonData = function (clickedOffset, polygonData, getPolygonOffsets) {
871
887
  var polygonOffsets = getPolygonOffsets(polygonData);
872
- if (!polygonOffsets) return false;
888
+ if (!polygonOffsets) return false; // 도넛 폴리곤 처리: 외부 폴리곤 내부에 있으면서 구멍(hole) 내부에 있지 않아야 함
873
889
 
874
- for (var _i = 0, polygonOffsets_1 = polygonOffsets; _i < polygonOffsets_1.length; _i++) {
875
- var multiPolygon = polygonOffsets_1[_i];
890
+ if (polygonData.isDonutPolygon) {
891
+ for (var _i = 0, polygonOffsets_1 = polygonOffsets; _i < polygonOffsets_1.length; _i++) {
892
+ var multiPolygon = polygonOffsets_1[_i];
893
+ if (multiPolygon.length === 0) continue; // 구멍이 없는 경우 일반 폴리곤과 동일
894
+
895
+ if (multiPolygon.length === 1) {
896
+ if (isPointInPolygon(clickedOffset, multiPolygon[0])) {
897
+ return true;
898
+ }
899
+
900
+ continue;
901
+ } // 외부 폴리곤 내부에 있는지 확인
902
+
903
+
904
+ var outerPolygon = multiPolygon[0];
876
905
 
877
- for (var _a = 0, multiPolygon_2 = multiPolygon; _a < multiPolygon_2.length; _a++) {
878
- var polygonGroup = multiPolygon_2[_a];
906
+ if (!isPointInPolygon(clickedOffset, outerPolygon)) {
907
+ continue;
908
+ } // 구멍 내부에 있으면 false (도넛의 빈 공간)
909
+
910
+
911
+ for (var i = 1; i < multiPolygon.length; i++) {
912
+ var hole = multiPolygon[i];
913
+
914
+ if (isPointInPolygon(clickedOffset, hole)) {
915
+ return false;
916
+ }
917
+ } // 외부 폴리곤 내부에 있으면서 모든 구멍 밖에 있으면 true
918
+
919
+
920
+ return true;
921
+ }
922
+
923
+ return false;
924
+ } // 일반 폴리곤 처리
925
+
926
+
927
+ for (var _a = 0, polygonOffsets_2 = polygonOffsets; _a < polygonOffsets_2.length; _a++) {
928
+ var multiPolygon = polygonOffsets_2[_a];
929
+
930
+ for (var _b = 0, multiPolygon_2 = multiPolygon; _b < multiPolygon_2.length; _b++) {
931
+ var polygonGroup = multiPolygon_2[_b];
932
+ if (polygonGroup.length === 0) continue;
879
933
 
880
934
  if (isPointInPolygon(clickedOffset, polygonGroup)) {
881
935
  return true;
@@ -886,7 +940,12 @@
886
940
  return false;
887
941
  };
888
942
  /**
889
- * 마커 히트 테스트
943
+ * 마커 히트 테스트 (꼬리 제외, 오프셋 지원)
944
+ *
945
+ * @param clickedOffset 클릭/마우스 위치 좌표
946
+ * @param markerData 마커 데이터
947
+ * @param getMarkerOffset 마커 좌표 변환 함수
948
+ * @returns 점이 마커 영역 내부에 있으면 true
890
949
  */
891
950
 
892
951
  var isPointInMarkerData = function (clickedOffset, markerData, getMarkerOffset) {
@@ -894,38 +953,79 @@
894
953
  if (!markerOffset) return false;
895
954
  var boxWidth = markerData.boxWidth || 50;
896
955
  var boxHeight = markerData.boxHeight || 28;
897
- var tailHeight = 6;
898
- var x = markerOffset.x - boxWidth / 2;
899
- var y = markerOffset.y - boxHeight - tailHeight;
956
+ var tailHeight = markerData.tailHeight || 0;
957
+ var offsetX = markerData.offsetX || 0;
958
+ var offsetY = markerData.offsetY || 0; // 오프셋을 적용한 마커 중심점 기준으로 박스 영역 계산 (꼬리는 제외)
959
+
960
+ var x = markerOffset.x + offsetX - boxWidth / 2;
961
+ var y = markerOffset.y + offsetY - boxHeight - tailHeight; // 클릭 위치가 박스 영역 내부에 있는지 확인
962
+
900
963
  return clickedOffset.x >= x && clickedOffset.x <= x + boxWidth && clickedOffset.y >= y && clickedOffset.y <= y + boxHeight;
964
+ }; // Hex 색상을 RGBA로 변환
965
+
966
+ var hexToRgba = function (hexColor, alpha) {
967
+ if (alpha === void 0) {
968
+ alpha = 1;
969
+ }
970
+
971
+ var hex = hexColor.replace('#', '');
972
+
973
+ if (hex.length !== 6) {
974
+ throw new Error('Invalid hex color format');
975
+ }
976
+
977
+ var r = parseInt(hex.substring(0, 2), 16);
978
+ var g = parseInt(hex.substring(2, 4), 16);
979
+ var b = parseInt(hex.substring(4, 6), 16);
980
+ return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
981
+ };
982
+ var tempCanvas = document.createElement('canvas');
983
+ var tempCtx = tempCanvas.getContext('2d');
984
+ /**
985
+ * 텍스트 박스 너비 계산
986
+ *
987
+ * @param params 파라미터 객체
988
+ * @param params.text 측정할 텍스트
989
+ * @param params.fontConfig 폰트 설정
990
+ * @param params.padding 패딩 값 (px)
991
+ * @param params.minWidth 최소 너비 (px)
992
+ * @returns 계산된 텍스트 박스 너비 (px)
993
+ */
994
+
995
+ var calculateTextBoxWidth = function (_a) {
996
+ var text = _a.text,
997
+ fontConfig = _a.fontConfig,
998
+ padding = _a.padding,
999
+ minWidth = _a.minWidth;
1000
+ if (!tempCtx) return 0;
1001
+ tempCtx.font = fontConfig;
1002
+ var textWidth = tempCtx.measureText(text).width;
1003
+ return Math.max(minWidth, textWidth + padding);
901
1004
  };
902
1005
 
903
- var KonvaMarkerContext = React.createContext(null);
904
- var KonvaMarkerProvider = function (_a) {
905
- var children = _a.children;
906
- var controller = useMintMapController(); // Refs
1006
+ var WoongCanvasContext = React.createContext(null);
1007
+ /**
1008
+ * WoongCanvasProvider 컴포넌트
1009
+ *
1010
+ * 다중 WoongCanvas 인스턴스를 관리하고 zIndex 기반 이벤트 우선순위를 처리합니다.
1011
+ */
907
1012
 
1013
+ var WoongCanvasProvider = function (_a) {
1014
+ var children = _a.children;
1015
+ var controller = useMintMapController();
908
1016
  var componentsRef = React.useRef([]);
909
1017
  var currentHoveredRef = React.useRef(null);
910
1018
  var currentHoveredDataRef = React.useRef(null);
911
- var draggingRef = React.useRef(false);
912
- /**
913
- * 컴포넌트 등록 (zIndex 내림차순 정렬)
914
- * 높은 zIndex가 먼저 처리됨
915
- */
1019
+ var draggingRef = React.useRef(false); // 컴포넌트 등록 (zIndex 내림차순 정렬)
916
1020
 
917
1021
  var registerComponent = React.useCallback(function (instance) {
918
1022
  componentsRef.current.push(instance);
919
1023
  componentsRef.current.sort(function (a, b) {
920
1024
  return b.zIndex - a.zIndex;
921
1025
  });
922
- }, []);
923
- /**
924
- * 컴포넌트 등록 해제
925
- */
1026
+ }, []); // 컴포넌트 등록 해제
926
1027
 
927
1028
  var unregisterComponent = React.useCallback(function (instance) {
928
- // Hover 중이던 컴포넌트면 초기화
929
1029
  if (currentHoveredRef.current === instance) {
930
1030
  currentHoveredRef.current = null;
931
1031
  currentHoveredDataRef.current = null;
@@ -934,95 +1034,77 @@
934
1034
  componentsRef.current = componentsRef.current.filter(function (c) {
935
1035
  return c !== instance;
936
1036
  });
937
- }, []);
938
- /**
939
- * 전역 클릭 핸들러 (zIndex 우선순위)
940
- */
1037
+ }, []); // 전역 클릭 핸들러 (zIndex 우선순위)
941
1038
 
942
1039
  var handleGlobalClick = React.useCallback(function (event) {
943
- var _a;
1040
+ var _a, _b;
944
1041
 
945
1042
  if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
946
- var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 순서대로 순회 (높은 것부터)
1043
+ var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
947
1044
 
948
- for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
949
- var component = _b[_i];
1045
+ for (var _i = 0, _c = componentsRef.current; _i < _c.length; _i++) {
1046
+ var component = _c[_i];
1047
+ if (component.isInteractionDisabled()) continue;
950
1048
  var data = component.findData(clickedOffset);
1049
+ if (!data) continue; // 첫 번째로 찾은 항목만 처리하고 종료 (zIndex 우선순위)
951
1050
 
952
- if (data) {
953
- component.handleLocalClick(data);
954
-
955
- if (component.onClick) {
956
- component.onClick(data, component.getSelectedIds());
957
- }
958
-
959
- return; // 첫 번째 히트만 처리
960
- }
1051
+ component.handleLocalClick(data);
1052
+ (_b = component.onClick) === null || _b === void 0 ? void 0 : _b.call(component, data, component.getSelectedIds());
1053
+ return;
961
1054
  }
962
- }, [controller]);
963
- /**
964
- * 전역 마우스 이동 핸들러 (zIndex 우선순위)
965
- */
1055
+ }, [controller]); // 전역 마우스 이동 핸들러 (zIndex 우선순위)
966
1056
 
967
1057
  var handleGlobalMouseMove = React.useCallback(function (event) {
968
1058
  var _a;
969
1059
 
970
1060
  if (draggingRef.current || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
971
- var mouseOffset = controller.positionToOffset(event.param.position); // zIndex 순서대로 순회하여 Hover 대상 찾기
972
-
1061
+ var mouseOffset = controller.positionToOffset(event.param.position);
973
1062
  var newHoveredComponent = null;
974
- var newHoveredData = null;
1063
+ var newHoveredData = null; // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
975
1064
 
976
1065
  for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
977
1066
  var component = _b[_i];
1067
+ if (component.isInteractionDisabled()) continue;
978
1068
  var data = component.findData(mouseOffset);
1069
+ if (!data) continue; // 첫 번째로 찾은 항목만 hover 처리 (zIndex 우선순위)
979
1070
 
980
- if (data) {
981
- newHoveredComponent = component;
982
- newHoveredData = data;
983
- break; // 번째 히트만 처리
984
- }
985
- } // Hover 상태 변경 감지 (최적화: 별도 ref로 직접 비교)
986
-
1071
+ newHoveredComponent = component;
1072
+ newHoveredData = data;
1073
+ break;
1074
+ } // hover 상태가 변경되지 않았으면 종료 (불필요한 렌더링 방지)
987
1075
 
988
- if (currentHoveredRef.current !== newHoveredComponent || currentHoveredDataRef.current !== newHoveredData) {
989
- // 이전 hover 해제
990
- if (currentHoveredRef.current) {
991
- currentHoveredRef.current.setHovered(null);
992
1076
 
993
- if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
994
- currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
995
- }
996
- } // 새 hover 설정
1077
+ if (currentHoveredRef.current === newHoveredComponent && currentHoveredDataRef.current === newHoveredData) {
1078
+ return;
1079
+ } // 기존 hover 항목에 mouseOut 이벤트 발생
997
1080
 
998
1081
 
999
- if (newHoveredComponent && newHoveredData) {
1000
- newHoveredComponent.setHovered(newHoveredData);
1082
+ if (currentHoveredRef.current) {
1083
+ currentHoveredRef.current.setHovered(null);
1001
1084
 
1002
- if (newHoveredComponent.onMouseOver) {
1003
- newHoveredComponent.onMouseOver(newHoveredData);
1004
- }
1085
+ if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
1086
+ currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
1005
1087
  }
1088
+ } // 새 hover 항목에 mouseOver 이벤트 발생
1006
1089
 
1007
- currentHoveredRef.current = newHoveredComponent;
1008
- currentHoveredDataRef.current = newHoveredData;
1090
+
1091
+ if (newHoveredComponent && newHoveredData) {
1092
+ newHoveredComponent.setHovered(newHoveredData);
1093
+
1094
+ if (newHoveredComponent.onMouseOver) {
1095
+ newHoveredComponent.onMouseOver(newHoveredData);
1096
+ }
1009
1097
  }
1010
- }, [controller]);
1011
- /**
1012
- * 줌/드래그 시작 (마우스 이동 이벤트 무시)
1013
- */
1014
1098
 
1099
+ currentHoveredRef.current = newHoveredComponent;
1100
+ currentHoveredDataRef.current = newHoveredData;
1101
+ }, [controller]);
1015
1102
  var handleZoomStart = React.useCallback(function () {
1016
1103
  draggingRef.current = true;
1017
1104
  }, []);
1018
- /**
1019
- * 지도 idle (마우스 이동 이벤트 재개)
1020
- */
1021
-
1022
1105
  var handleIdle = React.useCallback(function () {
1023
1106
  draggingRef.current = false;
1024
- }, []); // 이벤트 리스너 등록
1025
-
1107
+ }, []);
1026
1108
  React.useEffect(function () {
1027
1109
  controller.addEventListener('CLICK', handleGlobalClick);
1028
1110
  controller.addEventListener('MOUSEMOVE', handleGlobalMouseMove);
@@ -1034,77 +1116,68 @@
1034
1116
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
1035
1117
  controller.removeEventListener('IDLE', handleIdle);
1036
1118
  };
1037
- }, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]); // Context value 메모이제이션
1038
-
1119
+ }, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]);
1039
1120
  var contextValue = React.useMemo(function () {
1040
1121
  return {
1041
1122
  registerComponent: registerComponent,
1042
1123
  unregisterComponent: unregisterComponent
1043
1124
  };
1044
1125
  }, [registerComponent, unregisterComponent]);
1045
- return React__default["default"].createElement(KonvaMarkerContext.Provider, {
1126
+ return React__default["default"].createElement(WoongCanvasContext.Provider, {
1046
1127
  value: contextValue
1047
1128
  }, children);
1048
1129
  };
1049
- var useKonvaMarkerContext = function () {
1050
- var context = React.useContext(KonvaMarkerContext);
1051
- return context;
1052
- };
1130
+ /**
1131
+ * WoongCanvas Context Hook
1132
+ *
1133
+ * @returns WoongCanvasContextValue 또는 null (Provider 없으면)
1134
+ */
1053
1135
 
1054
- // ============================================================================
1055
- // 성능 최적화 상수 (30,000개 마커/폴리곤 기준 최적화)
1056
- // ============================================================================
1136
+ var useWoongCanvasContext = function () {
1137
+ return React.useContext(WoongCanvasContext);
1138
+ };
1057
1139
 
1058
1140
  /**
1059
- * 공간 인덱스 그리드 셀 크기 (px)
1141
+ * 공간 인덱스 그리드 셀 크기 (픽셀 단위)
1060
1142
  *
1061
- * 최적값 계산:
1062
- * - 목표: 클릭 시 셀당 10~30개 항목만 체크 (빠른 Hit Test)
1063
- * - 화면 크기: 1920×1080 기준
1064
- * - 30,000개 항목 → 50px 셀 크기 = 약 800개 셀 = 셀당 ~37개
1143
+ * @default 100
1065
1144
  *
1066
- * 성능 비교 (30,000개 기준):
1067
- * - 200px: 셀당 ~577개 Hit Test O(577) ❌ 느림
1068
- * - 50px: 셀당 ~37개 → Hit Test O(37) ✅ 15배 빠름!
1145
+ * @remarks
1146
+ * 크기는 평균 마커 크기의 1.5~2배가 적절합니다.
1147
+ * - 마커가 50px 이하: 50px 권장
1148
+ * - 마커가 60-80px: 100px 권장 (현재 설정)
1149
+ * - 마커가 100px 이상: 150-200px 권장
1069
1150
  *
1070
- * 트레이드오프:
1071
- * - 작을수록: Hit Test 빠름, 메모리 사용량 증가
1072
- * - 클수록: 메모리 효율적, Hit Test 느림
1151
+ * 셀 크기가 너무 작으면:
1152
+ * - 마커가 여러 셀에 등록되어 메모리 사용량 증가
1153
+ * - 인덱스 빌드 비용 증가
1154
+ *
1155
+ * 셀 크기가 너무 크면:
1156
+ * - 한 셀에 많은 마커가 들어가서 Hit Test 시 후보 항목이 많아짐
1157
+ * - Hit Test 성능 저하
1073
1158
  */
1074
- var SPATIAL_GRID_CELL_SIZE = 50;
1159
+ var SPATIAL_GRID_CELL_SIZE = 100;
1075
1160
  /**
1076
- * 뷰포트 컬링 여유 공간 (px)
1161
+ * 뷰포트 컬링 여유 공간 (픽셀 단위)
1077
1162
  *
1078
- * 화면 밖 100px까지 렌더링하여 스크롤 시 부드러운 전환
1079
- * 30,000개 중 실제 렌더링: 화면에 보이는 1,000~3,000개만
1163
+ * @default 100
1080
1164
  */
1081
1165
 
1082
1166
  var DEFAULT_CULLING_MARGIN = 100;
1083
1167
  /**
1084
1168
  * LRU 캐시 최대 항목 수
1085
1169
  *
1086
- * 좌표 변환 결과 캐싱 (positionToOffset 연산 비용 절약)
1087
- *
1088
- * 최적값 계산:
1089
- * - 전체 항목: 30,000개
1090
- * - 캐시 크기: 30,000개 → 100% 히트율 (메모리: ~2.4MB)
1091
- *
1092
- * 메모리 사용량 (항목당 ~80 bytes):
1093
- * - 10,000개: ~800KB → 캐시 히트율 33% ❌
1094
- * - 30,000개: ~2.4MB → 캐시 히트율 100% ✅
1095
- *
1096
- * zoom/pan 시 어차피 clear() 호출되므로 메모리 누적 없음
1170
+ * @default 30000
1097
1171
  */
1098
1172
 
1099
1173
  var DEFAULT_MAX_CACHE_SIZE = 30000;
1100
1174
  /**
1101
- * LRU (Least Recently Used) Cache
1102
- * 메모리 제한을 위한 캐시 구현 (최적화 버전)
1175
+ * LRU Cache (Least Recently Used)
1103
1176
  *
1104
- * 개선 사항:
1105
- * 1. get() 성능 향상: 접근 빈도 추적 없이 단순 조회만 수행 (delete+set 제거)
1106
- * 2. set() 버그 수정: 기존 업데이트 시 maxSize 체크 로직 개선
1107
- * 3. 메모리 효율: 단순 FIFO 캐시로 동작하여 오버헤드 최소화
1177
+ * 좌표 변환 결과를 캐싱하기 위한 캐시 구현
1178
+ *
1179
+ * @template K 캐시타입
1180
+ * @template V 캐시 타입
1108
1181
  */
1109
1182
 
1110
1183
  var LRUCache =
@@ -1117,64 +1190,43 @@
1117
1190
 
1118
1191
  this.cache = new Map();
1119
1192
  this.maxSize = maxSize;
1120
- }
1121
- /**
1122
- * 캐시에서 값 조회
1123
- *
1124
- * 최적화: delete+set 제거
1125
- * - 이전: 매번 delete+set으로 LRU 갱신 (해시 재계산 비용)
1126
- * - 현재: 단순 조회만 수행 (O(1) 해시 조회)
1127
- *
1128
- * 트레이드오프:
1129
- * - 장점: 읽기 성능 대폭 향상 (10,000번 get → 이전보다 2배 빠름)
1130
- * - 단점: 접근 빈도가 아닌 삽입 순서 기반 eviction (FIFO)
1131
- *
1132
- * WoongKonvaMarker 사용 사례에 최적:
1133
- * - 좌표 변환 결과는 zoom/pan 시 어차피 전체 초기화
1134
- * - 접근 빈도 추적보다 빠른 조회가 더 중요
1135
- */
1193
+ } // 캐시에서 값 조회
1136
1194
 
1137
1195
 
1138
1196
  LRUCache.prototype.get = function (key) {
1139
1197
  return this.cache.get(key);
1140
- };
1141
- /**
1142
- * 캐시에 값 저장 (버그 수정 + 최적화)
1143
- *
1144
- * 수정 사항:
1145
- * 1. 기존 키 업데이트 시 크기 체크 누락 버그 수정
1146
- * 2. 로직 명확화: 기존 항목/신규 항목 분리 처리
1147
- */
1198
+ }; // 캐시에 값 저장 (FIFO eviction)
1148
1199
 
1149
1200
 
1150
1201
  LRUCache.prototype.set = function (key, value) {
1151
1202
  var exists = this.cache.has(key);
1152
1203
 
1153
1204
  if (exists) {
1154
- // 기존 항목 업데이트: 단순 덮어쓰기 (크기 변화 없음)
1155
1205
  this.cache.set(key, value);
1156
- } else {
1157
- // 신규 항목 추가: 크기 체크 필요
1158
- if (this.cache.size >= this.maxSize) {
1159
- // 가장 오래된 항목 제거 (Map의 첫 번째 항목)
1160
- var firstKey = this.cache.keys().next().value;
1206
+ return;
1207
+ }
1161
1208
 
1162
- if (firstKey !== undefined) {
1163
- this.cache.delete(firstKey);
1164
- }
1165
- }
1209
+ if (this.cache.size >= this.maxSize) {
1210
+ var firstKey = this.cache.keys().next().value;
1166
1211
 
1167
- this.cache.set(key, value);
1212
+ if (firstKey !== undefined) {
1213
+ this.cache.delete(firstKey);
1214
+ }
1168
1215
  }
1169
- };
1216
+
1217
+ this.cache.set(key, value);
1218
+ }; // 캐시 초기화
1219
+
1170
1220
 
1171
1221
  LRUCache.prototype.clear = function () {
1172
1222
  this.cache.clear();
1173
- };
1223
+ }; // 캐시 크기 반환
1224
+
1174
1225
 
1175
1226
  LRUCache.prototype.size = function () {
1176
1227
  return this.cache.size;
1177
- };
1228
+ }; // 키 존재 여부 확인
1229
+
1178
1230
 
1179
1231
  LRUCache.prototype.has = function (key) {
1180
1232
  return this.cache.has(key);
@@ -1184,16 +1236,10 @@
1184
1236
  }();
1185
1237
  /**
1186
1238
  * Spatial Hash Grid (공간 해시 그리드)
1187
- * 공간 인덱싱을 위한 그리드 기반 자료구조 (개선 버전)
1188
1239
  *
1189
- * 개선 사항:
1190
- * 1. 중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전
1191
- * 2. 메모리 누수 방지: 기존 항목 자동 제거
1192
- * 3. 성능 최적화: 불필요한 배열 생성 최소화
1240
+ * 빠른 Hit Test를 위한 그리드 기반 공간 인덱싱 자료구조
1193
1241
  *
1194
- * 사용 사례:
1195
- * - 빠른 Hit Test (마우스 클릭 시 어떤 마커/폴리곤인지 찾기)
1196
- * - 30,000개 항목 → 클릭 위치 주변 ~10개만 체크 (3,000배 빠름)
1242
+ * @template T 인덱싱할 항목 타입
1197
1243
  */
1198
1244
 
1199
1245
  var SpatialHashGrid =
@@ -1201,34 +1247,30 @@
1201
1247
  function () {
1202
1248
  function SpatialHashGrid(cellSize) {
1203
1249
  if (cellSize === void 0) {
1204
- cellSize = 200;
1250
+ cellSize = SPATIAL_GRID_CELL_SIZE;
1205
1251
  }
1206
1252
 
1207
1253
  this.cellSize = cellSize;
1208
1254
  this.grid = new Map();
1209
1255
  this.itemToCells = new Map();
1210
- }
1211
- /**
1212
- * 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
1213
- */
1256
+ } // 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
1214
1257
 
1215
1258
 
1216
1259
  SpatialHashGrid.prototype.getCellKey = function (x, y) {
1260
+ // 좌표를 셀 크기로 나눈 몫으로 셀 인덱스 계산
1217
1261
  var cellX = Math.floor(x / this.cellSize);
1218
1262
  var cellY = Math.floor(y / this.cellSize);
1219
1263
  return "".concat(cellX, ",").concat(cellY);
1220
- };
1221
- /**
1222
- * 바운딩 박스가 걸치는 모든 셀 키 배열 반환
1223
- */
1264
+ }; // 바운딩 박스가 걸치는 모든 셀 키 배열 반환
1224
1265
 
1225
1266
 
1226
1267
  SpatialHashGrid.prototype.getCellsForBounds = function (minX, minY, maxX, maxY) {
1227
- var cells = [];
1268
+ var cells = []; // 바운딩 박스가 걸치는 셀 범위 계산
1269
+
1228
1270
  var startCellX = Math.floor(minX / this.cellSize);
1229
1271
  var startCellY = Math.floor(minY / this.cellSize);
1230
1272
  var endCellX = Math.floor(maxX / this.cellSize);
1231
- var endCellY = Math.floor(maxY / this.cellSize);
1273
+ var endCellY = Math.floor(maxY / this.cellSize); // 바운딩 박스가 걸치는 모든 셀을 배열에 추가
1232
1274
 
1233
1275
  for (var x = startCellX; x <= endCellX; x++) {
1234
1276
  for (var y = startCellY; y <= endCellY; y++) {
@@ -1237,22 +1279,15 @@
1237
1279
  }
1238
1280
 
1239
1281
  return cells;
1240
- };
1241
- /**
1242
- * 항목 추가 (바운딩 박스 기반)
1243
- *
1244
- * 개선 사항:
1245
- * - 중복 삽입 방지: 기존 항목이 있으면 먼저 제거 후 재삽입
1246
- * - 메모리 누수 방지: 이전 셀 참조 완전 제거
1247
- */
1282
+ }; // 항목 추가 (바운딩 박스 기반, 중복 삽입 방지)
1248
1283
 
1249
1284
 
1250
1285
  SpatialHashGrid.prototype.insert = function (item, minX, minY, maxX, maxY) {
1251
- // 1. 기존 항목 제거 (중복 방지)
1252
- this.remove(item); // 2. 위치에 삽입
1286
+ // 기존 항목 제거 (중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전)
1287
+ this.remove(item); // 바운딩 박스가 걸치는 모든 셀에 항목 등록
1253
1288
 
1254
1289
  var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
1255
- this.itemToCells.set(item, cells);
1290
+ this.itemToCells.set(item, cells); // 항목과 셀의 매핑 저장 (제거 시 필요)
1256
1291
 
1257
1292
  for (var _i = 0, cells_1 = cells; _i < cells_1.length; _i++) {
1258
1293
  var cell = cells_1[_i];
@@ -1263,17 +1298,12 @@
1263
1298
 
1264
1299
  this.grid.get(cell).push(item);
1265
1300
  }
1266
- };
1267
- /**
1268
- * 항목 제거
1269
- *
1270
- * 추가된 메서드: 메모리 누수 방지 및 업데이트 지원
1271
- */
1301
+ }; // 항목 제거 (모든 셀에서 참조 제거)
1272
1302
 
1273
1303
 
1274
1304
  SpatialHashGrid.prototype.remove = function (item) {
1275
1305
  var prevCells = this.itemToCells.get(item);
1276
- if (!prevCells) return; // 셀에서 항목 제거
1306
+ if (!prevCells) return; // 항목이 등록된 모든 셀에서 참조 제거 (메모리 누수 방지)
1277
1307
 
1278
1308
  for (var _i = 0, prevCells_1 = prevCells; _i < prevCells_1.length; _i++) {
1279
1309
  var cell = prevCells_1[_i];
@@ -1284,51 +1314,39 @@
1284
1314
 
1285
1315
  if (index !== -1) {
1286
1316
  cellItems.splice(index, 1);
1287
- } // 빈 셀 정리 (메모리 효율)
1317
+ } // 빈 셀 정리 (메모리 효율: 사용하지 않는 셀 제거)
1288
1318
 
1289
1319
 
1290
1320
  if (cellItems.length === 0) {
1291
1321
  this.grid.delete(cell);
1292
1322
  }
1293
1323
  }
1294
- }
1324
+ } // 항목과 셀의 매핑 제거
1325
+
1295
1326
 
1296
1327
  this.itemToCells.delete(item);
1297
- };
1298
- /**
1299
- * 항목 위치 업데이트
1300
- *
1301
- * 추가된 메서드: remove + insert의 편의 함수
1302
- */
1328
+ }; // 항목 위치 업데이트 (remove + insert)
1303
1329
 
1304
1330
 
1305
1331
  SpatialHashGrid.prototype.update = function (item, minX, minY, maxX, maxY) {
1306
1332
  this.insert(item, minX, minY, maxX, maxY);
1307
- };
1308
- /**
1309
- * 점 주변의 항목 조회 (1개 셀만)
1310
- *
1311
- * 성능: O(해당 셀의 항목 수) - 보통 ~10개
1312
- */
1333
+ }; // 점 주변의 항목 조회 (Hit Test용)
1313
1334
 
1314
1335
 
1315
1336
  SpatialHashGrid.prototype.queryPoint = function (x, y) {
1337
+ // 클릭 위치가 속한 셀의 모든 항목 조회 (O(1) 수준의 빠른 조회)
1316
1338
  var cellKey = this.getCellKey(x, y);
1317
1339
  var items = this.grid.get(cellKey); // 빈 배열 재사용 (메모리 할당 최소화)
1318
1340
 
1319
1341
  return items || [];
1320
- };
1321
- /**
1322
- * 영역 내 항목 조회
1323
- *
1324
- * 성능: O(셀 개수 × 셀당 평균 항목 수)
1325
- * Set으로 중복 제거 보장
1326
- */
1342
+ }; // 영역 내 항목 조회 (Viewport Culling용)
1327
1343
 
1328
1344
 
1329
1345
  SpatialHashGrid.prototype.queryBounds = function (minX, minY, maxX, maxY) {
1346
+ // 영역이 걸치는 모든 셀 찾기
1330
1347
  var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
1331
- var results = new Set();
1348
+ var results = new Set(); // 중복 제거를 위해 Set 사용
1349
+ // 각 셀의 모든 항목을 결과에 추가
1332
1350
 
1333
1351
  for (var _i = 0, cells_2 = cells; _i < cells_2.length; _i++) {
1334
1352
  var cell = cells_2[_i];
@@ -1337,37 +1355,24 @@
1337
1355
  if (items) {
1338
1356
  for (var _a = 0, items_1 = items; _a < items_1.length; _a++) {
1339
1357
  var item = items_1[_a];
1340
- results.add(item);
1358
+ results.add(item); // Set이므로 중복 자동 제거
1341
1359
  }
1342
1360
  }
1343
1361
  }
1344
1362
 
1345
1363
  return Array.from(results);
1346
- };
1347
- /**
1348
- * 항목 존재 여부 확인
1349
- *
1350
- * 추가된 메서드: 빠른 존재 여부 체크
1351
- */
1364
+ }; // 항목 존재 여부 확인
1352
1365
 
1353
1366
 
1354
1367
  SpatialHashGrid.prototype.has = function (item) {
1355
1368
  return this.itemToCells.has(item);
1356
- };
1357
- /**
1358
- * 전체 초기화
1359
- */
1369
+ }; // 전체 초기화
1360
1370
 
1361
1371
 
1362
1372
  SpatialHashGrid.prototype.clear = function () {
1363
1373
  this.grid.clear();
1364
1374
  this.itemToCells.clear();
1365
- };
1366
- /**
1367
- * 통계 정보
1368
- *
1369
- * 개선: totalItems는 실제 고유 항목 수를 정확히 반환
1370
- */
1375
+ }; // 통계 정보 반환
1371
1376
 
1372
1377
 
1373
1378
  SpatialHashGrid.prototype.stats = function () {
@@ -1385,89 +1390,347 @@
1385
1390
  return SpatialHashGrid;
1386
1391
  }();
1387
1392
 
1388
- var cn$3 = classNames__default["default"].bind(styles$1);
1389
- function MintMapCore(_a) {
1390
- var _this = this;
1393
+ /**
1394
+ * 현재 뷰포트 영역 계산
1395
+ *
1396
+ * @param stage Konva Stage 인스턴스
1397
+ * @param cullingMargin 컬링 여유 공간 (px)
1398
+ * @param viewportRef 뷰포트 경계를 저장할 ref
1399
+ */
1400
+ var updateViewport = function (stage, cullingMargin, viewportRef) {
1401
+ if (!stage) return;
1402
+ viewportRef.current = {
1403
+ minX: -cullingMargin,
1404
+ maxX: stage.width() + cullingMargin,
1405
+ minY: -cullingMargin,
1406
+ maxY: stage.height() + cullingMargin
1407
+ };
1408
+ };
1409
+ /**
1410
+ * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
1411
+ *
1412
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1413
+ * @param item 확인할 아이템
1414
+ * @param viewportRef 뷰포트 경계 ref
1415
+ * @param boundingBoxCacheRef 바운딩 박스 캐시 ref
1416
+ * @param computeBoundingBox 바운딩 박스 계산 함수
1417
+ * @returns 뷰포트 안에 있으면 true
1418
+ */
1391
1419
 
1392
- var onLoad = _a.onLoad,
1393
- _b = _a.visible,
1394
- visible = _b === void 0 ? true : _b,
1395
- zoomLevel = _a.zoomLevel,
1396
- center = _a.center,
1397
- _c = _a.centerMoveWithPanning,
1398
- centerMoveWithPanning = _c === void 0 ? false : _c,
1399
- children = _a.children; //controller
1420
+ var isInViewport = function (item, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
1421
+ if (!viewportRef.current) return true;
1422
+ var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
1400
1423
 
1401
- var controller = useMintMapController(); //맵 초기화
1424
+ var bbox = boundingBoxCacheRef.current.get(item.id);
1402
1425
 
1403
- var elementRef = React.useRef(null);
1426
+ if (!bbox) {
1427
+ // 바운딩 박스 계산 (공통 함수 사용)
1428
+ var computed = computeBoundingBox(item);
1429
+ if (!computed) return false;
1430
+ bbox = computed;
1431
+ boundingBoxCacheRef.current.set(item.id, bbox);
1432
+ } // 바운딩 박스와 viewport 교차 체크
1404
1433
 
1405
- var _d = React.useState(false),
1406
- mapInitialized = _d[0],
1407
- setMapInitialized = _d[1];
1408
1434
 
1409
- var currMapInitialized = React.useRef(false);
1410
- React.useEffect(function () {
1411
- (function () {
1412
- return tslib.__awaiter(_this, void 0, void 0, function () {
1413
- var map_1;
1414
- return tslib.__generator(this, function (_a) {
1415
- switch (_a.label) {
1416
- case 0:
1417
- if (!(elementRef && elementRef.current)) return [3
1418
- /*break*/
1419
- , 2];
1420
- return [4
1421
- /*yield*/
1422
- , controller.initializingMap(elementRef.current)];
1435
+ return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
1436
+ };
1423
1437
 
1424
- case 1:
1425
- map_1 = _a.sent();
1438
+ /**
1439
+ * 지도 이벤트 핸들러 생성 함수
1440
+ *
1441
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1442
+ * @param deps 이벤트 핸들러 생성에 필요한 의존성
1443
+ * @returns 지도 이벤트 핸들러 객체
1444
+ */
1426
1445
 
1427
- if (!currMapInitialized.current) {
1428
- currMapInitialized.current = true; //onload callback (setTimeout 으로 맵이 초기화 될 텀을 준다. 특히 google map..)
1446
+ var createMapEventHandlers = function (deps) {
1447
+ var controller = deps.controller,
1448
+ containerRef = deps.containerRef,
1449
+ markerRef = deps.markerRef,
1450
+ options = deps.options,
1451
+ prevCenterOffsetRef = deps.prevCenterOffsetRef,
1452
+ accumTranslateRef = deps.accumTranslateRef,
1453
+ offsetCacheRef = deps.offsetCacheRef,
1454
+ boundingBoxCacheRef = deps.boundingBoxCacheRef,
1455
+ renderAllImmediate = deps.renderAllImmediate; // 지도 이동/줌 완료 시 처리 (캐시 초기화 및 렌더링)
1429
1456
 
1430
- setTimeout(function () {
1431
- // console.log('setMapInitialized true');
1432
- setMapInitialized(true);
1433
- onLoad && onLoad(map_1, controller);
1434
- }, 100);
1435
- }
1457
+ var handleIdle = function () {
1458
+ prevCenterOffsetRef.current = null;
1459
+ accumTranslateRef.current = {
1460
+ x: 0,
1461
+ y: 0
1462
+ }; // 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
1436
1463
 
1437
- _a.label = 2;
1464
+ offsetCacheRef.current.clear();
1465
+ boundingBoxCacheRef.current.clear(); // 마커 위치 업데이트
1438
1466
 
1439
- case 2:
1440
- return [2
1441
- /*return*/
1442
- ];
1443
- }
1444
- });
1445
- });
1446
- })();
1447
- }, [controller, elementRef]); //줌레벨
1467
+ var bounds = controller.getCurrBounds();
1448
1468
 
1449
- React.useEffect(function () {
1450
- if (zoomLevel && controller && mapInitialized) {
1451
- var prevZoomLevel = controller === null || controller === void 0 ? void 0 : controller.getZoomLevel();
1469
+ var markerOptions = tslib.__assign({
1470
+ position: bounds.nw
1471
+ }, options);
1452
1472
 
1453
- if (prevZoomLevel !== zoomLevel) {
1454
- controller === null || controller === void 0 ? void 0 : controller.setZoomLevel(zoomLevel);
1455
- }
1456
- }
1457
- }, [zoomLevel]); //센터
1473
+ markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (transform 제거 시 잠깐 빈 화면이 보이는 것 방지)
1458
1474
 
1459
- React.useEffect(function () {
1460
- if (center && controller && mapInitialized) {
1461
- var prevCenter = controller.getCenter();
1475
+ if (containerRef.current) {
1476
+ containerRef.current.style.transform = '';
1477
+ containerRef.current.style.visibility = '';
1478
+ } // 새 위치에서 렌더링 (캐시는 이미 초기화됨)
1462
1479
 
1463
- if (!Position.equals(prevCenter, center)) {
1464
- centerMoveWithPanning ? controller === null || controller === void 0 ? void 0 : controller.panningTo(center) : controller === null || controller === void 0 ? void 0 : controller.setCenter(center);
1465
- }
1466
- }
1467
- }, [center]);
1468
- return React__default["default"].createElement("div", {
1469
- className: cn$3('mint-map-root')
1470
- }, mapInitialized && React__default["default"].createElement(KonvaMarkerProvider, null, children), React__default["default"].createElement("div", {
1480
+
1481
+ renderAllImmediate();
1482
+ }; // 줌 시작 시 처리 (일시적으로 숨김)
1483
+
1484
+
1485
+ var handleZoomStart = function () {
1486
+ if (!containerRef.current) return;
1487
+ containerRef.current.style.visibility = 'hidden';
1488
+ }; // 줌 종료 시 처리 (다시 표시)
1489
+
1490
+
1491
+ var handleZoomEnd = function () {
1492
+ if (!containerRef.current) return;
1493
+ containerRef.current.style.visibility = '';
1494
+ }; // 지도 중심 변경 시 처리 (transform으로 이동 추적, 캐시 유지)
1495
+
1496
+
1497
+ var handleCenterChanged = function () {
1498
+ var center = controller.getCurrBounds().getCenter();
1499
+ var curr = controller.positionToOffset(center);
1500
+ var prev = prevCenterOffsetRef.current; // 첫 번째 호출 시 이전 위치 저장만 하고 종료
1501
+
1502
+ if (!prev) {
1503
+ prevCenterOffsetRef.current = {
1504
+ x: curr.x,
1505
+ y: curr.y
1506
+ };
1507
+ return;
1508
+ } // 이전 위치와 현재 위치의 차이 계산 (이동 거리)
1509
+
1510
+
1511
+ var dx = prev.x - curr.x;
1512
+ var dy = prev.y - curr.y; // 누적 이동 거리 저장 (transform으로 화면만 이동, 캐시는 유지하여 성능 최적화)
1513
+
1514
+ accumTranslateRef.current = {
1515
+ x: accumTranslateRef.current.x + dx,
1516
+ y: accumTranslateRef.current.y + dy
1517
+ };
1518
+ prevCenterOffsetRef.current = {
1519
+ x: curr.x,
1520
+ y: curr.y
1521
+ }; // CSS transform으로 컨테이너 이동 (캐시된 좌표는 그대로 유지)
1522
+
1523
+ if (containerRef.current) {
1524
+ containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
1525
+ }
1526
+ };
1527
+
1528
+ var handleDragStart = function () {// 커서는 각 컴포넌트에서 처리
1529
+ };
1530
+
1531
+ var handleDragEnd = function () {// 커서는 각 컴포넌트에서 처리
1532
+ };
1533
+
1534
+ return {
1535
+ handleIdle: handleIdle,
1536
+ handleZoomStart: handleZoomStart,
1537
+ handleZoomEnd: handleZoomEnd,
1538
+ handleCenterChanged: handleCenterChanged,
1539
+ handleDragStart: handleDragStart,
1540
+ handleDragEnd: handleDragEnd
1541
+ };
1542
+ };
1543
+ /**
1544
+ * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
1545
+ *
1546
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1547
+ * @param data 공간 인덱스에 삽입할 데이터 배열
1548
+ * @param spatialIndex Spatial Hash Grid 인스턴스
1549
+ * @param computeBoundingBox 바운딩 박스 계산 함수
1550
+ */
1551
+
1552
+ var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
1553
+ spatialIndex.clear();
1554
+
1555
+ for (var _i = 0, data_1 = data; _i < data_1.length; _i++) {
1556
+ var item = data_1[_i];
1557
+ var bbox = computeBoundingBox(item);
1558
+
1559
+ if (bbox) {
1560
+ spatialIndex.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
1561
+ }
1562
+ }
1563
+ };
1564
+ /**
1565
+ * 선택 상태 동기화 (화면 밖 데이터도 선택 상태 유지)
1566
+ *
1567
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1568
+ * @param data 최신 데이터 배열
1569
+ * @param selectedIds 선택된 항목 ID Set
1570
+ * @param selectedItemsMap 현재 선택된 항목 Map
1571
+ * @returns 업데이트된 선택된 항목 Map
1572
+ */
1573
+
1574
+ var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
1575
+ var dataMap = new Map(data.map(function (m) {
1576
+ return [m.id, m];
1577
+ }));
1578
+ var newSelectedItemsMap = new Map();
1579
+ selectedIds.forEach(function (id) {
1580
+ // 현재 data에 있으면 최신 데이터 사용
1581
+ var currentItem = dataMap.get(id);
1582
+
1583
+ if (currentItem) {
1584
+ newSelectedItemsMap.set(id, currentItem);
1585
+ } else {
1586
+ // 화면 밖이면 기존 데이터 유지
1587
+ var prevItem = selectedItemsMap.get(id);
1588
+
1589
+ if (prevItem) {
1590
+ newSelectedItemsMap.set(id, prevItem);
1591
+ }
1592
+ }
1593
+ });
1594
+ return newSelectedItemsMap;
1595
+ };
1596
+ /**
1597
+ * 외부 selectedItems를 내부 상태로 동기화
1598
+ *
1599
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1600
+ * @param externalSelectedItems 외부에서 전달된 선택된 항목 배열
1601
+ * @param selectedIdsRef 선택된 ID Set ref
1602
+ * @param selectedItemsMapRef 선택된 항목 Map ref
1603
+ */
1604
+
1605
+ var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef, selectedItemsMapRef) {
1606
+ if (externalSelectedItems === undefined) return;
1607
+ var newSelectedIds = new Set();
1608
+ var newSelectedItemsMap = new Map();
1609
+ externalSelectedItems.forEach(function (item) {
1610
+ newSelectedIds.add(item.id);
1611
+ newSelectedItemsMap.set(item.id, item);
1612
+ });
1613
+ selectedIdsRef.current = newSelectedIds;
1614
+ selectedItemsMapRef.current = newSelectedItemsMap;
1615
+ };
1616
+
1617
+ /**
1618
+ * 이벤트 유효성 검증 및 좌표 변환
1619
+ *
1620
+ * @param event 이벤트 파라미터
1621
+ * @param context WoongCanvasContext 인스턴스
1622
+ * @param controller MintMapController 인스턴스
1623
+ * @returns 유효한 화면 좌표 또는 null
1624
+ */
1625
+ var validateEvent = function (event, context, controller) {
1626
+ var _a;
1627
+
1628
+ if (context) return null;
1629
+ if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return null;
1630
+
1631
+ try {
1632
+ return controller.positionToOffset(event.param.position);
1633
+ } catch (error) {
1634
+ console.error('[WoongCanvas] validateEvent error:', error);
1635
+ return null;
1636
+ }
1637
+ };
1638
+ /**
1639
+ * Map의 values를 배열로 변환
1640
+ *
1641
+ * @template T Map 값의 타입
1642
+ * @param map 변환할 Map
1643
+ * @returns Map의 값 배열
1644
+ */
1645
+
1646
+ var mapValuesToArray = function (map) {
1647
+ if (map.size === 0) return [];
1648
+ return Array.from(map.values());
1649
+ };
1650
+
1651
+ var cn$3 = classNames__default["default"].bind(styles$1);
1652
+ function MintMapCore(_a) {
1653
+ var _this = this;
1654
+
1655
+ var onLoad = _a.onLoad,
1656
+ _b = _a.visible,
1657
+ visible = _b === void 0 ? true : _b,
1658
+ zoomLevel = _a.zoomLevel,
1659
+ center = _a.center,
1660
+ _c = _a.centerMoveWithPanning,
1661
+ centerMoveWithPanning = _c === void 0 ? false : _c,
1662
+ children = _a.children; //controller
1663
+
1664
+ var controller = useMintMapController(); //맵 초기화
1665
+
1666
+ var elementRef = React.useRef(null);
1667
+
1668
+ var _d = React.useState(false),
1669
+ mapInitialized = _d[0],
1670
+ setMapInitialized = _d[1];
1671
+
1672
+ var currMapInitialized = React.useRef(false);
1673
+ React.useEffect(function () {
1674
+ (function () {
1675
+ return tslib.__awaiter(_this, void 0, void 0, function () {
1676
+ var map_1;
1677
+ return tslib.__generator(this, function (_a) {
1678
+ switch (_a.label) {
1679
+ case 0:
1680
+ if (!(elementRef && elementRef.current)) return [3
1681
+ /*break*/
1682
+ , 2];
1683
+ return [4
1684
+ /*yield*/
1685
+ , controller.initializingMap(elementRef.current)];
1686
+
1687
+ case 1:
1688
+ map_1 = _a.sent();
1689
+
1690
+ if (!currMapInitialized.current) {
1691
+ currMapInitialized.current = true; //onload callback (setTimeout 으로 맵이 초기화 될 텀을 준다. 특히 google map..)
1692
+
1693
+ setTimeout(function () {
1694
+ // console.log('setMapInitialized true');
1695
+ setMapInitialized(true);
1696
+ onLoad && onLoad(map_1, controller);
1697
+ }, 100);
1698
+ }
1699
+
1700
+ _a.label = 2;
1701
+
1702
+ case 2:
1703
+ return [2
1704
+ /*return*/
1705
+ ];
1706
+ }
1707
+ });
1708
+ });
1709
+ })();
1710
+ }, [controller, elementRef]); //줌레벨
1711
+
1712
+ React.useEffect(function () {
1713
+ if (zoomLevel && controller && mapInitialized) {
1714
+ var prevZoomLevel = controller === null || controller === void 0 ? void 0 : controller.getZoomLevel();
1715
+
1716
+ if (prevZoomLevel !== zoomLevel) {
1717
+ controller === null || controller === void 0 ? void 0 : controller.setZoomLevel(zoomLevel);
1718
+ }
1719
+ }
1720
+ }, [zoomLevel]); //센터
1721
+
1722
+ React.useEffect(function () {
1723
+ if (center && controller && mapInitialized) {
1724
+ var prevCenter = controller.getCenter();
1725
+
1726
+ if (!Position.equals(prevCenter, center)) {
1727
+ centerMoveWithPanning ? controller === null || controller === void 0 ? void 0 : controller.panningTo(center) : controller === null || controller === void 0 ? void 0 : controller.setCenter(center);
1728
+ }
1729
+ }
1730
+ }, [center]);
1731
+ return React__default["default"].createElement("div", {
1732
+ className: cn$3('mint-map-root')
1733
+ }, mapInitialized && React__default["default"].createElement(WoongCanvasProvider, null, children), React__default["default"].createElement("div", {
1471
1734
  className: cn$3('mint-map-container'),
1472
1735
  style: {
1473
1736
  visibility: visible ? 'inherit' : 'hidden'
@@ -5325,602 +5588,1138 @@
5325
5588
  }))));
5326
5589
  }
5327
5590
 
5328
- // 메인 컴포넌트
5329
- // ============================================================================
5330
-
5331
- /**
5332
- * Konva 기반 고성능 마커/폴리곤 렌더링 컴포넌트
5333
- *
5334
- * 특징:
5335
- * - Base/Event 레이어 분리로 성능 최적화
5336
- * - LRU 캐시로 좌표 변환 결과 캐싱
5337
- * - Spatial Hash Grid로 빠른 Hit Test
5338
- * - Viewport Culling으로 보이는 영역만 렌더링
5339
- *
5340
- * @template T 마커 데이터의 추가 속성 타입
5341
- */
5342
-
5343
- var WoongKonvaMarkerComponent = function (_a) {
5344
- var markers = _a.markers,
5345
- dataType = _a.dataType,
5346
- onClick = _a.onClick,
5347
- onMouseOver = _a.onMouseOver,
5348
- onMouseOut = _a.onMouseOut,
5349
- renderBase = _a.renderBase,
5350
- renderAnimation = _a.renderAnimation,
5351
- renderEvent = _a.renderEvent,
5352
- _b = _a.enableMultiSelect,
5353
- enableMultiSelect = _b === void 0 ? false : _b,
5354
- _c = _a.topOnHover,
5355
- topOnHover = _c === void 0 ? false : _c,
5356
- _d = _a.enableViewportCulling,
5357
- enableViewportCulling = _d === void 0 ? true : _d,
5358
- _e = _a.cullingMargin,
5359
- cullingMargin = _e === void 0 ? DEFAULT_CULLING_MARGIN : _e,
5360
- _f = _a.maxCacheSize,
5361
- maxCacheSize = _f === void 0 ? DEFAULT_MAX_CACHE_SIZE : _f,
5362
- externalSelectedItems = _a.selectedItems,
5363
- options = tslib.__rest(_a, ["markers", "dataType", "onClick", "onMouseOver", "onMouseOut", "renderBase", "renderAnimation", "renderEvent", "enableMultiSelect", "topOnHover", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems"]); // --------------------------------------------------------------------------
5364
- // Hooks & Context
5365
- // --------------------------------------------------------------------------
5366
-
5591
+ var WoongCanvasMarker = function (props) {
5592
+ var data = props.data,
5593
+ onClick = props.onClick,
5594
+ onMouseOver = props.onMouseOver,
5595
+ onMouseOut = props.onMouseOut,
5596
+ _a = props.enableMultiSelect,
5597
+ enableMultiSelect = _a === void 0 ? false : _a,
5598
+ _b = props.topOnHover,
5599
+ topOnHover = _b === void 0 ? false : _b,
5600
+ _c = props.enableViewportCulling,
5601
+ enableViewportCulling = _c === void 0 ? false : _c,
5602
+ _d = props.cullingMargin,
5603
+ cullingMargin = _d === void 0 ? DEFAULT_CULLING_MARGIN : _d,
5604
+ _e = props.maxCacheSize,
5605
+ maxCacheSize = _e === void 0 ? DEFAULT_MAX_CACHE_SIZE : _e,
5606
+ externalSelectedItems = props.selectedItems,
5607
+ externalSelectedItem = props.selectedItem,
5608
+ _f = props.disableInteraction,
5609
+ disableInteraction = _f === void 0 ? false : _f,
5610
+ renderBase = props.renderBase,
5611
+ renderEvent = props.renderEvent,
5612
+ options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderEvent"]);
5367
5613
 
5368
5614
  var controller = useMintMapController();
5369
- var context = useKonvaMarkerContext();
5370
- var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
5371
- // DOM Refs
5372
- // --------------------------------------------------------------------------
5615
+ var context = useWoongCanvasContext();
5616
+ var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
5373
5617
 
5374
5618
  var divRef = React.useRef(document.createElement('div'));
5375
5619
  var divElement = divRef.current;
5376
5620
  var containerRef = React.useRef(null);
5377
- var markerRef = React.useRef(); // --------------------------------------------------------------------------
5378
- // Konva Refs
5379
- // --------------------------------------------------------------------------
5621
+ var markerRef = React.useRef(); // Konva Refs
5380
5622
 
5381
5623
  var stageRef = React.useRef(null);
5382
5624
  var baseLayerRef = React.useRef(null);
5383
- var animationLayerRef = React.useRef(null);
5384
- var eventLayerRef = React.useRef(null); // --------------------------------------------------------------------------
5385
- // Data Refs - 선택 및 Hover 상태 관리
5386
- // --------------------------------------------------------------------------
5387
-
5388
- /** markers prop을 ref로 추적 (stale closure 방지, useEffect에서 동기화) */
5389
-
5390
- var markersRef = React.useRef(markers); // --------------------------------------------------------------------------
5391
- // State Refs - 선택 및 Hover 상태 관리
5392
- // --------------------------------------------------------------------------
5393
-
5394
- /** 현재 Hover 중인 항목 */
5625
+ var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
5395
5626
 
5627
+ var dataRef = React.useRef(data);
5628
+ var disableInteractionRef = React.useRef(disableInteraction);
5629
+ var enableViewportCullingRef = React.useRef(enableViewportCulling);
5396
5630
  var hoveredItemRef = React.useRef(null);
5397
- /** 마지막으로 클릭된 항목 */
5398
-
5399
- var lastClickedItemRef = React.useRef(null);
5400
- /**
5401
- * 선택된 항목의 ID Set
5402
- *
5403
- * 용도:
5404
- * 1. onClick 콜백에 전달 - onClick(data, selectedIdsRef.current)
5405
- * 2. 선택 여부 빠른 체크 - selectedIdsRef.current.has(id)
5406
- * 3. 메모리 효율 - ID만 저장 (작음)
5407
- *
5408
- * selectedItemsMapRef와 차이:
5409
- * - selectedIdsRef: ID만 저장 { "id1", "id2" }
5410
- * - selectedItemsMapRef: 전체 객체 저장 { id1: {...}, id2: {...} }
5411
- *
5412
- * 둘 다 필요: ID만 필요한 곳은 이것, 전체 데이터 필요한 곳은 Map
5413
- */
5414
-
5631
+ var selectedItemRef = React.useRef(externalSelectedItem);
5415
5632
  var selectedIdsRef = React.useRef(new Set());
5416
- /**
5417
- * 선택된 항목의 실제 데이터 Map (핵심 성능 최적화!)
5418
- *
5419
- * 목적: doRenderEvent에서 filter() 순회 제거
5420
- * - 이전: markersRef.current.filter() → O(전체 마커 수)
5421
- * - 현재: Map.values() → O(선택된 항목 수)
5422
- *
5423
- * 성능 개선: 10,000개 중 1개 선택 시
5424
- * - 이전: 10,000번 체크
5425
- * - 현재: 1번 접근 (10,000배 빠름!)
5426
- */
5427
-
5428
- var selectedItemsMapRef = React.useRef(new Map()); // --------------------------------------------------------------------------
5429
- // Drag Refs
5430
- // --------------------------------------------------------------------------
5633
+ var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
5431
5634
 
5432
5635
  var draggingRef = React.useRef(false);
5433
5636
  var prevCenterOffsetRef = React.useRef(null);
5434
5637
  var accumTranslateRef = React.useRef({
5435
5638
  x: 0,
5436
5639
  y: 0
5437
- }); // --------------------------------------------------------------------------
5438
- // Performance Refs (캐싱 & 최적화)
5439
- // --------------------------------------------------------------------------
5440
-
5441
- /** 좌표 변환 결과 LRU 캐시 */
5640
+ }); // 성능 최적화 Refs
5442
5641
 
5443
5642
  var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
5444
- /** 공간 인덱스 (빠른 Hit Test) */
5445
-
5446
5643
  var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
5447
- /** 바운딩 박스 캐시 (Viewport Culling 최적화) */
5448
-
5449
5644
  var boundingBoxCacheRef = React.useRef(new Map());
5450
- /** 뷰포트 경계 캐시 (Viewport Culling) */
5451
-
5452
- var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
5453
- // 유틸리티 함수: 뷰포트 관리
5454
- // --------------------------------------------------------------------------
5455
-
5456
- /**
5457
- * 현재 뷰포트 영역 계산
5458
- */
5459
-
5460
- var updateViewport = function () {
5461
- if (!stageRef.current) return;
5462
- var stage = stageRef.current;
5463
- viewportRef.current = {
5464
- minX: -cullingMargin,
5465
- maxX: stage.width() + cullingMargin,
5466
- minY: -cullingMargin,
5467
- maxY: stage.height() + cullingMargin
5468
- };
5469
- };
5470
- /**
5471
- * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
5472
- */
5645
+ var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling)
5473
5646
 
5647
+ var updateViewport$1 = function () {
5648
+ updateViewport(stageRef.current, cullingMargin, viewportRef);
5649
+ }; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
5474
5650
 
5475
- var isInViewport = function (item) {
5476
- if (!enableViewportCulling || !viewportRef.current) return true;
5477
- var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
5478
5651
 
5479
- var bbox = boundingBoxCacheRef.current.get(item.id);
5652
+ var isInViewport$1 = function (item) {
5653
+ return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
5654
+ }; // 마커 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
5480
5655
 
5481
- if (!bbox) {
5482
- // 폴리곤인 경우
5483
- if (dataType === exports.CanvasDataType.POLYGON) {
5484
- var offsets = getOrComputePolygonOffsets(item);
5485
- if (!offsets) return false; // 바운딩 박스 계산 (최적화: 직접 비교)
5486
5656
 
5487
- var minX = Infinity,
5488
- minY = Infinity,
5489
- maxX = -Infinity,
5490
- maxY = -Infinity;
5657
+ var getOrComputeMarkerOffset = function (markerData) {
5658
+ var cached = offsetCacheRef.current.get(markerData.id);
5659
+ if (cached && !Array.isArray(cached)) return cached;
5660
+ var result = computeMarkerOffset(markerData, controller);
5661
+ if (!result) return null;
5662
+ offsetCacheRef.current.set(markerData.id, result);
5663
+ return result;
5664
+ }; // 마커 바운딩 박스 계산 (Viewport Culling 및 Hit Test용, 오프셋 지원)
5491
5665
 
5492
- for (var _i = 0, offsets_1 = offsets; _i < offsets_1.length; _i++) {
5493
- var multiPolygon = offsets_1[_i];
5494
5666
 
5495
- for (var _a = 0, multiPolygon_1 = multiPolygon; _a < multiPolygon_1.length; _a++) {
5496
- var polygonGroup = multiPolygon_1[_a];
5667
+ var computeBoundingBox = function (item) {
5668
+ var offset = getOrComputeMarkerOffset(item);
5669
+ if (!offset) return null;
5670
+ var boxWidth = item.boxWidth || 50;
5671
+ var boxHeight = item.boxHeight || 28;
5672
+ var tailHeight = item.tailHeight || 0;
5673
+ var offsetX = item.offsetX || 0;
5674
+ var offsetY = item.offsetY || 0; // 오프셋을 적용한 마커 중심점 기준으로 바운딩 박스 계산
5497
5675
 
5498
- for (var _b = 0, polygonGroup_1 = polygonGroup; _b < polygonGroup_1.length; _b++) {
5499
- var _c = polygonGroup_1[_b],
5500
- x = _c[0],
5501
- y = _c[1];
5502
- if (x < minX) minX = x;
5503
- if (y < minY) minY = y;
5504
- if (x > maxX) maxX = x;
5505
- if (y > maxY) maxY = y;
5506
- }
5507
- }
5508
- }
5676
+ return {
5677
+ minX: offset.x + offsetX - boxWidth / 2,
5678
+ minY: offset.y + offsetY - boxHeight - tailHeight,
5679
+ maxX: offset.x + offsetX + boxWidth / 2,
5680
+ maxY: offset.y + offsetY
5681
+ };
5682
+ }; // 공간 인덱스 빌드 (빠른 Hit Test용)
5509
5683
 
5510
- bbox = {
5511
- minX: minX,
5512
- minY: minY,
5513
- maxX: maxX,
5514
- maxY: maxY
5515
- };
5516
- boundingBoxCacheRef.current.set(item.id, bbox);
5517
- } // 마커인 경우
5518
- else {
5519
- var offset = getOrComputeMarkerOffset(item);
5520
- if (!offset) return false;
5521
- var boxWidth = item.boxWidth || 50;
5522
- var boxHeight = item.boxHeight || 28;
5523
- bbox = {
5524
- minX: offset.x - boxWidth / 2,
5525
- minY: offset.y - boxHeight - 6,
5526
- maxX: offset.x + boxWidth / 2,
5527
- maxY: offset.y
5528
- };
5529
- boundingBoxCacheRef.current.set(item.id, bbox);
5530
- }
5531
- } // 바운딩 박스와 viewport 교차 체크
5532
5684
 
5685
+ var buildSpatialIndex$1 = function () {
5686
+ buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
5687
+ }; // 렌더링 유틸리티 객체
5533
5688
 
5534
- return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
5535
- }; // --------------------------------------------------------------------------
5536
- // 유틸리티 함수: 좌표 변환 캐싱
5537
- // --------------------------------------------------------------------------
5538
5689
 
5539
- /**
5540
- * 폴리곤 좌표 변환 결과를 캐시하고 반환
5541
- * @param polygonData 폴리곤 데이터
5542
- * @returns 변환된 좌표 배열 또는 null
5543
- */
5690
+ var renderUtils = {
5691
+ getOrComputePolygonOffsets: function () {
5692
+ return null;
5693
+ },
5694
+ getOrComputeMarkerOffset: getOrComputeMarkerOffset
5695
+ }; // Base Layer 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
5544
5696
 
5697
+ var doRenderBase = function () {
5698
+ var layer = baseLayerRef.current;
5699
+ if (!layer) return;
5700
+ var shape = layer.findOne('.base-render-shape');
5545
5701
 
5546
- var getOrComputePolygonOffsets = function (polygonData) {
5547
- var cached = offsetCacheRef.current.get(polygonData.id);
5548
- if (cached && Array.isArray(cached)) return cached;
5549
- var result = computePolygonOffsets(polygonData, controller);
5702
+ if (!shape) {
5703
+ shape = new Konva__default["default"].Shape({
5704
+ name: 'base-render-shape',
5705
+ sceneFunc: function (context, shape) {
5706
+ var ctx = context;
5707
+ var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
5550
5708
 
5551
- if (result) {
5552
- offsetCacheRef.current.set(polygonData.id, result);
5553
- }
5709
+ var visibleItems = enableViewportCullingRef.current ? dataRef.current.filter(function (item) {
5710
+ return isInViewport$1(item);
5711
+ }) : dataRef.current; // topOnHover 옵션: hover된 항목을 나중에 그려서 최상위에 표시
5554
5712
 
5555
- return result;
5556
- };
5557
- /**
5558
- * 마커 좌표 변환 결과를 캐시하고 반환
5559
- * @param markerData 마커 데이터
5560
- * @returns 변환된 좌표 또는 null
5561
- */
5713
+ if (topOnHover && !renderEvent && hovered) {
5714
+ visibleItems = visibleItems.filter(function (item) {
5715
+ return item.id !== hovered.id;
5716
+ });
5717
+ } // 일반 항목들 먼저 렌더링
5562
5718
 
5563
5719
 
5564
- var getOrComputeMarkerOffset = function (markerData) {
5565
- var cached = offsetCacheRef.current.get(markerData.id);
5566
- if (cached && !Array.isArray(cached)) return cached;
5567
- var result = computeMarkerOffset(markerData, controller);
5720
+ renderBase({
5721
+ ctx: ctx,
5722
+ items: visibleItems,
5723
+ selectedIds: selectedIdsRef.current,
5724
+ hoveredItem: hovered,
5725
+ utils: renderUtils
5726
+ }); // hover된 항목을 마지막에 렌더링하여 최상위에 표시
5727
+
5728
+ if (topOnHover && !renderEvent && hovered) {
5729
+ if (!enableViewportCullingRef.current || isInViewport$1(hovered)) {
5730
+ renderBase({
5731
+ ctx: ctx,
5732
+ items: [hovered],
5733
+ selectedIds: selectedIdsRef.current,
5734
+ hoveredItem: hovered,
5735
+ utils: renderUtils
5736
+ });
5737
+ }
5738
+ }
5739
+ },
5740
+ perfectDrawEnabled: false,
5741
+ listening: false,
5742
+ hitStrokeWidth: 0
5743
+ });
5744
+ layer.add(shape);
5745
+ }
5746
+
5747
+ layer.batchDraw();
5748
+ }; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
5568
5749
 
5569
- if (result) {
5570
- offsetCacheRef.current.set(markerData.id, result);
5750
+
5751
+ var doRenderEvent = function () {
5752
+ var layer = eventLayerRef.current;
5753
+ if (!layer || !renderEvent) return;
5754
+ var shape = layer.findOne('.event-render-shape');
5755
+
5756
+ if (!shape) {
5757
+ shape = new Konva__default["default"].Shape({
5758
+ name: 'event-render-shape',
5759
+ sceneFunc: function (context, shape) {
5760
+ var ctx = context;
5761
+ var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
5762
+ var hovered = hoveredItemRef.current;
5763
+
5764
+ if (topOnHover && hovered) {
5765
+ renderEvent({
5766
+ ctx: ctx,
5767
+ hoveredItem: null,
5768
+ utils: renderUtils,
5769
+ selectedItems: selectedItems.filter(function (item) {
5770
+ return item.id !== hovered.id;
5771
+ }),
5772
+ selectedItem: selectedItemRef.current
5773
+ });
5774
+
5775
+ if (!enableViewportCullingRef.current || isInViewport$1(hovered)) {
5776
+ var hoveredIsSelected = selectedItems.some(function (item) {
5777
+ return item.id === hovered.id;
5778
+ });
5779
+ var hoverSelectedItems = hoveredIsSelected ? [hovered] : [];
5780
+ renderEvent({
5781
+ ctx: ctx,
5782
+ hoveredItem: hovered,
5783
+ utils: renderUtils,
5784
+ selectedItems: hoverSelectedItems,
5785
+ selectedItem: selectedItemRef.current
5786
+ });
5787
+ }
5788
+ } else {
5789
+ renderEvent({
5790
+ ctx: ctx,
5791
+ hoveredItem: hovered,
5792
+ utils: renderUtils,
5793
+ selectedItems: selectedItems,
5794
+ selectedItem: selectedItemRef.current
5795
+ });
5796
+ }
5797
+ },
5798
+ perfectDrawEnabled: false,
5799
+ listening: false,
5800
+ hitStrokeWidth: 0
5801
+ });
5802
+ layer.add(shape);
5571
5803
  }
5572
5804
 
5573
- return result;
5574
- }; // --------------------------------------------------------------------------
5575
- // 유틸리티 함수: 공간 인덱싱
5576
- // --------------------------------------------------------------------------
5805
+ layer.batchDraw();
5806
+ }; // 전체 즉시 렌더링
5577
5807
 
5578
- /**
5579
- * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
5580
- */
5581
5808
 
5809
+ var renderAllImmediate = function () {
5810
+ if (enableViewportCullingRef.current) {
5811
+ updateViewport$1();
5812
+ }
5582
5813
 
5583
- var buildSpatialIndex = function () {
5584
- var spatial = spatialIndexRef.current;
5585
- spatial.clear();
5586
- var currentMarkers = markersRef.current;
5814
+ buildSpatialIndex$1();
5815
+ doRenderBase();
5816
+ doRenderEvent();
5817
+ }; // 지도 이벤트 핸들러 생성
5818
+
5819
+
5820
+ var _g = createMapEventHandlers({
5821
+ controller: controller,
5822
+ containerRef: containerRef,
5823
+ markerRef: markerRef,
5824
+ options: options,
5825
+ prevCenterOffsetRef: prevCenterOffsetRef,
5826
+ accumTranslateRef: accumTranslateRef,
5827
+ offsetCacheRef: offsetCacheRef,
5828
+ boundingBoxCacheRef: boundingBoxCacheRef,
5829
+ renderAllImmediate: renderAllImmediate
5830
+ }),
5831
+ handleIdle = _g.handleIdle,
5832
+ handleZoomStart = _g.handleZoomStart,
5833
+ handleZoomEnd = _g.handleZoomEnd,
5834
+ handleCenterChanged = _g.handleCenterChanged,
5835
+ handleDragStartShared = _g.handleDragStart,
5836
+ handleDragEndShared = _g.handleDragEnd;
5587
5837
 
5588
- for (var _i = 0, currentMarkers_1 = currentMarkers; _i < currentMarkers_1.length; _i++) {
5589
- var item = currentMarkers_1[_i];
5838
+ var handleDragStart = function () {
5839
+ handleDragStartShared();
5840
+ draggingRef.current = true;
5841
+ controller.setMapCursor('grabbing');
5842
+ };
5590
5843
 
5591
- if (dataType === exports.CanvasDataType.POLYGON) {
5592
- // 폴리곤: 바운딩 박스 계산 (최적화: 직접 비교)
5593
- var offsets = getOrComputePolygonOffsets(item);
5844
+ var handleDragEnd = function () {
5845
+ handleDragEndShared();
5846
+ draggingRef.current = false;
5847
+ controller.setMapCursor('grab');
5848
+ }; // Hit Test: 특정 좌표의 마커 찾기
5594
5849
 
5595
- if (offsets) {
5596
- var minX = Infinity,
5597
- minY = Infinity,
5598
- maxX = -Infinity,
5599
- maxY = -Infinity;
5600
5850
 
5601
- for (var _a = 0, offsets_2 = offsets; _a < offsets_2.length; _a++) {
5602
- var multiPolygon = offsets_2[_a];
5851
+ var findData = function (offset) {
5852
+ // topOnHover 옵션이 켜져 있으면 hover된 항목을 최우선으로 확인
5853
+ if (topOnHover && hoveredItemRef.current) {
5854
+ var hovered = hoveredItemRef.current;
5603
5855
 
5604
- for (var _b = 0, multiPolygon_2 = multiPolygon; _b < multiPolygon_2.length; _b++) {
5605
- var polygonGroup = multiPolygon_2[_b];
5856
+ if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
5857
+ return hovered;
5858
+ }
5859
+ } // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
5606
5860
 
5607
- for (var _c = 0, polygonGroup_2 = polygonGroup; _c < polygonGroup_2.length; _c++) {
5608
- var _d = polygonGroup_2[_c],
5609
- x = _d[0],
5610
- y = _d[1];
5611
- if (x < minX) minX = x;
5612
- if (y < minY) minY = y;
5613
- if (x > maxX) maxX = x;
5614
- if (y > maxY) maxY = y;
5615
- }
5616
- }
5617
- }
5618
5861
 
5619
- spatial.insert(item, minX, minY, maxX, maxY);
5620
- }
5862
+ var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
5863
+
5864
+ for (var i = candidates.length - 1; i >= 0; i--) {
5865
+ var item = candidates[i];
5866
+
5867
+ if (isPointInMarkerData(offset, item, getOrComputeMarkerOffset)) {
5868
+ return item;
5869
+ }
5870
+ }
5871
+
5872
+ return null;
5873
+ }; // Hover 상태 설정 및 렌더링
5874
+
5875
+
5876
+ var setHovered = function (data) {
5877
+ hoveredItemRef.current = data;
5878
+
5879
+ if (draggingRef.current) {
5880
+ controller.setMapCursor('grabbing');
5881
+ } else {
5882
+ controller.setMapCursor(data ? 'pointer' : 'grab');
5883
+ }
5884
+
5885
+ if (renderEvent) {
5886
+ doRenderEvent();
5887
+ } else if (topOnHover) {
5888
+ doRenderBase();
5889
+ }
5890
+ }; // 클릭 처리: 선택 상태 업데이트
5891
+
5892
+
5893
+ var handleLocalClick = function (data) {
5894
+ if (enableMultiSelect) {
5895
+ var newSelected = new Set(selectedIdsRef.current);
5896
+
5897
+ if (newSelected.has(data.id)) {
5898
+ newSelected.delete(data.id);
5899
+ selectedItemsMapRef.current.delete(data.id);
5621
5900
  } else {
5622
- // 마커: 점 기반 바운딩 박스
5623
- var offset = getOrComputeMarkerOffset(item);
5624
-
5625
- if (offset) {
5626
- var boxWidth = item.boxWidth || 50;
5627
- var boxHeight = item.boxHeight || 28;
5628
- var tailHeight = 6;
5629
- var minX = offset.x - boxWidth / 2;
5630
- var minY = offset.y - boxHeight - tailHeight;
5631
- var maxX = offset.x + boxWidth / 2;
5632
- var maxY = offset.y;
5633
- spatial.insert(item, minX, minY, maxX, maxY);
5634
- }
5901
+ newSelected.add(data.id);
5902
+ selectedItemsMapRef.current.set(data.id, data);
5635
5903
  }
5904
+
5905
+ selectedIdsRef.current = newSelected;
5906
+ } else {
5907
+ var newSelected = new Set();
5908
+
5909
+ if (!selectedIdsRef.current.has(data.id)) {
5910
+ newSelected.add(data.id);
5911
+ selectedItemsMapRef.current.clear();
5912
+ selectedItemsMapRef.current.set(data.id, data);
5913
+ } else {
5914
+ selectedItemsMapRef.current.clear();
5915
+ }
5916
+
5917
+ selectedIdsRef.current = newSelected;
5636
5918
  }
5637
- }; // --------------------------------------------------------------------------
5638
- // 렌더링 함수
5639
- // --------------------------------------------------------------------------
5640
5919
 
5641
- /**
5642
- * 외부 렌더링 함수에 전달할 유틸리티 객체
5643
- */
5920
+ doRenderBase();
5921
+ doRenderEvent();
5922
+ }; // 클릭 이벤트 핸들러
5644
5923
 
5645
5924
 
5646
- var renderUtils = {
5647
- getOrComputePolygonOffsets: getOrComputePolygonOffsets,
5648
- getOrComputeMarkerOffset: getOrComputeMarkerOffset
5649
- };
5650
- /** Base Layer에서 사용할 빈 Set (재사용) */
5925
+ var handleClick = function (event) {
5926
+ if (disableInteractionRef.current) return;
5927
+ var clickedOffset = validateEvent(event, context, controller);
5928
+ if (!clickedOffset) return;
5929
+ var data = findData(clickedOffset);
5930
+ if (!data) return;
5931
+ handleLocalClick(data);
5932
+ onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
5933
+ }; // 마우스 이동 이벤트 핸들러 (hover 감지)
5651
5934
 
5652
- React.useRef(new Set());
5653
- /**
5654
- * Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
5655
- *
5656
- * 🔥 최적화:
5657
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
5658
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
5659
- * 3. 클로저로 최신 데이터 참조
5660
- */
5661
5935
 
5662
- var doRenderBase = function () {
5663
- var layer = baseLayerRef.current;
5664
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
5936
+ var handleMouseMove = function (event) {
5937
+ if (disableInteractionRef.current) return;
5938
+ var mouseOffset = validateEvent(event, context, controller);
5939
+ if (!mouseOffset) return;
5940
+ var hoveredItem = findData(mouseOffset);
5941
+ var prevHovered = hoveredItemRef.current;
5942
+ if (prevHovered === hoveredItem) return;
5943
+ setHovered(hoveredItem);
5944
+ if (prevHovered) onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
5945
+ if (hoveredItem) onMouseOver === null || onMouseOver === void 0 ? void 0 : onMouseOver(hoveredItem);
5946
+ }; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
5665
5947
 
5666
- var shape = layer.findOne('.base-render-shape');
5667
5948
 
5668
- if (!shape) {
5669
- // 최초 생성 ( 번만 실행됨)
5670
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
5671
- shape = new Konva__default["default"].Shape({
5672
- name: 'base-render-shape',
5673
- sceneFunc: function (context, shape) {
5674
- var ctx = context; // 클로저로 최신 ref 값 참조
5949
+ var handleMouseLeave = function () {
5950
+ if (disableInteractionRef.current) return;
5951
+ var prevHovered = hoveredItemRef.current;
5952
+ if (!prevHovered) return;
5953
+ hoveredItemRef.current = null;
5954
+ controller.setMapCursor('grab');
5955
+ doRenderEvent();
5956
+ onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
5957
+ }; // DOM 초기화
5675
5958
 
5676
- var visibleMarkers = enableViewportCulling ? markersRef.current.filter(function (item) {
5677
- return isInViewport(item);
5678
- }) : markersRef.current;
5679
- renderBase({
5680
- ctx: ctx,
5681
- items: visibleMarkers,
5682
- selectedIds: selectedIdsRef.current,
5683
- utils: renderUtils
5684
- });
5685
- },
5686
- perfectDrawEnabled: false,
5687
- listening: false,
5688
- hitStrokeWidth: 0
5689
- });
5690
- layer.add(shape);
5691
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
5692
5959
 
5960
+ React.useEffect(function () {
5961
+ divElement.style.width = 'fit-content';
5962
+ return function () {
5963
+ if (!markerRef.current) return;
5964
+ controller.clearDrawable(markerRef.current);
5965
+ markerRef.current = undefined;
5966
+ };
5967
+ }, []); // 마커 생성/업데이트
5693
5968
 
5694
- layer.batchDraw();
5695
- };
5696
- /**
5697
- * Animation 레이어 렌더링 (선택된 마커 애니메이션)
5698
- *
5699
- * 🔥 최적화: sceneFunc 내부에서 최신 items 참조
5700
- * - 선택 변경 시에만 재생성
5701
- * - 지도 이동 시에는 기존 Animation 계속 실행
5702
- */
5969
+ React.useEffect(function () {
5970
+ if (!options) return;
5971
+ var bounds = controller.getCurrBounds();
5972
+
5973
+ var markerOptions = tslib.__assign({
5974
+ position: bounds.nw
5975
+ }, options);
5703
5976
 
5977
+ if (markerRef.current) {
5978
+ controller.updateMarker(markerRef.current, markerOptions);
5979
+ return;
5980
+ }
5704
5981
 
5705
- var doRenderAnimation = function () {
5706
- if (!renderAnimation) return;
5707
- var layer = animationLayerRef.current;
5708
- if (!layer) return;
5709
- renderAnimation({
5710
- layer: layer,
5711
- selectedIds: selectedIdsRef.current,
5712
- items: markersRef.current,
5713
- utils: renderUtils
5982
+ markerRef.current = new Marker(markerOptions);
5983
+ markerRef.current.element = divElement;
5984
+ controller.createMarker(markerRef.current);
5985
+
5986
+ if (divElement.parentElement) {
5987
+ divElement.parentElement.style.pointerEvents = 'none';
5988
+ }
5989
+
5990
+ if (options.zIndex !== undefined) {
5991
+ controller.setMarkerZIndex(markerRef.current, options.zIndex);
5992
+ }
5993
+ }, [options]); // Konva 초기화 및 이벤트 리스너 등록
5994
+
5995
+ React.useEffect(function () {
5996
+ var mapDiv = controller.mapDivElement;
5997
+ var stage = new Konva__default["default"].Stage({
5998
+ container: containerRef.current,
5999
+ width: mapDiv.offsetWidth,
6000
+ height: mapDiv.offsetHeight
5714
6001
  });
5715
- };
5716
- /**
5717
- * Event 레이어 렌더링 (hover + 선택 상태 표시)
5718
- *
5719
- * 🔥 최적화:
5720
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
5721
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
5722
- * 3. 클로저로 최신 데이터 참조
5723
- */
6002
+ stageRef.current = stage;
6003
+ var baseLayer = new Konva__default["default"].Layer({
6004
+ listening: false
6005
+ });
6006
+ var eventLayer = new Konva__default["default"].Layer({
6007
+ listening: false
6008
+ });
6009
+ baseLayerRef.current = baseLayer;
6010
+ eventLayerRef.current = eventLayer;
6011
+ stage.add(baseLayer);
6012
+ stage.add(eventLayer);
5724
6013
 
6014
+ if (enableViewportCulling) {
6015
+ updateViewport$1();
6016
+ } // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
5725
6017
 
5726
- var doRenderEvent = function () {
5727
- if (!renderEvent) return;
5728
- var layer = eventLayerRef.current;
5729
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
5730
6018
 
5731
- var shape = layer.findOne('.event-render-shape');
6019
+ var resizeRafId = null;
6020
+ var resizeObserver = new ResizeObserver(function () {
6021
+ if (resizeRafId !== null) {
6022
+ cancelAnimationFrame(resizeRafId);
6023
+ }
5732
6024
 
5733
- if (!shape) {
5734
- // 최초 생성 (한 번만 실행됨)
5735
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
5736
- shape = new Konva__default["default"].Shape({
5737
- name: 'event-render-shape',
5738
- sceneFunc: function (context, shape) {
5739
- var ctx = context; // 클로저로 최신 ref 값 참조
6025
+ resizeRafId = requestAnimationFrame(function () {
6026
+ stage.width(mapDiv.offsetWidth);
6027
+ stage.height(mapDiv.offsetHeight);
6028
+ offsetCacheRef.current.clear();
6029
+ boundingBoxCacheRef.current.clear();
5740
6030
 
5741
- var selectedItems = Array.from(selectedItemsMapRef.current.values());
5742
- renderEvent({
5743
- ctx: ctx,
5744
- hoveredItem: hoveredItemRef.current,
5745
- utils: renderUtils,
5746
- selectedItems: selectedItems,
5747
- lastClickedItem: lastClickedItemRef.current
5748
- });
5749
- },
5750
- perfectDrawEnabled: false,
5751
- listening: false,
5752
- hitStrokeWidth: 0
6031
+ if (enableViewportCullingRef.current) {
6032
+ updateViewport$1();
6033
+ }
6034
+
6035
+ renderAllImmediate();
6036
+ resizeRafId = null;
5753
6037
  });
5754
- layer.add(shape);
5755
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
6038
+ });
6039
+ resizeObserver.observe(mapDiv);
6040
+ controller.addEventListener('IDLE', handleIdle);
6041
+ controller.addEventListener('ZOOMSTART', handleZoomStart);
6042
+ controller.addEventListener('ZOOM_CHANGED', handleZoomEnd);
6043
+ controller.addEventListener('CENTER_CHANGED', handleCenterChanged);
6044
+ controller.addEventListener('CLICK', handleClick);
6045
+ controller.addEventListener('MOUSEMOVE', handleMouseMove);
6046
+ controller.addEventListener('DRAGSTART', handleDragStart);
6047
+ controller.addEventListener('DRAGEND', handleDragEnd);
6048
+ mapDiv.addEventListener('mouseleave', handleMouseLeave);
6049
+ renderAllImmediate(); // Context 사용 시 컴포넌트 등록
5756
6050
 
6051
+ var componentInstance = null;
5757
6052
 
5758
- layer.batchDraw();
5759
- };
5760
- /**
5761
- * 전체 즉시 렌더링 (IDLE 시 호출)
5762
- */
6053
+ if (context) {
6054
+ componentInstance = {
6055
+ zIndex: currentZIndex,
6056
+ hitTest: function (offset) {
6057
+ return findData(offset) !== null;
6058
+ },
6059
+ onClick: onClick,
6060
+ onMouseOver: onMouseOver,
6061
+ onMouseOut: onMouseOut,
6062
+ findData: findData,
6063
+ setHovered: setHovered,
6064
+ handleLocalClick: handleLocalClick,
6065
+ getSelectedIds: function () {
6066
+ return selectedIdsRef.current;
6067
+ },
6068
+ isInteractionDisabled: function () {
6069
+ return disableInteractionRef.current;
6070
+ }
6071
+ };
6072
+ context.registerComponent(componentInstance);
6073
+ }
5763
6074
 
6075
+ return function () {
6076
+ if (resizeRafId !== null) {
6077
+ cancelAnimationFrame(resizeRafId);
6078
+ }
5764
6079
 
5765
- var renderAllImmediate = function () {
5766
- updateViewport();
5767
- buildSpatialIndex();
6080
+ resizeObserver.disconnect();
6081
+ controller.removeEventListener('IDLE', handleIdle);
6082
+ controller.removeEventListener('ZOOMSTART', handleZoomStart);
6083
+ controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
6084
+ controller.removeEventListener('CENTER_CHANGED', handleCenterChanged);
6085
+ controller.removeEventListener('CLICK', handleClick);
6086
+ controller.removeEventListener('MOUSEMOVE', handleMouseMove);
6087
+ controller.removeEventListener('DRAGSTART', handleDragStart);
6088
+ controller.removeEventListener('DRAGEND', handleDragEnd);
6089
+ mapDiv.removeEventListener('mouseleave', handleMouseLeave);
6090
+
6091
+ if (context && componentInstance) {
6092
+ context.unregisterComponent(componentInstance);
6093
+ }
6094
+
6095
+ baseLayer.destroyChildren();
6096
+ eventLayer.destroyChildren();
6097
+ stage.destroy();
6098
+ offsetCacheRef.current.clear();
6099
+ boundingBoxCacheRef.current.clear();
6100
+ spatialIndexRef.current.clear();
6101
+ };
6102
+ }, []); // disableInteraction 동기화
6103
+
6104
+ React.useEffect(function () {
6105
+ disableInteractionRef.current = disableInteraction;
6106
+ }, [disableInteraction]); // enableViewportCulling 동기화
6107
+
6108
+ React.useEffect(function () {
6109
+ enableViewportCullingRef.current = enableViewportCulling;
6110
+
6111
+ if (stageRef.current) {
6112
+ // 뷰포트 컬링 설정이 변경되면 shape 재생성 필요
6113
+ var baseLayer = baseLayerRef.current;
6114
+
6115
+ if (baseLayer) {
6116
+ var shape = baseLayer.findOne('.base-render-shape');
6117
+
6118
+ if (shape) {
6119
+ shape.destroy();
6120
+ }
6121
+ }
6122
+
6123
+ var eventLayer = eventLayerRef.current;
6124
+
6125
+ if (eventLayer) {
6126
+ var shape = eventLayer.findOne('.event-render-shape');
6127
+
6128
+ if (shape) {
6129
+ shape.destroy();
6130
+ }
6131
+ }
6132
+
6133
+ renderAllImmediate();
6134
+ }
6135
+ }, [enableViewportCulling]); // 외부 selectedItems 동기화
6136
+
6137
+ React.useEffect(function () {
6138
+ if (!stageRef.current) return;
6139
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
5768
6140
  doRenderBase();
5769
- doRenderAnimation();
5770
6141
  doRenderEvent();
5771
- }; // --------------------------------------------------------------------------
5772
- // 이벤트 핸들러: 지도 이벤트
5773
- // --------------------------------------------------------------------------
6142
+ }, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
5774
6143
 
5775
- /**
5776
- * 지도 이동/줌 완료 시 처리
5777
- */
6144
+ React.useEffect(function () {
6145
+ if (!stageRef.current) return;
6146
+ selectedItemRef.current = externalSelectedItem;
6147
+ doRenderEvent();
6148
+ }, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
5778
6149
 
6150
+ React.useEffect(function () {
6151
+ if (!stageRef.current) return;
6152
+ dataRef.current = data;
6153
+
6154
+ if (containerRef.current) {
6155
+ containerRef.current.style.transform = '';
6156
+ }
5779
6157
 
5780
- var handleIdle = function () {
5781
6158
  prevCenterOffsetRef.current = null;
5782
6159
  accumTranslateRef.current = {
5783
6160
  x: 0,
5784
6161
  y: 0
5785
- }; // 2. 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
5786
-
6162
+ };
5787
6163
  offsetCacheRef.current.clear();
5788
- boundingBoxCacheRef.current.clear(); // 3. 마커 위치 업데이트
6164
+ boundingBoxCacheRef.current.clear();
6165
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
6166
+ renderAllImmediate();
6167
+ }, [data]);
6168
+ return reactDom.createPortal(React__default["default"].createElement("div", {
6169
+ ref: containerRef,
6170
+ style: {
6171
+ position: 'absolute',
6172
+ width: '100%',
6173
+ height: '100%'
6174
+ }
6175
+ }), divElement);
6176
+ };
5789
6177
 
5790
- var bounds = controller.getCurrBounds();
6178
+ /**
6179
+ * 폴리곤 렌더링 유틸리티
6180
+ *
6181
+ * 이 파일은 폴리곤 렌더링을 위한 헬퍼 함수와 팩토리 함수를 제공합니다.
6182
+ * GeoJSON MultiPolygon 형식을 지원하며, 도넛 폴리곤(구멍이 있는 폴리곤)도 처리할 수 있습니다.
6183
+ */
5791
6184
 
5792
- var markerOptions = tslib.__assign({
5793
- position: bounds.nw
5794
- }, options);
6185
+ /**
6186
+ * 폴리곤 그리기 헬퍼 함수 (도넛 폴리곤 지원)
6187
+ *
6188
+ * Canvas 2D Context를 사용하여 폴리곤을 그립니다.
6189
+ * 도넛 폴리곤의 경우 evenodd fill rule을 사용하여 구멍을 처리합니다.
6190
+ *
6191
+ * @param params 폴리곤 그리기 파라미터
6192
+ *
6193
+ * @remarks
6194
+ * - **도넛 폴리곤 처리**:
6195
+ * - 외부 폴리곤과 내부 구멍들을 같은 path에 추가
6196
+ * - `fill('evenodd')`를 사용하여 구멍 뚫기
6197
+ * - **일반 폴리곤 처리**: 각 폴리곤 그룹을 개별적으로 그리기
6198
+ * - **성능**: O(n), n은 폴리곤의 총 좌표 수
6199
+ *
6200
+ * @example
6201
+ * ```typescript
6202
+ * drawPolygon({
6203
+ * ctx,
6204
+ * polygonOffsets: [[[[100, 200], [200, 200], [200, 100], [100, 100]]]],
6205
+ * isDonutPolygon: false,
6206
+ * fillColor: 'rgba(255, 0, 0, 0.5)',
6207
+ * strokeColor: 'rgba(255, 0, 0, 1)',
6208
+ * lineWidth: 2
6209
+ * });
6210
+ * ```
6211
+ */
6212
+ var drawPolygon = function (_a) {
6213
+ var ctx = _a.ctx,
6214
+ polygonOffsets = _a.polygonOffsets,
6215
+ isDonutPolygon = _a.isDonutPolygon,
6216
+ fillColor = _a.fillColor,
6217
+ strokeColor = _a.strokeColor,
6218
+ lineWidth = _a.lineWidth;
5795
6219
 
5796
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 데이터로 즉시 렌더링 (겹침 방지)
6220
+ for (var _i = 0, polygonOffsets_1 = polygonOffsets; _i < polygonOffsets_1.length; _i++) {
6221
+ var multiPolygon = polygonOffsets_1[_i];
5797
6222
 
5798
- if (containerRef.current) {
5799
- containerRef.current.style.transform = '';
5800
- containerRef.current.style.visibility = '';
5801
- } // 5. 새 위치에서 렌더링
6223
+ if (isDonutPolygon) {
6224
+ // 도넛 폴리곤 처리: 외부 폴리곤 + 내부 구멍들을 같은 path에 추가
6225
+ ctx.beginPath(); // 1. 외부 폴리곤 그리기 (첫 번째 폴리곤)
5802
6226
 
6227
+ var outerPolygon = multiPolygon[0];
5803
6228
 
5804
- renderAllImmediate();
6229
+ if (outerPolygon && outerPolygon.length > 0) {
6230
+ ctx.moveTo(outerPolygon[0][0], outerPolygon[0][1]);
6231
+
6232
+ for (var i = 1; i < outerPolygon.length; i++) {
6233
+ ctx.lineTo(outerPolygon[i][0], outerPolygon[i][1]);
6234
+ }
6235
+
6236
+ ctx.closePath();
6237
+ } // 2. 내부 폴리곤 (구멍들) 그리기 - 같은 path에 추가
6238
+
6239
+
6240
+ for (var j = 1; j < multiPolygon.length; j++) {
6241
+ var innerPolygon = multiPolygon[j];
6242
+ if (innerPolygon.length === 0) continue;
6243
+ ctx.moveTo(innerPolygon[0][0], innerPolygon[0][1]);
6244
+
6245
+ for (var i = 1; i < innerPolygon.length; i++) {
6246
+ ctx.lineTo(innerPolygon[i][0], innerPolygon[i][1]);
6247
+ }
6248
+
6249
+ ctx.closePath();
6250
+ } // 3. evenodd fill rule로 구멍 뚫기
6251
+
6252
+
6253
+ ctx.fillStyle = fillColor;
6254
+ ctx.fill('evenodd'); // 4. 외곽선 그리기
6255
+
6256
+ ctx.strokeStyle = strokeColor;
6257
+ ctx.lineWidth = lineWidth;
6258
+ ctx.stroke();
6259
+ } else {
6260
+ // 일반 폴리곤 처리: 각 폴리곤 그룹을 개별적으로 그리기
6261
+ for (var _b = 0, multiPolygon_1 = multiPolygon; _b < multiPolygon_1.length; _b++) {
6262
+ var polygonGroup = multiPolygon_1[_b];
6263
+ if (!polygonGroup.length) continue;
6264
+ ctx.beginPath();
6265
+ var firstPoint = polygonGroup[0];
6266
+ ctx.moveTo(firstPoint[0], firstPoint[1]);
6267
+
6268
+ for (var i = 1; i < polygonGroup.length; i++) {
6269
+ var point = polygonGroup[i];
6270
+ ctx.lineTo(point[0], point[1]);
6271
+ }
6272
+
6273
+ ctx.closePath(); // 스타일 설정 및 렌더링
6274
+
6275
+ ctx.fillStyle = fillColor;
6276
+ ctx.strokeStyle = strokeColor;
6277
+ ctx.lineWidth = lineWidth;
6278
+ ctx.fill();
6279
+ ctx.stroke();
6280
+ }
6281
+ }
6282
+ }
6283
+ };
6284
+ /**
6285
+ * 폴리곤 Base 렌더링 함수 팩토리
6286
+ *
6287
+ * Base Layer에서 사용할 렌더링 함수를 생성합니다.
6288
+ * 선택되지 않은 폴리곤만 렌더링하며, 선택된 항목은 Event Layer에서 처리됩니다.
6289
+ *
6290
+ * @template T 폴리곤 데이터의 추가 속성 타입
6291
+ * @param baseFillColor 기본 폴리곤 채우기 색상
6292
+ * @param baseStrokeColor 기본 폴리곤 테두리 색상
6293
+ * @param baseLineWidth 기본 폴리곤 테두리 두께
6294
+ * @returns Base Layer 렌더링 함수
6295
+ *
6296
+ * @remarks
6297
+ * - 선택된 항목은 Event Layer에서 그려지므로 Base Layer에서는 스킵
6298
+ * - 성능: O(n), n은 렌더링할 폴리곤 개수
6299
+ * - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
6300
+ *
6301
+ * @example
6302
+ * ```typescript
6303
+ * const renderBase = renderPolygonBase(
6304
+ * 'rgba(255, 100, 100, 0.5)',
6305
+ * 'rgba(200, 50, 50, 0.8)',
6306
+ * 2
6307
+ * );
6308
+ * ```
6309
+ */
6310
+
6311
+ var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth) {
6312
+ return function (_a) {
6313
+ var ctx = _a.ctx,
6314
+ items = _a.items,
6315
+ selectedIds = _a.selectedIds,
6316
+ utils = _a.utils;
6317
+
6318
+ for (var _i = 0, items_1 = items; _i < items_1.length; _i++) {
6319
+ var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림 (중복 렌더링 방지)
6320
+
6321
+ if (selectedIds.has(item.id)) continue; // paths가 없으면 스킵
6322
+
6323
+ if (!item.paths) continue; // 좌표 변환 (자동 캐싱)
6324
+
6325
+ var polygonOffsets = utils.getOrComputePolygonOffsets(item);
6326
+ if (!polygonOffsets) continue; // 폴리곤 그리기
6327
+
6328
+ drawPolygon({
6329
+ ctx: ctx,
6330
+ polygonOffsets: polygonOffsets,
6331
+ isDonutPolygon: item.isDonutPolygon || false,
6332
+ fillColor: baseFillColor,
6333
+ strokeColor: baseStrokeColor,
6334
+ lineWidth: baseLineWidth
6335
+ });
6336
+ }
5805
6337
  };
5806
- /**
5807
- * 줌 시작 시 처리 (일시적으로 숨김)
5808
- */
6338
+ };
6339
+ /**
6340
+ * 폴리곤 Event 렌더링 함수 팩토리
6341
+ *
6342
+ * Event Layer에서 사용할 렌더링 함수를 생성합니다.
6343
+ * 선택된 항목, hover된 항목, 마지막 선택된 항목을 각각 다른 스타일로 렌더링합니다.
6344
+ *
6345
+ * @template T 폴리곤 데이터의 추가 속성 타입
6346
+ * @param baseFillColor 기본 폴리곤 채우기 색상 (필수, fallback용)
6347
+ * @param baseStrokeColor 기본 폴리곤 테두리 색상 (필수, fallback용)
6348
+ * @param baseLineWidth 기본 폴리곤 테두리 두께 (필수, fallback용)
6349
+ * @param selectedFillColor 선택된 폴리곤 채우기 색상 (선택, 기본값: baseFillColor)
6350
+ * @param selectedStrokeColor 선택된 폴리곤 테두리 색상 (선택, 기본값: baseStrokeColor)
6351
+ * @param selectedLineWidth 선택된 폴리곤 테두리 두께 (선택, 기본값: baseLineWidth)
6352
+ * @param activeFillColor 마지막 선택된 폴리곤 채우기 색상 (선택, 기본값: selectedFillColor)
6353
+ * @param activeStrokeColor 마지막 선택된 폴리곤 테두리 색상 (선택, 기본값: selectedStrokeColor)
6354
+ * @param activeLineWidth 마지막 선택된 폴리곤 테두리 두께 (선택, 기본값: selectedLineWidth)
6355
+ * @param hoveredFillColor Hover 시 폴리곤 채우기 색상 (선택, 기본값: selectedFillColor)
6356
+ * @param hoveredStrokeColor Hover 시 폴리곤 테두리 색상 (선택, 기본값: selectedStrokeColor)
6357
+ * @param hoveredLineWidth Hover 시 폴리곤 테두리 두께 (선택, 기본값: selectedLineWidth)
6358
+ * @returns Event Layer 렌더링 함수
6359
+ *
6360
+ * @remarks
6361
+ * - **렌더링 순서**: 선택된 항목 → 마지막 선택된 항목 → hover된 항목 (최상단)
6362
+ * - **성능**: O(m), m은 선택된 항목 수 + hover된 항목 수
6363
+ * - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
6364
+ * - hover된 항목이 선택되어 있으면 active 스타일 적용
6365
+ *
6366
+ * @example
6367
+ * ```typescript
6368
+ * const renderEvent = renderPolygonEvent(
6369
+ * 'rgba(255, 100, 100, 0.5)', // baseFillColor
6370
+ * 'rgba(200, 50, 50, 0.8)', // baseStrokeColor
6371
+ * 2, // baseLineWidth
6372
+ * 'rgba(255, 193, 7, 0.7)', // selectedFillColor
6373
+ * 'rgba(255, 152, 0, 1)', // selectedStrokeColor
6374
+ * 4 // selectedLineWidth
6375
+ * );
6376
+ * ```
6377
+ */
6378
+
6379
+ var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth) {
6380
+ // 기본값 설정 (base 기준)
6381
+ var _selectedFillColor = selectedFillColor || baseFillColor;
6382
+
6383
+ var _selectedStrokeColor = selectedStrokeColor || baseStrokeColor;
6384
+
6385
+ var _selectedLineWidth = selectedLineWidth || baseLineWidth;
6386
+
6387
+ var _activeFillColor = activeFillColor || _selectedFillColor;
6388
+
6389
+ var _activeStrokeColor = activeStrokeColor || _selectedStrokeColor;
6390
+
6391
+ var _activeLineWidth = activeLineWidth || _selectedLineWidth;
6392
+
6393
+ var _hoveredFillColor = hoveredFillColor || _selectedFillColor;
6394
+
6395
+ var _hoveredStrokeColor = hoveredStrokeColor || _selectedStrokeColor;
6396
+
6397
+ var _hoveredLineWidth = hoveredLineWidth || _selectedLineWidth;
6398
+
6399
+ return function (_a) {
6400
+ var ctx = _a.ctx,
6401
+ hoveredItem = _a.hoveredItem,
6402
+ utils = _a.utils,
6403
+ selectedItems = _a.selectedItems,
6404
+ selectedItem = _a.selectedItem; // 성능 최적화: selectedItems를 Set으로 변환하여 O(1) 조회 (매번 some() 체크 방지)
6405
+
6406
+ var selectedIdsSet = selectedItems ? new Set(selectedItems.map(function (item) {
6407
+ return item.id;
6408
+ })) : new Set();
6409
+ var hoveredItemId = hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id;
6410
+ var selectedItemId = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id; // 1. 선택된 항목들 그리기 (마지막 선택 항목과 호버된 항목 제외)
6411
+
6412
+ if (selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.length) {
6413
+ for (var _i = 0, selectedItems_1 = selectedItems; _i < selectedItems_1.length; _i++) {
6414
+ var item = selectedItems_1[_i]; // 마지막 선택 항목과 호버된 항목은 나중에 따로 그림
6415
+
6416
+ if (item.id === selectedItemId || item.id === hoveredItemId) continue;
6417
+ if (!item.paths) continue;
6418
+ var polygonOffsets = utils.getOrComputePolygonOffsets(item);
6419
+ if (!polygonOffsets) continue;
6420
+ drawPolygon({
6421
+ ctx: ctx,
6422
+ polygonOffsets: polygonOffsets,
6423
+ isDonutPolygon: item.isDonutPolygon || false,
6424
+ fillColor: _selectedFillColor,
6425
+ strokeColor: _selectedStrokeColor,
6426
+ lineWidth: _selectedLineWidth
6427
+ });
6428
+ }
6429
+ } // 2. 마지막 선택된 항목 그리기 (호버되지 않은 경우)
6430
+
6431
+
6432
+ if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) && hoveredItemId !== selectedItemId) {
6433
+ var polygonOffsets = utils.getOrComputePolygonOffsets(selectedItem);
6434
+
6435
+ if (polygonOffsets) {
6436
+ drawPolygon({
6437
+ ctx: ctx,
6438
+ polygonOffsets: polygonOffsets,
6439
+ isDonutPolygon: selectedItem.isDonutPolygon || false,
6440
+ fillColor: _activeFillColor,
6441
+ strokeColor: _activeStrokeColor,
6442
+ lineWidth: _activeLineWidth
6443
+ });
6444
+ }
6445
+ } // 3. 호버된 항목 그리기 (가장 위에 표시)
6446
+
6447
+
6448
+ if (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.paths) {
6449
+ var polygonOffsets = utils.getOrComputePolygonOffsets(hoveredItem);
6450
+ if (!polygonOffsets) return; // 좌표 변환 실패 시 스킵 (return은 렌더링 함수 종료)
6451
+ // 성능 최적화: Set을 사용하여 O(1) 조회 (이전: O(m) some() 체크)
6452
+
6453
+ var isSelected = selectedIdsSet.has(hoveredItem.id);
6454
+ drawPolygon({
6455
+ ctx: ctx,
6456
+ polygonOffsets: polygonOffsets,
6457
+ isDonutPolygon: hoveredItem.isDonutPolygon || false,
6458
+ fillColor: isSelected ? _activeFillColor : _hoveredFillColor,
6459
+ strokeColor: isSelected ? _activeStrokeColor : _hoveredStrokeColor,
6460
+ lineWidth: isSelected ? _activeLineWidth : _hoveredLineWidth
6461
+ });
6462
+ }
6463
+ };
6464
+ };
6465
+
6466
+ var WoongCanvasPolygon = function (props) {
6467
+ var data = props.data,
6468
+ onClick = props.onClick,
6469
+ _a = props.enableMultiSelect,
6470
+ enableMultiSelect = _a === void 0 ? false : _a,
6471
+ _b = props.enableViewportCulling,
6472
+ enableViewportCulling = _b === void 0 ? false : _b,
6473
+ _c = props.cullingMargin,
6474
+ cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
6475
+ _d = props.maxCacheSize,
6476
+ maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
6477
+ externalSelectedItems = props.selectedItems,
6478
+ externalSelectedItem = props.selectedItem,
6479
+ _e = props.disableInteraction,
6480
+ disableInteraction = _e === void 0 ? false : _e,
6481
+ baseFillColor = props.baseFillColor,
6482
+ baseStrokeColor = props.baseStrokeColor,
6483
+ baseLineWidth = props.baseLineWidth,
6484
+ selectedFillColor = props.selectedFillColor,
6485
+ selectedStrokeColor = props.selectedStrokeColor,
6486
+ selectedLineWidth = props.selectedLineWidth,
6487
+ activeFillColor = props.activeFillColor,
6488
+ activeStrokeColor = props.activeStrokeColor,
6489
+ activeLineWidth = props.activeLineWidth,
6490
+ hoveredFillColor = props.hoveredFillColor,
6491
+ hoveredStrokeColor = props.hoveredStrokeColor,
6492
+ hoveredLineWidth = props.hoveredLineWidth,
6493
+ options = tslib.__rest(props, ["data", "onClick", "enableMultiSelect", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
6494
+ // Hooks & Context
6495
+ // --------------------------------------------------------------------------
6496
+
6497
+
6498
+ var controller = useMintMapController();
6499
+ var context = useWoongCanvasContext();
6500
+ var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
6501
+
6502
+ var divRef = React.useRef(document.createElement('div'));
6503
+ var divElement = divRef.current;
6504
+ var containerRef = React.useRef(null);
6505
+ var markerRef = React.useRef(); // Konva Refs
6506
+
6507
+ var stageRef = React.useRef(null);
6508
+ var baseLayerRef = React.useRef(null);
6509
+ var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
6510
+
6511
+ var dataRef = React.useRef(data);
6512
+ var disableInteractionRef = React.useRef(disableInteraction);
6513
+ var enableViewportCullingRef = React.useRef(enableViewportCulling);
6514
+ var hoveredItemRef = React.useRef(null);
6515
+ var selectedItemRef = React.useRef(externalSelectedItem);
6516
+ var selectedIdsRef = React.useRef(new Set());
6517
+ var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
6518
+
6519
+ var draggingRef = React.useRef(false);
6520
+ var prevCenterOffsetRef = React.useRef(null);
6521
+ var accumTranslateRef = React.useRef({
6522
+ x: 0,
6523
+ y: 0
6524
+ }); // 성능 최적화 Refs
6525
+
6526
+ var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
6527
+ var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
6528
+ var boundingBoxCacheRef = React.useRef(new Map());
6529
+ var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling용)
6530
+
6531
+ var updateViewport$1 = function () {
6532
+ updateViewport(stageRef.current, cullingMargin, viewportRef);
6533
+ }; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
6534
+
6535
+
6536
+ var isInViewport$1 = function (item) {
6537
+ return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
6538
+ }; // 폴리곤 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
6539
+
6540
+
6541
+ var getOrComputePolygonOffsets = function (polygonData) {
6542
+ var cached = offsetCacheRef.current.get(polygonData.id);
6543
+ if (cached && Array.isArray(cached)) return cached;
6544
+ var result = computePolygonOffsets(polygonData, controller);
6545
+ if (!result) return null;
6546
+ offsetCacheRef.current.set(polygonData.id, result);
6547
+ return result;
6548
+ }; // 폴리곤 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
6549
+
6550
+
6551
+ var computeBoundingBox = function (item) {
6552
+ var offsets = getOrComputePolygonOffsets(item);
6553
+ if (!offsets) return null; // 모든 좌표를 순회하며 최소/최대값 찾기
6554
+
6555
+ var minX = Infinity,
6556
+ minY = Infinity,
6557
+ maxX = -Infinity,
6558
+ maxY = -Infinity;
6559
+
6560
+ for (var _i = 0, offsets_1 = offsets; _i < offsets_1.length; _i++) {
6561
+ var multiPolygon = offsets_1[_i];
6562
+
6563
+ for (var _a = 0, multiPolygon_1 = multiPolygon; _a < multiPolygon_1.length; _a++) {
6564
+ var polygonGroup = multiPolygon_1[_a];
6565
+
6566
+ for (var _b = 0, polygonGroup_1 = polygonGroup; _b < polygonGroup_1.length; _b++) {
6567
+ var _c = polygonGroup_1[_b],
6568
+ x = _c[0],
6569
+ y = _c[1];
6570
+ if (x < minX) minX = x;
6571
+ if (y < minY) minY = y;
6572
+ if (x > maxX) maxX = x;
6573
+ if (y > maxY) maxY = y;
6574
+ }
6575
+ }
6576
+ }
6577
+
6578
+ return {
6579
+ minX: minX,
6580
+ minY: minY,
6581
+ maxX: maxX,
6582
+ maxY: maxY
6583
+ };
6584
+ }; // 공간 인덱스 빌드 (빠른 Hit Test용)
6585
+
6586
+
6587
+ var buildSpatialIndex$1 = function () {
6588
+ buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
6589
+ }; // 렌더링 유틸리티 객체
6590
+
6591
+
6592
+ var renderUtils = {
6593
+ getOrComputePolygonOffsets: getOrComputePolygonOffsets,
6594
+ getOrComputeMarkerOffset: function () {
6595
+ return null;
6596
+ }
6597
+ }; // 렌더링 함수 생성
6598
+
6599
+ var renderBase = renderPolygonBase(baseFillColor, baseStrokeColor, baseLineWidth);
6600
+ var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth); // Base Layer 렌더링 (뷰포트 컬링 적용)
6601
+
6602
+ var doRenderBase = function () {
6603
+ var layer = baseLayerRef.current;
6604
+ if (!layer) return;
6605
+ var shape = layer.findOne('.base-render-shape');
6606
+
6607
+ if (!shape) {
6608
+ shape = new Konva__default["default"].Shape({
6609
+ name: 'base-render-shape',
6610
+ sceneFunc: function (context, shape) {
6611
+ var ctx = context;
6612
+ var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
6613
+
6614
+ var visibleItems = enableViewportCullingRef.current ? dataRef.current.filter(function (item) {
6615
+ return isInViewport$1(item);
6616
+ }) : dataRef.current;
6617
+ renderBase({
6618
+ ctx: ctx,
6619
+ items: visibleItems,
6620
+ selectedIds: selectedIdsRef.current,
6621
+ hoveredItem: hovered,
6622
+ utils: renderUtils
6623
+ });
6624
+ },
6625
+ perfectDrawEnabled: false,
6626
+ listening: false,
6627
+ hitStrokeWidth: 0
6628
+ });
6629
+ layer.add(shape);
6630
+ }
5809
6631
 
6632
+ layer.batchDraw();
6633
+ }; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
5810
6634
 
5811
- var handleZoomStart = function () {
5812
- if (containerRef.current) {
5813
- containerRef.current.style.visibility = 'hidden';
5814
- }
5815
- };
5816
- /**
5817
- * 줌 종료 시 처리 (다시 표시)
5818
- */
5819
6635
 
6636
+ var doRenderEvent = function () {
6637
+ var layer = eventLayerRef.current;
6638
+ if (!layer) return;
6639
+ var shape = layer.findOne('.event-render-shape');
5820
6640
 
5821
- var handleZoomEnd = function () {
5822
- if (containerRef.current) {
5823
- containerRef.current.style.visibility = '';
6641
+ if (!shape) {
6642
+ shape = new Konva__default["default"].Shape({
6643
+ name: 'event-render-shape',
6644
+ sceneFunc: function (context, shape) {
6645
+ var ctx = context;
6646
+ var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
6647
+ var hovered = hoveredItemRef.current;
6648
+ renderEvent({
6649
+ ctx: ctx,
6650
+ hoveredItem: hovered,
6651
+ utils: renderUtils,
6652
+ selectedItems: selectedItems,
6653
+ selectedItem: selectedItemRef.current
6654
+ });
6655
+ },
6656
+ perfectDrawEnabled: false,
6657
+ listening: false,
6658
+ hitStrokeWidth: 0
6659
+ });
6660
+ layer.add(shape);
5824
6661
  }
5825
- };
5826
- /**
5827
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
5828
- */
5829
6662
 
6663
+ layer.batchDraw();
6664
+ }; // 전체 즉시 렌더링
5830
6665
 
5831
- var handleCenterChanged = function () {
5832
- var center = controller.getCurrBounds().getCenter();
5833
- var curr = controller.positionToOffset(center);
5834
- var prev = prevCenterOffsetRef.current;
5835
6666
 
5836
- if (!prev) {
5837
- prevCenterOffsetRef.current = {
5838
- x: curr.x,
5839
- y: curr.y
5840
- };
5841
- return;
6667
+ var renderAllImmediate = function () {
6668
+ if (enableViewportCullingRef.current) {
6669
+ updateViewport$1();
5842
6670
  }
5843
6671
 
5844
- var dx = prev.x - curr.x;
5845
- var dy = prev.y - curr.y;
5846
- accumTranslateRef.current = {
5847
- x: accumTranslateRef.current.x + dx,
5848
- y: accumTranslateRef.current.y + dy
5849
- };
5850
- prevCenterOffsetRef.current = {
5851
- x: curr.x,
5852
- y: curr.y
5853
- };
6672
+ buildSpatialIndex$1();
6673
+ doRenderBase();
6674
+ doRenderEvent();
6675
+ }; // 지도 이벤트 핸들러 생성
6676
+
6677
+
6678
+ var _f = createMapEventHandlers({
6679
+ controller: controller,
6680
+ containerRef: containerRef,
6681
+ markerRef: markerRef,
6682
+ options: options,
6683
+ prevCenterOffsetRef: prevCenterOffsetRef,
6684
+ accumTranslateRef: accumTranslateRef,
6685
+ offsetCacheRef: offsetCacheRef,
6686
+ boundingBoxCacheRef: boundingBoxCacheRef,
6687
+ renderAllImmediate: renderAllImmediate
6688
+ }),
6689
+ handleIdle = _f.handleIdle,
6690
+ handleZoomStart = _f.handleZoomStart,
6691
+ handleZoomEnd = _f.handleZoomEnd,
6692
+ handleCenterChanged = _f.handleCenterChanged,
6693
+ handleDragStartShared = _f.handleDragStart,
6694
+ handleDragEndShared = _f.handleDragEnd;
5854
6695
 
5855
- if (containerRef.current) {
5856
- containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
5857
- }
5858
- }; // --------------------------------------------------------------------------
5859
- // Hit Test & 상태 관리
5860
- // --------------------------------------------------------------------------
6696
+ var handleDragStart = function () {
6697
+ handleDragStartShared();
6698
+ draggingRef.current = true;
6699
+ controller.setMapCursor('grabbing');
6700
+ };
5861
6701
 
5862
- /**
5863
- * 특정 좌표의 마커/폴리곤 데이터 찾기 (Spatial Index 사용)
5864
- *
5865
- * topOnHover가 true일 때:
5866
- * - 현재 hover된 항목을 최우선으로 체크
5867
- * - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
5868
- *
5869
- * @param offset 검사할 좌표
5870
- * @returns 찾은 마커/폴리곤 데이터 또는 null
5871
- */
6702
+ var handleDragEnd = function () {
6703
+ handleDragEndShared();
6704
+ draggingRef.current = false;
6705
+ controller.setMapCursor('grab');
6706
+ }; // Hit Test: 특정 좌표의 폴리곤 찾기
5872
6707
 
5873
6708
 
5874
6709
  var findData = function (offset) {
5875
- // topOnHover가 true이고 현재 hover된 항목이 있으면, 그것을 먼저 체크
5876
- if (topOnHover && hoveredItemRef.current) {
5877
- var hovered = hoveredItemRef.current; // 폴리곤인 경우
5878
-
5879
- if (dataType === exports.CanvasDataType.POLYGON) {
5880
- if (isPointInPolygonData(offset, hovered, getOrComputePolygonOffsets)) {
5881
- return hovered; // 여전히 hover된 항목 위에 있음
5882
- }
5883
- } // 마커인 경우
5884
- else {
5885
- if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
5886
- return hovered; // 여전히 hover된 항목 위에 있음
5887
- }
5888
- }
5889
- } // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 → ~10개)
5890
-
5891
-
5892
- var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 데이터 타입에 따라 적절한 히트 테스트 수행
5893
-
5894
- if (dataType === exports.CanvasDataType.MARKER) {
5895
- // 마커 체크
5896
- for (var i = candidates.length - 1; i >= 0; i--) {
5897
- var item = candidates[i];
6710
+ // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
6711
+ var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
5898
6712
 
5899
- if (isPointInMarkerData(offset, item, getOrComputeMarkerOffset)) {
5900
- return item;
5901
- }
5902
- }
5903
- } else {
5904
- // 폴리곤 체크
5905
- for (var i = candidates.length - 1; i >= 0; i--) {
5906
- var item = candidates[i];
6713
+ for (var i = candidates.length - 1; i >= 0; i--) {
6714
+ var item = candidates[i]; // 정확한 Hit Test: Ray Casting 알고리즘으로 폴리곤 내부 여부 확인
5907
6715
 
5908
- if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
5909
- return item;
5910
- }
6716
+ if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
6717
+ return item;
5911
6718
  }
5912
6719
  }
5913
6720
 
5914
6721
  return null;
5915
- };
5916
- /**
5917
- * Hover 상태 설정 및 Event 레이어 렌더링
5918
- *
5919
- * @param data hover된 마커/폴리곤 데이터 또는 null
5920
- *
5921
- * 최적화: RAF 제거하여 즉시 렌더링 (16ms 지연 제거)
5922
- * topOnHover가 true일 때는 Base 레이어도 다시 그려서 z-order 변경
5923
- */
6722
+ }; // Hover 상태 설정 및 렌더링
5924
6723
 
5925
6724
 
5926
6725
  var setHovered = function (data) {
@@ -5930,29 +6729,14 @@
5930
6729
  controller.setMapCursor('grabbing');
5931
6730
  } else {
5932
6731
  controller.setMapCursor(data ? 'pointer' : 'grab');
5933
- } // 즉시 렌더링 (RAF 없이)
5934
- // topOnHover는 Event Layer에서만 처리 (Base Layer 재렌더링 제거로 성능 향상)
5935
-
6732
+ }
5936
6733
 
5937
6734
  doRenderEvent();
5938
- };
5939
- /**
5940
- * 클릭 처리 (단일/다중 선택)
5941
- *
5942
- * @param data 클릭된 마커/폴리곤 데이터
5943
- *
5944
- * 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
5945
- * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
5946
- * - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
5947
- */
6735
+ }; // 클릭 처리: 선택 상태 업데이트
5948
6736
 
5949
6737
 
5950
6738
  var handleLocalClick = function (data) {
5951
- // 0. 마지막 클릭 항목 저장
5952
- lastClickedItemRef.current = data; // 1. 선택 상태 업데이트
5953
-
5954
6739
  if (enableMultiSelect) {
5955
- // 다중 선택: Set과 Map 동시 업데이트
5956
6740
  var newSelected = new Set(selectedIdsRef.current);
5957
6741
 
5958
6742
  if (newSelected.has(data.id)) {
@@ -5965,7 +6749,6 @@
5965
6749
 
5966
6750
  selectedIdsRef.current = newSelected;
5967
6751
  } else {
5968
- // 단일 선택: 토글
5969
6752
  var newSelected = new Set();
5970
6753
 
5971
6754
  if (!selectedIdsRef.current.has(data.id)) {
@@ -5979,144 +6762,77 @@
5979
6762
  selectedIdsRef.current = newSelected;
5980
6763
  }
5981
6764
 
5982
- if (!!renderAnimation) {
5983
- // 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
5984
- doRenderBase(); // 3. Animation Layer 렌더링 (선택된 마커 애니메이션)
5985
-
5986
- doRenderAnimation();
5987
- } // 4. Event Layer 렌더링 (hover 처리)
5988
-
5989
-
6765
+ doRenderBase();
5990
6766
  doRenderEvent();
5991
- }; // --------------------------------------------------------------------------
5992
- // 이벤트 핸들러: UI 이벤트
5993
- // --------------------------------------------------------------------------
5994
-
5995
- /**
5996
- * 클릭 이벤트 처리
5997
- */
6767
+ }; // 클릭 이벤트 핸들러
5998
6768
 
5999
6769
 
6000
6770
  var handleClick = function (event) {
6001
- var _a;
6002
-
6003
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
6004
-
6005
- try {
6006
- var clickedOffset = controller.positionToOffset(event.param.position);
6007
- var data = findData(clickedOffset);
6008
-
6009
- if (data) {
6010
- handleLocalClick(data);
6011
- if (onClick) onClick(data, selectedIdsRef.current);
6012
- }
6013
- } catch (error) {
6014
- console.error('[WoongKonvaMarker] handleClick error:', error);
6015
- }
6016
- };
6017
- /**
6018
- * 마우스 이동 이벤트 처리 (hover 감지)
6019
- */
6771
+ if (disableInteractionRef.current) return;
6772
+ var clickedOffset = validateEvent(event, context, controller);
6773
+ if (!clickedOffset) return;
6774
+ var data = findData(clickedOffset);
6775
+ if (!data) return;
6776
+ handleLocalClick(data);
6777
+ onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
6778
+ }; // 마우스 이동 이벤트 핸들러 (hover 감지)
6020
6779
 
6021
6780
 
6022
6781
  var handleMouseMove = function (event) {
6023
- var _a;
6024
-
6025
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
6026
-
6027
- try {
6028
- var mouseOffset = controller.positionToOffset(event.param.position);
6029
- var hoveredItem = findData(mouseOffset);
6030
- var prevHovered = hoveredItemRef.current;
6031
-
6032
- if (prevHovered !== hoveredItem) {
6033
- setHovered(hoveredItem);
6034
- if (prevHovered && onMouseOut) onMouseOut(prevHovered);
6035
- if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
6036
- }
6037
- } catch (error) {
6038
- console.error('[WoongKonvaMarker] handleMouseMove error:', error);
6039
- }
6040
- };
6041
- /**
6042
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
6043
- */
6044
-
6045
-
6046
- var handleDragStart = function () {
6047
- draggingRef.current = true;
6048
- controller.setMapCursor('grabbing');
6049
- };
6050
- /**
6051
- * 드래그 종료 처리 (커서를 기본으로 복원)
6052
- */
6053
-
6054
-
6055
- var handleDragEnd = function () {
6056
- draggingRef.current = false;
6057
- controller.setMapCursor('grab');
6058
- };
6059
- /**
6060
- * 마우스가 canvas를 벗어날 때 hover cleanup
6061
- */
6782
+ if (disableInteractionRef.current) return;
6783
+ var mouseOffset = validateEvent(event, context, controller);
6784
+ if (!mouseOffset) return;
6785
+ var hoveredItem = findData(mouseOffset);
6786
+ var prevHovered = hoveredItemRef.current;
6787
+ if (prevHovered === hoveredItem) return;
6788
+ setHovered(hoveredItem);
6789
+ }; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
6062
6790
 
6063
6791
 
6064
6792
  var handleMouseLeave = function () {
6793
+ if (disableInteractionRef.current) return;
6065
6794
  var prevHovered = hoveredItemRef.current;
6066
-
6067
- if (prevHovered) {
6068
- hoveredItemRef.current = null;
6069
- controller.setMapCursor('grab');
6070
- doRenderEvent();
6071
-
6072
- if (onMouseOut) {
6073
- onMouseOut(prevHovered);
6074
- }
6075
- }
6076
- }; // --------------------------------------------------------------------------
6077
- // Lifecycle: DOM 초기화
6078
- // --------------------------------------------------------------------------
6795
+ if (!prevHovered) return;
6796
+ hoveredItemRef.current = null;
6797
+ controller.setMapCursor('grab');
6798
+ doRenderEvent();
6799
+ }; // DOM 초기화
6079
6800
 
6080
6801
 
6081
6802
  React.useEffect(function () {
6082
6803
  divElement.style.width = 'fit-content';
6083
6804
  return function () {
6084
- if (markerRef.current) {
6085
- controller.clearDrawable(markerRef.current);
6086
- markerRef.current = undefined;
6087
- }
6805
+ if (!markerRef.current) return;
6806
+ controller.clearDrawable(markerRef.current);
6807
+ markerRef.current = undefined;
6088
6808
  };
6089
- }, []); // --------------------------------------------------------------------------
6090
- // Lifecycle: 마커 생성/업데이트
6091
- // --------------------------------------------------------------------------
6809
+ }, []); // 마커 생성/업데이트
6092
6810
 
6093
6811
  React.useEffect(function () {
6094
- if (options) {
6095
- var bounds = controller.getCurrBounds();
6812
+ if (!options) return;
6813
+ var bounds = controller.getCurrBounds();
6096
6814
 
6097
- var markerOptions = tslib.__assign({
6098
- position: bounds.nw
6099
- }, options);
6815
+ var markerOptions = tslib.__assign({
6816
+ position: bounds.nw
6817
+ }, options);
6100
6818
 
6101
- if (markerRef.current) {
6102
- controller.updateMarker(markerRef.current, markerOptions);
6103
- } else {
6104
- markerRef.current = new Marker(markerOptions);
6105
- markerRef.current.element = divElement;
6106
- controller.createMarker(markerRef.current);
6819
+ if (markerRef.current) {
6820
+ controller.updateMarker(markerRef.current, markerOptions);
6821
+ return;
6822
+ }
6107
6823
 
6108
- if (divElement.parentElement) {
6109
- divElement.parentElement.style.pointerEvents = 'none';
6110
- }
6824
+ markerRef.current = new Marker(markerOptions);
6825
+ markerRef.current.element = divElement;
6826
+ controller.createMarker(markerRef.current);
6111
6827
 
6112
- if (options.zIndex !== undefined) {
6113
- controller.setMarkerZIndex(markerRef.current, options.zIndex);
6114
- }
6115
- }
6828
+ if (divElement.parentElement) {
6829
+ divElement.parentElement.style.pointerEvents = 'none';
6116
6830
  }
6117
- }, [options]); // --------------------------------------------------------------------------
6118
- // Lifecycle: Konva 초기화 및 이벤트 리스너 등록
6119
- // --------------------------------------------------------------------------
6831
+
6832
+ if (options.zIndex !== undefined) {
6833
+ controller.setMarkerZIndex(markerRef.current, options.zIndex);
6834
+ }
6835
+ }, [options]); // Konva 초기화 및 이벤트 리스너 등록
6120
6836
 
6121
6837
  React.useEffect(function () {
6122
6838
  var mapDiv = controller.mapDivElement;
@@ -6125,34 +6841,25 @@
6125
6841
  width: mapDiv.offsetWidth,
6126
6842
  height: mapDiv.offsetHeight
6127
6843
  });
6128
- stageRef.current = stage; // 레이어 최적화 설정
6129
-
6844
+ stageRef.current = stage;
6130
6845
  var baseLayer = new Konva__default["default"].Layer({
6131
- listening: false // 이벤트 리스닝 비활성화로 성능 향상
6132
-
6133
- });
6134
- var animationLayer = new Konva__default["default"].Layer({
6135
6846
  listening: false
6136
6847
  });
6137
6848
  var eventLayer = new Konva__default["default"].Layer({
6138
6849
  listening: false
6139
6850
  });
6140
6851
  baseLayerRef.current = baseLayer;
6141
- animationLayerRef.current = animationLayer;
6142
6852
  eventLayerRef.current = eventLayer;
6143
6853
  stage.add(baseLayer);
6854
+ stage.add(eventLayer);
6144
6855
 
6145
- if (renderAnimation) {
6146
- stage.add(animationLayer);
6147
- }
6148
-
6149
- stage.add(eventLayer); // 초기 뷰포트 설정
6856
+ if (enableViewportCulling) {
6857
+ updateViewport$1();
6858
+ } // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
6150
6859
 
6151
- updateViewport(); // ResizeObserver (맵 크기 변경 감지)
6152
6860
 
6153
6861
  var resizeRafId = null;
6154
6862
  var resizeObserver = new ResizeObserver(function () {
6155
- // RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
6156
6863
  if (resizeRafId !== null) {
6157
6864
  cancelAnimationFrame(resizeRafId);
6158
6865
  }
@@ -6162,7 +6869,11 @@
6162
6869
  stage.height(mapDiv.offsetHeight);
6163
6870
  offsetCacheRef.current.clear();
6164
6871
  boundingBoxCacheRef.current.clear();
6165
- updateViewport();
6872
+
6873
+ if (enableViewportCullingRef.current) {
6874
+ updateViewport$1();
6875
+ }
6876
+
6166
6877
  renderAllImmediate();
6167
6878
  resizeRafId = null;
6168
6879
  });
@@ -6175,10 +6886,9 @@
6175
6886
  controller.addEventListener('CLICK', handleClick);
6176
6887
  controller.addEventListener('MOUSEMOVE', handleMouseMove);
6177
6888
  controller.addEventListener('DRAGSTART', handleDragStart);
6178
- controller.addEventListener('DRAGEND', handleDragEnd); // 맵 컨테이너에 mouseleave 이벤트 추가
6179
-
6889
+ controller.addEventListener('DRAGEND', handleDragEnd);
6180
6890
  mapDiv.addEventListener('mouseleave', handleMouseLeave);
6181
- renderAllImmediate(); // Context 사용 시 컴포넌트 등록 (다중 인스턴스 관리)
6891
+ renderAllImmediate(); // Context 사용 시 컴포넌트 등록
6182
6892
 
6183
6893
  var componentInstance = null;
6184
6894
 
@@ -6189,28 +6899,25 @@
6189
6899
  return findData(offset) !== null;
6190
6900
  },
6191
6901
  onClick: onClick,
6192
- onMouseOver: onMouseOver,
6193
- onMouseOut: onMouseOut,
6194
6902
  findData: findData,
6195
6903
  setHovered: setHovered,
6196
6904
  handleLocalClick: handleLocalClick,
6197
6905
  getSelectedIds: function () {
6198
6906
  return selectedIdsRef.current;
6907
+ },
6908
+ isInteractionDisabled: function () {
6909
+ return disableInteractionRef.current;
6199
6910
  }
6200
6911
  };
6201
6912
  context.registerComponent(componentInstance);
6202
- } // Cleanup 함수
6203
-
6913
+ }
6204
6914
 
6205
6915
  return function () {
6206
- // RAF 정리
6207
6916
  if (resizeRafId !== null) {
6208
6917
  cancelAnimationFrame(resizeRafId);
6209
- } // 옵저버 정리
6210
-
6211
-
6212
- resizeObserver.disconnect(); // 이벤트 리스너 정리
6918
+ }
6213
6919
 
6920
+ resizeObserver.disconnect();
6214
6921
  controller.removeEventListener('IDLE', handleIdle);
6215
6922
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
6216
6923
  controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
@@ -6219,52 +6926,70 @@
6219
6926
  controller.removeEventListener('MOUSEMOVE', handleMouseMove);
6220
6927
  controller.removeEventListener('DRAGSTART', handleDragStart);
6221
6928
  controller.removeEventListener('DRAGEND', handleDragEnd);
6222
- mapDiv.removeEventListener('mouseleave', handleMouseLeave); // Context 정리
6929
+ mapDiv.removeEventListener('mouseleave', handleMouseLeave);
6223
6930
 
6224
6931
  if (context && componentInstance) {
6225
6932
  context.unregisterComponent(componentInstance);
6226
- } // Konva 리소스 정리
6227
-
6933
+ }
6228
6934
 
6229
6935
  baseLayer.destroyChildren();
6230
- animationLayer.destroyChildren();
6231
6936
  eventLayer.destroyChildren();
6232
- stage.destroy(); // 캐시 정리
6233
-
6937
+ stage.destroy();
6234
6938
  offsetCacheRef.current.clear();
6235
6939
  boundingBoxCacheRef.current.clear();
6236
6940
  spatialIndexRef.current.clear();
6237
6941
  };
6238
- }, []); // --------------------------------------------------------------------------
6239
- // Lifecycle: 외부 selectedItems 동기화
6240
- // --------------------------------------------------------------------------
6942
+ }, []); // disableInteraction 동기화
6943
+
6944
+ React.useEffect(function () {
6945
+ disableInteractionRef.current = disableInteraction;
6946
+ }, [disableInteraction]); // enableViewportCulling 동기화
6241
6947
 
6242
6948
  React.useEffect(function () {
6243
- if (!stageRef.current) return; // externalSelectedItems가 undefined면 외부 제어 안 함
6949
+ enableViewportCullingRef.current = enableViewportCulling;
6244
6950
 
6245
- if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
6951
+ if (stageRef.current) {
6952
+ // 뷰포트 컬링 설정이 변경되면 shape 재생성 필요
6953
+ var baseLayer = baseLayerRef.current;
6246
6954
 
6247
- var newSelectedIds = new Set();
6248
- var newSelectedItemsMap = new Map();
6249
- externalSelectedItems.forEach(function (item) {
6250
- newSelectedIds.add(item.id);
6251
- newSelectedItemsMap.set(item.id, item);
6252
- });
6253
- selectedIdsRef.current = newSelectedIds;
6254
- selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
6955
+ if (baseLayer) {
6956
+ var shape = baseLayer.findOne('.base-render-shape');
6957
+
6958
+ if (shape) {
6959
+ shape.destroy();
6960
+ }
6961
+ }
6962
+
6963
+ var eventLayer = eventLayerRef.current;
6964
+
6965
+ if (eventLayer) {
6966
+ var shape = eventLayer.findOne('.event-render-shape');
6967
+
6968
+ if (shape) {
6969
+ shape.destroy();
6970
+ }
6971
+ }
6972
+
6973
+ renderAllImmediate();
6974
+ }
6975
+ }, [enableViewportCulling]); // 외부 selectedItems 동기화
6255
6976
 
6977
+ React.useEffect(function () {
6978
+ if (!stageRef.current) return;
6979
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
6256
6980
  doRenderBase();
6257
- doRenderAnimation();
6258
6981
  doRenderEvent();
6259
- }, [externalSelectedItems]); // 배열 자체를 dependency로 사용
6260
- // --------------------------------------------------------------------------
6261
- // Lifecycle: 마커 데이터 변경 시 렌더링
6262
- // --------------------------------------------------------------------------
6982
+ }, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
6263
6983
 
6264
6984
  React.useEffect(function () {
6265
- if (!stageRef.current) return; // markersRef 동기화
6985
+ if (!stageRef.current) return;
6986
+ selectedItemRef.current = externalSelectedItem;
6987
+ doRenderEvent();
6988
+ }, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
6266
6989
 
6267
- markersRef.current = markers; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
6990
+ React.useEffect(function () {
6991
+ if (!stageRef.current) return;
6992
+ dataRef.current = data;
6268
6993
 
6269
6994
  if (containerRef.current) {
6270
6995
  containerRef.current.style.transform = '';
@@ -6274,48 +6999,12 @@
6274
6999
  accumTranslateRef.current = {
6275
7000
  x: 0,
6276
7001
  y: 0
6277
- }; // 캐시 정리 (새 데이터이므로 기존 캐시는 무효)
6278
-
7002
+ };
6279
7003
  offsetCacheRef.current.clear();
6280
7004
  boundingBoxCacheRef.current.clear();
6281
- /**
6282
- * 선택 상태 동기화 (최적화 버전)
6283
- *
6284
- * markers가 변경되면 selectedItemsMapRef도 업데이트 필요
6285
- * (참조가 바뀌므로 기존 Map의 데이터는 stale 상태)
6286
- *
6287
- * 🔥 중요: 화면 밖 마커도 선택 상태 유지!
6288
- * - 현재 markers에 있으면 최신 데이터로 업데이트
6289
- * - 없으면 기존 selectedItemsMapRef의 데이터 유지
6290
- *
6291
- * 최적화: markers를 Map으로 먼저 변환하여 find() 순회 제거
6292
- * - O(전체 마커 수 + 선택된 개수) - 매우 효율적
6293
- */
6294
-
6295
- var markersMap = new Map(markers.map(function (m) {
6296
- return [m.id, m];
6297
- }));
6298
- var newSelectedItemsMap = new Map();
6299
- selectedIdsRef.current.forEach(function (id) {
6300
- // 현재 markers에 있으면 최신 데이터 사용
6301
- var currentItem = markersMap.get(id);
6302
-
6303
- if (currentItem) {
6304
- newSelectedItemsMap.set(id, currentItem);
6305
- } else {
6306
- // 화면 밖이면 기존 데이터 유지
6307
- var prevItem = selectedItemsMapRef.current.get(id);
6308
-
6309
- if (prevItem) {
6310
- newSelectedItemsMap.set(id, prevItem);
6311
- }
6312
- }
6313
- }); // selectedIdsRef는 그대로 유지 (화면 밖 마커도 선택 상태 유지)
6314
-
6315
- selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
6316
-
7005
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
6317
7006
  renderAllImmediate();
6318
- }, [markers]);
7007
+ }, [data]);
6319
7008
  return reactDom.createPortal(React__default["default"].createElement("div", {
6320
7009
  ref: containerRef,
6321
7010
  style: {
@@ -6325,40 +7014,6 @@
6325
7014
  }
6326
7015
  }), divElement);
6327
7016
  };
6328
- /**
6329
- * 🔥 React.memo 최적화: 마커 배열과 selectedItems 변경 체크
6330
- *
6331
- * 비교 전략:
6332
- * 1. markers 배열 비교
6333
- * 2. selectedItems 배열 비교 (외부 제어)
6334
- *
6335
- * 주의: JSON.stringify() 사용 금지! (매우 느림)
6336
- */
6337
-
6338
-
6339
- var WoongKonvaMarker = React__default["default"].memo(WoongKonvaMarkerComponent, function (prevProps, nextProps) {
6340
- // 1. markers 비교
6341
- var prevMarkers = prevProps.markers;
6342
- var nextMarkers = nextProps.markers; // 참조가 같으면 스킵
6343
-
6344
- if (prevMarkers !== nextMarkers) {
6345
- // 길이가 다르면 변경됨
6346
- if (prevMarkers.length !== nextMarkers.length) return false; // 각 마커의 ID 비교
6347
-
6348
- for (var i = 0; i < prevMarkers.length; i++) {
6349
- if (prevMarkers[i].id !== nextMarkers[i].id) {
6350
- return false; // 변경됨 → 리렌더링
6351
- }
6352
- }
6353
- } // 2. selectedItems 비교 (참조만 비교)
6354
-
6355
-
6356
- if (prevProps.selectedItems !== nextProps.selectedItems) {
6357
- return false; // 변경됨 → 리렌더링
6358
- }
6359
-
6360
- return true; // 같음 → 리렌더링 스킵
6361
- });
6362
7017
 
6363
7018
  var css_248z = ".MintMapWrapper-module_mint-map-control-wrapper-container__DONh7 {\n position: absolute;\n width: 100%;\n height: 100%;\n display: flex;\n pointer-events: none;\n z-index: 101;\n}\n\n.MintMapWrapper-module_mint-map-overlay-wrapper__Jn4wV {\n position: absolute;\n z-index: 1;\n}";
6364
7019
  var styles = {"mint-map-control-wrapper-container":"MintMapWrapper-module_mint-map-control-wrapper-container__DONh7","mint-map-overlay-wrapper":"MintMapWrapper-module_mint-map-overlay-wrapper__Jn4wV"};
@@ -9202,7 +9857,6 @@
9202
9857
  exports.Drawable = Drawable;
9203
9858
  exports.GeoCalulator = GeoCalulator;
9204
9859
  exports.GoogleMintMapController = GoogleMintMapController;
9205
- exports.KonvaMarkerProvider = KonvaMarkerProvider;
9206
9860
  exports.LRUCache = LRUCache;
9207
9861
  exports.MapBuildingProjection = MapBuildingProjection;
9208
9862
  exports.MapCanvasMarkerWrapper = MapCanvasMarkerWrapper;
@@ -9235,18 +9889,30 @@
9235
9889
  exports.Spacing = Spacing;
9236
9890
  exports.SpatialHashGrid = SpatialHashGrid;
9237
9891
  exports.Status = Status;
9238
- exports.WoongKonvaMarker = WoongKonvaMarker;
9892
+ exports.WoongCanvasMarker = WoongCanvasMarker;
9893
+ exports.WoongCanvasPolygon = WoongCanvasPolygon;
9894
+ exports.WoongCanvasProvider = WoongCanvasProvider;
9895
+ exports.buildSpatialIndex = buildSpatialIndex;
9896
+ exports.calculateTextBoxWidth = calculateTextBoxWidth;
9239
9897
  exports.computeMarkerOffset = computeMarkerOffset;
9240
9898
  exports.computePolygonOffsets = computePolygonOffsets;
9899
+ exports.createMapEventHandlers = createMapEventHandlers;
9241
9900
  exports.getClusterInfo = getClusterInfo;
9242
9901
  exports.getMapOfType = getMapOfType;
9902
+ exports.hexToRgba = hexToRgba;
9903
+ exports.isInViewport = isInViewport;
9243
9904
  exports.isPointInMarkerData = isPointInMarkerData;
9244
9905
  exports.isPointInPolygon = isPointInPolygon;
9245
9906
  exports.isPointInPolygonData = isPointInPolygonData;
9246
9907
  exports.log = log;
9247
- exports.useKonvaMarkerContext = useKonvaMarkerContext;
9908
+ exports.mapValuesToArray = mapValuesToArray;
9909
+ exports.syncExternalSelectedItems = syncExternalSelectedItems;
9910
+ exports.syncSelectedItems = syncSelectedItems;
9911
+ exports.updateViewport = updateViewport;
9248
9912
  exports.useMarkerMoving = useMarkerMoving;
9249
9913
  exports.useMintMapController = useMintMapController;
9914
+ exports.useWoongCanvasContext = useWoongCanvasContext;
9915
+ exports.validateEvent = validateEvent;
9250
9916
  exports.waiting = waiting;
9251
9917
 
9252
9918
  Object.defineProperty(exports, '__esModule', { value: true });