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

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