@mint-ui/map 1.2.0-test.34 → 1.2.0-test.36

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 (25) hide show
  1. package/dist/components/mint-map/core/advanced/shared/context.d.ts +19 -12
  2. package/dist/components/mint-map/core/advanced/shared/context.js +54 -75
  3. package/dist/components/mint-map/core/advanced/shared/helpers.d.ts +20 -0
  4. package/dist/components/mint-map/core/advanced/shared/helpers.js +40 -0
  5. package/dist/components/mint-map/core/advanced/shared/hooks.d.ts +74 -0
  6. package/dist/components/mint-map/core/advanced/shared/hooks.js +189 -0
  7. package/dist/components/mint-map/core/advanced/shared/index.d.ts +3 -0
  8. package/dist/components/mint-map/core/advanced/shared/performance.d.ts +12 -110
  9. package/dist/components/mint-map/core/advanced/shared/performance.js +56 -151
  10. package/dist/components/mint-map/core/advanced/shared/types.d.ts +18 -153
  11. package/dist/components/mint-map/core/advanced/shared/types.js +0 -1
  12. package/dist/components/mint-map/core/advanced/shared/utils.d.ts +36 -27
  13. package/dist/components/mint-map/core/advanced/shared/utils.js +58 -52
  14. package/dist/components/mint-map/core/advanced/shared/viewport.d.ts +42 -0
  15. package/dist/components/mint-map/core/advanced/shared/viewport.js +51 -0
  16. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.d.ts +22 -74
  17. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.js +156 -617
  18. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.d.ts +26 -76
  19. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.js +152 -551
  20. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.d.ts +67 -8
  21. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.js +81 -20
  22. package/dist/index.es.js +917 -1575
  23. package/dist/index.js +11 -0
  24. package/dist/index.umd.js +923 -1573
  25. package/package.json +1 -1
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,30 +839,37 @@
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;
@@ -866,23 +877,20 @@
866
877
  /**
867
878
  * 폴리곤 히트 테스트 (도넛 폴리곤 지원)
868
879
  *
869
- * 로직:
870
- * 1. 외부 폴리곤(첫 번째): 내부에 있어야 함
871
- * 2. 내부 구멍들(나머지): 내부에 있으면 안 됨 (evenodd 규칙)
872
- *
873
- * 중요: 도넛 폴리곤과 내부 폴리곤은 별개의 polygonData로 처리됨
874
- * - 도넛 폴리곤 A: isDonutPolygon=true
875
- * - 내부 폴리곤 B: isDonutPolygon=false (별도 데이터)
880
+ * @param clickedOffset 클릭/마우스 위치 좌표
881
+ * @param polygonData 폴리곤 데이터
882
+ * @param getPolygonOffsets 폴리곤 좌표 변환 함수
883
+ * @returns 점이 폴리곤 내부에 있으면 true
876
884
  */
877
885
 
878
886
  var isPointInPolygonData = function (clickedOffset, polygonData, getPolygonOffsets) {
879
887
  var polygonOffsets = getPolygonOffsets(polygonData);
880
- if (!polygonOffsets) return false; // 🍩 도넛 폴리곤 처리 (isDonutPolygon === true)
888
+ if (!polygonOffsets) return false; // 도넛 폴리곤 처리: 외부 폴리곤 내부에 있으면서 구멍(hole) 내부에 있지 않아야 함
881
889
 
882
890
  if (polygonData.isDonutPolygon) {
883
891
  for (var _i = 0, polygonOffsets_1 = polygonOffsets; _i < polygonOffsets_1.length; _i++) {
884
892
  var multiPolygon = polygonOffsets_1[_i];
885
- if (multiPolygon.length === 0) continue; // 외부 폴리곤만 있는 경우 (구멍 없음) - 일반 폴리곤처럼 처리
893
+ if (multiPolygon.length === 0) continue; // 구멍이 없는 경우 일반 폴리곤과 동일
886
894
 
887
895
  if (multiPolygon.length === 1) {
888
896
  if (isPointInPolygon(clickedOffset, multiPolygon[0])) {
@@ -890,33 +898,30 @@
890
898
  }
891
899
 
892
900
  continue;
893
- } // 1. 외부 폴리곤(첫 번째)에 포함되는지 확인
901
+ } // 외부 폴리곤 내부에 있는지 확인
894
902
 
895
903
 
896
904
  var outerPolygon = multiPolygon[0];
897
905
 
898
906
  if (!isPointInPolygon(clickedOffset, outerPolygon)) {
899
- continue; // 외부 폴리곤 밖이면 다음 multiPolygon 확인
900
- } // 2. 내부 구멍들(나머지)에 포함되는지 확인
907
+ continue;
908
+ } // 구멍 내부에 있으면 false (도넛의 공간)
901
909
 
902
910
 
903
911
  for (var i = 1; i < multiPolygon.length; i++) {
904
912
  var hole = multiPolygon[i];
905
913
 
906
914
  if (isPointInPolygon(clickedOffset, hole)) {
907
- // ❌ 구멍 안에 있음 → 이 도넛 폴리곤은 히트 안 됨
908
- // 다른 multiPolygon 체크하지 않고 바로 false 반환
909
- // (도넛 폴리곤의 구멍 안은 무조건 클릭 불가)
910
915
  return false;
911
916
  }
912
- } // 외부 폴리곤 + 구멍 = 히트!
917
+ } // 외부 폴리곤 내부에 있으면서 모든 구멍 밖에 있으면 true
913
918
 
914
919
 
915
920
  return true;
916
921
  }
917
922
 
918
923
  return false;
919
- } // 일반 폴리곤 처리 (isDonutPolygon === false 또는 undefined)
924
+ } // 일반 폴리곤 처리
920
925
 
921
926
 
922
927
  for (var _a = 0, polygonOffsets_2 = polygonOffsets; _a < polygonOffsets_2.length; _a++) {
@@ -935,12 +940,12 @@
935
940
  return false;
936
941
  };
937
942
  /**
938
- * 마커 히트 테스트 (클릭/hover 영역 체크)
943
+ * 마커 히트 테스트 (꼬리 제외)
939
944
  *
940
- * 🎯 중요: 꼬리(tail)는 Hit Test 영역에서 제외됩니다!
941
- * - markerOffset.y는 마커 최하단(꼬리 끝) 좌표
942
- * - boxHeight는 마커 본체만 포함 (꼬리 제외)
943
- * - tailHeight만큼 위로 올려서 본체만 Hit Test 영역으로 사용
945
+ * @param clickedOffset 클릭/마우스 위치 좌표
946
+ * @param markerData 마커 데이터
947
+ * @param getMarkerOffset 마커 좌표 변환 함수
948
+ * @returns 점이 마커 영역 내부에 있으면 true
944
949
  */
945
950
 
946
951
  var isPointInMarkerData = function (clickedOffset, markerData, getMarkerOffset) {
@@ -948,41 +953,41 @@
948
953
  if (!markerOffset) return false;
949
954
  var boxWidth = markerData.boxWidth || 50;
950
955
  var boxHeight = markerData.boxHeight || 28;
951
- var tailHeight = markerData.tailHeight || 0; // 🎯 tailHeight 사용!
956
+ var tailHeight = markerData.tailHeight || 0; // 마커 중심점 기준으로 박스 영역 계산 (꼬리는 제외)
952
957
 
953
958
  var x = markerOffset.x - boxWidth / 2;
954
- var y = markerOffset.y - boxHeight - tailHeight; // 🔥 꼬리만큼 위로!
959
+ var y = markerOffset.y - boxHeight - tailHeight; // 클릭 위치가 박스 영역 내부에 있는지 확인
955
960
 
956
961
  return clickedOffset.x >= x && clickedOffset.x <= x + boxWidth && clickedOffset.y >= y && clickedOffset.y <= y + boxHeight;
957
- };
962
+ }; // Hex 색상을 RGBA로 변환
963
+
958
964
  var hexToRgba = function (hexColor, alpha) {
959
965
  if (alpha === void 0) {
960
966
  alpha = 1;
961
- } // NOTE: 입력된 hexColor에서 "#" 제거
962
-
967
+ }
963
968
 
964
- var hex = hexColor.replace('#', ''); // NOTE: 6자리일 경우 알파 값은 사용자가 제공한 alpha 값으로 설정
969
+ var hex = hexColor.replace('#', '');
965
970
 
966
- if (hex.length === 6) {
967
- var r = parseInt(hex.substring(0, 2), 16);
968
- var g = parseInt(hex.substring(2, 4), 16);
969
- var b = parseInt(hex.substring(4, 6), 16);
970
- return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
971
+ if (hex.length !== 6) {
972
+ throw new Error('Invalid hex color format');
971
973
  }
972
974
 
973
- throw new Error('Invalid hex color format');
975
+ var r = parseInt(hex.substring(0, 2), 16);
976
+ var g = parseInt(hex.substring(2, 4), 16);
977
+ var b = parseInt(hex.substring(4, 6), 16);
978
+ return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
974
979
  };
975
980
  var tempCanvas = document.createElement('canvas');
976
981
  var tempCtx = tempCanvas.getContext('2d');
977
982
  /**
978
- * 텍스트 박스의 너비를 계산합니다.
983
+ * 텍스트 박스 너비 계산
979
984
  *
980
- * @param {Object} params - 파라미터 객체
981
- * @param {string} params.text - 측정할 텍스트
982
- * @param {string} params.fontConfig - 폰트 설정 (예: 'bold 16px Arial')
983
- * @param {number} params.padding - 텍스트 박스에 적용할 패딩 값
984
- * @param {number} params.minWidth - 최소 너비
985
- * @returns {number} 계산된 텍스트 박스의 너비
985
+ * @param params 파라미터 객체
986
+ * @param params.text 측정할 텍스트
987
+ * @param params.fontConfig 폰트 설정
988
+ * @param params.padding 패딩 값 (px)
989
+ * @param params.minWidth 최소 너비 (px)
990
+ * @returns 계산된 텍스트 박스 너비 (px)
986
991
  */
987
992
 
988
993
  var calculateTextBoxWidth = function (_a) {
@@ -997,31 +1002,28 @@
997
1002
  };
998
1003
 
999
1004
  var WoongCanvasContext = React.createContext(null);
1005
+ /**
1006
+ * WoongCanvasProvider 컴포넌트
1007
+ *
1008
+ * 다중 WoongCanvas 인스턴스를 관리하고 zIndex 기반 이벤트 우선순위를 처리합니다.
1009
+ */
1010
+
1000
1011
  var WoongCanvasProvider = function (_a) {
1001
1012
  var children = _a.children;
1002
- var controller = useMintMapController(); // Refs
1003
-
1013
+ var controller = useMintMapController();
1004
1014
  var componentsRef = React.useRef([]);
1005
1015
  var currentHoveredRef = React.useRef(null);
1006
1016
  var currentHoveredDataRef = React.useRef(null);
1007
- var draggingRef = React.useRef(false);
1008
- /**
1009
- * 컴포넌트 등록 (zIndex 내림차순 정렬)
1010
- * 높은 zIndex가 먼저 처리됨
1011
- */
1017
+ var draggingRef = React.useRef(false); // 컴포넌트 등록 (zIndex 내림차순 정렬)
1012
1018
 
1013
1019
  var registerComponent = React.useCallback(function (instance) {
1014
1020
  componentsRef.current.push(instance);
1015
1021
  componentsRef.current.sort(function (a, b) {
1016
1022
  return b.zIndex - a.zIndex;
1017
1023
  });
1018
- }, []);
1019
- /**
1020
- * 컴포넌트 등록 해제
1021
- */
1024
+ }, []); // 컴포넌트 등록 해제
1022
1025
 
1023
1026
  var unregisterComponent = React.useCallback(function (instance) {
1024
- // Hover 중이던 컴포넌트면 초기화
1025
1027
  if (currentHoveredRef.current === instance) {
1026
1028
  currentHoveredRef.current = null;
1027
1029
  currentHoveredDataRef.current = null;
@@ -1030,99 +1032,77 @@
1030
1032
  componentsRef.current = componentsRef.current.filter(function (c) {
1031
1033
  return c !== instance;
1032
1034
  });
1033
- }, []);
1034
- /**
1035
- * 전역 클릭 핸들러 (zIndex 우선순위)
1036
- */
1035
+ }, []); // 전역 클릭 핸들러 (zIndex 우선순위)
1037
1036
 
1038
1037
  var handleGlobalClick = React.useCallback(function (event) {
1039
- var _a;
1038
+ var _a, _b;
1040
1039
 
1041
1040
  if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
1042
- var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 순서대로 순회 (높은 것부터)
1043
-
1044
- for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
1045
- var component = _b[_i]; // 🚫 상호작용이 비활성화된 컴포넌트는 스킵
1041
+ var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
1046
1042
 
1043
+ for (var _i = 0, _c = componentsRef.current; _i < _c.length; _i++) {
1044
+ var component = _c[_i];
1047
1045
  if (component.isInteractionDisabled()) continue;
1048
1046
  var data = component.findData(clickedOffset);
1047
+ if (!data) continue; // 첫 번째로 찾은 항목만 처리하고 종료 (zIndex 우선순위)
1049
1048
 
1050
- if (data) {
1051
- component.handleLocalClick(data);
1052
-
1053
- if (component.onClick) {
1054
- component.onClick(data, component.getSelectedIds());
1055
- }
1056
-
1057
- return; // 첫 번째 히트만 처리
1058
- }
1049
+ component.handleLocalClick(data);
1050
+ (_b = component.onClick) === null || _b === void 0 ? void 0 : _b.call(component, data, component.getSelectedIds());
1051
+ return;
1059
1052
  }
1060
- }, [controller]);
1061
- /**
1062
- * 전역 마우스 이동 핸들러 (zIndex 우선순위)
1063
- */
1053
+ }, [controller]); // 전역 마우스 이동 핸들러 (zIndex 우선순위)
1064
1054
 
1065
1055
  var handleGlobalMouseMove = React.useCallback(function (event) {
1066
1056
  var _a;
1067
1057
 
1068
1058
  if (draggingRef.current || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
1069
- var mouseOffset = controller.positionToOffset(event.param.position); // zIndex 순서대로 순회하여 Hover 대상 찾기
1070
-
1059
+ var mouseOffset = controller.positionToOffset(event.param.position);
1071
1060
  var newHoveredComponent = null;
1072
- var newHoveredData = null;
1061
+ var newHoveredData = null; // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
1073
1062
 
1074
1063
  for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
1075
- var component = _b[_i]; // 🚫 상호작용이 비활성화된 컴포넌트는 스킵
1076
-
1064
+ var component = _b[_i];
1077
1065
  if (component.isInteractionDisabled()) continue;
1078
1066
  var data = component.findData(mouseOffset);
1067
+ if (!data) continue; // 첫 번째로 찾은 항목만 hover 처리 (zIndex 우선순위)
1079
1068
 
1080
- if (data) {
1081
- newHoveredComponent = component;
1082
- newHoveredData = data;
1083
- break; // 번째 히트만 처리
1084
- }
1085
- } // Hover 상태 변경 감지 (최적화: 별도 ref로 직접 비교)
1086
-
1069
+ newHoveredComponent = component;
1070
+ newHoveredData = data;
1071
+ break;
1072
+ } // hover 상태가 변경되지 않았으면 종료 (불필요한 렌더링 방지)
1087
1073
 
1088
- if (currentHoveredRef.current !== newHoveredComponent || currentHoveredDataRef.current !== newHoveredData) {
1089
- // 이전 hover 해제
1090
- if (currentHoveredRef.current) {
1091
- currentHoveredRef.current.setHovered(null);
1092
1074
 
1093
- if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
1094
- currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
1095
- }
1096
- } // 새 hover 설정
1075
+ if (currentHoveredRef.current === newHoveredComponent && currentHoveredDataRef.current === newHoveredData) {
1076
+ return;
1077
+ } // 기존 hover 항목에 mouseOut 이벤트 발생
1097
1078
 
1098
1079
 
1099
- if (newHoveredComponent && newHoveredData) {
1100
- newHoveredComponent.setHovered(newHoveredData);
1080
+ if (currentHoveredRef.current) {
1081
+ currentHoveredRef.current.setHovered(null);
1101
1082
 
1102
- if (newHoveredComponent.onMouseOver) {
1103
- newHoveredComponent.onMouseOver(newHoveredData);
1104
- }
1083
+ if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
1084
+ currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
1105
1085
  }
1086
+ } // 새 hover 항목에 mouseOver 이벤트 발생
1106
1087
 
1107
- currentHoveredRef.current = newHoveredComponent;
1108
- currentHoveredDataRef.current = newHoveredData;
1088
+
1089
+ if (newHoveredComponent && newHoveredData) {
1090
+ newHoveredComponent.setHovered(newHoveredData);
1091
+
1092
+ if (newHoveredComponent.onMouseOver) {
1093
+ newHoveredComponent.onMouseOver(newHoveredData);
1094
+ }
1109
1095
  }
1110
- }, [controller]);
1111
- /**
1112
- * 줌/드래그 시작 (마우스 이동 이벤트 무시)
1113
- */
1114
1096
 
1097
+ currentHoveredRef.current = newHoveredComponent;
1098
+ currentHoveredDataRef.current = newHoveredData;
1099
+ }, [controller]);
1115
1100
  var handleZoomStart = React.useCallback(function () {
1116
1101
  draggingRef.current = true;
1117
1102
  }, []);
1118
- /**
1119
- * 지도 idle (마우스 이동 이벤트 재개)
1120
- */
1121
-
1122
1103
  var handleIdle = React.useCallback(function () {
1123
1104
  draggingRef.current = false;
1124
- }, []); // 이벤트 리스너 등록
1125
-
1105
+ }, []);
1126
1106
  React.useEffect(function () {
1127
1107
  controller.addEventListener('CLICK', handleGlobalClick);
1128
1108
  controller.addEventListener('MOUSEMOVE', handleGlobalMouseMove);
@@ -1134,8 +1114,7 @@
1134
1114
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
1135
1115
  controller.removeEventListener('IDLE', handleIdle);
1136
1116
  };
1137
- }, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]); // Context value 메모이제이션
1138
-
1117
+ }, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]);
1139
1118
  var contextValue = React.useMemo(function () {
1140
1119
  return {
1141
1120
  registerComponent: registerComponent,
@@ -1146,65 +1125,43 @@
1146
1125
  value: contextValue
1147
1126
  }, children);
1148
1127
  };
1128
+ /**
1129
+ * WoongCanvas Context Hook
1130
+ *
1131
+ * @returns WoongCanvasContextValue 또는 null (Provider 없으면)
1132
+ */
1133
+
1149
1134
  var useWoongCanvasContext = function () {
1150
- var context = React.useContext(WoongCanvasContext);
1151
- return context;
1135
+ return React.useContext(WoongCanvasContext);
1152
1136
  };
1153
1137
 
