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

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