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