1154
- // ============================================================================
1155
- // 성능 최적화 상수 (30,000개 마커/폴리곤 기준 최적화)
1156
- // ============================================================================
1157
-
1158
1138
  /**
1159
- * 공간 인덱스 그리드 셀 크기 (px)
1160
- *
1161
- * 최적값 계산:
1162
- * - 목표: 클릭 시 셀당 10~30개 항목만 체크 (빠른 Hit Test)
1163
- * - 화면 크기: 1920×1080 기준
1164
- * - 30,000개 항목 → 50px 셀 크기 = 약 800개 셀 = 셀당 ~37개
1165
- *
1166
- * 성능 비교 (30,000개 기준):
1167
- * - 200px: 셀당 ~577개 → Hit Test O(577) ❌ 느림
1168
- * - 50px: 셀당 ~37개 → Hit Test O(37) ✅ 15배 빠름!
1139
+ * 공간 인덱스 그리드 셀 크기 (픽셀 단위)
1169
1140
  *
1170
- * 트레이드오프:
1171
- * - 작을수록: Hit Test 빠름, 메모리 사용량 증가
1172
- * - 클수록: 메모리 효율적, Hit Test 느림
1141
+ * @default 50
1173
1142
  */
1174
1143
  var SPATIAL_GRID_CELL_SIZE = 50;
1175
1144
  /**
1176
- * 뷰포트 컬링 여유 공간 (px)
1145
+ * 뷰포트 컬링 여유 공간 (픽셀 단위)
1177
1146
  *
1178
- * 화면 밖 100px까지 렌더링하여 스크롤 시 부드러운 전환
1179
- * 30,000개 중 실제 렌더링: 화면에 보이는 1,000~3,000개만
1147
+ * @default 100
1180
1148
  */
1181
1149
 
1182
1150
  var DEFAULT_CULLING_MARGIN = 100;
1183
1151
  /**
1184
1152
  * LRU 캐시 최대 항목 수
1185
1153
  *
1186
- * 좌표 변환 결과 캐싱 (positionToOffset 연산 비용 절약)
1187
- *
1188
- * 최적값 계산:
1189
- * - 전체 항목: 30,000개
1190
- * - 캐시 크기: 30,000개 → 100% 히트율 (메모리: ~2.4MB)
1191
- *
1192
- * 메모리 사용량 (항목당 ~80 bytes):
1193
- * - 10,000개: ~800KB → 캐시 히트율 33% ❌
1194
- * - 30,000개: ~2.4MB → 캐시 히트율 100% ✅
1195
- *
1196
- * zoom/pan 시 어차피 clear() 호출되므로 메모리 누적 없음
1154
+ * @default 30000
1197
1155
  */
1198
1156
 
1199
1157
  var DEFAULT_MAX_CACHE_SIZE = 30000;
1200
1158
  /**
1201
- * LRU (Least Recently Used) Cache
1202
- * 메모리 제한을 위한 캐시 구현 (최적화 버전)
1159
+ * LRU Cache (Least Recently Used)
1160
+ *
1161
+ * 좌표 변환 결과를 캐싱하기 위한 캐시 구현
1203
1162
  *
1204
- * 개선 사항:
1205
- * 1. get() 성능 향상: 접근 빈도 추적 없이 단순 조회만 수행 (delete+set 제거)
1206
- * 2. set() 버그 수정: 기존 키 업데이트 시 maxSize 체크 로직 개선
1207
- * 3. 메모리 효율: 단순 FIFO 캐시로 동작하여 오버헤드 최소화
1163
+ * @template K 캐시 키 타입
1164
+ * @template V 캐시 타입
1208
1165
  */
1209
1166
 
1210
1167
  var LRUCache =
@@ -1217,64 +1174,43 @@
1217
1174
 
1218
1175
  this.cache = new Map();
1219
1176
  this.maxSize = maxSize;
1220
- }
1221
- /**
1222
- * 캐시에서 값 조회
1223
- *
1224
- * 최적화: delete+set 제거
1225
- * - 이전: 매번 delete+set으로 LRU 갱신 (해시 재계산 비용)
1226
- * - 현재: 단순 조회만 수행 (O(1) 해시 조회)
1227
- *
1228
- * 트레이드오프:
1229
- * - 장점: 읽기 성능 대폭 향상 (10,000번 get → 이전보다 2배 빠름)
1230
- * - 단점: 접근 빈도가 아닌 삽입 순서 기반 eviction (FIFO)
1231
- *
1232
- * WoongCanvasMarker 사용 사례에 최적:
1233
- * - 좌표 변환 결과는 zoom/pan 시 어차피 전체 초기화
1234
- * - 접근 빈도 추적보다 빠른 조회가 더 중요
1235
- */
1177
+ } // 캐시에서 값 조회
1236
1178
 
1237
1179
 
1238
1180
  LRUCache.prototype.get = function (key) {
1239
1181
  return this.cache.get(key);
1240
- };
1241
- /**
1242
- * 캐시에 값 저장 (버그 수정 + 최적화)
1243
- *
1244
- * 수정 사항:
1245
- * 1. 기존 키 업데이트 시 크기 체크 누락 버그 수정
1246
- * 2. 로직 명확화: 기존 항목/신규 항목 분리 처리
1247
- */
1182
+ }; // 캐시에 값 저장 (FIFO eviction)
1248
1183
 
1249
1184
 
1250
1185
  LRUCache.prototype.set = function (key, value) {
1251
1186
  var exists = this.cache.has(key);
1252
1187
 
1253
1188
  if (exists) {
1254
- // 기존 항목 업데이트: 단순 덮어쓰기 (크기 변화 없음)
1255
1189
  this.cache.set(key, value);
1256
- } else {
1257
- // 신규 항목 추가: 크기 체크 필요
1258
- if (this.cache.size >= this.maxSize) {
1259
- // 가장 오래된 항목 제거 (Map의 첫 번째 항목)
1260
- var firstKey = this.cache.keys().next().value;
1190
+ return;
1191
+ }
1261
1192
 
1262
- if (firstKey !== undefined) {
1263
- this.cache.delete(firstKey);
1264
- }
1265
- }
1193
+ if (this.cache.size >= this.maxSize) {
1194
+ var firstKey = this.cache.keys().next().value;
1266
1195
 
1267
- this.cache.set(key, value);
1196
+ if (firstKey !== undefined) {
1197
+ this.cache.delete(firstKey);
1198
+ }
1268
1199
  }
1269
- };
1200
+
1201
+ this.cache.set(key, value);
1202
+ }; // 캐시 초기화
1203
+
1270
1204
 
1271
1205
  LRUCache.prototype.clear = function () {
1272
1206
  this.cache.clear();
1273
- };
1207
+ }; // 캐시 크기 반환
1208
+
1274
1209
 
1275
1210
  LRUCache.prototype.size = function () {
1276
1211
  return this.cache.size;
1277
- };
1212
+ }; // 키 존재 여부 확인
1213
+
1278
1214
 
1279
1215
  LRUCache.prototype.has = function (key) {
1280
1216
  return this.cache.has(key);
@@ -1284,16 +1220,10 @@
1284
1220
  }();
1285
1221
  /**
1286
1222
  * Spatial Hash Grid (공간 해시 그리드)
1287
- * 공간 인덱싱을 위한 그리드 기반 자료구조 (개선 버전)
1288
1223
  *
1289
- * 개선 사항:
1290
- * 1. 중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전
1291
- * 2. 메모리 누수 방지: 기존 항목 자동 제거
1292
- * 3. 성능 최적화: 불필요한 배열 생성 최소화
1224
+ * 빠른 Hit Test를 위한 그리드 기반 공간 인덱싱 자료구조
1293
1225
  *
1294
- * 사용 사례:
1295
- * - 빠른 Hit Test (마우스 클릭 시 어떤 마커/폴리곤인지 찾기)
1296
- * - 30,000개 항목 → 클릭 위치 주변 ~10개만 체크 (3,000배 빠름)
1226
+ * @template T 인덱싱할 항목 타입
1297
1227
  */
1298
1228
 
1299
1229
  var SpatialHashGrid =
@@ -1307,28 +1237,24 @@
1307
1237
  this.cellSize = cellSize;
1308
1238
  this.grid = new Map();
1309
1239
  this.itemToCells = new Map();
1310
- }
1311
- /**
1312
- * 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
1313
- */
1240
+ } // 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
1314
1241
 
1315
1242
 
1316
1243
  SpatialHashGrid.prototype.getCellKey = function (x, y) {
1244
+ // 좌표를 셀 크기로 나눈 몫으로 셀 인덱스 계산
1317
1245
  var cellX = Math.floor(x / this.cellSize);
1318
1246
  var cellY = Math.floor(y / this.cellSize);
1319
1247
  return "".concat(cellX, ",").concat(cellY);
1320
- };
1321
- /**
1322
- * 바운딩 박스가 걸치는 모든 셀 키 배열 반환
1323
- */
1248
+ }; // 바운딩 박스가 걸치는 모든 셀 키 배열 반환
1324
1249
 
1325
1250
 
1326
1251
  SpatialHashGrid.prototype.getCellsForBounds = function (minX, minY, maxX, maxY) {
1327
- var cells = [];
1252
+ var cells = []; // 바운딩 박스가 걸치는 셀 범위 계산
1253
+
1328
1254
  var startCellX = Math.floor(minX / this.cellSize);
1329
1255
  var startCellY = Math.floor(minY / this.cellSize);
1330
1256
  var endCellX = Math.floor(maxX / this.cellSize);
1331
- var endCellY = Math.floor(maxY / this.cellSize);
1257
+ var endCellY = Math.floor(maxY / this.cellSize); // 바운딩 박스가 걸치는 모든 셀을 배열에 추가
1332
1258
 
1333
1259
  for (var x = startCellX; x <= endCellX; x++) {
1334
1260
  for (var y = startCellY; y <= endCellY; y++) {
@@ -1337,22 +1263,15 @@
1337
1263
  }
1338
1264
 
1339
1265
  return cells;
1340
- };
1341
- /**
1342
- * 항목 추가 (바운딩 박스 기반)
1343
- *
1344
- * 개선 사항:
1345
- * - 중복 삽입 방지: 기존 항목이 있으면 먼저 제거 후 재삽입
1346
- * - 메모리 누수 방지: 이전 셀 참조 완전 제거
1347
- */
1266
+ }; // 항목 추가 (바운딩 박스 기반, 중복 삽입 방지)
1348
1267
 
1349
1268
 
1350
1269
  SpatialHashGrid.prototype.insert = function (item, minX, minY, maxX, maxY) {
1351
- // 1. 기존 항목 제거 (중복 방지)
1352
- this.remove(item); // 2. 위치에 삽입
1270
+ // 기존 항목 제거 (중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전)
1271
+ this.remove(item); // 바운딩 박스가 걸치는 모든 셀에 항목 등록
1353
1272
 
1354
1273
  var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
1355
- this.itemToCells.set(item, cells);
1274
+ this.itemToCells.set(item, cells); // 항목과 셀의 매핑 저장 (제거 시 필요)
1356
1275
 
1357
1276
  for (var _i = 0, cells_1 = cells; _i < cells_1.length; _i++) {
1358
1277
  var cell = cells_1[_i];
@@ -1363,17 +1282,12 @@
1363
1282
 
1364
1283
  this.grid.get(cell).push(item);
1365
1284
  }
1366
- };
1367
- /**
1368
- * 항목 제거
1369
- *
1370
- * 추가된 메서드: 메모리 누수 방지 및 업데이트 지원
1371
- */
1285
+ }; // 항목 제거 (모든 셀에서 참조 제거)
1372
1286
 
1373
1287
 
1374
1288
  SpatialHashGrid.prototype.remove = function (item) {
1375
1289
  var prevCells = this.itemToCells.get(item);
1376
- if (!prevCells) return; // 셀에서 항목 제거
1290
+ if (!prevCells) return; // 항목이 등록된 모든 셀에서 참조 제거 (메모리 누수 방지)
1377
1291
 
1378
1292
  for (var _i = 0, prevCells_1 = prevCells; _i < prevCells_1.length; _i++) {
1379
1293
  var cell = prevCells_1[_i];
@@ -1384,51 +1298,39 @@
1384
1298
 
1385
1299
  if (index !== -1) {
1386
1300
  cellItems.splice(index, 1);
1387
- } // 빈 셀 정리 (메모리 효율)
1301
+ } // 빈 셀 정리 (메모리 효율: 사용하지 않는 셀 제거)
1388
1302
 
1389
1303
 
1390
1304
  if (cellItems.length === 0) {
1391
1305
  this.grid.delete(cell);
1392
1306
  }
1393
1307
  }
1394
- }
1308
+ } // 항목과 셀의 매핑 제거
1309
+
1395
1310
 
1396
1311
  this.itemToCells.delete(item);
1397
- };
1398
- /**
1399
- * 항목 위치 업데이트
1400
- *
1401
- * 추가된 메서드: remove + insert의 편의 함수
1402
- */
1312
+ }; // 항목 위치 업데이트 (remove + insert)
1403
1313
 
1404
1314
 
1405
1315
  SpatialHashGrid.prototype.update = function (item, minX, minY, maxX, maxY) {
1406
1316
  this.insert(item, minX, minY, maxX, maxY);
1407
- };
1408
- /**
1409
- * 점 주변의 항목 조회 (1개 셀만)
1410
- *
1411
- * 성능: O(해당 셀의 항목 수) - 보통 ~10개
1412
- */
1317
+ }; // 점 주변의 항목 조회 (Hit Test용)
1413
1318
 
1414
1319
 
1415
1320
  SpatialHashGrid.prototype.queryPoint = function (x, y) {
1321
+ // 클릭 위치가 속한 셀의 모든 항목 조회 (O(1) 수준의 빠른 조회)
1416
1322
  var cellKey = this.getCellKey(x, y);
1417
1323
  var items = this.grid.get(cellKey); // 빈 배열 재사용 (메모리 할당 최소화)
1418
1324
 
1419
1325
  return items || [];
1420
- };
1421
- /**
1422
- * 영역 내 항목 조회
1423
- *
1424
- * 성능: O(셀 개수 × 셀당 평균 항목 수)
1425
- * Set으로 중복 제거 보장
1426
- */
1326
+ }; // 영역 내 항목 조회 (Viewport Culling용)
1427
1327
 
1428
1328
 
1429
1329
  SpatialHashGrid.prototype.queryBounds = function (minX, minY, maxX, maxY) {
1330
+ // 영역이 걸치는 모든 셀 찾기
1430
1331
  var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
1431
- var results = new Set();
1332
+ var results = new Set(); // 중복 제거를 위해 Set 사용
1333
+ // 각 셀의 모든 항목을 결과에 추가
1432
1334
 
1433
1335
  for (var _i = 0, cells_2 = cells; _i < cells_2.length; _i++) {
1434
1336
  var cell = cells_2[_i];
@@ -1437,37 +1339,24 @@
1437
1339
  if (items) {
1438
1340
  for (var _a = 0, items_1 = items; _a < items_1.length; _a++) {
1439
1341
  var item = items_1[_a];
1440
- results.add(item);
1342
+ results.add(item); // Set이므로 중복 자동 제거
1441
1343
  }
1442
1344
  }
1443
1345
  }
1444
1346
 
1445
1347
  return Array.from(results);
1446
- };
1447
- /**
1448
- * 항목 존재 여부 확인
1449
- *
1450
- * 추가된 메서드: 빠른 존재 여부 체크
1451
- */
1348
+ }; // 항목 존재 여부 확인
1452
1349
 
1453
1350
 
1454
1351
  SpatialHashGrid.prototype.has = function (item) {
1455
1352
  return this.itemToCells.has(item);
1456
- };
1457
- /**
1458
- * 전체 초기화
1459
- */
1353
+ }; // 전체 초기화
1460
1354
 
1461
1355
 
1462
1356
  SpatialHashGrid.prototype.clear = function () {
1463
1357
  this.grid.clear();
1464
1358
  this.itemToCells.clear();
1465
- };
1466
- /**
1467
- * 통계 정보
1468
- *
1469
- * 개선: totalItems는 실제 고유 항목 수를 정확히 반환
1470
- */
1359
+ }; // 통계 정보 반환
1471
1360
 
1472
1361
 
1473
1362
  SpatialHashGrid.prototype.stats = function () {
@@ -1485,116 +1374,374 @@
1485
1374
  return SpatialHashGrid;
1486
1375
  }();
1487
1376
 
