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

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