1488
- var cn$3 = classNames__default["default"].bind(styles$1);
1489
- function MintMapCore(_a) {
1490
- var _this = this;
1377
+ /**
1378
+ * 현재 뷰포트 영역 계산
1379
+ *
1380
+ * @param stage Konva Stage 인스턴스
1381
+ * @param cullingMargin 컬링 여유 공간 (px)
1382
+ * @param viewportRef 뷰포트 경계를 저장할 ref
1383
+ */
1384
+ var updateViewport = function (stage, cullingMargin, viewportRef) {
1385
+ if (!stage) return;
1386
+ viewportRef.current = {
1387
+ minX: -cullingMargin,
1388
+ maxX: stage.width() + cullingMargin,
1389
+ minY: -cullingMargin,
1390
+ maxY: stage.height() + cullingMargin
1391
+ };
1392
+ };
1393
+ /**
1394
+ * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
1395
+ *
1396
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1397
+ * @param item 확인할 아이템
1398
+ * @param viewportRef 뷰포트 경계 ref
1399
+ * @param boundingBoxCacheRef 바운딩 박스 캐시 ref
1400
+ * @param computeBoundingBox 바운딩 박스 계산 함수
1401
+ * @returns 뷰포트 안에 있으면 true
1402
+ */
1491
1403
 
1492
- var onLoad = _a.onLoad,
1493
- _b = _a.visible,
1494
- visible = _b === void 0 ? true : _b,
1495
- zoomLevel = _a.zoomLevel,
1496
- center = _a.center,
1497
- _c = _a.centerMoveWithPanning,
1498
- centerMoveWithPanning = _c === void 0 ? false : _c,
1499
- children = _a.children; //controller
1404
+ var isInViewport = function (item, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
1405
+ if (!viewportRef.current) return true;
1406
+ var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
1500
1407
 
1501
- var controller = useMintMapController(); //맵 초기화
1408
+ var bbox = boundingBoxCacheRef.current.get(item.id);
1502
1409
 
1503
- var elementRef = React.useRef(null);
1410
+ if (!bbox) {
1411
+ // 바운딩 박스 계산 (공통 함수 사용)
1412
+ var computed = computeBoundingBox(item);
1413
+ if (!computed) return false;
1414
+ bbox = computed;
1415
+ boundingBoxCacheRef.current.set(item.id, bbox);
1416
+ } // 바운딩 박스와 viewport 교차 체크
1504
1417
 
1505
- var _d = React.useState(false),
1506
- mapInitialized = _d[0],
1507
- setMapInitialized = _d[1];
1508
1418
 
1509
- var currMapInitialized = React.useRef(false);
1510
- React.useEffect(function () {
1511
- (function () {
1512
- return tslib.__awaiter(_this, void 0, void 0, function () {
1513
- var map_1;
1514
- return tslib.__generator(this, function (_a) {
1515
- switch (_a.label) {
1516
- case 0:
1517
- if (!(elementRef && elementRef.current)) return [3
1518
- /*break*/
1519
- , 2];
1520
- return [4
1521
- /*yield*/
1522
- , controller.initializingMap(elementRef.current)];
1419
+ return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
1420
+ };
1523
1421
 
1524
- case 1:
1525
- map_1 = _a.sent();
1422
+ /**
1423
+ * 지도 이벤트 핸들러 생성 함수
1424
+ *
1425
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1426
+ * @param deps 이벤트 핸들러 생성에 필요한 의존성
1427
+ * @returns 지도 이벤트 핸들러 객체
1428
+ */
1526
1429
 
1527
- if (!currMapInitialized.current) {
1528
- currMapInitialized.current = true; //onload callback (setTimeout 으로 맵이 초기화 될 텀을 준다. 특히 google map..)
1430
+ var createMapEventHandlers = function (deps) {
1431
+ var controller = deps.controller,
1432
+ containerRef = deps.containerRef,
1433
+ markerRef = deps.markerRef,
1434
+ options = deps.options,
1435
+ prevCenterOffsetRef = deps.prevCenterOffsetRef,
1436
+ accumTranslateRef = deps.accumTranslateRef,
1437
+ offsetCacheRef = deps.offsetCacheRef,
1438
+ boundingBoxCacheRef = deps.boundingBoxCacheRef,
1439
+ renderAllImmediate = deps.renderAllImmediate; // 지도 이동/줌 완료 시 처리 (캐시 초기화 및 렌더링)
1529
1440
 
1530
- setTimeout(function () {
1531
- // console.log('setMapInitialized true');
1532
- setMapInitialized(true);
1533
- onLoad && onLoad(map_1, controller);
1534
- }, 100);
1535
- }
1441
+ var handleIdle = function () {
1442
+ prevCenterOffsetRef.current = null;
1443
+ accumTranslateRef.current = {
1444
+ x: 0,
1445
+ y: 0
1446
+ }; // 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
1536
1447
 
1537
- _a.label = 2;
1448
+ offsetCacheRef.current.clear();
1449
+ boundingBoxCacheRef.current.clear(); // 마커 위치 업데이트
1538
1450
 
1539
- case 2:
1540
- return [2
1541
- /*return*/
1542
- ];
1543
- }
1544
- });
1545
- });
1546
- })();
1547
- }, [controller, elementRef]); //줌레벨
1451
+ var bounds = controller.getCurrBounds();
1548
1452
 
1549
- React.useEffect(function () {
1550
- if (zoomLevel && controller && mapInitialized) {
1551
- var prevZoomLevel = controller === null || controller === void 0 ? void 0 : controller.getZoomLevel();
1453
+ var markerOptions = tslib.__assign({
1454
+ position: bounds.nw
1455
+ }, options);
1552
1456
 
1553
- if (prevZoomLevel !== zoomLevel) {
1554
- controller === null || controller === void 0 ? void 0 : controller.setZoomLevel(zoomLevel);
1555
- }
1556
- }
1557
- }, [zoomLevel]); //센터
1457
+ markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (transform 제거 시 잠깐 빈 화면이 보이는 것 방지)
1558
1458
 
1559
- React.useEffect(function () {
1560
- if (center && controller && mapInitialized) {
1561
- var prevCenter = controller.getCenter();
1459
+ if (containerRef.current) {
1460
+ containerRef.current.style.transform = '';
1461
+ containerRef.current.style.visibility = '';
1462
+ } // 새 위치에서 렌더링 (캐시는 이미 초기화됨)
1562
1463
 
1563
- if (!Position.equals(prevCenter, center)) {
1564
- centerMoveWithPanning ? controller === null || controller === void 0 ? void 0 : controller.panningTo(center) : controller === null || controller === void 0 ? void 0 : controller.setCenter(center);
1565
- }
1566
- }
1567
- }, [center]);
1568
- return React__default["default"].createElement("div", {
1569
- className: cn$3('mint-map-root')
1570
- }, mapInitialized && React__default["default"].createElement(WoongCanvasProvider, null, children), React__default["default"].createElement("div", {
1571
- className: cn$3('mint-map-container'),
1572
- style: {
1573
- visibility: visible ? 'inherit' : 'hidden'
1574
- },
1575
- ref: elementRef
1576
- }));
1577
- }
1578
1464
 
1579
- var AnimationPlayer =
1580
- /** @class */
1581
- function () {
1582
- function AnimationPlayer(drawFunction, fps) {
1583
- this.prevtime = 0;
1584
- this.elapsedTime = 0;
1585
- this.fps = null;
1586
- this.baseDrawGapTime = null;
1587
- this.deltaTime = 0;
1588
- this.playing = false;
1589
- this.draw = drawFunction;
1590
- this.fps = fps || null;
1465
+ renderAllImmediate();
1466
+ }; // 줌 시작 시 처리 (일시적으로 숨김)
1591
1467
 
1592
- if (fps !== undefined) {
1593
- this.baseDrawGapTime = 1000 / fps;
1594
- }
1595
1468
 
1596
- this.init();
1597
- }
1469
+ var handleZoomStart = function () {
1470
+ if (!containerRef.current) return;
1471
+ containerRef.current.style.visibility = 'hidden';
1472
+ }; // 줌 종료 시 처리 (다시 표시)
1473
+
1474
+
1475
+ var handleZoomEnd = function () {
1476
+ if (!containerRef.current) return;
1477
+ containerRef.current.style.visibility = '';
1478
+ }; // 지도 중심 변경 시 처리 (transform으로 이동 추적, 캐시 유지)
1479
+
1480
+
1481
+ var handleCenterChanged = function () {
1482
+ var center = controller.getCurrBounds().getCenter();
1483
+ var curr = controller.positionToOffset(center);
1484
+ var prev = prevCenterOffsetRef.current; // 첫 번째 호출 시 이전 위치 저장만 하고 종료
1485
+
1486
+ if (!prev) {
1487
+ prevCenterOffsetRef.current = {
1488
+ x: curr.x,
1489
+ y: curr.y
1490
+ };
1491
+ return;
1492
+ } // 이전 위치와 현재 위치의 차이 계산 (이동 거리)
1493
+
1494
+
1495
+ var dx = prev.x - curr.x;
1496
+ var dy = prev.y - curr.y; // 누적 이동 거리 저장 (transform으로 화면만 이동, 캐시는 유지하여 성능 최적화)
1497
+
1498
+ accumTranslateRef.current = {
1499
+ x: accumTranslateRef.current.x + dx,
1500
+ y: accumTranslateRef.current.y + dy
1501
+ };
1502
+ prevCenterOffsetRef.current = {
1503
+ x: curr.x,
1504
+ y: curr.y
1505
+ }; // CSS transform으로 컨테이너 이동 (캐시된 좌표는 그대로 유지)
1506
+
1507
+ if (containerRef.current) {
1508
+ containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
1509
+ }
1510
+ };
1511
+
1512
+ var handleDragStart = function () {// 커서는 각 컴포넌트에서 처리
1513
+ };
1514
+
1515
+ var handleDragEnd = function () {// 커서는 각 컴포넌트에서 처리
1516
+ };
1517
+
1518
+ return {
1519
+ handleIdle: handleIdle,
1520
+ handleZoomStart: handleZoomStart,
1521
+ handleZoomEnd: handleZoomEnd,
1522
+ handleCenterChanged: handleCenterChanged,
1523
+ handleDragStart: handleDragStart,
1524
+ handleDragEnd: handleDragEnd
1525
+ };
1526
+ };
1527
+ /**
1528
+ * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
1529
+ *
1530
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1531
+ * @param data 공간 인덱스에 삽입할 데이터 배열
1532
+ * @param spatialIndex Spatial Hash Grid 인스턴스
1533
+ * @param computeBoundingBox 바운딩 박스 계산 함수
1534
+ */
1535
+
1536
+ var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
1537
+ spatialIndex.clear();
1538
+
1539
+ for (var _i = 0, data_1 = data; _i < data_1.length; _i++) {
1540
+ var item = data_1[_i];
1541
+ var bbox = computeBoundingBox(item);
1542
+
1543
+ if (bbox) {
1544
+ spatialIndex.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
1545
+ }
1546
+ }
1547
+ };
1548
+ /**
1549
+ * 선택 상태 동기화 (화면 밖 데이터도 선택 상태 유지)
1550
+ *
1551
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1552
+ * @param data 최신 데이터 배열
1553
+ * @param selectedIds 선택된 항목 ID Set
1554
+ * @param selectedItemsMap 현재 선택된 항목 Map
1555
+ * @returns 업데이트된 선택된 항목 Map
1556
+ */
1557
+
1558
+ var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
1559
+ var dataMap = new Map(data.map(function (m) {
1560
+ return [m.id, m];
1561
+ }));
1562
+ var newSelectedItemsMap = new Map();
1563
+ selectedIds.forEach(function (id) {
1564
+ // 현재 data에 있으면 최신 데이터 사용
1565
+ var currentItem = dataMap.get(id);
1566
+
1567
+ if (currentItem) {
1568
+ newSelectedItemsMap.set(id, currentItem);
1569
+ } else {
1570
+ // 화면 밖이면 기존 데이터 유지
1571
+ var prevItem = selectedItemsMap.get(id);
1572
+
1573
+ if (prevItem) {
1574
+ newSelectedItemsMap.set(id, prevItem);
1575
+ }
1576
+ }
1577
+ });
1578
+ return newSelectedItemsMap;
1579
+ };
1580
+ /**
1581
+ * 외부 selectedItems를 내부 상태로 동기화
1582
+ *
1583
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1584
+ * @param externalSelectedItems 외부에서 전달된 선택된 항목 배열
1585
+ * @param selectedIdsRef 선택된 ID Set ref
1586
+ * @param selectedItemsMapRef 선택된 항목 Map ref
1587
+ */
1588
+
1589
+ var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef, selectedItemsMapRef) {
1590
+ if (externalSelectedItems === undefined) return;
1591
+ var newSelectedIds = new Set();
1592
+ var newSelectedItemsMap = new Map();
1593
+ externalSelectedItems.forEach(function (item) {
1594
+ newSelectedIds.add(item.id);
1595
+ newSelectedItemsMap.set(item.id, item);
1596
+ });
1597
+ selectedIdsRef.current = newSelectedIds;
1598
+ selectedItemsMapRef.current = newSelectedItemsMap;
1599
+ };
1600
+
1601
+ /**
1602
+ * 이벤트 유효성 검증 및 좌표 변환
1603
+ *
1604
+ * @param event 이벤트 파라미터
1605
+ * @param context WoongCanvasContext 인스턴스
1606
+ * @param controller MintMapController 인스턴스
1607
+ * @returns 유효한 화면 좌표 또는 null
1608
+ */
1609
+ var validateEvent = function (event, context, controller) {
1610
+ var _a;
1611
+
1612
+ if (context) return null;
1613
+ if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return null;
1614
+
1615
+ try {
1616
+ return controller.positionToOffset(event.param.position);
1617
+ } catch (error) {
1618
+ console.error('[WoongCanvas] validateEvent error:', error);
1619
+ return null;
1620
+ }
1621
+ };
1622
+ /**
1623
+ * Map의 values를 배열로 변환
1624
+ *
1625
+ * @template T Map 값의 타입
1626
+ * @param map 변환할 Map
1627
+ * @returns Map의 값 배열
1628
+ */
1629
+
1630
+ var mapValuesToArray = function (map) {
1631
+ if (map.size === 0) return [];
1632
+ return Array.from(map.values());
1633
+ };
1634
+
1635
+ var cn$3 = classNames__default["default"].bind(styles$1);
1636
+ function MintMapCore(_a) {
1637
+ var _this = this;
1638
+
1639
+ var onLoad = _a.onLoad,
1640
+ _b = _a.visible,
1641
+ visible = _b === void 0 ? true : _b,
1642
+ zoomLevel = _a.zoomLevel,
1643
+ center = _a.center,
1644
+ _c = _a.centerMoveWithPanning,
1645
+ centerMoveWithPanning = _c === void 0 ? false : _c,
1646
+ children = _a.children; //controller
1647
+
1648
+ var controller = useMintMapController(); //맵 초기화
1649
+
1650
+ var elementRef = React.useRef(null);
1651
+
1652
+ var _d = React.useState(false),
1653
+ mapInitialized = _d[0],
1654
+ setMapInitialized = _d[1];
1655
+
1656
+ var currMapInitialized = React.useRef(false);
1657
+ React.useEffect(function () {
1658
+ (function () {
1659
+ return tslib.__awaiter(_this, void 0, void 0, function () {
1660
+ var map_1;
1661
+ return tslib.__generator(this, function (_a) {
1662
+ switch (_a.label) {
1663
+ case 0:
1664
+ if (!(elementRef && elementRef.current)) return [3
1665
+ /*break*/
1666
+ , 2];
1667
+ return [4
1668
+ /*yield*/
1669
+ , controller.initializingMap(elementRef.current)];
1670
+
1671
+ case 1:
1672
+ map_1 = _a.sent();
1673
+
1674
+ if (!currMapInitialized.current) {
1675
+ currMapInitialized.current = true; //onload callback (setTimeout 으로 맵이 초기화 될 텀을 준다. 특히 google map..)
1676
+
1677
+ setTimeout(function () {
1678
+ // console.log('setMapInitialized true');
1679
+ setMapInitialized(true);
1680
+ onLoad && onLoad(map_1, controller);
1681
+ }, 100);
1682
+ }
1683
+
1684
+ _a.label = 2;
1685
+
1686
+ case 2:
1687
+ return [2
1688
+ /*return*/
1689
+ ];
1690
+ }
1691
+ });
1692
+ });
1693
+ })();
1694
+ }, [controller, elementRef]); //줌레벨
1695
+
1696
+ React.useEffect(function () {
1697
+ if (zoomLevel && controller && mapInitialized) {
1698
+ var prevZoomLevel = controller === null || controller === void 0 ? void 0 : controller.getZoomLevel();
1699
+
1700
+ if (prevZoomLevel !== zoomLevel) {
1701
+ controller === null || controller === void 0 ? void 0 : controller.setZoomLevel(zoomLevel);
1702
+ }
1703
+ }
1704
+ }, [zoomLevel]); //센터
1705
+
1706
+ React.useEffect(function () {
1707
+ if (center && controller && mapInitialized) {
1708
+ var prevCenter = controller.getCenter();
1709
+
1710
+ if (!Position.equals(prevCenter, center)) {
1711
+ centerMoveWithPanning ? controller === null || controller === void 0 ? void 0 : controller.panningTo(center) : controller === null || controller === void 0 ? void 0 : controller.setCenter(center);
1712
+ }
1713
+ }
1714
+ }, [center]);
1715
+ return React__default["default"].createElement("div", {
1716
+ className: cn$3('mint-map-root')
1717
+ }, mapInitialized && React__default["default"].createElement(WoongCanvasProvider, null, children), React__default["default"].createElement("div", {
1718
+ className: cn$3('mint-map-container'),
1719
+ style: {
1720
+ visibility: visible ? 'inherit' : 'hidden'
1721
+ },
1722
+ ref: elementRef
1723
+ }));
1724
+ }
1725
+
1726
+ var AnimationPlayer =
1727
+ /** @class */
1728
+ function () {
1729
+ function AnimationPlayer(drawFunction, fps) {
1730
+ this.prevtime = 0;
1731
+ this.elapsedTime = 0;
1732
+ this.fps = null;
1733
+ this.baseDrawGapTime = null;
1734
+ this.deltaTime = 0;
1735
+ this.playing = false;
1736
+ this.draw = drawFunction;
1737
+ this.fps = fps || null;
1738
+
1739
+ if (fps !== undefined) {
1740
+ this.baseDrawGapTime = 1000 / fps;
1741
+ }
1742
+
1743
+ this.init();
1744
+ }
1598
1745
 
1599
1746
  AnimationPlayer.prototype.init = function () {
1600
1747
  this.deltaTime = 0;
@@ -5425,9 +5572,6 @@
5425
5572
  }))));
5426
5573
  }
5427
5574
 
5428
- // 메인 컴포넌트
5429
- // ============================================================================
5430
-
5431
5575
  var WoongCanvasMarker = function (props) {
5432
5576
  var data = props.data,
5433
5577
  onClick = props.onClick,
@@ -5437,232 +5581,88 @@
5437
5581
  enableMultiSelect = _a === void 0 ? false : _a,
5438
5582
  _b = props.topOnHover,
5439
5583
  topOnHover = _b === void 0 ? false : _b,
5440
- _c = props.enableViewportCulling,
5441
- enableViewportCulling = _c === void 0 ? true : _c,
5442
- _d = props.cullingMargin,
5443
- cullingMargin = _d === void 0 ? DEFAULT_CULLING_MARGIN : _d,
5444
- _e = props.maxCacheSize,
5445
- maxCacheSize = _e === void 0 ? DEFAULT_MAX_CACHE_SIZE : _e,
5584
+ _c = props.cullingMargin,
5585
+ cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
5586
+ _d = props.maxCacheSize,
5587
+ maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
5446
5588
  externalSelectedItems = props.selectedItems,
5447
5589
  externalSelectedItem = props.selectedItem,
5448
- _f = props.disableInteraction,
5449
- disableInteraction = _f === void 0 ? false : _f,
5590
+ _e = props.disableInteraction,
5591
+ disableInteraction = _e === void 0 ? false : _e,
5450
5592
  renderBase = props.renderBase,
5451
- renderAnimation = props.renderAnimation,
5452
5593
  renderEvent = props.renderEvent,
5453
- options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderAnimation", "renderEvent"]); // --------------------------------------------------------------------------
5454
- // Hooks & Context
5455
- // --------------------------------------------------------------------------
5456
-
5594
+ options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderEvent"]);
5457
5595
 
5458
5596
  var controller = useMintMapController();
5459
5597
  var context = useWoongCanvasContext();
5460
- var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
5461
- // DOM Refs
5462
- // --------------------------------------------------------------------------
5598
+ var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
5463
5599
 
5464
5600
  var divRef = React.useRef(document.createElement('div'));
5465
5601
  var divElement = divRef.current;
5466
5602
  var containerRef = React.useRef(null);
5467
- var markerRef = React.useRef(); // --------------------------------------------------------------------------
5468
- // Konva Refs
5469
- // --------------------------------------------------------------------------
5603
+ var markerRef = React.useRef(); // Konva Refs
5470
5604
 
5471
5605
  var stageRef = React.useRef(null);
5472
5606
  var baseLayerRef = React.useRef(null);
5473
- var animationLayerRef = React.useRef(null);
5474
- var eventLayerRef = React.useRef(null); // --------------------------------------------------------------------------
5475
- // Data Refs - 선택 및 Hover 상태 관리
5476
- // --------------------------------------------------------------------------
5477
-
5478
- /** data prop을 ref로 추적 (stale closure 방지, useEffect에서 동기화) */
5479
-
5480
- var dataRef = React.useRef(data); // --------------------------------------------------------------------------
5481
- // State Refs - 선택 및 Hover 상태 관리
5482
- // --------------------------------------------------------------------------
5483
-
5484
- /** 상호작용 비활성화 상태 (Ref로 관리하여 클로저 문제 해결) */
5607
+ var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
5485
5608
 
5609
+ var dataRef = React.useRef(data);
5486
5610
  var disableInteractionRef = React.useRef(disableInteraction);
5487
- /** 현재 Hover 중인 항목 */
5488
-
5489
5611
  var hoveredItemRef = React.useRef(null);
5490
- /** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
5491
-
5492
5612
  var selectedItemRef = React.useRef(externalSelectedItem);
5493
- /**
5494
- * 선택된 항목의 ID Set
5495
- *
5496
- * 용도:
5497
- * 1. onClick 콜백에 전달 - onClick(data, selectedIdsRef.current)
5498
- * 2. 선택 여부 빠른 체크 - selectedIdsRef.current.has(id)
5499
- * 3. 메모리 효율 - ID만 저장 (작음)
5500
- *
5501
- * selectedItemsMapRef와 차이:
5502
- * - selectedIdsRef: ID만 저장 { "id1", "id2" }
5503
- * - selectedItemsMapRef: 전체 객체 저장 { id1: {...}, id2: {...} }
5504
- *
5505
- * 둘 다 필요: ID만 필요한 곳은 이것, 전체 데이터 필요한 곳은 Map
5506
- */
5507
-
5508
5613
  var selectedIdsRef = React.useRef(new Set());
5509
- /**
5510
- * 선택된 항목의 실제 데이터 Map (핵심 성능 최적화!)
5511
- *
5512
- * 목적: doRenderEvent에서 filter() 순회 제거
5513
- * - 이전: markersRef.current.filter() → O(전체 마커 수)
5514
- * - 현재: Map.values() → O(선택된 항목 수)
5515
- *
5516
- * 성능 개선: 10,000개 중 1개 선택 시
5517
- * - 이전: 10,000번 체크
5518
- * - 현재: 1번 접근 (10,000배 빠름!)
5519
- */
5520
-
5521
- var selectedItemsMapRef = React.useRef(new Map()); // --------------------------------------------------------------------------
5522
- // Drag Refs
5523
- // --------------------------------------------------------------------------
5614
+ var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
5524
5615
 
5525
5616
  var draggingRef = React.useRef(false);
5526
5617
  var prevCenterOffsetRef = React.useRef(null);
5527
5618
  var accumTranslateRef = React.useRef({
5528
5619
  x: 0,
5529
5620
  y: 0
5530
- }); // --------------------------------------------------------------------------
5531
- // Performance Refs (캐싱 & 최적화)
5532
- // --------------------------------------------------------------------------
5533
-
5534
- /** 좌표 변환 결과 LRU 캐시 */
5621
+ }); // 성능 최적화 Refs
5535
5622
 
5536
5623
  var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
5537
- /** 공간 인덱스 (빠른 Hit Test) */
5538
-
5539
5624
  var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
5540
- /** 바운딩 박스 캐시 (Viewport Culling 최적화) */
5541
-
5542
5625
  var boundingBoxCacheRef = React.useRef(new Map());
5543
- /** 뷰포트 경계 캐시 (Viewport Culling) */
5544
-
5545
- var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
5546
- // 유틸리티 함수: 뷰포트 관리
5547
- // --------------------------------------------------------------------------
5548
-
5549
- /**
5550
- * 현재 뷰포트 영역 계산
5551
- */
5552
-
5553
- var updateViewport = function () {
5554
- if (!stageRef.current) return;
5555
- var stage = stageRef.current;
5556
- viewportRef.current = {
5557
- minX: -cullingMargin,
5558
- maxX: stage.width() + cullingMargin,
5559
- minY: -cullingMargin,
5560
- maxY: stage.height() + cullingMargin
5561
- };
5562
- };
5563
- /**
5564
- * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
5565
- */
5566
-
5567
-
5568
- var isInViewport = function (item) {
5569
- if (!enableViewportCulling || !viewportRef.current) return true;
5570
- var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
5571
-
5572
- var bbox = boundingBoxCacheRef.current.get(item.id);
5626
+ var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling)
5573
5627
 
5574
- if (!bbox) {
5575
- // 바운딩 박스 계산 (공통 함수 사용)
5576
- var computed = computeBoundingBox(item);
5577
- if (!computed) return false;
5578
- bbox = computed;
5579
- boundingBoxCacheRef.current.set(item.id, bbox);
5580
- } // 바운딩 박스와 viewport 교차 체크
5628
+ var updateViewport$1 = function () {
5629
+ updateViewport(stageRef.current, cullingMargin, viewportRef);
5630
+ }; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
5581
5631
 
5582
5632
 
5583
- return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
5584
- }; // --------------------------------------------------------------------------
5585
- // 유틸리티 함수: 좌표 변환 캐싱
5586
- // --------------------------------------------------------------------------
5587
-
5588
- /**
5589
- * 마커 좌표 변환 결과를 캐시하고 반환
5590
- *
5591
- * @param markerData 마커 데이터
5592
- * @returns 변환된 좌표 또는 null
5593
- */
5633
+ var isInViewport$1 = function (item) {
5634
+ return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
5635
+ }; // 마커 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
5594
5636
 
5595
5637
 
5596
5638
  var getOrComputeMarkerOffset = function (markerData) {
5597
5639
  var cached = offsetCacheRef.current.get(markerData.id);
5598
5640
  if (cached && !Array.isArray(cached)) return cached;
5599
5641
  var result = computeMarkerOffset(markerData, controller);
5600
-
5601
- if (result) {
5602
- offsetCacheRef.current.set(markerData.id, result);
5603
- }
5604
-
5642
+ if (!result) return null;
5643
+ offsetCacheRef.current.set(markerData.id, result);
5605
5644
  return result;
5606
- }; // --------------------------------------------------------------------------
5607
- // 유틸리티 함수: 바운딩 박스 계산
5608
- // --------------------------------------------------------------------------
5609
-
5610
- /**
5611
- * 마커의 바운딩 박스 계산
5612
- *
5613
- * 🎯 마커의 경우:
5614
- * - boxHeight: 본체만 (Hit Test 영역)
5615
- * - tailHeight: 꼬리 높이 (Viewport Culling용, 화면에 보이는 전체 영역)
5616
- *
5617
- * @param item 마커 데이터
5618
- * @returns 바운딩 박스 또는 null
5619
- */
5645
+ }; // 마커 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
5620
5646
 
5621
5647
 
5622
5648
  var computeBoundingBox = function (item) {
5623
- // 마커: 중심점 기준 박스 크기 계산 (꼬리 포함)
5624
5649
  var offset = getOrComputeMarkerOffset(item);
5625
5650
  if (!offset) return null;
5626
5651
  var boxWidth = item.boxWidth || 50;
5627
5652
  var boxHeight = item.boxHeight || 28;
5628
- var tailHeight = item.tailHeight || 0; // 🎯 tailHeight 사용 (Viewport Culling용)
5629
-
5653
+ var tailHeight = item.tailHeight || 0;
5630
5654
  return {
5631
5655
  minX: offset.x - boxWidth / 2,
5632
5656
  minY: offset.y - boxHeight - tailHeight,
5633
5657
  maxX: offset.x + boxWidth / 2,
5634
5658
  maxY: offset.y
5635
5659
  };
5636
- }; // --------------------------------------------------------------------------
5637
- // 유틸리티 함수: 공간 인덱싱
5638
- // --------------------------------------------------------------------------
5660
+ }; // 공간 인덱스 빌드 (빠른 Hit Test용)
5639
5661
 
5640
- /**
5641
- * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
5642
- */
5643
-
5644
-
5645
- var buildSpatialIndex = function () {
5646
- var spatial = spatialIndexRef.current;
5647
- spatial.clear();
5648
- var currentData = dataRef.current;
5649
-
5650
- for (var _i = 0, currentData_1 = currentData; _i < currentData_1.length; _i++) {
5651
- var item = currentData_1[_i]; // 바운딩 박스 계산 (공통 함수 사용)
5652
-
5653
- var bbox = computeBoundingBox(item);
5654
-
5655
- if (bbox) {
5656
- spatial.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
5657
- }
5658
- }
5659
- }; // --------------------------------------------------------------------------
5660
- // 렌더링 함수 결정 (dataType에 따라)
5661
- // --------------------------------------------------------------------------
5662
5662
 
5663
- /**
5664
- * 외부 렌더링 함수에 전달할 유틸리티 객체
5665
- */
5663
+ var buildSpatialIndex$1 = function () {
5664
+ buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
5665
+ }; // 렌더링 유틸리티 객체
5666
5666
 
5667
5667
 
5668
5668
  var renderUtils = {
@@ -5670,48 +5670,29 @@
5670
5670
  return null;
5671
5671
  },
5672
5672
  getOrComputeMarkerOffset: getOrComputeMarkerOffset
5673
- };
5674
- /** Base Layer에서 사용할 빈 Set (재사용) */
5675
-
5676
- React.useRef(new Set());
5677
- /**
5678
- * Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
5679
- *
5680
- * 🔥 최적화:
5681
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
5682
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
5683
- * 3. 클로저로 최신 데이터 참조
5684
- *
5685
- * 🎯 topOnHover 지원:
5686
- * - renderEvent가 없을 때 Base Layer에서 hover 처리 (fallback)
5687
- * - renderEvent가 있으면 Event Layer에서 처리 (성능 최적화)
5688
- */
5673
+ }; // Base Layer 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
5689
5674
 
5690
5675
  var doRenderBase = function () {
5691
5676
  var layer = baseLayerRef.current;
5692
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
5693
-
5677
+ if (!layer) return;
5694
5678
  var shape = layer.findOne('.base-render-shape');
5695
5679
 
5696
5680
  if (!shape) {
5697
- // 최초 생성 (한 번만 실행됨)
5698
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
5699
5681
  shape = new Konva__default["default"].Shape({
5700
5682
  name: 'base-render-shape',
5701
5683
  sceneFunc: function (context, shape) {
5702
5684
  var ctx = context;
5703
- var hovered = hoveredItemRef.current; // 클로저로 최신 ref 참조
5685
+ var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
5704
5686
 
5705
- var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
5706
- return isInViewport(item);
5707
- }) : dataRef.current; // topOnHover true이고 renderEvent가 없으면 Base Layer에서 hover 처리
5687
+ var visibleItems = dataRef.current.filter(function (item) {
5688
+ return isInViewport$1(item);
5689
+ }); // topOnHover 옵션: hover된 항목을 나중에 그려서 최상위에 표시
5708
5690
 
5709
5691
  if (topOnHover && !renderEvent && hovered) {
5710
- // hover된 항목 제외하고 렌더링
5711
5692
  visibleItems = visibleItems.filter(function (item) {
5712
5693
  return item.id !== hovered.id;
5713
5694
  });
5714
- } // 일반 항목 렌더링
5695
+ } // 일반 항목들 먼저 렌더링
5715
5696
 
5716
5697
 
5717
5698
  renderBase({
@@ -5720,12 +5701,10 @@
5720
5701
  selectedIds: selectedIdsRef.current,
5721
5702
  hoveredItem: hovered,
5722
5703
  utils: renderUtils
5723
- }); // hover된 항목을 최상단에 렌더링 (renderEvent가 없을 때만)
5704
+ }); // hover된 항목을 마지막에 렌더링하여 최상위에 표시
5724
5705
 
5725
5706
  if (topOnHover && !renderEvent && hovered) {
5726
- var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
5727
-
5728
- if (isHoveredInViewport) {
5707
+ if (isInViewport$1(hovered)) {
5729
5708
  renderBase({
5730
5709
  ctx: ctx,
5731
5710
  items: [hovered],
@@ -5741,61 +5720,26 @@
5741
5720
  hitStrokeWidth: 0
5742
5721
  });
5743
5722
  layer.add(shape);
5744
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
5745
-
5723
+ }
5746
5724
 
5747
5725
  layer.batchDraw();
5748
- };
5749
- /**
5750
- * Animation 레이어 렌더링 (선택된 마커 애니메이션)
5751
- *
5752
- * 🔥 최적화: sceneFunc 내부에서 최신 items 참조
5753
- * - 선택 변경 시에만 재생성
5754
- * - 지도 이동 시에는 기존 Animation 계속 실행
5755
- */
5756
-
5757
-
5758
- var doRenderAnimation = function () {
5759
- if (!renderAnimation) return;
5760
- var layer = animationLayerRef.current;
5761
- if (!layer) return;
5762
- renderAnimation({
5763
- layer: layer,
5764
- selectedIds: selectedIdsRef.current,
5765
- items: dataRef.current,
5766
- utils: renderUtils
5767
- });
5768
- };
5769
- /**
5770
- * Event 레이어 렌더링 (hover + 선택 상태 표시)
5771
- *
5772
- * 🔥 최적화:
5773
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
5774
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
5775
- * 3. 클로저로 최신 데이터 참조
5776
- */
5726
+ }; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
5777
5727
 
5778
5728
 
5779
5729
  var doRenderEvent = function () {
5780
5730
  var layer = eventLayerRef.current;
5781
- if (!layer) return;
5782
- if (!renderEvent) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
5783
-
5731
+ if (!layer || !renderEvent) return;
5784
5732
  var shape = layer.findOne('.event-render-shape');
5785
5733
 
5786
5734
  if (!shape) {
5787
- // 최초 생성 (한 번만 실행됨)
5788
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
5789
5735
  shape = new Konva__default["default"].Shape({
5790
5736
  name: 'event-render-shape',
5791
5737
  sceneFunc: function (context, shape) {
5792
- var ctx = context; // 클로저로 최신 ref 값 참조
5793
-
5794
- var selectedItems = Array.from(selectedItemsMapRef.current.values());
5795
- var hovered = hoveredItemRef.current; // topOnHover가 true이면 hover된 항목을 최상단에 렌더링
5738
+ var ctx = context;
5739
+ var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
5740
+ var hovered = hoveredItemRef.current;
5796
5741
 
5797
5742
  if (topOnHover && hovered) {
5798
- // 1. 먼저 일반 항목들 렌더링 (hover된 항목 제외)
5799
5743
  renderEvent({
5800
5744
  ctx: ctx,
5801
5745
  hoveredItem: null,
@@ -5804,13 +5748,9 @@
5804
5748
  return item.id !== hovered.id;
5805
5749
  }),
5806
5750
  selectedItem: selectedItemRef.current
5807
- }); // 2. hover된 항목을 최상단에 렌더링
5808
-
5809
- var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
5751
+ });
5810
5752
 
5811
- if (isHoveredInViewport) {
5812
- // hover된 항목이 선택되어 있다면 hoverSelectedItems에 포함시켜서
5813
- // renderEvent에서 hover 스타일만 적용되도록 함
5753
+ if (isInViewport$1(hovered)) {
5814
5754
  var hoveredIsSelected = selectedItems.some(function (item) {
5815
5755
  return item.id === hovered.id;
5816
5756
  });
@@ -5824,7 +5764,6 @@
5824
5764
  });
5825
5765
  }
5826
5766
  } else {
5827
- // topOnHover가 false이거나 hover된 항목이 없으면 일반 렌더링
5828
5767
  renderEvent({
5829
5768
  ctx: ctx,
5830
5769
  hoveredItem: hovered,
@@ -5839,137 +5778,63 @@
5839
5778
  hitStrokeWidth: 0
5840
5779
  });
5841
5780
  layer.add(shape);
5842
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
5843
-
5781
+ }
5844
5782
 
5845
5783
  layer.batchDraw();
5846
- };
5847
- /**
5848
- * 전체 즉시 렌더링 (IDLE 시 호출)
5849
- */
5784
+ }; // 전체 즉시 렌더링
5850
5785
 
5851
5786
 
5852
5787
  var renderAllImmediate = function () {
5853
- updateViewport();
5854
- buildSpatialIndex();
5788
+ updateViewport$1();
5789
+ buildSpatialIndex$1();
5855
5790
  doRenderBase();
5856
- doRenderAnimation();
5857
5791
  doRenderEvent();
5858
- }; // --------------------------------------------------------------------------
5859
- // 이벤트 핸들러: 지도 이벤트
5860
- // --------------------------------------------------------------------------
5861
-
5862
- /**
5863
- * 지도 이동/줌 완료 시 처리
5864
- */
5865
-
5866
-
5867
- var handleIdle = function () {
5868
- prevCenterOffsetRef.current = null;
5869
- accumTranslateRef.current = {
5870
- x: 0,
5871
- y: 0
5872
- }; // 2. 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
5873
-
5874
- offsetCacheRef.current.clear();
5875
- boundingBoxCacheRef.current.clear(); // 3. 마커 위치 업데이트
5876
-
5877
- var bounds = controller.getCurrBounds();
5878
-
5879
- var markerOptions = tslib.__assign({
5880
- position: bounds.nw
5881
- }, options);
5882
-
5883
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
5884
-
5885
- if (containerRef.current) {
5886
- containerRef.current.style.transform = '';
5887
- containerRef.current.style.visibility = '';
5888
- } // 5. 새 위치에서 렌더링
5889
-
5890
-
5891
- renderAllImmediate();
5892
- };
5893
- /**
5894
- * 줌 시작 시 처리 (일시적으로 숨김)
5895
- */
5896
-
5897
-
5898
- var handleZoomStart = function () {
5899
- if (containerRef.current) {
5900
- containerRef.current.style.visibility = 'hidden';
5901
- }
5902
- };
5903
- /**
5904
- * 줌 종료 시 처리 (다시 표시)
5905
- */
5792
+ }; // 지도 이벤트 핸들러 생성
5793
+
5794
+
5795
+ var _f = createMapEventHandlers({
5796
+ controller: controller,
5797
+ containerRef: containerRef,
5798
+ markerRef: markerRef,
5799
+ options: options,
5800
+ prevCenterOffsetRef: prevCenterOffsetRef,
5801
+ accumTranslateRef: accumTranslateRef,
5802
+ offsetCacheRef: offsetCacheRef,
5803
+ boundingBoxCacheRef: boundingBoxCacheRef,
5804
+ renderAllImmediate: renderAllImmediate
5805
+ }),
5806
+ handleIdle = _f.handleIdle,
5807
+ handleZoomStart = _f.handleZoomStart,
5808
+ handleZoomEnd = _f.handleZoomEnd,
5809
+ handleCenterChanged = _f.handleCenterChanged,
5810
+ handleDragStartShared = _f.handleDragStart,
5811
+ handleDragEndShared = _f.handleDragEnd;
5906
5812
 
5907
-
5908
- var handleZoomEnd = function () {
5909
- if (containerRef.current) {
5910
- containerRef.current.style.visibility = '';
5911
- }
5813
+ var handleDragStart = function () {
5814
+ handleDragStartShared();
5815
+ draggingRef.current = true;
5816
+ controller.setMapCursor('grabbing');
5912
5817
  };
5913
- /**
5914
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
5915
- */
5916
-
5917
-
5918
- var handleCenterChanged = function () {
5919
- var center = controller.getCurrBounds().getCenter();
5920
- var curr = controller.positionToOffset(center);
5921
- var prev = prevCenterOffsetRef.current;
5922
-
5923
- if (!prev) {
5924
- prevCenterOffsetRef.current = {
5925
- x: curr.x,
5926
- y: curr.y
5927
- };
5928
- return;
5929
- }
5930
-
5931
- var dx = prev.x - curr.x;
5932
- var dy = prev.y - curr.y;
5933
- accumTranslateRef.current = {
5934
- x: accumTranslateRef.current.x + dx,
5935
- y: accumTranslateRef.current.y + dy
5936
- };
5937
- prevCenterOffsetRef.current = {
5938
- x: curr.x,
5939
- y: curr.y
5940
- };
5941
-
5942
- if (containerRef.current) {
5943
- containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
5944
- }
5945
- }; // --------------------------------------------------------------------------
5946
- // Hit Test & 상태 관리
5947
- // --------------------------------------------------------------------------
5948
5818
 
5949
- /**
5950
- * 특정 좌표의 마커 데이터 찾기 (Spatial Index 사용)
5951
- *
5952
- * topOnHover가 true일 때:
5953
- * - 현재 hover된 항목을 최우선으로 체크
5954
- * - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
5955
- *
5956
- * @param offset 검사할 좌표
5957
- * @returns 찾은 마커 데이터 또는 null
5958
- */
5819
+ var handleDragEnd = function () {
5820
+ handleDragEndShared();
5821
+ draggingRef.current = false;
5822
+ controller.setMapCursor('grab');
5823
+ }; // Hit Test: 특정 좌표의 마커 찾기
5959
5824
 
5960
5825
 
5961
5826
  var findData = function (offset) {
5962
- // topOnHover true이고 현재 hover된 항목이 있으면, 그것을 먼저 체크
5827
+ // topOnHover 옵션이 켜져 있으면 hover된 항목을 최우선으로 확인
5963
5828
  if (topOnHover && hoveredItemRef.current) {
5964
5829
  var hovered = hoveredItemRef.current;
5965
5830
 
5966
5831
  if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
5967
- return hovered; // 여전히 hover된 항목 위에 있음
5832
+ return hovered;
5968
5833
  }
5969
- } // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 ~10개)
5834
+ } // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
5970
5835
 
5971
5836
 
5972
- var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 마커 체크
5837
+ var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
5973
5838
 
5974
5839
  for (var i = candidates.length - 1; i >= 0; i--) {
5975
5840
  var item = candidates[i];
@@ -5980,18 +5845,7 @@
5980
5845
  }
5981
5846
 
5982
5847
  return null;
5983
- };
5984
- /**
5985
- * Hover 상태 설정 및 레이어 렌더링
5986
- *
5987
- * @param data hover된 마커/폴리곤 데이터 또는 null
5988
- *
5989
- * 최적화: RAF 제거하여 즉시 렌더링 (16ms 지연 제거)
5990
- *
5991
- * 🎯 topOnHover 지원:
5992
- * - renderEvent가 있으면: Event Layer에서만 처리 (성능 최적화)
5993
- * - renderEvent가 없고 topOnHover=true면: Base Layer에서 처리
5994
- */
5848
+ }; // Hover 상태 설정 및 렌더링
5995
5849
 
5996
5850
 
5997
5851
  var setHovered = function (data) {
@@ -6001,32 +5855,18 @@
6001
5855
  controller.setMapCursor('grabbing');
6002
5856
  } else {
6003
5857
  controller.setMapCursor(data ? 'pointer' : 'grab');
6004
- } // 즉시 렌더링 (RAF 없이)
6005
-
5858
+ }
6006
5859
 
6007
5860
  if (renderEvent) {
6008
- // renderEvent가 있으면 Event Layer에서만 처리 (성능 최적화)
6009
5861
  doRenderEvent();
6010
5862
  } else if (topOnHover) {
6011
- // renderEvent가 없고 topOnHover가 true면 Base Layer에서 처리
6012
5863
  doRenderBase();
6013
5864
  }
6014
- };
6015
- /**
6016
- * 클릭 처리 (단일/다중 선택)
6017
- *
6018
- * @param data 클릭된 마커/폴리곤 데이터
6019
- *
6020
- * 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
6021
- * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
6022
- * - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
6023
- */
5865
+ }; // 클릭 처리: 선택 상태 업데이트
6024
5866
 
6025
5867
 
6026
5868
  var handleLocalClick = function (data) {
6027
- // 1. 선택 상태 업데이트
6028
5869
  if (enableMultiSelect) {
6029
- // 다중 선택: Set과 Map 동시 업데이트
6030
5870
  var newSelected = new Set(selectedIdsRef.current);
6031
5871
 
6032
5872
  if (newSelected.has(data.id)) {
@@ -6039,7 +5879,6 @@
6039
5879
 
6040
5880
  selectedIdsRef.current = newSelected;
6041
5881
  } else {
6042
- // 단일 선택: 토글
6043
5882
  var newSelected = new Set();
6044
5883
 
6045
5884
  if (!selectedIdsRef.current.has(data.id)) {
@@ -6053,153 +5892,80 @@
6053
5892
  selectedIdsRef.current = newSelected;
6054
5893
  }
6055
5894
 
6056
- if (!!renderAnimation) {
6057
- // 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
6058
- doRenderBase(); // 3. Animation Layer 렌더링 (선택된 마커 애니메이션)
6059
-
6060
- doRenderAnimation();
6061
- } // 4. Event Layer 렌더링 (hover 처리)
6062
-
6063
-
5895
+ doRenderBase();
6064
5896
  doRenderEvent();
6065
- }; // --------------------------------------------------------------------------
6066
- // 이벤트 핸들러: UI 이벤트
6067
- // --------------------------------------------------------------------------
6068
-
6069
- /**
6070
- * 클릭 이벤트 처리
6071
- */
5897
+ }; // 클릭 이벤트 핸들러
6072
5898
 
6073
5899
 
6074
5900
  var handleClick = function (event) {
6075
- var _a;
6076
-
6077
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6078
-
6079
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
6080
-
6081
- try {
6082
- var clickedOffset = controller.positionToOffset(event.param.position);
6083
- var data_1 = findData(clickedOffset);
6084
-
6085
- if (data_1) {
6086
- handleLocalClick(data_1);
6087
-
6088
- if (onClick) {
6089
- onClick(data_1, selectedIdsRef.current);
6090
- }
6091
- }
6092
- } catch (error) {
6093
- console.error('[WoongCanvasMarker] handleClick error:', error);
6094
- }
6095
- };
6096
- /**
6097
- * 마우스 이동 이벤트 처리 (hover 감지)
6098
- */
5901
+ if (disableInteractionRef.current) return;
5902
+ var clickedOffset = validateEvent(event, context, controller);
5903
+ if (!clickedOffset) return;
5904
+ var data = findData(clickedOffset);
5905
+ if (!data) return;
5906
+ handleLocalClick(data);
5907
+ onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
5908
+ }; // 마우스 이동 이벤트 핸들러 (hover 감지)
6099
5909
 
6100
5910
 
6101
5911
  var handleMouseMove = function (event) {
6102
- var _a;
6103
-
6104
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6105
-
6106
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
6107
-
6108
- try {
6109
- var mouseOffset = controller.positionToOffset(event.param.position);
6110
- var hoveredItem = findData(mouseOffset);
6111
- var prevHovered = hoveredItemRef.current;
6112
-
6113
- if (prevHovered !== hoveredItem) {
6114
- setHovered(hoveredItem);
6115
- if (prevHovered && onMouseOut) onMouseOut(prevHovered);
6116
- if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
6117
- }
6118
- } catch (error) {
6119
- console.error('[WoongCanvasMarker] handleMouseMove error:', error);
6120
- }
6121
- };
6122
- /**
6123
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
6124
- */
6125
-
6126
-
6127
- var handleDragStart = function () {
6128
- draggingRef.current = true;
6129
- controller.setMapCursor('grabbing');
6130
- };
6131
- /**
6132
- * 드래그 종료 처리 (커서를 기본으로 복원)
6133
- */
6134
-
6135
-
6136
- var handleDragEnd = function () {
6137
- draggingRef.current = false;
6138
- controller.setMapCursor('grab');
6139
- };
6140
- /**
6141
- * 마우스가 canvas를 벗어날 때 hover cleanup
6142
- */
5912
+ if (disableInteractionRef.current) return;
5913
+ var mouseOffset = validateEvent(event, context, controller);
5914
+ if (!mouseOffset) return;
5915
+ var hoveredItem = findData(mouseOffset);
5916
+ var prevHovered = hoveredItemRef.current;
5917
+ if (prevHovered === hoveredItem) return;
5918
+ setHovered(hoveredItem);
5919
+ if (prevHovered) onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
5920
+ if (hoveredItem) onMouseOver === null || onMouseOver === void 0 ? void 0 : onMouseOver(hoveredItem);
5921
+ }; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
6143
5922
 
6144
5923
 
6145
5924
  var handleMouseLeave = function () {
6146
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6147
-
5925
+ if (disableInteractionRef.current) return;
6148
5926
  var prevHovered = hoveredItemRef.current;
6149
-
6150
- if (prevHovered) {
6151
- hoveredItemRef.current = null;
6152
- controller.setMapCursor('grab');
6153
- doRenderEvent();
6154
-
6155
- if (onMouseOut) {
6156
- onMouseOut(prevHovered);
6157
- }
6158
- }
6159
- }; // --------------------------------------------------------------------------
6160
- // Lifecycle: DOM 초기화
6161
- // --------------------------------------------------------------------------
5927
+ if (!prevHovered) return;
5928
+ hoveredItemRef.current = null;
5929
+ controller.setMapCursor('grab');
5930
+ doRenderEvent();
5931
+ onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
5932
+ }; // DOM 초기화
6162
5933
 
6163
5934
 
6164
5935
  React.useEffect(function () {
6165
5936
  divElement.style.width = 'fit-content';
6166
5937
  return function () {
6167
- if (markerRef.current) {
6168
- controller.clearDrawable(markerRef.current);
6169
- markerRef.current = undefined;
6170
- }
5938
+ if (!markerRef.current) return;
5939
+ controller.clearDrawable(markerRef.current);
5940
+ markerRef.current = undefined;
6171
5941
  };
6172
- }, []); // --------------------------------------------------------------------------
6173
- // Lifecycle: 마커 생성/업데이트
6174
- // --------------------------------------------------------------------------
5942
+ }, []); // 마커 생성/업데이트
6175
5943
 
6176
5944
  React.useEffect(function () {
6177
- if (options) {
6178
- var bounds = controller.getCurrBounds();
5945
+ if (!options) return;
5946
+ var bounds = controller.getCurrBounds();
6179
5947
 
6180
- var markerOptions = tslib.__assign({
6181
- position: bounds.nw
6182
- }, options);
5948
+ var markerOptions = tslib.__assign({
5949
+ position: bounds.nw
5950
+ }, options);
6183
5951
 
6184
- if (markerRef.current) {
6185
- controller.updateMarker(markerRef.current, markerOptions);
6186
- } else {
6187
- markerRef.current = new Marker(markerOptions);
6188
- markerRef.current.element = divElement;
6189
- controller.createMarker(markerRef.current);
5952
+ if (markerRef.current) {
5953
+ controller.updateMarker(markerRef.current, markerOptions);
5954
+ return;
5955
+ }
6190
5956
 
6191
- if (divElement.parentElement) {
6192
- divElement.parentElement.style.pointerEvents = 'none';
6193
- }
5957
+ markerRef.current = new Marker(markerOptions);
5958
+ markerRef.current.element = divElement;
5959
+ controller.createMarker(markerRef.current);
6194
5960
 
6195
- if (options.zIndex !== undefined) {
6196
- controller.setMarkerZIndex(markerRef.current, options.zIndex);
6197
- }
6198
- }
5961
+ if (divElement.parentElement) {
5962
+ divElement.parentElement.style.pointerEvents = 'none';
6199
5963
  }
6200
- }, [options]); // --------------------------------------------------------------------------
6201
- // Lifecycle: Konva 초기화 및 이벤트 리스너 등록
6202
- // --------------------------------------------------------------------------
5964
+
5965
+ if (options.zIndex !== undefined) {
5966
+ controller.setMarkerZIndex(markerRef.current, options.zIndex);
5967
+ }
5968
+ }, [options]); // Konva 초기화 및 이벤트 리스너 등록
6203
5969
 
6204
5970
  React.useEffect(function () {
6205
5971
  var mapDiv = controller.mapDivElement;
@@ -6208,34 +5974,21 @@
6208
5974
  width: mapDiv.offsetWidth,
6209
5975
  height: mapDiv.offsetHeight
6210
5976
  });
6211
- stageRef.current = stage; // 레이어 최적화 설정
6212
-
5977
+ stageRef.current = stage;
6213
5978
  var baseLayer = new Konva__default["default"].Layer({
6214
- listening: false // 이벤트 리스닝 비활성화로 성능 향상
6215
-
6216
- });
6217
- var animationLayer = new Konva__default["default"].Layer({
6218
5979
  listening: false
6219
5980
  });
6220
5981
  var eventLayer = new Konva__default["default"].Layer({
6221
5982
  listening: false
6222
5983
  });
6223
5984
  baseLayerRef.current = baseLayer;
6224
- animationLayerRef.current = animationLayer;
6225
5985
  eventLayerRef.current = eventLayer;
6226
5986
  stage.add(baseLayer);
6227
-
6228
- if (renderAnimation) {
6229
- stage.add(animationLayer);
6230
- }
6231
-
6232
- stage.add(eventLayer); // 초기 뷰포트 설정
6233
-
6234
- updateViewport(); // ResizeObserver (맵 크기 변경 감지)
5987
+ stage.add(eventLayer);
5988
+ updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
6235
5989
 
6236
5990
  var resizeRafId = null;
6237
5991
  var resizeObserver = new ResizeObserver(function () {
6238
- // RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
6239
5992
  if (resizeRafId !== null) {
6240
5993
  cancelAnimationFrame(resizeRafId);
6241
5994
  }
@@ -6245,7 +5998,7 @@
6245
5998
  stage.height(mapDiv.offsetHeight);
6246
5999
  offsetCacheRef.current.clear();
6247
6000
  boundingBoxCacheRef.current.clear();
6248
- updateViewport();
6001
+ updateViewport$1();
6249
6002
  renderAllImmediate();
6250
6003
  resizeRafId = null;
6251
6004
  });
@@ -6258,10 +6011,9 @@
6258
6011
  controller.addEventListener('CLICK', handleClick);
6259
6012
  controller.addEventListener('MOUSEMOVE', handleMouseMove);
6260
6013
  controller.addEventListener('DRAGSTART', handleDragStart);
6261
- controller.addEventListener('DRAGEND', handleDragEnd); // 맵 컨테이너에 mouseleave 이벤트 추가
6262
-
6014
+ controller.addEventListener('DRAGEND', handleDragEnd);
6263
6015
  mapDiv.addEventListener('mouseleave', handleMouseLeave);
6264
- renderAllImmediate(); // Context 사용 시 컴포넌트 등록 (다중 인스턴스 관리)
6016
+ renderAllImmediate(); // Context 사용 시 컴포넌트 등록
6265
6017
 
6266
6018
  var componentInstance = null;
6267
6019
 
@@ -6282,22 +6034,17 @@
6282
6034
  },
6283
6035
  isInteractionDisabled: function () {
6284
6036
  return disableInteractionRef.current;
6285
- } // 🚫 상호작용 비활성화 여부 반환
6286
-
6037
+ }
6287
6038
  };
6288
6039
  context.registerComponent(componentInstance);
6289
- } // Cleanup 함수
6290
-
6040
+ }
6291
6041
 
6292
6042
  return function () {
6293
- // RAF 정리
6294
6043
  if (resizeRafId !== null) {
6295
6044
  cancelAnimationFrame(resizeRafId);
6296
- } // 옵저버 정리
6297
-
6298
-
6299
- resizeObserver.disconnect(); // 이벤트 리스너 정리
6045
+ }
6300
6046
 
6047
+ resizeObserver.disconnect();
6301
6048
  controller.removeEventListener('IDLE', handleIdle);
6302
6049
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
6303
6050
  controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
@@ -6306,69 +6053,41 @@
6306
6053
  controller.removeEventListener('MOUSEMOVE', handleMouseMove);
6307
6054
  controller.removeEventListener('DRAGSTART', handleDragStart);
6308
6055
  controller.removeEventListener('DRAGEND', handleDragEnd);
6309
- mapDiv.removeEventListener('mouseleave', handleMouseLeave); // Context 정리
6056
+ mapDiv.removeEventListener('mouseleave', handleMouseLeave);
6310
6057
 
6311
6058
  if (context && componentInstance) {
6312
6059
  context.unregisterComponent(componentInstance);
6313
- } // Konva 리소스 정리
6314
-
6060
+ }
6315
6061
 
6316
6062
  baseLayer.destroyChildren();
6317
- animationLayer.destroyChildren();
6318
6063
  eventLayer.destroyChildren();
6319
- stage.destroy(); // 캐시 정리
6320
-
6064
+ stage.destroy();
6321
6065
  offsetCacheRef.current.clear();
6322
6066
  boundingBoxCacheRef.current.clear();
6323
6067
  spatialIndexRef.current.clear();
6324
6068
  };
6325
- }, []); // 초기화는 한 번만
6326
- // --------------------------------------------------------------------------
6327
- // Lifecycle: disableInteraction 동기화
6328
- // --------------------------------------------------------------------------
6069
+ }, []); // disableInteraction 동기화
6329
6070
 
6330
6071
  React.useEffect(function () {
6331
6072
  disableInteractionRef.current = disableInteraction;
6332
- }, [disableInteraction]); // --------------------------------------------------------------------------
6333
- // Lifecycle: 외부 selectedItems 동기화
6334
- // --------------------------------------------------------------------------
6073
+ }, [disableInteraction]); // 외부 selectedItems 동기화
6335
6074
 
6336
6075
  React.useEffect(function () {
6337
- if (!stageRef.current) return; // externalSelectedItems가 undefined면 외부 제어 안 함
6338
-
6339
- if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
6340
-
6341
- var newSelectedIds = new Set();
6342
- var newSelectedItemsMap = new Map();
6343
- externalSelectedItems.forEach(function (item) {
6344
- newSelectedIds.add(item.id);
6345
- newSelectedItemsMap.set(item.id, item);
6346
- });
6347
- selectedIdsRef.current = newSelectedIds;
6348
- selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
6349
-
6076
+ if (!stageRef.current) return;
6077
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
6350
6078
  doRenderBase();
6351
- doRenderAnimation();
6352
6079
  doRenderEvent();
6353
- }, [externalSelectedItems]); // 배열 자체를 dependency로 사용
6354
- // --------------------------------------------------------------------------
6355
- // Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
6356
- // --------------------------------------------------------------------------
6080
+ }, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
6357
6081
 
6358
6082
  React.useEffect(function () {
6359
- if (!stageRef.current) return; // Ref 동기화
6360
-
6361
- selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
6362
-
6083
+ if (!stageRef.current) return;
6084
+ selectedItemRef.current = externalSelectedItem;
6363
6085
  doRenderEvent();
6364
- }, [externalSelectedItem]); // --------------------------------------------------------------------------
6365
- // Lifecycle: 데이터 변경 시 렌더링
6366
- // --------------------------------------------------------------------------
6086
+ }, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
6367
6087
 
6368
6088
  React.useEffect(function () {
6369
- if (!stageRef.current) return; // dataRef 동기화
6370
-
6371
- dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
6089
+ if (!stageRef.current) return;
6090
+ dataRef.current = data;
6372
6091
 
6373
6092
  if (containerRef.current) {
6374
6093
  containerRef.current.style.transform = '';
@@ -6378,46 +6097,10 @@
6378
6097
  accumTranslateRef.current = {
6379
6098
  x: 0,
6380
6099
  y: 0
6381
- }; // 캐시 정리 (새 데이터이므로 기존 캐시는 무효)
6382
-
6100
+ };
6383
6101
  offsetCacheRef.current.clear();
6384
6102
  boundingBoxCacheRef.current.clear();
6385
- /**
6386
- * 선택 상태 동기화 (최적화 버전)
6387
- *
6388
- * data가 변경되면 selectedItemsMapRef도 업데이트 필요
6389
- * (참조가 바뀌므로 기존 Map의 데이터는 stale 상태)
6390
- *
6391
- * 🔥 중요: 화면 밖 데이터도 선택 상태 유지!
6392
- * - 현재 data에 있으면 최신 데이터로 업데이트
6393
- * - 없으면 기존 selectedItemsMapRef의 데이터 유지
6394
- *
6395
- * 최적화: data를 Map으로 먼저 변환하여 find() 순회 제거
6396
- * - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
6397
- */
6398
-
6399
- var dataMap = new Map(data.map(function (m) {
6400
- return [m.id, m];
6401
- }));
6402
- var newSelectedItemsMap = new Map();
6403
- selectedIdsRef.current.forEach(function (id) {
6404
- // 현재 data에 있으면 최신 데이터 사용
6405
- var currentItem = dataMap.get(id);
6406
-
6407
- if (currentItem) {
6408
- newSelectedItemsMap.set(id, currentItem);
6409
- } else {
6410
- // 화면 밖이면 기존 데이터 유지
6411
- var prevItem = selectedItemsMapRef.current.get(id);
6412
-
6413
- if (prevItem) {
6414
- newSelectedItemsMap.set(id, prevItem);
6415
- }
6416
- }
6417
- }); // selectedIdsRef는 그대로 유지 (화면 밖 항목도 선택 상태 유지)
6418
-
6419
- selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
6420
-
6103
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
6421
6104
  renderAllImmediate();
6422
6105
  }, [data]);
6423
6106
  return reactDom.createPortal(React__default["default"].createElement("div", {
@@ -6434,10 +6117,35 @@
6434
6117
  * 폴리곤 렌더링 유틸리티
6435
6118
  *
6436
6119
  * 이 파일은 폴리곤 렌더링을 위한 헬퍼 함수와 팩토리 함수를 제공합니다.
6120
+ * GeoJSON MultiPolygon 형식을 지원하며, 도넛 폴리곤(구멍이 있는 폴리곤)도 처리할 수 있습니다.
6437
6121
  */
6438
6122
 
6439
6123
  /**
6440
6124
  * 폴리곤 그리기 헬퍼 함수 (도넛 폴리곤 지원)
6125
+ *
6126
+ * Canvas 2D Context를 사용하여 폴리곤을 그립니다.
6127
+ * 도넛 폴리곤의 경우 evenodd fill rule을 사용하여 구멍을 처리합니다.
6128
+ *
6129
+ * @param params 폴리곤 그리기 파라미터
6130
+ *
6131
+ * @remarks
6132
+ * - **도넛 폴리곤 처리**:
6133
+ * - 외부 폴리곤과 내부 구멍들을 같은 path에 추가
6134
+ * - `fill('evenodd')`를 사용하여 구멍 뚫기
6135
+ * - **일반 폴리곤 처리**: 각 폴리곤 그룹을 개별적으로 그리기
6136
+ * - **성능**: O(n), n은 폴리곤의 총 좌표 수
6137
+ *
6138
+ * @example
6139
+ * ```typescript
6140
+ * drawPolygon({
6141
+ * ctx,
6142
+ * polygonOffsets: [[[[100, 200], [200, 200], [200, 100], [100, 100]]]],
6143
+ * isDonutPolygon: false,
6144
+ * fillColor: 'rgba(255, 0, 0, 0.5)',
6145
+ * strokeColor: 'rgba(255, 0, 0, 1)',
6146
+ * lineWidth: 2
6147
+ * });
6148
+ * ```
6441
6149
  */
6442
6150
  var drawPolygon = function (_a) {
6443
6151
  var ctx = _a.ctx,
@@ -6451,13 +6159,16 @@
6451
6159
  var multiPolygon = polygonOffsets_1[_i];
6452
6160
 
6453
6161
  if (isDonutPolygon) {
6454
- ctx.beginPath(); // 1. 외부 폴리곤 그리기
6162
+ // 도넛 폴리곤 처리: 외부 폴리곤 + 내부 구멍들을 같은 path에 추가
6163
+ ctx.beginPath(); // 1. 외부 폴리곤 그리기 (첫 번째 폴리곤)
6455
6164
 
6456
- if (multiPolygon[0] && multiPolygon[0].length > 0) {
6457
- ctx.moveTo(multiPolygon[0][0][0], multiPolygon[0][0][1]);
6165
+ var outerPolygon = multiPolygon[0];
6166
+
6167
+ if (outerPolygon && outerPolygon.length > 0) {
6168
+ ctx.moveTo(outerPolygon[0][0], outerPolygon[0][1]);
6458
6169
 
6459
- for (var i = 1; i < multiPolygon[0].length; i++) {
6460
- ctx.lineTo(multiPolygon[0][i][0], multiPolygon[0][i][1]);
6170
+ for (var i = 1; i < outerPolygon.length; i++) {
6171
+ ctx.lineTo(outerPolygon[i][0], outerPolygon[i][1]);
6461
6172
  }
6462
6173
 
6463
6174
  ctx.closePath();
@@ -6484,7 +6195,7 @@
6484
6195
  ctx.lineWidth = lineWidth;
6485
6196
  ctx.stroke();
6486
6197
  } else {
6487
- // 일반 폴리곤
6198
+ // 일반 폴리곤 처리: 각 폴리곤 그룹을 개별적으로 그리기
6488
6199
  for (var _b = 0, multiPolygon_1 = multiPolygon; _b < multiPolygon_1.length; _b++) {
6489
6200
  var polygonGroup = multiPolygon_1[_b];
6490
6201
  if (!polygonGroup.length) continue;
@@ -6497,7 +6208,8 @@
6497
6208
  ctx.lineTo(point[0], point[1]);
6498
6209
  }
6499
6210
 
6500
- ctx.closePath();
6211
+ ctx.closePath(); // 스타일 설정 및 렌더링
6212
+
6501
6213
  ctx.fillStyle = fillColor;
6502
6214
  ctx.strokeStyle = strokeColor;
6503
6215
  ctx.lineWidth = lineWidth;
@@ -6508,19 +6220,30 @@
6508
6220
  }
6509
6221
  };
6510
6222
  /**
6511
- * 폴리곤 Base 렌더링 함수
6223
+ * 폴리곤 Base 렌더링 함수 팩토리
6224
+ *
6225
+ * Base Layer에서 사용할 렌더링 함수를 생성합니다.
6226
+ * 선택되지 않은 폴리곤만 렌더링하며, 선택된 항목은 Event Layer에서 처리됩니다.
6512
6227
  *
6228
+ * @template T 폴리곤 데이터의 추가 속성 타입
6513
6229
  * @param baseFillColor 기본 폴리곤 채우기 색상
6514
6230
  * @param baseStrokeColor 기본 폴리곤 테두리 색상
6515
6231
  * @param baseLineWidth 기본 폴리곤 테두리 두께
6516
6232
  * @returns Base Layer 렌더링 함수
6517
6233
  *
6234
+ * @remarks
6235
+ * - 선택된 항목은 Event Layer에서 그려지므로 Base Layer에서는 스킵
6236
+ * - 성능: O(n), n은 렌더링할 폴리곤 개수
6237
+ * - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
6238
+ *
6518
6239
  * @example
6240
+ * ```typescript
6519
6241
  * const renderBase = renderPolygonBase(
6520
6242
  * 'rgba(255, 100, 100, 0.5)',
6521
6243
  * 'rgba(200, 50, 50, 0.8)',
6522
6244
  * 2
6523
6245
  * );
6246
+ * ```
6524
6247
  */
6525
6248
 
6526
6249
  var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth) {
@@ -6531,12 +6254,15 @@
6531
6254
  utils = _a.utils;
6532
6255
 
6533
6256
  for (var _i = 0, items_1 = items; _i < items_1.length; _i++) {
6534
- var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림
6257
+ var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림 (중복 렌더링 방지)
6258
+
6259
+ if (selectedIds.has(item.id)) continue; // paths가 없으면 스킵
6260
+
6261
+ if (!item.paths) continue; // 좌표 변환 (자동 캐싱)
6535
6262
 
6536
- if (selectedIds.has(item.id)) continue;
6537
- if (!item.paths) continue;
6538
6263
  var polygonOffsets = utils.getOrComputePolygonOffsets(item);
6539
- if (!polygonOffsets) continue;
6264
+ if (!polygonOffsets) continue; // 폴리곤 그리기
6265
+
6540
6266
  drawPolygon({
6541
6267
  ctx: ctx,
6542
6268
  polygonOffsets: polygonOffsets,
@@ -6549,8 +6275,12 @@
6549
6275
  };
6550
6276
  };
6551
6277
  /**
6552
- * 폴리곤 Event 렌더링 함수
6278
+ * 폴리곤 Event 렌더링 함수 팩토리
6553
6279
  *
6280
+ * Event Layer에서 사용할 렌더링 함수를 생성합니다.
6281
+ * 선택된 항목, hover된 항목, 마지막 선택된 항목을 각각 다른 스타일로 렌더링합니다.
6282
+ *
6283
+ * @template T 폴리곤 데이터의 추가 속성 타입
6554
6284
  * @param baseFillColor 기본 폴리곤 채우기 색상 (필수, fallback용)
6555
6285
  * @param baseStrokeColor 기본 폴리곤 테두리 색상 (필수, fallback용)
6556
6286
  * @param baseLineWidth 기본 폴리곤 테두리 두께 (필수, fallback용)
@@ -6565,7 +6295,14 @@
6565
6295
  * @param hoveredLineWidth Hover 시 폴리곤 테두리 두께 (선택, 기본값: selectedLineWidth)
6566
6296
  * @returns Event Layer 렌더링 함수
6567
6297
  *
6298
+ * @remarks
6299
+ * - **렌더링 순서**: 선택된 항목 → 마지막 선택된 항목 → hover된 항목 (최상단)
6300
+ * - **성능**: O(m), m은 선택된 항목 수 + hover된 항목 수
6301
+ * - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
6302
+ * - hover된 항목이 선택되어 있으면 active 스타일 적용
6303
+ *
6568
6304
  * @example
6305
+ * ```typescript
6569
6306
  * const renderEvent = renderPolygonEvent(
6570
6307
  * 'rgba(255, 100, 100, 0.5)', // baseFillColor
6571
6308
  * 'rgba(200, 50, 50, 0.8)', // baseStrokeColor
@@ -6574,6 +6311,7 @@
6574
6311
  * 'rgba(255, 152, 0, 1)', // selectedStrokeColor
6575
6312
  * 4 // selectedLineWidth
6576
6313
  * );
6314
+ * ```
6577
6315
  */
6578
6316
 
6579
6317
  var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth) {
@@ -6601,13 +6339,19 @@
6601
6339
  hoveredItem = _a.hoveredItem,
6602
6340
  utils = _a.utils,
6603
6341
  selectedItems = _a.selectedItems,
6604
- selectedItem = _a.selectedItem; // 1. 선택된 항목들 그리기 (마지막 선택 항목과 호버된 항목 제외)
6342
+ selectedItem = _a.selectedItem; // 성능 최적화: selectedItems를 Set으로 변환하여 O(1) 조회 (매번 some() 체크 방지)
6343
+
6344
+ var selectedIdsSet = selectedItems ? new Set(selectedItems.map(function (item) {
6345
+ return item.id;
6346
+ })) : new Set();
6347
+ var hoveredItemId = hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id;
6348
+ var selectedItemId = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id; // 1. 선택된 항목들 그리기 (마지막 선택 항목과 호버된 항목 제외)
6605
6349
 
6606
6350
  if (selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.length) {
6607
6351
  for (var _i = 0, selectedItems_1 = selectedItems; _i < selectedItems_1.length; _i++) {
6608
6352
  var item = selectedItems_1[_i]; // 마지막 선택 항목과 호버된 항목은 나중에 따로 그림
6609
6353
 
6610
- if (item.id === (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) || (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id) === item.id) continue;
6354
+ if (item.id === selectedItemId || item.id === hoveredItemId) continue;
6611
6355
  if (!item.paths) continue;
6612
6356
  var polygonOffsets = utils.getOrComputePolygonOffsets(item);
6613
6357
  if (!polygonOffsets) continue;
@@ -6623,7 +6367,7 @@
6623
6367
  } // 2. 마지막 선택된 항목 그리기 (호버되지 않은 경우)
6624
6368
 
6625
6369
 
6626
- if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) && (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id) !== selectedItem.id) {
6370
+ if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) && hoveredItemId !== selectedItemId) {
6627
6371
  var polygonOffsets = utils.getOrComputePolygonOffsets(selectedItem);
6628
6372
 
6629
6373
  if (polygonOffsets) {
@@ -6641,10 +6385,10 @@
6641
6385
 
6642
6386
  if (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.paths) {
6643
6387
  var polygonOffsets = utils.getOrComputePolygonOffsets(hoveredItem);
6644
- if (!polygonOffsets) return;
6645
- var isSelected = selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.some(function (item) {
6646
- return item.id === hoveredItem.id;
6647
- });
6388
+ if (!polygonOffsets) return; // 좌표 변환 실패 시 스킵 (return은 렌더링 함수 종료)
6389
+ // 성능 최적화: Set을 사용하여 O(1) 조회 (이전: O(m) some() 체크)
6390
+
6391
+ var isSelected = selectedIdsSet.has(hoveredItem.id);
6648
6392
  drawPolygon({
6649
6393
  ctx: ctx,
6650
6394
  polygonOffsets: polygonOffsets,
@@ -6657,24 +6401,19 @@
6657
6401
  };
6658
6402
  };
6659
6403
 
6660
- // 메인 컴포넌트
6661
- // ============================================================================
6662
-
6663
6404
  var WoongCanvasPolygon = function (props) {
6664
6405
  var data = props.data,
6665
6406
  onClick = props.onClick,
6666
6407
  _a = props.enableMultiSelect,
6667
6408
  enableMultiSelect = _a === void 0 ? false : _a,
6668
- _b = props.enableViewportCulling,
6669
- enableViewportCulling = _b === void 0 ? true : _b,
6670
- _c = props.cullingMargin,
6671
- cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
6672
- _d = props.maxCacheSize,
6673
- maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
6409
+ _b = props.cullingMargin,
6410
+ cullingMargin = _b === void 0 ? DEFAULT_CULLING_MARGIN : _b,
6411
+ _c = props.maxCacheSize,
6412
+ maxCacheSize = _c === void 0 ? DEFAULT_MAX_CACHE_SIZE : _c,
6674
6413
  externalSelectedItems = props.selectedItems,
6675
6414
  externalSelectedItem = props.selectedItem,
6676
- _e = props.disableInteraction,
6677
- disableInteraction = _e === void 0 ? false : _e,
6415
+ _d = props.disableInteraction,
6416
+ disableInteraction = _d === void 0 ? false : _d,
6678
6417
  baseFillColor = props.baseFillColor,
6679
6418
  baseStrokeColor = props.baseStrokeColor,
6680
6419
  baseLineWidth = props.baseLineWidth,
@@ -6687,173 +6426,67 @@
6687
6426
  hoveredFillColor = props.hoveredFillColor,
6688
6427
  hoveredStrokeColor = props.hoveredStrokeColor,
6689
6428
  hoveredLineWidth = props.hoveredLineWidth,
6690
- 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"]); // --------------------------------------------------------------------------
6429
+ options = tslib.__rest(props, ["data", "onClick", "enableMultiSelect", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
6691
6430
  // Hooks & Context
6692
6431
  // --------------------------------------------------------------------------
6693
6432
 
6694
6433
 
6695
6434
  var controller = useMintMapController();
6696
6435
  var context = useWoongCanvasContext();
6697
- var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
6698
- // DOM Refs
6699
- // --------------------------------------------------------------------------
6436
+ var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
6700
6437
 
6701
6438
  var divRef = React.useRef(document.createElement('div'));
6702
6439
  var divElement = divRef.current;
6703
6440
  var containerRef = React.useRef(null);
6704
- var markerRef = React.useRef(); // --------------------------------------------------------------------------
6705
- // Konva Refs
6706
- // --------------------------------------------------------------------------
6441
+ var markerRef = React.useRef(); // Konva Refs
6707
6442
 
6708
6443
  var stageRef = React.useRef(null);
6709
6444
  var baseLayerRef = React.useRef(null);
6710
- var eventLayerRef = React.useRef(null); // --------------------------------------------------------------------------
6711
- // Data Refs - 선택 및 Hover 상태 관리
6712
- // --------------------------------------------------------------------------
6713
-
6714
- /** data prop을 ref로 추적 (stale closure 방지, useEffect에서 동기화) */
6715
-
6716
- var dataRef = React.useRef(data); // --------------------------------------------------------------------------
6717
- // State Refs - 선택 및 Hover 상태 관리
6718
- // --------------------------------------------------------------------------
6719
-
6720
- /** 상호작용 비활성화 상태 (Ref로 관리하여 클로저 문제 해결) */
6445
+ var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
6721
6446
 
6447
+ var dataRef = React.useRef(data);
6722
6448
  var disableInteractionRef = React.useRef(disableInteraction);
6723
- /** 현재 Hover 중인 항목 */
6724
-
6725
6449
  var hoveredItemRef = React.useRef(null);
6726
- /** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
6727
-
6728
6450
  var selectedItemRef = React.useRef(externalSelectedItem);
6729
- /**
6730
- * 선택된 항목의 ID Set
6731
- *
6732
- * 용도:
6733
- * 1. onClick 콜백에 전달 - onClick(data, selectedIdsRef.current)
6734
- * 2. 선택 여부 빠른 체크 - selectedIdsRef.current.has(id)
6735
- * 3. 메모리 효율 - ID만 저장 (작음)
6736
- *
6737
- * selectedItemsMapRef와 차이:
6738
- * - selectedIdsRef: ID만 저장 { "id1", "id2" }
6739
- * - selectedItemsMapRef: 전체 객체 저장 { id1: {...}, id2: {...} }
6740
- *
6741
- * 둘 다 필요: ID만 필요한 곳은 이것, 전체 데이터 필요한 곳은 Map
6742
- */
6743
-
6744
6451
  var selectedIdsRef = React.useRef(new Set());
6745
- /**
6746
- * 선택된 항목의 실제 데이터 Map (핵심 성능 최적화!)
6747
- *
6748
- * 목적: doRenderEvent에서 filter() 순회 제거
6749
- * - 이전: markersRef.current.filter() → O(전체 마커 수)
6750
- * - 현재: Map.values() → O(선택된 항목 수)
6751
- *
6752
- * 성능 개선: 10,000개 중 1개 선택 시
6753
- * - 이전: 10,000번 체크
6754
- * - 현재: 1번 접근 (10,000배 빠름!)
6755
- */
6756
-
6757
- var selectedItemsMapRef = React.useRef(new Map()); // --------------------------------------------------------------------------
6758
- // Drag Refs
6759
- // --------------------------------------------------------------------------
6452
+ var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
6760
6453
 
6761
6454
  var draggingRef = React.useRef(false);
6762
6455
  var prevCenterOffsetRef = React.useRef(null);
6763
6456
  var accumTranslateRef = React.useRef({
6764
6457
  x: 0,
6765
6458
  y: 0
6766
- }); // --------------------------------------------------------------------------
6767
- // Performance Refs (캐싱 & 최적화)
6768
- // --------------------------------------------------------------------------
6769
-
6770
- /** 좌표 변환 결과 LRU 캐시 */
6459
+ }); // 성능 최적화 Refs
6771
6460
 
6772
6461
  var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
6773
- /** 공간 인덱스 (빠른 Hit Test) */
6774
-
6775
6462
  var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
6776
- /** 바운딩 박스 캐시 (Viewport Culling 최적화) */
6777
-
6778
6463
  var boundingBoxCacheRef = React.useRef(new Map());
6779
- /** 뷰포트 경계 캐시 (Viewport Culling) */
6780
-
6781
- var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
6782
- // 유틸리티 함수: 뷰포트 관리
6783
- // --------------------------------------------------------------------------
6784
-
6785
- /**
6786
- * 현재 뷰포트 영역 계산
6787
- */
6788
-
6789
- var updateViewport = function () {
6790
- if (!stageRef.current) return;
6791
- var stage = stageRef.current;
6792
- viewportRef.current = {
6793
- minX: -cullingMargin,
6794
- maxX: stage.width() + cullingMargin,
6795
- minY: -cullingMargin,
6796
- maxY: stage.height() + cullingMargin
6797
- };
6798
- };
6799
- /**
6800
- * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
6801
- */
6802
-
6803
-
6804
- var isInViewport = function (item) {
6805
- if (!enableViewportCulling || !viewportRef.current) return true;
6806
- var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
6464
+ var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling)
6807
6465
 
6808
- var bbox = boundingBoxCacheRef.current.get(item.id);
6466
+ var updateViewport$1 = function () {
6467
+ updateViewport(stageRef.current, cullingMargin, viewportRef);
6468
+ }; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
6809
6469
 
6810
- if (!bbox) {
6811
- // 바운딩 박스 계산 (공통 함수 사용)
6812
- var computed = computeBoundingBox(item);
6813
- if (!computed) return false;
6814
- bbox = computed;
6815
- boundingBoxCacheRef.current.set(item.id, bbox);
6816
- } // 바운딩 박스와 viewport 교차 체크
6817
6470
 
6818
-
6819
- return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
6820
- }; // --------------------------------------------------------------------------
6821
- // 유틸리티 함수: 좌표 변환 캐싱
6822
- // --------------------------------------------------------------------------
6823
-
6824
- /**
6825
- * 폴리곤 좌표 변환 결과를 캐시하고 반환
6826
- * @param polygonData 폴리곤 데이터
6827
- * @returns 변환된 좌표 배열 또는 null
6828
- */
6471
+ var isInViewport$1 = function (item) {
6472
+ return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
6473
+ }; // 폴리곤 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
6829
6474
 
6830
6475
 
6831
6476
  var getOrComputePolygonOffsets = function (polygonData) {
6832
6477
  var cached = offsetCacheRef.current.get(polygonData.id);
6833
6478
  if (cached && Array.isArray(cached)) return cached;
6834
6479
  var result = computePolygonOffsets(polygonData, controller);
6835
-
6836
- if (result) {
6837
- offsetCacheRef.current.set(polygonData.id, result);
6838
- }
6839
-
6480
+ if (!result) return null;
6481
+ offsetCacheRef.current.set(polygonData.id, result);
6840
6482
  return result;
6841
- }; // --------------------------------------------------------------------------
6842
- // 유틸리티 함수: 바운딩 박스 계산
6843
- // --------------------------------------------------------------------------
6844
-
6845
- /**
6846
- * 폴리곤의 바운딩 박스 계산
6847
- *
6848
- * @param item 폴리곤 데이터
6849
- * @returns 바운딩 박스 또는 null
6850
- */
6483
+ }; // 폴리곤 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
6851
6484
 
6852
6485
 
6853
6486
  var computeBoundingBox = function (item) {
6854
- // 폴리곤: 모든 좌표의 최소/최대값 계산
6855
6487
  var offsets = getOrComputePolygonOffsets(item);
6856
- if (!offsets) return null;
6488
+ if (!offsets) return null; // 모든 좌표를 순회하며 최소/최대값 찾기
6489
+
6857
6490
  var minX = Infinity,
6858
6491
  minY = Infinity,
6859
6492
  maxX = -Infinity,
@@ -6883,86 +6516,39 @@
6883
6516
  maxX: maxX,
6884
6517
  maxY: maxY
6885
6518
  };
6886
- }; // --------------------------------------------------------------------------
6887
- // 유틸리티 함수: 공간 인덱싱
6888
- // --------------------------------------------------------------------------
6519
+ }; // 공간 인덱스 빌드 (빠른 Hit Test용)
6889
6520
 
6890
- /**
6891
- * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
6892
- */
6893
-
6894
-
6895
- var buildSpatialIndex = function () {
6896
- var spatial = spatialIndexRef.current;
6897
- spatial.clear();
6898
- var currentData = dataRef.current;
6899
-
6900
- for (var _i = 0, currentData_1 = currentData; _i < currentData_1.length; _i++) {
6901
- var item = currentData_1[_i]; // 바운딩 박스 계산 (공통 함수 사용)
6902
-
6903
- var bbox = computeBoundingBox(item);
6904
6521
 
6905
- if (bbox) {
6906
- spatial.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
6907
- }
6908
- }
6909
- }; // --------------------------------------------------------------------------
6910
- // 렌더링 함수 결정 (dataType에 따라)
6911
- // --------------------------------------------------------------------------
6912
-
6913
- /**
6914
- * 외부 렌더링 함수에 전달할 유틸리티 객체
6915
- */
6522
+ var buildSpatialIndex$1 = function () {
6523
+ buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
6524
+ }; // 렌더링 유틸리티 객체
6916
6525
 
6917
6526
 
6918
6527
  var renderUtils = {
6919
6528
  getOrComputePolygonOffsets: getOrComputePolygonOffsets,
6920
6529
  getOrComputeMarkerOffset: function () {
6921
6530
  return null;
6922
- } // 폴리곤에서는 사용하지 않음
6923
-
6924
- };
6925
- /**
6926
- * 렌더링 함수 생성 (props 기반)
6927
- */
6531
+ }
6532
+ }; // 렌더링 함수 생성
6928
6533
 
6929
6534
  var renderBase = renderPolygonBase(baseFillColor, baseStrokeColor, baseLineWidth);
6930
- var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth);
6931
- /** Base Layer에서 사용할 빈 Set (재사용) */
6932
-
6933
- React.useRef(new Set());
6934
- /**
6935
- * Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
6936
- *
6937
- * 🔥 최적화:
6938
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
6939
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
6940
- * 3. 클로저로 최신 데이터 참조
6941
- *
6942
- * 🎯 topOnHover 지원:
6943
- * - renderEvent가 없을 때 Base Layer에서 hover 처리 (fallback)
6944
- * - renderEvent가 있으면 Event Layer에서 처리 (성능 최적화)
6945
- */
6535
+ var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth); // Base Layer 렌더링 (뷰포트 컬링 적용)
6946
6536
 
6947
6537
  var doRenderBase = function () {
6948
6538
  var layer = baseLayerRef.current;
6949
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
6950
-
6539
+ if (!layer) return;
6951
6540
  var shape = layer.findOne('.base-render-shape');
6952
6541
 
6953
6542
  if (!shape) {
6954
- // 최초 생성 (한 번만 실행됨)
6955
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
6956
6543
  shape = new Konva__default["default"].Shape({
6957
6544
  name: 'base-render-shape',
6958
6545
  sceneFunc: function (context, shape) {
6959
6546
  var ctx = context;
6960
- var hovered = hoveredItemRef.current; // 클로저로 최신 ref 참조
6961
-
6962
- var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
6963
- return isInViewport(item);
6964
- }) : dataRef.current; // 일반 항목 렌더링
6547
+ var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
6965
6548
 
6549
+ var visibleItems = dataRef.current.filter(function (item) {
6550
+ return isInViewport$1(item);
6551
+ });
6966
6552
  renderBase({
6967
6553
  ctx: ctx,
6968
6554
  items: visibleItems,
@@ -6976,38 +6562,24 @@
6976
6562
  hitStrokeWidth: 0
6977
6563
  });
6978
6564
  layer.add(shape);
6979
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
6980
-
6565
+ }
6981
6566
 
6982
6567
  layer.batchDraw();
6983
- };
6984
- /**
6985
- * Event 레이어 렌더링 (hover + 선택 상태 표시)
6986
- *
6987
- * 🔥 최적화:
6988
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
6989
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
6990
- * 3. 클로저로 최신 데이터 참조
6991
- */
6568
+ }; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
6992
6569
 
6993
6570
 
6994
6571
  var doRenderEvent = function () {
6995
6572
  var layer = eventLayerRef.current;
6996
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
6997
-
6573
+ if (!layer) return;
6998
6574
  var shape = layer.findOne('.event-render-shape');
6999
6575
 
7000
6576
  if (!shape) {
7001
- // 최초 생성 (한 번만 실행됨)
7002
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
7003
6577
  shape = new Konva__default["default"].Shape({
7004
6578
  name: 'event-render-shape',
7005
6579
  sceneFunc: function (context, shape) {
7006
- var ctx = context; // 클로저로 최신 ref 값 참조
7007
-
7008
- var selectedItems = Array.from(selectedItemsMapRef.current.values());
7009
- var hovered = hoveredItemRef.current; // 일반 렌더링
7010
-
6580
+ var ctx = context;
6581
+ var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
6582
+ var hovered = hoveredItemRef.current;
7011
6583
  renderEvent({
7012
6584
  ctx: ctx,
7013
6585
  hoveredItem: hovered,
@@ -7021,126 +6593,57 @@
7021
6593
  hitStrokeWidth: 0
7022
6594
  });
7023
6595
  layer.add(shape);
7024
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
7025
-
6596
+ }
7026
6597
 
7027
6598
  layer.batchDraw();
7028
- };
7029
- /**
7030
- * 전체 즉시 렌더링 (IDLE 시 호출)
7031
- */
6599
+ }; // 전체 즉시 렌더링
7032
6600
 
7033
6601
 
7034
6602
  var renderAllImmediate = function () {
7035
- updateViewport();
7036
- buildSpatialIndex();
6603
+ updateViewport$1();
6604
+ buildSpatialIndex$1();
7037
6605
  doRenderBase();
7038
6606
  doRenderEvent();
7039
- }; // --------------------------------------------------------------------------
7040
- // 이벤트 핸들러: 지도 이벤트
7041
- // --------------------------------------------------------------------------
7042
-
7043
- /**
7044
- * 지도 이동/줌 완료 시 처리
7045
- */
7046
-
7047
-
7048
- var handleIdle = function () {
7049
- prevCenterOffsetRef.current = null;
7050
- accumTranslateRef.current = {
7051
- x: 0,
7052
- y: 0
7053
- }; // 2. 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
7054
-
7055
- offsetCacheRef.current.clear();
7056
- boundingBoxCacheRef.current.clear(); // 3. 마커 위치 업데이트
7057
-
7058
- var bounds = controller.getCurrBounds();
7059
-
7060
- var markerOptions = tslib.__assign({
7061
- position: bounds.nw
7062
- }, options);
7063
-
7064
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
7065
-
7066
- if (containerRef.current) {
7067
- containerRef.current.style.transform = '';
7068
- containerRef.current.style.visibility = '';
7069
- } // 5. 새 위치에서 렌더링
7070
-
7071
-
7072
- renderAllImmediate();
7073
- };
7074
- /**
7075
- * 줌 시작 시 처리 (일시적으로 숨김)
7076
- */
6607
+ }; // 지도 이벤트 핸들러 생성
6608
+
6609
+
6610
+ var _e = createMapEventHandlers({
6611
+ controller: controller,
6612
+ containerRef: containerRef,
6613
+ markerRef: markerRef,
6614
+ options: options,
6615
+ prevCenterOffsetRef: prevCenterOffsetRef,
6616
+ accumTranslateRef: accumTranslateRef,
6617
+ offsetCacheRef: offsetCacheRef,
6618
+ boundingBoxCacheRef: boundingBoxCacheRef,
6619
+ renderAllImmediate: renderAllImmediate
6620
+ }),
6621
+ handleIdle = _e.handleIdle,
6622
+ handleZoomStart = _e.handleZoomStart,
6623
+ handleZoomEnd = _e.handleZoomEnd,
6624
+ handleCenterChanged = _e.handleCenterChanged,
6625
+ handleDragStartShared = _e.handleDragStart,
6626
+ handleDragEndShared = _e.handleDragEnd;
7077
6627
 
7078
-
7079
- var handleZoomStart = function () {
7080
- if (containerRef.current) {
7081
- containerRef.current.style.visibility = 'hidden';
7082
- }
7083
- };
7084
- /**
7085
- * 줌 종료 시 처리 (다시 표시)
7086
- */
7087
-
7088
-
7089
- var handleZoomEnd = function () {
7090
- if (containerRef.current) {
7091
- containerRef.current.style.visibility = '';
7092
- }
6628
+ var handleDragStart = function () {
6629
+ handleDragStartShared();
6630
+ draggingRef.current = true;
6631
+ controller.setMapCursor('grabbing');
7093
6632
  };
7094
- /**
7095
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
7096
- */
7097
-
7098
-
7099
- var handleCenterChanged = function () {
7100
- var center = controller.getCurrBounds().getCenter();
7101
- var curr = controller.positionToOffset(center);
7102
- var prev = prevCenterOffsetRef.current;
7103
-
7104
- if (!prev) {
7105
- prevCenterOffsetRef.current = {
7106
- x: curr.x,
7107
- y: curr.y
7108
- };
7109
- return;
7110
- }
7111
-
7112
- var dx = prev.x - curr.x;
7113
- var dy = prev.y - curr.y;
7114
- accumTranslateRef.current = {
7115
- x: accumTranslateRef.current.x + dx,
7116
- y: accumTranslateRef.current.y + dy
7117
- };
7118
- prevCenterOffsetRef.current = {
7119
- x: curr.x,
7120
- y: curr.y
7121
- };
7122
-
7123
- if (containerRef.current) {
7124
- containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
7125
- }
7126
- }; // --------------------------------------------------------------------------
7127
- // Hit Test & 상태 관리
7128
- // --------------------------------------------------------------------------
7129
6633
 
7130
- /**
7131
- * 특정 좌표의 폴리곤 데이터 찾기 (Spatial Index 사용)
7132
- *
7133
- * @param offset 검사할 좌표
7134
- * @returns 찾은 폴리곤 데이터 또는 null
7135
- */
6634
+ var handleDragEnd = function () {
6635
+ handleDragEndShared();
6636
+ draggingRef.current = false;
6637
+ controller.setMapCursor('grab');
6638
+ }; // Hit Test: 특정 좌표의 폴리곤 찾기
7136
6639
 
7137
6640
 
7138
6641
  var findData = function (offset) {
7139
- // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 ~10개)
7140
- var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 폴리곤 체크
6642
+ // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
6643
+ var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
7141
6644
 
7142
6645
  for (var i = candidates.length - 1; i >= 0; i--) {
7143
- var item = candidates[i];
6646
+ var item = candidates[i]; // 정확한 Hit Test: Ray Casting 알고리즘으로 폴리곤 내부 여부 확인
7144
6647
 
7145
6648
  if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
7146
6649
  return item;
@@ -7148,18 +6651,7 @@
7148
6651
  }
7149
6652
 
7150
6653
  return null;
7151
- };
7152
- /**
7153
- * Hover 상태 설정 및 레이어 렌더링
7154
- *
7155
- * @param data hover된 마커/폴리곤 데이터 또는 null
7156
- *
7157
- * 최적화: RAF 제거하여 즉시 렌더링 (16ms 지연 제거)
7158
- *
7159
- * 🎯 topOnHover 지원:
7160
- * - renderEvent가 있으면: Event Layer에서만 처리 (성능 최적화)
7161
- * - renderEvent가 없고 topOnHover=true면: Base Layer에서 처리
7162
- */
6654
+ }; // Hover 상태 설정 및 렌더링
7163
6655
 
7164
6656
 
7165
6657
  var setHovered = function (data) {
@@ -7169,26 +6661,14 @@
7169
6661
  controller.setMapCursor('grabbing');
7170
6662
  } else {
7171
6663
  controller.setMapCursor(data ? 'pointer' : 'grab');
7172
- } // 즉시 렌더링 (RAF 없이)
7173
-
6664
+ }
7174
6665
 
7175
6666
  doRenderEvent();
7176
- };
7177
- /**
7178
- * 클릭 처리 (단일/다중 선택)
7179
- *
7180
- * @param data 클릭된 마커/폴리곤 데이터
7181
- *
7182
- * 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
7183
- * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
7184
- * - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
7185
- */
6667
+ }; // 클릭 처리: 선택 상태 업데이트
7186
6668
 
7187
6669
 
7188
6670
  var handleLocalClick = function (data) {
7189
- // 1. 선택 상태 업데이트
7190
6671
  if (enableMultiSelect) {
7191
- // 다중 선택: Set과 Map 동시 업데이트
7192
6672
  var newSelected = new Set(selectedIdsRef.current);
7193
6673
 
7194
6674
  if (newSelected.has(data.id)) {
@@ -7201,7 +6681,6 @@
7201
6681
 
7202
6682
  selectedIdsRef.current = newSelected;
7203
6683
  } else {
7204
- // 단일 선택: 토글
7205
6684
  var newSelected = new Set();
7206
6685
 
7207
6686
  if (!selectedIdsRef.current.has(data.id)) {
@@ -7213,144 +6692,79 @@
7213
6692
  }
7214
6693
 
7215
6694
  selectedIdsRef.current = newSelected;
7216
- } // 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
7217
-
7218
-
7219
- doRenderBase(); // 3. Event Layer 렌더링 (hover 처리)
6695
+ }
7220
6696
 
6697
+ doRenderBase();
7221
6698
  doRenderEvent();
7222
- }; // --------------------------------------------------------------------------
7223
- // 이벤트 핸들러: UI 이벤트
7224
- // --------------------------------------------------------------------------
7225
-
7226
- /**
7227
- * 클릭 이벤트 처리
7228
- */
6699
+ }; // 클릭 이벤트 핸들러
7229
6700
 
7230
6701
 
7231
6702
  var handleClick = function (event) {
7232
- var _a;
7233
-
7234
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7235
-
7236
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
7237
-
7238
- try {
7239
- var clickedOffset = controller.positionToOffset(event.param.position);
7240
- var data_1 = findData(clickedOffset);
7241
-
7242
- if (data_1) {
7243
- handleLocalClick(data_1);
7244
-
7245
- if (onClick) {
7246
- onClick(data_1, selectedIdsRef.current);
7247
- }
7248
- }
7249
- } catch (error) {
7250
- console.error('[WoongCanvasPolygon] handleClick error:', error);
7251
- }
7252
- };
7253
- /**
7254
- * 마우스 이동 이벤트 처리 (hover 감지)
7255
- */
6703
+ if (disableInteractionRef.current) return;
6704
+ var clickedOffset = validateEvent(event, context, controller);
6705
+ if (!clickedOffset) return;
6706
+ var data = findData(clickedOffset);
6707
+ if (!data) return;
6708
+ handleLocalClick(data);
6709
+ onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
6710
+ }; // 마우스 이동 이벤트 핸들러 (hover 감지)
7256
6711
 
7257
6712
 
7258
6713
  var handleMouseMove = function (event) {
7259
- var _a;
7260
-
7261
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7262
-
7263
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
7264
-
7265
- try {
7266
- var mouseOffset = controller.positionToOffset(event.param.position);
7267
- var hoveredItem = findData(mouseOffset);
7268
- var prevHovered = hoveredItemRef.current;
7269
-
7270
- if (prevHovered !== hoveredItem) {
7271
- setHovered(hoveredItem);
7272
- }
7273
- } catch (error) {
7274
- console.error('[WoongCanvasPolygon] handleMouseMove error:', error);
7275
- }
7276
- };
7277
- /**
7278
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
7279
- */
7280
-
7281
-
7282
- var handleDragStart = function () {
7283
- draggingRef.current = true;
7284
- controller.setMapCursor('grabbing');
7285
- };
7286
- /**
7287
- * 드래그 종료 처리 (커서를 기본으로 복원)
7288
- */
7289
-
7290
-
7291
- var handleDragEnd = function () {
7292
- draggingRef.current = false;
7293
- controller.setMapCursor('grab');
7294
- };
7295
- /**
7296
- * 마우스가 canvas를 벗어날 때 hover cleanup
7297
- */
6714
+ if (disableInteractionRef.current) return;
6715
+ var mouseOffset = validateEvent(event, context, controller);
6716
+ if (!mouseOffset) return;
6717
+ var hoveredItem = findData(mouseOffset);
6718
+ var prevHovered = hoveredItemRef.current;
6719
+ if (prevHovered === hoveredItem) return;
6720
+ setHovered(hoveredItem);
6721
+ }; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
7298
6722
 
7299
6723
 
7300
6724
  var handleMouseLeave = function () {
7301
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7302
-
6725
+ if (disableInteractionRef.current) return;
7303
6726
  var prevHovered = hoveredItemRef.current;
7304
-
7305
- if (prevHovered) {
7306
- hoveredItemRef.current = null;
7307
- controller.setMapCursor('grab');
7308
- doRenderEvent();
7309
- }
7310
- }; // --------------------------------------------------------------------------
7311
- // Lifecycle: DOM 초기화
7312
- // --------------------------------------------------------------------------
6727
+ if (!prevHovered) return;
6728
+ hoveredItemRef.current = null;
6729
+ controller.setMapCursor('grab');
6730
+ doRenderEvent();
6731
+ }; // DOM 초기화
7313
6732
 
7314
6733
 
7315
6734
  React.useEffect(function () {
7316
6735
  divElement.style.width = 'fit-content';
7317
6736
  return function () {
7318
- if (markerRef.current) {
7319
- controller.clearDrawable(markerRef.current);
7320
- markerRef.current = undefined;
7321
- }
6737
+ if (!markerRef.current) return;
6738
+ controller.clearDrawable(markerRef.current);
6739
+ markerRef.current = undefined;
7322
6740
  };
7323
- }, []); // --------------------------------------------------------------------------
7324
- // Lifecycle: 마커 생성/업데이트
7325
- // --------------------------------------------------------------------------
6741
+ }, []); // 마커 생성/업데이트
7326
6742
 
7327
6743
  React.useEffect(function () {
7328
- if (options) {
7329
- var bounds = controller.getCurrBounds();
6744
+ if (!options) return;
6745
+ var bounds = controller.getCurrBounds();
7330
6746
 
7331
- var markerOptions = tslib.__assign({
7332
- position: bounds.nw
7333
- }, options);
6747
+ var markerOptions = tslib.__assign({
6748
+ position: bounds.nw
6749
+ }, options);
7334
6750
 
7335
- if (markerRef.current) {
7336
- controller.updateMarker(markerRef.current, markerOptions);
7337
- } else {
7338
- markerRef.current = new Marker(markerOptions);
7339
- markerRef.current.element = divElement;
7340
- controller.createMarker(markerRef.current);
6751
+ if (markerRef.current) {
6752
+ controller.updateMarker(markerRef.current, markerOptions);
6753
+ return;
6754
+ }
7341
6755
 
7342
- if (divElement.parentElement) {
7343
- divElement.parentElement.style.pointerEvents = 'none';
7344
- }
6756
+ markerRef.current = new Marker(markerOptions);
6757
+ markerRef.current.element = divElement;
6758
+ controller.createMarker(markerRef.current);
7345
6759
 
7346
- if (options.zIndex !== undefined) {
7347
- controller.setMarkerZIndex(markerRef.current, options.zIndex);
7348
- }
7349
- }
6760
+ if (divElement.parentElement) {
6761
+ divElement.parentElement.style.pointerEvents = 'none';
7350
6762
  }
7351
- }, [options]); // --------------------------------------------------------------------------
7352
- // Lifecycle: Konva 초기화 및 이벤트 리스너 등록
7353
- // --------------------------------------------------------------------------
6763
+
6764
+ if (options.zIndex !== undefined) {
6765
+ controller.setMarkerZIndex(markerRef.current, options.zIndex);
6766
+ }
6767
+ }, [options]); // Konva 초기화 및 이벤트 리스너 등록
7354
6768
 
7355
6769
  React.useEffect(function () {
7356
6770
  var mapDiv = controller.mapDivElement;
@@ -7359,11 +6773,9 @@
7359
6773
  width: mapDiv.offsetWidth,
7360
6774
  height: mapDiv.offsetHeight
7361
6775
  });
7362
- stageRef.current = stage; // 레이어 최적화 설정
7363
-
6776
+ stageRef.current = stage;
7364
6777
  var baseLayer = new Konva__default["default"].Layer({
7365
- listening: false // 이벤트 리스닝 비활성화로 성능 향상
7366
-
6778
+ listening: false
7367
6779
  });
7368
6780
  var eventLayer = new Konva__default["default"].Layer({
7369
6781
  listening: false
@@ -7371,13 +6783,11 @@
7371
6783
  baseLayerRef.current = baseLayer;
7372
6784
  eventLayerRef.current = eventLayer;
7373
6785
  stage.add(baseLayer);
7374
- stage.add(eventLayer); // 초기 뷰포트 설정
7375
-
7376
- updateViewport(); // ResizeObserver (맵 크기 변경 감지)
6786
+ stage.add(eventLayer);
6787
+ updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
7377
6788
 
7378
6789
  var resizeRafId = null;
7379
6790
  var resizeObserver = new ResizeObserver(function () {
7380
- // RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
7381
6791
  if (resizeRafId !== null) {
7382
6792
  cancelAnimationFrame(resizeRafId);
7383
6793
  }
@@ -7387,7 +6797,7 @@
7387
6797
  stage.height(mapDiv.offsetHeight);
7388
6798
  offsetCacheRef.current.clear();
7389
6799
  boundingBoxCacheRef.current.clear();
7390
- updateViewport();
6800
+ updateViewport$1();
7391
6801
  renderAllImmediate();
7392
6802
  resizeRafId = null;
7393
6803
  });
@@ -7400,10 +6810,9 @@
7400
6810
  controller.addEventListener('CLICK', handleClick);
7401
6811
  controller.addEventListener('MOUSEMOVE', handleMouseMove);
7402
6812
  controller.addEventListener('DRAGSTART', handleDragStart);
7403
- controller.addEventListener('DRAGEND', handleDragEnd); // 맵 컨테이너에 mouseleave 이벤트 추가
7404
-
6813
+ controller.addEventListener('DRAGEND', handleDragEnd);
7405
6814
  mapDiv.addEventListener('mouseleave', handleMouseLeave);
7406
- renderAllImmediate(); // Context 사용 시 컴포넌트 등록 (다중 인스턴스 관리)
6815
+ renderAllImmediate(); // Context 사용 시 컴포넌트 등록
7407
6816
 
7408
6817
  var componentInstance = null;
7409
6818
 
@@ -7422,22 +6831,17 @@
7422
6831
  },
7423
6832
  isInteractionDisabled: function () {
7424
6833
  return disableInteractionRef.current;
7425
- } // 🚫 상호작용 비활성화 여부 반환
7426
-
6834
+ }
7427
6835
  };
7428
6836
  context.registerComponent(componentInstance);
7429
- } // Cleanup 함수
7430
-
6837
+ }
7431
6838
 
7432
6839
  return function () {
7433
- // RAF 정리
7434
6840
  if (resizeRafId !== null) {
7435
6841
  cancelAnimationFrame(resizeRafId);
7436
- } // 옵저버 정리
7437
-
7438
-
7439
- resizeObserver.disconnect(); // 이벤트 리스너 정리
6842
+ }
7440
6843
 
6844
+ resizeObserver.disconnect();
7441
6845
  controller.removeEventListener('IDLE', handleIdle);
7442
6846
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
7443
6847
  controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
@@ -7446,67 +6850,41 @@
7446
6850
  controller.removeEventListener('MOUSEMOVE', handleMouseMove);
7447
6851
  controller.removeEventListener('DRAGSTART', handleDragStart);
7448
6852
  controller.removeEventListener('DRAGEND', handleDragEnd);
7449
- mapDiv.removeEventListener('mouseleave', handleMouseLeave); // Context 정리
6853
+ mapDiv.removeEventListener('mouseleave', handleMouseLeave);
7450
6854
 
7451
6855
  if (context && componentInstance) {
7452
6856
  context.unregisterComponent(componentInstance);
7453
- } // Konva 리소스 정리
7454
-
6857
+ }
7455
6858
 
7456
6859
  baseLayer.destroyChildren();
7457
6860
  eventLayer.destroyChildren();
7458
- stage.destroy(); // 캐시 정리
7459
-
6861
+ stage.destroy();
7460
6862
  offsetCacheRef.current.clear();
7461
6863
  boundingBoxCacheRef.current.clear();
7462
6864
  spatialIndexRef.current.clear();
7463
6865
  };
7464
- }, []); // 초기화는 한 번만
7465
- // --------------------------------------------------------------------------
7466
- // Lifecycle: disableInteraction 동기화
7467
- // --------------------------------------------------------------------------
6866
+ }, []); // disableInteraction 동기화
7468
6867
 
7469
6868
  React.useEffect(function () {
7470
6869
  disableInteractionRef.current = disableInteraction;
7471
- }, [disableInteraction]); // --------------------------------------------------------------------------
7472
- // Lifecycle: 외부 selectedItems 동기화
7473
- // --------------------------------------------------------------------------
6870
+ }, [disableInteraction]); // 외부 selectedItems 동기화
7474
6871
 
7475
6872
  React.useEffect(function () {
7476
- if (!stageRef.current) return; // externalSelectedItems가 undefined면 외부 제어 안 함
7477
-
7478
- if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
7479
-
7480
- var newSelectedIds = new Set();
7481
- var newSelectedItemsMap = new Map();
7482
- externalSelectedItems.forEach(function (item) {
7483
- newSelectedIds.add(item.id);
7484
- newSelectedItemsMap.set(item.id, item);
7485
- });
7486
- selectedIdsRef.current = newSelectedIds;
7487
- selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
7488
-
6873
+ if (!stageRef.current) return;
6874
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
7489
6875
  doRenderBase();
7490
6876
  doRenderEvent();
7491
- }, [externalSelectedItems]); // 배열 자체를 dependency로 사용
7492
- // --------------------------------------------------------------------------
7493
- // Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
7494
- // --------------------------------------------------------------------------
6877
+ }, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
7495
6878
 
7496
6879
  React.useEffect(function () {
7497
- if (!stageRef.current) return; // Ref 동기화
7498
-
7499
- selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
7500
-
6880
+ if (!stageRef.current) return;
6881
+ selectedItemRef.current = externalSelectedItem;
7501
6882
  doRenderEvent();
7502
- }, [externalSelectedItem]); // --------------------------------------------------------------------------
7503
- // Lifecycle: 데이터 변경 시 렌더링
7504
- // --------------------------------------------------------------------------
6883
+ }, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
7505
6884
 
7506
6885
  React.useEffect(function () {
7507
- if (!stageRef.current) return; // dataRef 동기화
7508
-
7509
- dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
6886
+ if (!stageRef.current) return;
6887
+ dataRef.current = data;
7510
6888
 
7511
6889
  if (containerRef.current) {
7512
6890
  containerRef.current.style.transform = '';
@@ -7516,46 +6894,10 @@
7516
6894
  accumTranslateRef.current = {
7517
6895
  x: 0,
7518
6896
  y: 0
7519
- }; // 캐시 정리 (새 데이터이므로 기존 캐시는 무효)
7520
-
6897
+ };
7521
6898
  offsetCacheRef.current.clear();
7522
6899
  boundingBoxCacheRef.current.clear();
7523
- /**
7524
- * 선택 상태 동기화 (최적화 버전)
7525
- *
7526
- * data가 변경되면 selectedItemsMapRef도 업데이트 필요
7527
- * (참조가 바뀌므로 기존 Map의 데이터는 stale 상태)
7528
- *
7529
- * 🔥 중요: 화면 밖 데이터도 선택 상태 유지!
7530
- * - 현재 data에 있으면 최신 데이터로 업데이트
7531
- * - 없으면 기존 selectedItemsMapRef의 데이터 유지
7532
- *
7533
- * 최적화: data를 Map으로 먼저 변환하여 find() 순회 제거
7534
- * - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
7535
- */
7536
-
7537
- var dataMap = new Map(data.map(function (m) {
7538
- return [m.id, m];
7539
- }));
7540
- var newSelectedItemsMap = new Map();
7541
- selectedIdsRef.current.forEach(function (id) {
7542
- // 현재 data에 있으면 최신 데이터 사용
7543
- var currentItem = dataMap.get(id);
7544
-
7545
- if (currentItem) {
7546
- newSelectedItemsMap.set(id, currentItem);
7547
- } else {
7548
- // 화면 밖이면 기존 데이터 유지
7549
- var prevItem = selectedItemsMapRef.current.get(id);
7550
-
7551
- if (prevItem) {
7552
- newSelectedItemsMap.set(id, prevItem);
7553
- }
7554
- }
7555
- }); // selectedIdsRef는 그대로 유지 (화면 밖 항목도 선택 상태 유지)
7556
-
7557
- selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
7558
-
6900
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
7559
6901
  renderAllImmediate();
7560
6902
  }, [data]);
7561
6903
  return reactDom.createPortal(React__default["default"].createElement("div", {
@@ -10445,19 +9787,27 @@
10445
9787
  exports.WoongCanvasMarker = WoongCanvasMarker;
10446
9788
  exports.WoongCanvasPolygon = WoongCanvasPolygon;
10447
9789
  exports.WoongCanvasProvider = WoongCanvasProvider;
9790
+ exports.buildSpatialIndex = buildSpatialIndex;
10448
9791
  exports.calculateTextBoxWidth = calculateTextBoxWidth;
10449
9792
  exports.computeMarkerOffset = computeMarkerOffset;
10450
9793
  exports.computePolygonOffsets = computePolygonOffsets;
9794
+ exports.createMapEventHandlers = createMapEventHandlers;
10451
9795
  exports.getClusterInfo = getClusterInfo;
10452
9796
  exports.getMapOfType = getMapOfType;
10453
9797
  exports.hexToRgba = hexToRgba;
9798
+ exports.isInViewport = isInViewport;
10454
9799
  exports.isPointInMarkerData = isPointInMarkerData;
10455
9800
  exports.isPointInPolygon = isPointInPolygon;
10456
9801
  exports.isPointInPolygonData = isPointInPolygonData;
10457
9802
  exports.log = log;
9803
+ exports.mapValuesToArray = mapValuesToArray;
9804
+ exports.syncExternalSelectedItems = syncExternalSelectedItems;
9805
+ exports.syncSelectedItems = syncSelectedItems;
9806
+ exports.updateViewport = updateViewport;
10458
9807
  exports.useMarkerMoving = useMarkerMoving;
10459
9808
  exports.useMintMapController = useMintMapController;
10460
9809
  exports.useWoongCanvasContext = useWoongCanvasContext;
9810
+ exports.validateEvent = validateEvent;
10461
9811
  exports.waiting = waiting;
10462
9812
 
10463
9813
  Object.defineProperty(exports, '__esModule', { value: true });