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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/dist/components/mint-map/core/advanced/shared/context.d.ts +9 -71
  2. package/dist/components/mint-map/core/advanced/shared/context.js +43 -137
  3. package/dist/components/mint-map/core/advanced/shared/helpers.d.ts +5 -13
  4. package/dist/components/mint-map/core/advanced/shared/helpers.js +8 -20
  5. package/dist/components/mint-map/core/advanced/shared/hooks.d.ts +6 -76
  6. package/dist/components/mint-map/core/advanced/shared/hooks.js +18 -112
  7. package/dist/components/mint-map/core/advanced/shared/performance.d.ts +9 -188
  8. package/dist/components/mint-map/core/advanced/shared/performance.js +53 -229
  9. package/dist/components/mint-map/core/advanced/shared/types.d.ts +18 -153
  10. package/dist/components/mint-map/core/advanced/shared/types.js +0 -1
  11. package/dist/components/mint-map/core/advanced/shared/utils.d.ts +21 -126
  12. package/dist/components/mint-map/core/advanced/shared/utils.js +43 -151
  13. package/dist/components/mint-map/core/advanced/shared/viewport.d.ts +4 -34
  14. package/dist/components/mint-map/core/advanced/shared/viewport.js +4 -34
  15. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.d.ts +22 -74
  16. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.js +122 -516
  17. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.d.ts +26 -76
  18. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.js +118 -432
  19. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.d.ts +3 -3
  20. package/dist/index.es.js +409 -1632
  21. package/dist/index.umd.js +409 -1632
  22. package/package.json +1 -1
package/dist/index.umd.js CHANGED
@@ -656,7 +656,6 @@
656
656
 
657
657
  /**
658
658
  * 캔버스 데이터 타입 Enum
659
- * 마커인지 폴리곤인지 구분하는 상수
660
659
  */
661
660
  exports.CanvasDataType = void 0;
662
661
 
@@ -799,31 +798,11 @@
799
798
  }();
800
799
 
801
800
  /**
802
- * 폴리곤 offset 계산
801
+ * 폴리곤 좌표 변환 (위경도 → 화면 좌표)
803
802
  *
804
- * GeoJSON MultiPolygon 형식의 위경도 좌표를 화면 픽셀 좌표로 변환합니다.
805
- *
806
- * @param polygonData 폴리곤 데이터 (paths 필드 필수)
803
+ * @param polygonData 폴리곤 데이터
807
804
  * @param controller MintMapController 인스턴스
808
- * @returns 변환된 화면 좌표 배열 (4차원 배열) 또는 null (변환 실패 시)
809
- *
810
- * @remarks
811
- * - 반환 형식: [MultiPolygon][Polygon][Point][x/y]
812
- * - 성능: O(n), n은 폴리곤의 총 좌표 수
813
- * - GeoJSON MultiPolygon 형식 지원
814
- *
815
- * @example
816
- * ```typescript
817
- * const offsets = computePolygonOffsets(polygonData, controller);
818
- * if (!offsets) return; // 변환 실패
819
- *
820
- * // offsets 구조: [MultiPolygon][Polygon][Point][x/y]
821
- * for (const multiPolygon of offsets) {
822
- * for (const polygon of multiPolygon) {
823
- * // polygon은 [Point][x/y] 배열
824
- * }
825
- * }
826
- * ```
805
+ * @returns 변환된 화면 좌표 배열 (4차원 배열) 또는 null
827
806
  */
828
807
 
829
808
  var computePolygonOffsets = function (polygonData, controller) {
@@ -833,7 +812,7 @@
833
812
  return null;
834
813
  }
835
814
 
836
- var result = [];
815
+ var result = []; // GeoJSON MultiPolygon 구조: [MultiPolygon][PolygonGroup][Coordinate][lng, lat]
837
816
 
838
817
  for (var _i = 0, _a = paths.coordinates; _i < _a.length; _i++) {
839
818
  var multiPolygon = _a[_i];
@@ -844,7 +823,8 @@
844
823
  var polygonOffsets = [];
845
824
 
846
825
  for (var _c = 0, polygonGroup_1 = polygonGroup; _c < polygonGroup_1.length; _c++) {
847
- var coord = polygonGroup_1[_c];
826
+ var coord = polygonGroup_1[_c]; // GeoJSON은 [lng, lat] 순서이지만 Position은 [lat, lng] 순서
827
+
848
828
  var pos = new Position(coord[1], coord[0]);
849
829
  var offset = controller.positionToOffset(pos);
850
830
  polygonOffsets.push([offset.x, offset.y]);
@@ -859,65 +839,37 @@
859
839
  return result;
860
840
  };
861
841
  /**
862
- * 마커 offset 계산
863
- *
864
- * 마커의 위경도 좌표를 화면 픽셀 좌표로 변환합니다.
842
+ * 마커 좌표 변환 (위경도 → 화면 좌표)
865
843
  *
866
- * @param markerData 마커 데이터 (position 필드 필수)
844
+ * @param markerData 마커 데이터
867
845
  * @param controller MintMapController 인스턴스
868
- * @returns 변환된 화면 좌표 (Offset) 또는 null (변환 실패 시)
869
- *
870
- * @remarks
871
- * - 반환된 좌표는 마커의 중심점 (x, y)
872
- * - 성능: O(1) - 단일 좌표 변환
873
- *
874
- * @example
875
- * ```typescript
876
- * const offset = computeMarkerOffset(markerData, controller);
877
- * if (!offset) return; // 변환 실패
878
- * // offset.x, offset.y는 화면 픽셀 좌표
879
- * ```
846
+ * @returns 변환된 화면 좌표 또는 null
880
847
  */
881
848
 
882
849
  var computeMarkerOffset = function (markerData, controller) {
883
- if (!markerData.position) {
884
- return null;
885
- }
886
-
850
+ if (!markerData.position) return null;
887
851
  return controller.positionToOffset(markerData.position);
888
852
  };
889
853
  /**
890
- * Point-in-Polygon 알고리즘
891
- *
892
- * Ray Casting 알고리즘을 사용하여 점이 폴리곤 내부에 있는지 확인합니다.
854
+ * Point-in-Polygon 알고리즘 (Ray Casting)
893
855
  *
894
856
  * @param point 확인할 점의 좌표
895
- * @param polygon 폴리곤 좌표 배열 (각 요소는 [x, y] 형식)
896
- * @returns 점이 폴리곤 내부에 있으면 true, 아니면 false
897
- *
898
- * @remarks
899
- * - **알고리즘**: Ray Casting (Ray Crossing)
900
- * - **성능**: O(n), n은 폴리곤의 좌표 수
901
- * - **경계 처리**: 경계선 위의 점은 내부로 간주
902
- *
903
- * @example
904
- * ```typescript
905
- * const point = { x: 100, y: 200 };
906
- * const polygon = [[0, 0], [100, 0], [100, 100], [0, 100]];
907
- * const isInside = isPointInPolygon(point, polygon);
908
- * ```
857
+ * @param polygon 폴리곤 좌표 배열
858
+ * @returns 점이 폴리곤 내부에 있으면 true
909
859
  */
910
860
 
911
861
  var isPointInPolygon = function (point, polygon) {
862
+ // Ray Casting 알고리즘: 점에서 오른쪽으로 무한히 뻗은 선과 폴리곤 변의 교차 횟수로 판단
912
863
  var inside = false;
913
864
 
914
865
  for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
915
866
  var xi = polygon[i][0],
916
867
  yi = polygon[i][1];
917
868
  var xj = polygon[j][0],
918
- yj = polygon[j][1];
869
+ yj = polygon[j][1]; // 점의 y 좌표가 변의 양 끝점 사이에 있고, 교차점의 x 좌표가 점의 x 좌표보다 큰지 확인
870
+
919
871
  var intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
920
- if (intersect) inside = !inside;
872
+ if (intersect) inside = !inside; // 교차할 때마다 inside 상태 토글
921
873
  }
922
874
 
923
875
  return inside;
@@ -925,42 +877,20 @@
925
877
  /**
926
878
  * 폴리곤 히트 테스트 (도넛 폴리곤 지원)
927
879
  *
928
- * 점이 폴리곤 내부에 있는지 확인합니다. 도넛 폴리곤(구멍이 있는 폴리곤)을 지원합니다.
929
- *
930
880
  * @param clickedOffset 클릭/마우스 위치 좌표
931
881
  * @param polygonData 폴리곤 데이터
932
882
  * @param getPolygonOffsets 폴리곤 좌표 변환 함수
933
- * @returns 점이 폴리곤 내부에 있으면 true, 아니면 false
934
- *
935
- * @remarks
936
- * - **도넛 폴리곤 처리** (isDonutPolygon === true):
937
- * 1. 외부 폴리곤(첫 번째): 내부에 있어야 함
938
- * 2. 내부 구멍들(나머지): 내부에 있으면 안 됨 (evenodd 규칙)
939
- * - **일반 폴리곤 처리**: Point-in-Polygon 알고리즘 사용
940
- * - **성능**: O(n), n은 폴리곤의 총 좌표 수
941
- *
942
- * **중요**: 도넛 폴리곤과 내부 폴리곤은 별개의 polygonData로 처리됩니다.
943
- * - 도넛 폴리곤 A: isDonutPolygon=true
944
- * - 내부 폴리곤 B: isDonutPolygon=false (별도 데이터)
945
- *
946
- * @example
947
- * ```typescript
948
- * const isHit = isPointInPolygonData(
949
- * clickedOffset,
950
- * polygonData,
951
- * getOrComputePolygonOffsets
952
- * );
953
- * ```
883
+ * @returns 점이 폴리곤 내부에 있으면 true
954
884
  */
955
885
 
956
886
  var isPointInPolygonData = function (clickedOffset, polygonData, getPolygonOffsets) {
957
887
  var polygonOffsets = getPolygonOffsets(polygonData);
958
- if (!polygonOffsets) return false; // 🍩 도넛 폴리곤 처리 (isDonutPolygon === true)
888
+ if (!polygonOffsets) return false; // 도넛 폴리곤 처리: 외부 폴리곤 내부에 있으면서 구멍(hole) 내부에 있지 않아야 함
959
889
 
960
890
  if (polygonData.isDonutPolygon) {
961
891
  for (var _i = 0, polygonOffsets_1 = polygonOffsets; _i < polygonOffsets_1.length; _i++) {
962
892
  var multiPolygon = polygonOffsets_1[_i];
963
- if (multiPolygon.length === 0) continue; // 외부 폴리곤만 있는 경우 (구멍 없음) - 일반 폴리곤처럼 처리
893
+ if (multiPolygon.length === 0) continue; // 구멍이 없는 경우 일반 폴리곤과 동일
964
894
 
965
895
  if (multiPolygon.length === 1) {
966
896
  if (isPointInPolygon(clickedOffset, multiPolygon[0])) {
@@ -968,33 +898,30 @@
968
898
  }
969
899
 
970
900
  continue;
971
- } // 1. 외부 폴리곤(첫 번째)에 포함되는지 확인
901
+ } // 외부 폴리곤 내부에 있는지 확인
972
902
 
973
903
 
974
904
  var outerPolygon = multiPolygon[0];
975
905
 
976
906
  if (!isPointInPolygon(clickedOffset, outerPolygon)) {
977
- continue; // 외부 폴리곤 밖이면 다음 multiPolygon 확인
978
- } // 2. 내부 구멍들(나머지)에 포함되는지 확인
907
+ continue;
908
+ } // 구멍 내부에 있으면 false (도넛의 공간)
979
909
 
980
910
 
981
911
  for (var i = 1; i < multiPolygon.length; i++) {
982
912
  var hole = multiPolygon[i];
983
913
 
984
914
  if (isPointInPolygon(clickedOffset, hole)) {
985
- // ❌ 구멍 안에 있음 → 이 도넛 폴리곤은 히트 안 됨
986
- // 다른 multiPolygon 체크하지 않고 바로 false 반환
987
- // (도넛 폴리곤의 구멍 안은 무조건 클릭 불가)
988
915
  return false;
989
916
  }
990
- } // 외부 폴리곤 + 구멍 = 히트!
917
+ } // 외부 폴리곤 내부에 있으면서 모든 구멍 밖에 있으면 true
991
918
 
992
919
 
993
920
  return true;
994
921
  }
995
922
 
996
923
  return false;
997
- } // 일반 폴리곤 처리 (isDonutPolygon === false 또는 undefined)
924
+ } // 일반 폴리곤 처리
998
925
 
999
926
 
1000
927
  for (var _a = 0, polygonOffsets_2 = polygonOffsets; _a < polygonOffsets_2.length; _a++) {
@@ -1013,31 +940,12 @@
1013
940
  return false;
1014
941
  };
1015
942
  /**
1016
- * 마커 히트 테스트 (클릭/hover 영역 체크)
1017
- *
1018
- * 점이 마커의 클릭/호버 영역 내부에 있는지 확인합니다.
1019
- * 마커의 꼬리(tail)는 Hit Test 영역에서 제외됩니다.
943
+ * 마커 히트 테스트 (꼬리 제외)
1020
944
  *
1021
945
  * @param clickedOffset 클릭/마우스 위치 좌표
1022
946
  * @param markerData 마커 데이터
1023
947
  * @param getMarkerOffset 마커 좌표 변환 함수
1024
- * @returns 점이 마커 영역 내부에 있으면 true, 아니면 false
1025
- *
1026
- * @remarks
1027
- * - **꼬리 제외**: 꼬리(tail)는 Hit Test 영역에서 제외됩니다
1028
- * - markerOffset.y는 마커 최하단(꼬리 끝) 좌표
1029
- * - boxHeight는 마커 본체만 포함 (꼬리 제외)
1030
- * - tailHeight만큼 위로 올려서 본체만 Hit Test 영역으로 사용
1031
- * - **성능**: O(1) - 단순 사각형 영역 체크
1032
- *
1033
- * @example
1034
- * ```typescript
1035
- * const isHit = isPointInMarkerData(
1036
- * clickedOffset,
1037
- * markerData,
1038
- * getOrComputeMarkerOffset
1039
- * );
1040
- * ```
948
+ * @returns 점이 마커 영역 내부에 있으면 true
1041
949
  */
1042
950
 
1043
951
  var isPointInMarkerData = function (clickedOffset, markerData, getMarkerOffset) {
@@ -1045,58 +953,41 @@
1045
953
  if (!markerOffset) return false;
1046
954
  var boxWidth = markerData.boxWidth || 50;
1047
955
  var boxHeight = markerData.boxHeight || 28;
1048
- var tailHeight = markerData.tailHeight || 0; // 🎯 tailHeight 사용!
956
+ var tailHeight = markerData.tailHeight || 0; // 마커 중심점 기준으로 박스 영역 계산 (꼬리는 제외)
1049
957
 
1050
958
  var x = markerOffset.x - boxWidth / 2;
1051
- var y = markerOffset.y - boxHeight - tailHeight; // 🔥 꼬리만큼 위로!
959
+ var y = markerOffset.y - boxHeight - tailHeight; // 클릭 위치가 박스 영역 내부에 있는지 확인
1052
960
 
1053
961
  return clickedOffset.x >= x && clickedOffset.x <= x + boxWidth && clickedOffset.y >= y && clickedOffset.y <= y + boxHeight;
1054
- };
962
+ }; // Hex 색상을 RGBA로 변환
963
+
1055
964
  var hexToRgba = function (hexColor, alpha) {
1056
965
  if (alpha === void 0) {
1057
966
  alpha = 1;
1058
- } // NOTE: 입력된 hexColor에서 "#" 제거
1059
-
967
+ }
1060
968
 
1061
- var hex = hexColor.replace('#', ''); // NOTE: 6자리일 경우 알파 값은 사용자가 제공한 alpha 값으로 설정
969
+ var hex = hexColor.replace('#', '');
1062
970
 
1063
- if (hex.length === 6) {
1064
- var r = parseInt(hex.substring(0, 2), 16);
1065
- var g = parseInt(hex.substring(2, 4), 16);
1066
- var b = parseInt(hex.substring(4, 6), 16);
1067
- return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
971
+ if (hex.length !== 6) {
972
+ throw new Error('Invalid hex color format');
1068
973
  }
1069
974
 
1070
- throw new Error('Invalid hex color format');
975
+ var r = parseInt(hex.substring(0, 2), 16);
976
+ var g = parseInt(hex.substring(2, 4), 16);
977
+ var b = parseInt(hex.substring(4, 6), 16);
978
+ return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
1071
979
  };
1072
980
  var tempCanvas = document.createElement('canvas');
1073
981
  var tempCtx = tempCanvas.getContext('2d');
1074
982
  /**
1075
- * 텍스트 박스의 너비를 계산합니다.
1076
- *
1077
- * Canvas 2D Context의 measureText()를 사용하여 텍스트의 실제 너비를 계산하고,
1078
- * 패딩과 최소 너비를 고려하여 최종 너비를 반환합니다.
983
+ * 텍스트 박스 너비 계산
1079
984
  *
1080
985
  * @param params 파라미터 객체
1081
986
  * @param params.text 측정할 텍스트
1082
- * @param params.fontConfig 폰트 설정 (예: 'bold 16px Arial')
1083
- * @param params.padding 텍스트 박스에 적용할 패딩 값 (px)
987
+ * @param params.fontConfig 폰트 설정
988
+ * @param params.padding 패딩 값 (px)
1084
989
  * @param params.minWidth 최소 너비 (px)
1085
- * @returns 계산된 텍스트 박스의 너비 (px)
1086
- *
1087
- * @remarks
1088
- * - 성능: O(1) - 단일 텍스트 측정
1089
- * - 임시 Canvas를 사용하여 정확한 너비 측정
1090
- *
1091
- * @example
1092
- * ```typescript
1093
- * const width = calculateTextBoxWidth({
1094
- * text: "Hello World",
1095
- * fontConfig: 'bold 16px Arial',
1096
- * padding: 20,
1097
- * minWidth: 60
1098
- * });
1099
- * ```
990
+ * @returns 계산된 텍스트 박스 너비 (px)
1100
991
  */
1101
992
 
1102
993
  var calculateTextBoxWidth = function (_a) {
@@ -1114,62 +1005,25 @@
1114
1005
  /**
1115
1006
  * WoongCanvasProvider 컴포넌트
1116
1007
  *
1117
- * 다중 WoongCanvas 컴포넌트 인스턴스를 관리하는 Context Provider입니다.
1118
- * 여러 WoongCanvasMarker/WoongCanvasPolygon이 함께 사용될 때 전역 이벤트 조정을 수행합니다.
1119
- *
1120
- * @param props 컴포넌트 props
1121
- * @param props.children 자식 컴포넌트
1122
- *
1123
- * @remarks
1124
- * - zIndex 기반으로 이벤트 우선순위 처리
1125
- * - 전역 클릭/호버 이벤트 조정
1126
- * - 여러 레이어 간 상호작용 관리
1127
- *
1128
- * @example
1129
- * ```tsx
1130
- * <WoongCanvasProvider>
1131
- * <WoongCanvasMarker data={markers} zIndex={10} />
1132
- * <WoongCanvasPolygon data={polygons} zIndex={5} />
1133
- * </WoongCanvasProvider>
1134
- * ```
1008
+ * 다중 WoongCanvas 인스턴스를 관리하고 zIndex 기반 이벤트 우선순위를 처리합니다.
1135
1009
  */
1136
1010
 
1137
1011
  var WoongCanvasProvider = function (_a) {
1138
1012
  var children = _a.children;
1139
- var controller = useMintMapController(); // Refs
1140
-
1013
+ var controller = useMintMapController();
1141
1014
  var componentsRef = React.useRef([]);
1142
1015
  var currentHoveredRef = React.useRef(null);
1143
1016
  var currentHoveredDataRef = React.useRef(null);
1144
- var draggingRef = React.useRef(false);
1145
- /**
1146
- * 컴포넌트 등록 (zIndex 내림차순 정렬)
1147
- *
1148
- * 컴포넌트 인스턴스를 등록하고 zIndex 기준으로 내림차순 정렬합니다.
1149
- * 높은 zIndex를 가진 컴포넌트가 이벤트 처리에서 우선순위를 가집니다.
1150
- *
1151
- * @template T 마커/폴리곤 데이터의 추가 속성 타입
1152
- * @param instance 등록할 컴포넌트 인스턴스
1153
- */
1017
+ var draggingRef = React.useRef(false); // 컴포넌트 등록 (zIndex 내림차순 정렬)
1154
1018
 
1155
1019
  var registerComponent = React.useCallback(function (instance) {
1156
1020
  componentsRef.current.push(instance);
1157
1021
  componentsRef.current.sort(function (a, b) {
1158
1022
  return b.zIndex - a.zIndex;
1159
1023
  });
1160
- }, []);
1161
- /**
1162
- * 컴포넌트 등록 해제
1163
- *
1164
- * 컴포넌트 인스턴스를 등록 해제합니다.
1165
- * hover 중이던 컴포넌트면 hover 상태도 초기화합니다.
1166
- *
1167
- * @template T 마커/폴리곤 데이터의 추가 속성 타입
1168
- * @param instance 등록 해제할 컴포넌트 인스턴스
1169
- */
1024
+ }, []); // 컴포넌트 등록 해제
1170
1025
 
1171
1026
  var unregisterComponent = React.useCallback(function (instance) {
1172
- // Hover 중이던 컴포넌트면 초기화
1173
1027
  if (currentHoveredRef.current === instance) {
1174
1028
  currentHoveredRef.current = null;
1175
1029
  currentHoveredDataRef.current = null;
@@ -1178,118 +1032,77 @@
1178
1032
  componentsRef.current = componentsRef.current.filter(function (c) {
1179
1033
  return c !== instance;
1180
1034
  });
1181
- }, []);
1182
- /**
1183
- * 전역 클릭 핸들러 (zIndex 우선순위)
1184
- *
1185
- * 모든 등록된 WoongCanvas 컴포넌트 중 zIndex가 높은 컴포넌트부터 클릭 이벤트를 처리합니다.
1186
- *
1187
- * @param event 클릭 이벤트 파라미터
1188
- *
1189
- * @remarks
1190
- * - zIndex가 높은 컴포넌트부터 순회하여 첫 번째 히트만 처리
1191
- * - 상호작용이 비활성화된 컴포넌트는 스킵
1192
- * - Context가 없으면 각 컴포넌트의 로컬 핸들러가 처리
1193
- */
1035
+ }, []); // 전역 클릭 핸들러 (zIndex 우선순위)
1194
1036
 
1195
1037
  var handleGlobalClick = React.useCallback(function (event) {
1196
- var _a;
1038
+ var _a, _b;
1197
1039
 
1198
1040
  if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
1199
- var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 순서대로 순회 (높은 것부터)
1200
-
1201
- for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
1202
- var component = _b[_i]; // 🚫 상호작용이 비활성화된 컴포넌트는 스킵
1041
+ var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
1203
1042
 
1043
+ for (var _i = 0, _c = componentsRef.current; _i < _c.length; _i++) {
1044
+ var component = _c[_i];
1204
1045
  if (component.isInteractionDisabled()) continue;
1205
1046
  var data = component.findData(clickedOffset);
1047
+ if (!data) continue; // 첫 번째로 찾은 항목만 처리하고 종료 (zIndex 우선순위)
1206
1048
 
1207
- if (data) {
1208
- component.handleLocalClick(data);
1209
-
1210
- if (component.onClick) {
1211
- component.onClick(data, component.getSelectedIds());
1212
- }
1213
-
1214
- return; // 첫 번째 히트만 처리
1215
- }
1049
+ component.handleLocalClick(data);
1050
+ (_b = component.onClick) === null || _b === void 0 ? void 0 : _b.call(component, data, component.getSelectedIds());
1051
+ return;
1216
1052
  }
1217
- }, [controller]);
1218
- /**
1219
- * 전역 마우스 이동 핸들러 (zIndex 우선순위)
1220
- *
1221
- * 모든 등록된 WoongCanvas 컴포넌트 중 zIndex가 높은 컴포넌트부터 hover 이벤트를 처리합니다.
1222
- *
1223
- * @param event 마우스 이동 이벤트 파라미터
1224
- *
1225
- * @remarks
1226
- * - zIndex가 높은 컴포넌트부터 순회하여 첫 번째 히트만 처리
1227
- * - 상호작용이 비활성화된 컴포넌트는 스킵
1228
- * - 드래그 중이면 이벤트 무시 (성능 최적화)
1229
- * - hover 상태 변경 감지 후 이전 hover 해제 및 새 hover 설정
1230
- */
1053
+ }, [controller]); // 전역 마우스 이동 핸들러 (zIndex 우선순위)
1231
1054
 
1232
1055
  var handleGlobalMouseMove = React.useCallback(function (event) {
1233
1056
  var _a;
1234
1057
 
1235
1058
  if (draggingRef.current || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
1236
- var mouseOffset = controller.positionToOffset(event.param.position); // zIndex 순서대로 순회하여 Hover 대상 찾기
1237
-
1059
+ var mouseOffset = controller.positionToOffset(event.param.position);
1238
1060
  var newHoveredComponent = null;
1239
- var newHoveredData = null;
1061
+ var newHoveredData = null; // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
1240
1062
 
1241
1063
  for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
1242
- var component = _b[_i]; // 🚫 상호작용이 비활성화된 컴포넌트는 스킵
1243
-
1064
+ var component = _b[_i];
1244
1065
  if (component.isInteractionDisabled()) continue;
1245
1066
  var data = component.findData(mouseOffset);
1067
+ if (!data) continue; // 첫 번째로 찾은 항목만 hover 처리 (zIndex 우선순위)
1246
1068
 
1247
- if (data) {
1248
- newHoveredComponent = component;
1249
- newHoveredData = data;
1250
- break; // 번째 히트만 처리
1251
- }
1252
- } // Hover 상태 변경 감지 (최적화: 별도 ref로 직접 비교)
1253
-
1069
+ newHoveredComponent = component;
1070
+ newHoveredData = data;
1071
+ break;
1072
+ } // hover 상태가 변경되지 않았으면 종료 (불필요한 렌더링 방지)
1254
1073
 
1255
- if (currentHoveredRef.current !== newHoveredComponent || currentHoveredDataRef.current !== newHoveredData) {
1256
- // 이전 hover 해제
1257
- if (currentHoveredRef.current) {
1258
- currentHoveredRef.current.setHovered(null);
1259
1074
 
1260
- if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
1261
- currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
1262
- }
1263
- } // 새 hover 설정
1075
+ if (currentHoveredRef.current === newHoveredComponent && currentHoveredDataRef.current === newHoveredData) {
1076
+ return;
1077
+ } // 기존 hover 항목에 mouseOut 이벤트 발생
1264
1078
 
1265
1079
 
1266
- if (newHoveredComponent && newHoveredData) {
1267
- newHoveredComponent.setHovered(newHoveredData);
1080
+ if (currentHoveredRef.current) {
1081
+ currentHoveredRef.current.setHovered(null);
1268
1082
 
1269
- if (newHoveredComponent.onMouseOver) {
1270
- newHoveredComponent.onMouseOver(newHoveredData);
1271
- }
1083
+ if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
1084
+ currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
1272
1085
  }
1086
+ } // 새 hover 항목에 mouseOver 이벤트 발생
1087
+
1273
1088
 
1274
- currentHoveredRef.current = newHoveredComponent;
1275
- currentHoveredDataRef.current = newHoveredData;
1089
+ if (newHoveredComponent && newHoveredData) {
1090
+ newHoveredComponent.setHovered(newHoveredData);
1091
+
1092
+ if (newHoveredComponent.onMouseOver) {
1093
+ newHoveredComponent.onMouseOver(newHoveredData);
1094
+ }
1276
1095
  }
1277
- }, [controller]);
1278
- /**
1279
- * 줌/드래그 시작 (마우스 이동 이벤트 무시)
1280
- */
1281
1096
 
1097
+ currentHoveredRef.current = newHoveredComponent;
1098
+ currentHoveredDataRef.current = newHoveredData;
1099
+ }, [controller]);
1282
1100
  var handleZoomStart = React.useCallback(function () {
1283
1101
  draggingRef.current = true;
1284
1102
  }, []);
1285
- /**
1286
- * 지도 idle (마우스 이동 이벤트 재개)
1287
- */
1288
-
1289
1103
  var handleIdle = React.useCallback(function () {
1290
1104
  draggingRef.current = false;
1291
- }, []); // 이벤트 리스너 등록
1292
-
1105
+ }, []);
1293
1106
  React.useEffect(function () {
1294
1107
  controller.addEventListener('CLICK', handleGlobalClick);
1295
1108
  controller.addEventListener('MOUSEMOVE', handleGlobalMouseMove);
@@ -1301,8 +1114,7 @@
1301
1114
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
1302
1115
  controller.removeEventListener('IDLE', handleIdle);
1303
1116
  };
1304
- }, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]); // Context value 메모이제이션
1305
-
1117
+ }, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]);
1306
1118
  var contextValue = React.useMemo(function () {
1307
1119
  return {
1308
1120
  registerComponent: registerComponent,
@@ -1316,102 +1128,40 @@
1316
1128
  /**
1317
1129
  * WoongCanvas Context Hook
1318
1130
  *
1319
- * WoongCanvasProvider로 감싸진 컴포넌트에서 Context에 접근할 수 있는 hook입니다.
1320
- *
1321
1131
  * @returns WoongCanvasContextValue 또는 null (Provider 없으면)
1322
- *
1323
- * @remarks
1324
- * - Provider로 감싸지지 않으면 null 반환 (각 컴포넌트가 로컬 이벤트 처리)
1325
- * - Provider로 감싸져 있으면 전역 이벤트 조정 활성화
1326
- *
1327
- * @example
1328
- * ```typescript
1329
- * const context = useWoongCanvasContext();
1330
- * if (context) {
1331
- * // Context 사용 중 (전역 이벤트 조정)
1332
- * }
1333
- * ```
1334
1132
  */
1335
1133
 
1336
1134
  var useWoongCanvasContext = function () {
1337
- var context = React.useContext(WoongCanvasContext);
1338
- return context;
1135
+ return React.useContext(WoongCanvasContext);
1339
1136
  };
1340
1137
 
1341
- // ============================================================================
1342
- // 성능 최적화 상수 (30,000개 마커/폴리곤 기준 최적화)
1343
- // ============================================================================
1344
-
1345
1138
  /**
1346
- * 공간 인덱스 그리드 셀 크기 (px)
1347
- *
1348
- * 최적값 계산:
1349
- * - 목표: 클릭 시 셀당 10~30개 항목만 체크 (빠른 Hit Test)
1350
- * - 화면 크기: 1920×1080 기준
1351
- * - 30,000개 항목 → 50px 셀 크기 = 약 800개 셀 = 셀당 ~37개
1139
+ * 공간 인덱스 그리드 셀 크기 (픽셀 단위)
1352
1140
  *
1353
- * 성능 비교 (30,000개 기준):
1354
- * - 200px: 셀당 ~577개 → Hit Test O(577) ❌ 느림
1355
- * - 50px: 셀당 ~37개 → Hit Test O(37) ✅ 15배 빠름!
1356
- *
1357
- * 트레이드오프:
1358
- * - 작을수록: Hit Test 빠름, 메모리 사용량 증가
1359
- * - 클수록: 메모리 효율적, Hit Test 느림
1141
+ * @default 50
1360
1142
  */
1361
1143
  var SPATIAL_GRID_CELL_SIZE = 50;
1362
1144
  /**
1363
- * 뷰포트 컬링 여유 공간 (px)
1145
+ * 뷰포트 컬링 여유 공간 (픽셀 단위)
1364
1146
  *
1365
- * 화면 밖 100px까지 렌더링하여 스크롤 시 부드러운 전환
1366
- * 30,000개 중 실제 렌더링: 화면에 보이는 1,000~3,000개만
1147
+ * @default 100
1367
1148
  */
1368
1149
 
1369
1150
  var DEFAULT_CULLING_MARGIN = 100;
1370
1151
  /**
1371
1152
  * LRU 캐시 최대 항목 수
1372
1153
  *
1373
- * 좌표 변환 결과 캐싱 (positionToOffset 연산 비용 절약)
1374
- *
1375
- * 최적값 계산:
1376
- * - 전체 항목: 30,000개
1377
- * - 캐시 크기: 30,000개 → 100% 히트율 (메모리: ~2.4MB)
1378
- *
1379
- * 메모리 사용량 (항목당 ~80 bytes):
1380
- * - 10,000개: ~800KB → 캐시 히트율 33% ❌
1381
- * - 30,000개: ~2.4MB → 캐시 히트율 100% ✅
1382
- *
1383
- * zoom/pan 시 어차피 clear() 호출되므로 메모리 누적 없음
1154
+ * @default 30000
1384
1155
  */
1385
1156
 
1386
1157
  var DEFAULT_MAX_CACHE_SIZE = 30000;
1387
1158
  /**
1388
- * LRU (Least Recently Used) Cache
1159
+ * LRU Cache (Least Recently Used)
1389
1160
  *
1390
- * 메모리 제한을 위한 캐시 구현입니다. WoongCanvas 컴포넌트에서 좌표 변환 결과를 캐싱하는데 사용됩니다.
1161
+ * 좌표 변환 결과를 캐싱하기 위한 캐시 구현
1391
1162
  *
1392
1163
  * @template K 캐시 키 타입
1393
1164
  * @template V 캐시 값 타입
1394
- *
1395
- * @remarks
1396
- * **개선 사항**:
1397
- * 1. get() 성능 향상: 접근 빈도 추적 없이 단순 조회만 수행 (delete+set 제거)
1398
- * 2. set() 버그 수정: 기존 키 업데이트 시 maxSize 체크 로직 개선
1399
- * 3. 메모리 효율: 단순 FIFO 캐시로 동작하여 오버헤드 최소화
1400
- *
1401
- * **트레이드오프**:
1402
- * - 장점: 읽기 성능 대폭 향상 (10,000번 get → 이전보다 2배 빠름)
1403
- * - 단점: 접근 빈도가 아닌 삽입 순서 기반 eviction (FIFO)
1404
- *
1405
- * WoongCanvasMarker 사용 사례에 최적:
1406
- * - 좌표 변환 결과는 zoom/pan 시 어차피 전체 초기화
1407
- * - 접근 빈도 추적보다 빠른 조회가 더 중요
1408
- *
1409
- * @example
1410
- * ```typescript
1411
- * const cache = new LRUCache<string, Offset>(30000);
1412
- * cache.set(item.id, offset);
1413
- * const cached = cache.get(item.id);
1414
- * ```
1415
1165
  */
1416
1166
 
1417
1167
  var LRUCache =
@@ -1424,63 +1174,43 @@
1424
1174
 
1425
1175
  this.cache = new Map();
1426
1176
  this.maxSize = maxSize;
1427
- }
1428
- /**
1429
- * 캐시에서 값 조회
1430
- *
1431
- * @param key 조회할 키
1432
- * @returns 캐시된 값 또는 undefined (캐시 미스 시)
1433
- *
1434
- * @remarks
1435
- * - 성능: O(1) 해시 조회
1436
- * - 최적화: delete+set 제거로 읽기 성능 대폭 향상
1437
- */
1177
+ } // 캐시에서 값 조회
1438
1178
 
1439
1179
 
1440
1180
  LRUCache.prototype.get = function (key) {
1441
1181
  return this.cache.get(key);
1442
- };
1443
- /**
1444
- * 캐시에 값 저장
1445
- *
1446
- * @param key 저장할 키
1447
- * @param value 저장할 값
1448
- *
1449
- * @remarks
1450
- * - 기존 키 업데이트: 단순 덮어쓰기 (크기 변화 없음)
1451
- * - 신규 키 추가: 크기 체크 후 필요시 가장 오래된 항목 제거 (FIFO)
1452
- * - 성능: O(1) 평균 시간복잡도
1453
- */
1182
+ }; // 캐시에 값 저장 (FIFO eviction)
1454
1183
 
1455
1184
 
1456
1185
  LRUCache.prototype.set = function (key, value) {
1457
1186
  var exists = this.cache.has(key);
1458
1187
 
1459
1188
  if (exists) {
1460
- // 기존 항목 업데이트: 단순 덮어쓰기 (크기 변화 없음)
1461
1189
  this.cache.set(key, value);
1462
- } else {
1463
- // 신규 항목 추가: 크기 체크 필요
1464
- if (this.cache.size >= this.maxSize) {
1465
- // 가장 오래된 항목 제거 (Map의 첫 번째 항목)
1466
- var firstKey = this.cache.keys().next().value;
1190
+ return;
1191
+ }
1467
1192
 
1468
- if (firstKey !== undefined) {
1469
- this.cache.delete(firstKey);
1470
- }
1471
- }
1193
+ if (this.cache.size >= this.maxSize) {
1194
+ var firstKey = this.cache.keys().next().value;
1472
1195
 
1473
- this.cache.set(key, value);
1196
+ if (firstKey !== undefined) {
1197
+ this.cache.delete(firstKey);
1198
+ }
1474
1199
  }
1475
- };
1200
+
1201
+ this.cache.set(key, value);
1202
+ }; // 캐시 초기화
1203
+
1476
1204
 
1477
1205
  LRUCache.prototype.clear = function () {
1478
1206
  this.cache.clear();
1479
- };
1207
+ }; // 캐시 크기 반환
1208
+
1480
1209
 
1481
1210
  LRUCache.prototype.size = function () {
1482
1211
  return this.cache.size;
1483
- };
1212
+ }; // 키 존재 여부 확인
1213
+
1484
1214
 
1485
1215
  LRUCache.prototype.has = function (key) {
1486
1216
  return this.cache.has(key);
@@ -1490,16 +1220,10 @@
1490
1220
  }();
1491
1221
  /**
1492
1222
  * Spatial Hash Grid (공간 해시 그리드)
1493
- * 공간 인덱싱을 위한 그리드 기반 자료구조 (개선 버전)
1494
1223
  *
1495
- * 개선 사항:
1496
- * 1. 중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전
1497
- * 2. 메모리 누수 방지: 기존 항목 자동 제거
1498
- * 3. 성능 최적화: 불필요한 배열 생성 최소화
1224
+ * 빠른 Hit Test를 위한 그리드 기반 공간 인덱싱 자료구조
1499
1225
  *
1500
- * 사용 사례:
1501
- * - 빠른 Hit Test (마우스 클릭 시 어떤 마커/폴리곤인지 찾기)
1502
- * - 30,000개 항목 → 클릭 위치 주변 ~10개만 체크 (3,000배 빠름)
1226
+ * @template T 인덱싱할 항목 타입
1503
1227
  */
1504
1228
 
1505
1229
  var SpatialHashGrid =
@@ -1513,28 +1237,24 @@
1513
1237
  this.cellSize = cellSize;
1514
1238
  this.grid = new Map();
1515
1239
  this.itemToCells = new Map();
1516
- }
1517
- /**
1518
- * 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
1519
- */
1240
+ } // 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
1520
1241
 
1521
1242
 
1522
1243
  SpatialHashGrid.prototype.getCellKey = function (x, y) {
1244
+ // 좌표를 셀 크기로 나눈 몫으로 셀 인덱스 계산
1523
1245
  var cellX = Math.floor(x / this.cellSize);
1524
1246
  var cellY = Math.floor(y / this.cellSize);
1525
1247
  return "".concat(cellX, ",").concat(cellY);
1526
- };
1527
- /**
1528
- * 바운딩 박스가 걸치는 모든 셀 키 배열 반환
1529
- */
1248
+ }; // 바운딩 박스가 걸치는 모든 셀 키 배열 반환
1530
1249
 
1531
1250
 
1532
1251
  SpatialHashGrid.prototype.getCellsForBounds = function (minX, minY, maxX, maxY) {
1533
- var cells = [];
1252
+ var cells = []; // 바운딩 박스가 걸치는 셀 범위 계산
1253
+
1534
1254
  var startCellX = Math.floor(minX / this.cellSize);
1535
1255
  var startCellY = Math.floor(minY / this.cellSize);
1536
1256
  var endCellX = Math.floor(maxX / this.cellSize);
1537
- var endCellY = Math.floor(maxY / this.cellSize);
1257
+ var endCellY = Math.floor(maxY / this.cellSize); // 바운딩 박스가 걸치는 모든 셀을 배열에 추가
1538
1258
 
1539
1259
  for (var x = startCellX; x <= endCellX; x++) {
1540
1260
  for (var y = startCellY; y <= endCellY; y++) {
@@ -1543,31 +1263,15 @@
1543
1263
  }
1544
1264
 
1545
1265
  return cells;
1546
- };
1547
- /**
1548
- * 항목 추가 (바운딩 박스 기반)
1549
- *
1550
- * 항목을 공간 인덱스에 추가합니다. 바운딩 박스가 걸치는 모든 셀에 삽입됩니다.
1551
- *
1552
- * @param item 추가할 항목
1553
- * @param minX 바운딩 박스 최소 X 좌표
1554
- * @param minY 바운딩 박스 최소 Y 좌표
1555
- * @param maxX 바운딩 박스 최대 X 좌표
1556
- * @param maxY 바운딩 박스 최대 Y 좌표
1557
- *
1558
- * @remarks
1559
- * - 중복 삽입 방지: 기존 항목이 있으면 먼저 제거 후 재삽입
1560
- * - 메모리 누수 방지: 이전 셀 참조 완전 제거
1561
- * - 성능: O(1) 평균 시간복잡도
1562
- */
1266
+ }; // 항목 추가 (바운딩 박스 기반, 중복 삽입 방지)
1563
1267
 
1564
1268
 
1565
1269
  SpatialHashGrid.prototype.insert = function (item, minX, minY, maxX, maxY) {
1566
- // 1. 기존 항목 제거 (중복 방지)
1567
- this.remove(item); // 2. 위치에 삽입
1270
+ // 기존 항목 제거 (중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전)
1271
+ this.remove(item); // 바운딩 박스가 걸치는 모든 셀에 항목 등록
1568
1272
 
1569
1273
  var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
1570
- this.itemToCells.set(item, cells);
1274
+ this.itemToCells.set(item, cells); // 항목과 셀의 매핑 저장 (제거 시 필요)
1571
1275
 
1572
1276
  for (var _i = 0, cells_1 = cells; _i < cells_1.length; _i++) {
1573
1277
  var cell = cells_1[_i];
@@ -1578,24 +1282,12 @@
1578
1282
 
1579
1283
  this.grid.get(cell).push(item);
1580
1284
  }
1581
- };
1582
- /**
1583
- * 항목 제거
1584
- *
1585
- * 공간 인덱스에서 항목을 제거합니다.
1586
- *
1587
- * @param item 제거할 항목
1588
- *
1589
- * @remarks
1590
- * - 메모리 누수 방지: 모든 셀에서 참조 완전 제거
1591
- * - 빈 셀 정리: 항목이 없어진 셀은 자동으로 정리됨
1592
- * - 성능: O(셀 개수), 보통 O(1)
1593
- */
1285
+ }; // 항목 제거 (모든 셀에서 참조 제거)
1594
1286
 
1595
1287
 
1596
1288
  SpatialHashGrid.prototype.remove = function (item) {
1597
1289
  var prevCells = this.itemToCells.get(item);
1598
- if (!prevCells) return; // 셀에서 항목 제거
1290
+ if (!prevCells) return; // 항목이 등록된 모든 셀에서 참조 제거 (메모리 누수 방지)
1599
1291
 
1600
1292
  for (var _i = 0, prevCells_1 = prevCells; _i < prevCells_1.length; _i++) {
1601
1293
  var cell = prevCells_1[_i];
@@ -1606,86 +1298,39 @@
1606
1298
 
1607
1299
  if (index !== -1) {
1608
1300
  cellItems.splice(index, 1);
1609
- } // 빈 셀 정리 (메모리 효율)
1301
+ } // 빈 셀 정리 (메모리 효율: 사용하지 않는 셀 제거)
1610
1302
 
1611
1303
 
1612
1304
  if (cellItems.length === 0) {
1613
1305
  this.grid.delete(cell);
1614
1306
  }
1615
1307
  }
1616
- }
1308
+ } // 항목과 셀의 매핑 제거
1309
+
1617
1310
 
1618
1311
  this.itemToCells.delete(item);
1619
- };
1620
- /**
1621
- * 항목 위치 업데이트
1622
- *
1623
- * 항목의 위치를 업데이트합니다. remove + insert의 편의 함수입니다.
1624
- *
1625
- * @param item 업데이트할 항목
1626
- * @param minX 새로운 바운딩 박스 최소 X 좌표
1627
- * @param minY 새로운 바운딩 박스 최소 Y 좌표
1628
- * @param maxX 새로운 바운딩 박스 최대 X 좌표
1629
- * @param maxY 새로운 바운딩 박스 최대 Y 좌표
1630
- */
1312
+ }; // 항목 위치 업데이트 (remove + insert)
1631
1313
 
1632
1314
 
1633
1315
  SpatialHashGrid.prototype.update = function (item, minX, minY, maxX, maxY) {
1634
1316
  this.insert(item, minX, minY, maxX, maxY);
1635
- };
1636
- /**
1637
- * 점 주변의 항목 조회 (1개 셀만)
1638
- *
1639
- * 특정 좌표가 속한 셀의 모든 항목을 반환합니다.
1640
- *
1641
- * @param x 조회할 X 좌표
1642
- * @param y 조회할 Y 좌표
1643
- * @returns 해당 셀의 항목 배열 (없으면 빈 배열)
1644
- *
1645
- * @remarks
1646
- * - 성능: O(해당 셀의 항목 수) - 보통 ~10개 (30,000개 전체를 체크하지 않음)
1647
- * - Hit Test에 최적화된 메서드
1648
- * - 빈 배열 재사용으로 메모리 할당 최소화
1649
- *
1650
- * @example
1651
- * ```typescript
1652
- * const candidates = grid.queryPoint(mouseX, mouseY);
1653
- * for (const item of candidates) {
1654
- * if (isPointInItem(item, mouseX, mouseY)) {
1655
- * return item;
1656
- * }
1657
- * }
1658
- * ```
1659
- */
1317
+ }; // 점 주변의 항목 조회 (Hit Test용)
1660
1318
 
1661
1319
 
1662
1320
  SpatialHashGrid.prototype.queryPoint = function (x, y) {
1321
+ // 클릭 위치가 속한 셀의 모든 항목 조회 (O(1) 수준의 빠른 조회)
1663
1322
  var cellKey = this.getCellKey(x, y);
1664
1323
  var items = this.grid.get(cellKey); // 빈 배열 재사용 (메모리 할당 최소화)
1665
1324
 
1666
1325
  return items || [];
1667
- };
1668
- /**
1669
- * 영역 내 항목 조회
1670
- *
1671
- * 특정 영역(바운딩 박스)과 교차하는 모든 항목을 반환합니다.
1672
- *
1673
- * @param minX 영역 최소 X 좌표
1674
- * @param minY 영역 최소 Y 좌표
1675
- * @param maxX 영역 최대 X 좌표
1676
- * @param maxY 영역 최대 Y 좌표
1677
- * @returns 영역과 교차하는 항목 배열 (중복 제거됨)
1678
- *
1679
- * @remarks
1680
- * - 성능: O(셀 개수 × 셀당 평균 항목 수)
1681
- * - Set으로 중복 제거 보장 (항목이 여러 셀에 걸쳐 있어도 한 번만 반환)
1682
- * - Viewport Culling에 유용
1683
- */
1326
+ }; // 영역 내 항목 조회 (Viewport Culling용)
1684
1327
 
1685
1328
 
1686
1329
  SpatialHashGrid.prototype.queryBounds = function (minX, minY, maxX, maxY) {
1330
+ // 영역이 걸치는 모든 셀 찾기
1687
1331
  var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
1688
- var results = new Set();
1332
+ var results = new Set(); // 중복 제거를 위해 Set 사용
1333
+ // 각 셀의 모든 항목을 결과에 추가
1689
1334
 
1690
1335
  for (var _i = 0, cells_2 = cells; _i < cells_2.length; _i++) {
1691
1336
  var cell = cells_2[_i];
@@ -1694,48 +1339,24 @@
1694
1339
  if (items) {
1695
1340
  for (var _a = 0, items_1 = items; _a < items_1.length; _a++) {
1696
1341
  var item = items_1[_a];
1697
- results.add(item);
1342
+ results.add(item); // Set이므로 중복 자동 제거
1698
1343
  }
1699
1344
  }
1700
1345
  }
1701
1346
 
1702
1347
  return Array.from(results);
1703
- };
1704
- /**
1705
- * 항목 존재 여부 확인
1706
- *
1707
- * @param item 확인할 항목
1708
- * @returns 항목이 인덱스에 있으면 true, 아니면 false
1709
- *
1710
- * @remarks
1711
- * - 성능: O(1) 해시 조회
1712
- */
1348
+ }; // 항목 존재 여부 확인
1713
1349
 
1714
1350
 
1715
1351
  SpatialHashGrid.prototype.has = function (item) {
1716
1352
  return this.itemToCells.has(item);
1717
- };
1718
- /**
1719
- * 전체 초기화
1720
- */
1353
+ }; // 전체 초기화
1721
1354
 
1722
1355
 
1723
1356
  SpatialHashGrid.prototype.clear = function () {
1724
1357
  this.grid.clear();
1725
1358
  this.itemToCells.clear();
1726
- };
1727
- /**
1728
- * 통계 정보
1729
- *
1730
- * 공간 인덱스의 현재 상태를 반환합니다. 디버깅 및 성능 분석에 유용합니다.
1731
- *
1732
- * @returns 통계 정보 객체
1733
- *
1734
- * @remarks
1735
- * - totalCells: 현재 사용 중인 셀 개수
1736
- * - totalItems: 인덱스에 등록된 고유 항목 수 (정확)
1737
- * - avgItemsPerCell: 셀당 평균 항목 수
1738
- */
1359
+ }; // 통계 정보 반환
1739
1360
 
1740
1361
 
1741
1362
  SpatialHashGrid.prototype.stats = function () {
@@ -1756,20 +1377,9 @@
1756
1377
  /**
1757
1378
  * 현재 뷰포트 영역 계산
1758
1379
  *
1759
- * Konva Stage 크기와 컬링 마진을 기반으로 뷰포트 경계를 계산합니다.
1760
- *
1761
- * @param stage Konva Stage 인스턴스 (width, height 메서드 제공)
1380
+ * @param stage Konva Stage 인스턴스
1762
1381
  * @param cullingMargin 컬링 여유 공간 (px)
1763
1382
  * @param viewportRef 뷰포트 경계를 저장할 ref
1764
- *
1765
- * @remarks
1766
- * - 화면 밖 cullingMargin만큼의 영역까지 포함하여 계산
1767
- * - 스크롤 시 부드러운 전환을 위해 여유 공간 포함
1768
- *
1769
- * @example
1770
- * ```typescript
1771
- * updateViewport(stageRef.current, cullingMargin, viewportRef);
1772
- * ```
1773
1383
  */
1774
1384
  var updateViewport = function (stage, cullingMargin, viewportRef) {
1775
1385
  if (!stage) return;
@@ -1783,35 +1393,16 @@
1783
1393
  /**
1784
1394
  * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
1785
1395
  *
1786
- * 뷰포트 컬링을 위한 함수입니다. 바운딩 박스와 뷰포트 경계의 교차를 확인합니다.
1787
- * 바운딩 박스는 캐시되어 성능을 최적화합니다.
1788
- *
1789
1396
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
1790
1397
  * @param item 확인할 아이템
1791
- * @param enableViewportCulling 뷰포트 컬링 활성화 여부
1792
1398
  * @param viewportRef 뷰포트 경계 ref
1793
1399
  * @param boundingBoxCacheRef 바운딩 박스 캐시 ref
1794
1400
  * @param computeBoundingBox 바운딩 박스 계산 함수
1795
- * @returns 뷰포트 안에 있으면 true, 아니면 false
1796
- *
1797
- * @remarks
1798
- * - 성능: O(1) (캐시 히트 시) 또는 O(바운딩 박스 계산 비용) (캐시 미스 시)
1799
- * - 바운딩 박스는 자동으로 캐시되어 재사용됨
1800
- *
1801
- * @example
1802
- * ```typescript
1803
- * const isVisible = isInViewport(
1804
- * item,
1805
- * enableViewportCulling,
1806
- * viewportRef,
1807
- * boundingBoxCacheRef,
1808
- * computeBoundingBox
1809
- * );
1810
- * ```
1401
+ * @returns 뷰포트 안에 있으면 true
1811
1402
  */
1812
1403
 
1813
- var isInViewport = function (item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
1814
- if (!enableViewportCulling || !viewportRef.current) return true;
1404
+ var isInViewport = function (item, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
1405
+ if (!viewportRef.current) return true;
1815
1406
  var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
1816
1407
 
1817
1408
  var bbox = boundingBoxCacheRef.current.get(item.id);
@@ -1831,33 +1422,9 @@
1831
1422
  /**
1832
1423
  * 지도 이벤트 핸들러 생성 함수
1833
1424
  *
1834
- * 지도 이동, 줌, 드래그 등의 이벤트를 처리하는 핸들러들을 생성합니다.
1835
- *
1836
1425
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
1837
1426
  * @param deps 이벤트 핸들러 생성에 필요한 의존성
1838
1427
  * @returns 지도 이벤트 핸들러 객체
1839
- *
1840
- * @example
1841
- * ```typescript
1842
- * const {
1843
- * handleIdle,
1844
- * handleZoomStart,
1845
- * handleZoomEnd,
1846
- * handleCenterChanged,
1847
- * handleDragStart,
1848
- * handleDragEnd,
1849
- * } = createMapEventHandlers({
1850
- * controller,
1851
- * containerRef,
1852
- * markerRef,
1853
- * options,
1854
- * prevCenterOffsetRef,
1855
- * accumTranslateRef,
1856
- * offsetCacheRef,
1857
- * boundingBoxCacheRef,
1858
- * renderAllImmediate,
1859
- * });
1860
- * ```
1861
1428
  */
1862
1429
 
1863
1430
  var createMapEventHandlers = function (deps) {
@@ -1869,14 +1436,7 @@
1869
1436
  accumTranslateRef = deps.accumTranslateRef,
1870
1437
  offsetCacheRef = deps.offsetCacheRef,
1871
1438
  boundingBoxCacheRef = deps.boundingBoxCacheRef,
1872
- renderAllImmediate = deps.renderAllImmediate;
1873
- /**
1874
- * 지도 이동/줌 완료 시 처리
1875
- *
1876
- * - 캐시 초기화: 좌표 변환 결과가 변경되었으므로 캐시 무효화
1877
- * - 마커 위치 업데이트: 새로운 지도 위치에 맞게 마커 재배치
1878
- * - 렌더링: 새 위치에서 전체 렌더링 수행
1879
- */
1439
+ renderAllImmediate = deps.renderAllImmediate; // 지도 이동/줌 완료 시 처리 (캐시 초기화 및 렌더링)
1880
1440
 
1881
1441
  var handleIdle = function () {
1882
1442
  prevCenterOffsetRef.current = null;
@@ -1894,45 +1454,34 @@
1894
1454
  position: bounds.nw
1895
1455
  }, options);
1896
1456
 
1897
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
1457
+ markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (transform 제거 시 잠깐 빈 화면이 보이는 것 방지)
1898
1458
 
1899
1459
  if (containerRef.current) {
1900
1460
  containerRef.current.style.transform = '';
1901
1461
  containerRef.current.style.visibility = '';
1902
- } // 새 위치에서 렌더링
1462
+ } // 새 위치에서 렌더링 (캐시는 이미 초기화됨)
1903
1463
 
1904
1464
 
1905
1465
  renderAllImmediate();
1906
- };
1907
- /**
1908
- * 줌 시작 시 처리 (일시적으로 숨김)
1909
- */
1466
+ }; // 줌 시작 시 처리 (일시적으로 숨김)
1910
1467
 
1911
1468
 
1912
1469
  var handleZoomStart = function () {
1913
- if (containerRef.current) {
1914
- containerRef.current.style.visibility = 'hidden';
1915
- }
1916
- };
1917
- /**
1918
- * 줌 종료 시 처리 (다시 표시)
1919
- */
1470
+ if (!containerRef.current) return;
1471
+ containerRef.current.style.visibility = 'hidden';
1472
+ }; // 줌 종료 시 처리 (다시 표시)
1920
1473
 
1921
1474
 
1922
1475
  var handleZoomEnd = function () {
1923
- if (containerRef.current) {
1924
- containerRef.current.style.visibility = '';
1925
- }
1926
- };
1927
- /**
1928
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
1929
- */
1476
+ if (!containerRef.current) return;
1477
+ containerRef.current.style.visibility = '';
1478
+ }; // 지도 중심 변경 시 처리 (transform으로 이동 추적, 캐시 유지)
1930
1479
 
1931
1480
 
1932
1481
  var handleCenterChanged = function () {
1933
1482
  var center = controller.getCurrBounds().getCenter();
1934
1483
  var curr = controller.positionToOffset(center);
1935
- var prev = prevCenterOffsetRef.current;
1484
+ var prev = prevCenterOffsetRef.current; // 첫 번째 호출 시 이전 위치 저장만 하고 종료
1936
1485
 
1937
1486
  if (!prev) {
1938
1487
  prevCenterOffsetRef.current = {
@@ -1940,10 +1489,12 @@
1940
1489
  y: curr.y
1941
1490
  };
1942
1491
  return;
1943
- }
1492
+ } // 이전 위치와 현재 위치의 차이 계산 (이동 거리)
1493
+
1944
1494
 
1945
1495
  var dx = prev.x - curr.x;
1946
- var dy = prev.y - curr.y;
1496
+ var dy = prev.y - curr.y; // 누적 이동 거리 저장 (transform으로 화면만 이동, 캐시는 유지하여 성능 최적화)
1497
+
1947
1498
  accumTranslateRef.current = {
1948
1499
  x: accumTranslateRef.current.x + dx,
1949
1500
  y: accumTranslateRef.current.y + dy
@@ -1951,23 +1502,15 @@
1951
1502
  prevCenterOffsetRef.current = {
1952
1503
  x: curr.x,
1953
1504
  y: curr.y
1954
- };
1505
+ }; // CSS transform으로 컨테이너 이동 (캐시된 좌표는 그대로 유지)
1955
1506
 
1956
1507
  if (containerRef.current) {
1957
1508
  containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
1958
1509
  }
1959
1510
  };
1960
- /**
1961
- * 드래그 시작 처리
1962
- */
1963
-
1964
1511
 
1965
1512
  var handleDragStart = function () {// 커서는 각 컴포넌트에서 처리
1966
1513
  };
1967
- /**
1968
- * 드래그 종료 처리
1969
- */
1970
-
1971
1514
 
1972
1515
  var handleDragEnd = function () {// 커서는 각 컴포넌트에서 처리
1973
1516
  };
@@ -1984,26 +1527,10 @@
1984
1527
  /**
1985
1528
  * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
1986
1529
  *
1987
- * Spatial Hash Grid에 모든 데이터의 바운딩 박스를 삽입합니다.
1988
- * 이를 통해 클릭/호버 시 O(1) 수준의 빠른 Hit Test가 가능합니다.
1989
- *
1990
1530
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
1991
1531
  * @param data 공간 인덱스에 삽입할 데이터 배열
1992
1532
  * @param spatialIndex Spatial Hash Grid 인스턴스
1993
1533
  * @param computeBoundingBox 바운딩 박스 계산 함수
1994
- *
1995
- * @remarks
1996
- * - 성능: O(n) 시간복잡도, n은 데이터 개수
1997
- * - 호출 시점: 데이터 변경 시 또는 지도 이동/줌 완료 시
1998
- *
1999
- * @example
2000
- * ```typescript
2001
- * buildSpatialIndex(
2002
- * dataRef.current,
2003
- * spatialIndexRef.current,
2004
- * computeBoundingBox
2005
- * );
2006
- * ```
2007
1534
  */
2008
1535
 
2009
1536
  var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
@@ -2019,29 +1546,13 @@
2019
1546
  }
2020
1547
  };
2021
1548
  /**
2022
- * 선택 상태 동기화 유틸리티
2023
- *
2024
- * 데이터 변경 시 선택된 항목의 참조를 최신 데이터로 업데이트합니다.
2025
- * 화면 밖에 있는 선택된 항목도 선택 상태를 유지합니다.
1549
+ * 선택 상태 동기화 (화면 밖 데이터도 선택 상태 유지)
2026
1550
  *
2027
1551
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
2028
1552
  * @param data 최신 데이터 배열
2029
1553
  * @param selectedIds 선택된 항목 ID Set
2030
1554
  * @param selectedItemsMap 현재 선택된 항목 Map
2031
1555
  * @returns 업데이트된 선택된 항목 Map
2032
- *
2033
- * @remarks
2034
- * - 성능: O(n + m), n은 전체 데이터 수, m은 선택된 항목 수
2035
- * - 화면 밖 데이터도 선택 상태 유지 (최신 데이터가 없으면 기존 데이터 유지)
2036
- *
2037
- * @example
2038
- * ```typescript
2039
- * selectedItemsMapRef.current = syncSelectedItems(
2040
- * data,
2041
- * selectedIdsRef.current,
2042
- * selectedItemsMapRef.current
2043
- * );
2044
- * ```
2045
1556
  */
2046
1557
 
2047
1558
  var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
@@ -2069,24 +1580,10 @@
2069
1580
  /**
2070
1581
  * 외부 selectedItems를 내부 상태로 동기화
2071
1582
  *
2072
- * 외부에서 전달된 selectedItems prop을 내부 ref 상태로 동기화합니다.
2073
- *
2074
1583
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
2075
- * @param externalSelectedItems 외부에서 전달된 선택된 항목 배열 (undefined면 동기화 안 함)
1584
+ * @param externalSelectedItems 외부에서 전달된 선택된 항목 배열
2076
1585
  * @param selectedIdsRef 선택된 ID Set ref
2077
1586
  * @param selectedItemsMapRef 선택된 항목 Map ref
2078
- *
2079
- * @remarks
2080
- * - externalSelectedItems가 undefined면 외부 제어가 아니므로 아무 작업도 하지 않음
2081
- * - 성능: O(m), m은 externalSelectedItems의 길이
2082
- *
2083
- * @example
2084
- * ```typescript
2085
- * useEffect(() => {
2086
- * syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
2087
- * // 렌더링...
2088
- * }, [externalSelectedItems]);
2089
- * ```
2090
1587
  */
2091
1588
 
2092
1589
  var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef, selectedItemsMapRef) {
@@ -2102,22 +1599,17 @@
2102
1599
  };
2103
1600
 
2104
1601
  /**
2105
- * 이벤트 유효성 검증 헬퍼
1602
+ * 이벤트 유효성 검증 및 좌표 변환
2106
1603
  *
2107
1604
  * @param event 이벤트 파라미터
2108
- * @param context Context가 있는지 여부
1605
+ * @param context WoongCanvasContext 인스턴스
2109
1606
  * @param controller MintMapController 인스턴스
2110
- * @returns 유효한 offset 또는 null
2111
- *
2112
- * @remarks
2113
- * Context가 있으면 전역 이벤트 핸들러가 처리하므로 로컬 핸들러는 스킵
1607
+ * @returns 유효한 화면 좌표 또는 null
2114
1608
  */
2115
1609
  var validateEvent = function (event, context, controller) {
2116
- var _a; // Context가 있으면 전역 핸들러가 처리
2117
-
2118
-
2119
- if (context) return null; // 이벤트 파라미터 검증
1610
+ var _a;
2120
1611
 
1612
+ if (context) return null;
2121
1613
  if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return null;
2122
1614
 
2123
1615
  try {
@@ -2128,22 +1620,15 @@
2128
1620
  }
2129
1621
  };
2130
1622
  /**
2131
- * Map의 values를 배열로 변환 (최적화 버전)
1623
+ * Map의 values를 배열로 변환
2132
1624
  *
1625
+ * @template T Map 값의 타입
2133
1626
  * @param map 변환할 Map
2134
1627
  * @returns Map의 값 배열
2135
- *
2136
- * @remarks
2137
- * Map.values()는 IterableIterator를 반환하므로 배열 변환이 필요할 때 사용합니다.
2138
- * 성능: O(n) 시간복잡도
2139
- *
2140
- * 최적화: Array.from을 사용하되, 크기를 미리 할당하여 메모리 재할당 최소화
2141
1628
  */
2142
1629
 
2143
1630
  var mapValuesToArray = function (map) {
2144
- // Map이 비어있으면 배열 반환 (메모리 할당 최소화)
2145
- if (map.size === 0) return []; // Array.from 사용 (TypeScript 컴파일러 호환성)
2146
-
1631
+ if (map.size === 0) return [];
2147
1632
  return Array.from(map.values());
2148
1633
  };
2149
1634
 
@@ -6087,9 +5572,6 @@
6087
5572
  }))));
6088
5573
  }
6089
5574
 
6090
- // 메인 컴포넌트
6091
- // ============================================================================
6092
-
6093
5575
  var WoongCanvasMarker = function (props) {
6094
5576
  var data = props.data,
6095
5577
  onClick = props.onClick,
@@ -6099,225 +5581,88 @@
6099
5581
  enableMultiSelect = _a === void 0 ? false : _a,
6100
5582
  _b = props.topOnHover,
6101
5583
  topOnHover = _b === void 0 ? false : _b,
6102
- _c = props.enableViewportCulling,
6103
- enableViewportCulling = _c === void 0 ? true : _c,
6104
- _d = props.cullingMargin,
6105
- cullingMargin = _d === void 0 ? DEFAULT_CULLING_MARGIN : _d,
6106
- _e = props.maxCacheSize,
6107
- maxCacheSize = _e === void 0 ? DEFAULT_MAX_CACHE_SIZE : _e,
5584
+ _c = props.cullingMargin,
5585
+ cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
5586
+ _d = props.maxCacheSize,
5587
+ maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
6108
5588
  externalSelectedItems = props.selectedItems,
6109
5589
  externalSelectedItem = props.selectedItem,
6110
- _f = props.disableInteraction,
6111
- disableInteraction = _f === void 0 ? false : _f,
5590
+ _e = props.disableInteraction,
5591
+ disableInteraction = _e === void 0 ? false : _e,
6112
5592
  renderBase = props.renderBase,
6113
- renderAnimation = props.renderAnimation,
6114
5593
  renderEvent = props.renderEvent,
6115
- options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderAnimation", "renderEvent"]); // --------------------------------------------------------------------------
6116
- // Hooks & Context
6117
- // --------------------------------------------------------------------------
6118
-
5594
+ options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderEvent"]);
6119
5595
 
6120
5596
  var controller = useMintMapController();
6121
5597
  var context = useWoongCanvasContext();
6122
- var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
6123
- // DOM Refs
6124
- // --------------------------------------------------------------------------
5598
+ var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
6125
5599
 
6126
5600
  var divRef = React.useRef(document.createElement('div'));
6127
5601
  var divElement = divRef.current;
6128
5602
  var containerRef = React.useRef(null);
6129
- var markerRef = React.useRef(); // --------------------------------------------------------------------------
6130
- // Konva Refs
6131
- // --------------------------------------------------------------------------
5603
+ var markerRef = React.useRef(); // Konva Refs
6132
5604
 
6133
5605
  var stageRef = React.useRef(null);
6134
5606
  var baseLayerRef = React.useRef(null);
6135
- var animationLayerRef = React.useRef(null);
6136
- var eventLayerRef = React.useRef(null); // --------------------------------------------------------------------------
6137
- // Data Refs - 선택 및 Hover 상태 관리
6138
- // --------------------------------------------------------------------------
6139
-
6140
- /** data prop을 ref로 추적 (stale closure 방지, useEffect에서 동기화) */
6141
-
6142
- var dataRef = React.useRef(data); // --------------------------------------------------------------------------
6143
- // State Refs - 선택 및 Hover 상태 관리
6144
- // --------------------------------------------------------------------------
6145
-
6146
- /** 상호작용 비활성화 상태 (Ref로 관리하여 클로저 문제 해결) */
5607
+ var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
6147
5608
 
5609
+ var dataRef = React.useRef(data);
6148
5610
  var disableInteractionRef = React.useRef(disableInteraction);
6149
- /** 현재 Hover 중인 항목 */
6150
-
6151
5611
  var hoveredItemRef = React.useRef(null);
6152
- /** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
6153
-
6154
5612
  var selectedItemRef = React.useRef(externalSelectedItem);
6155
- /**
6156
- * 선택된 항목의 ID Set
6157
- *
6158
- * 용도:
6159
- * 1. onClick 콜백에 전달 - onClick(data, selectedIdsRef.current)
6160
- * 2. 선택 여부 빠른 체크 - selectedIdsRef.current.has(id)
6161
- * 3. 메모리 효율 - ID만 저장 (작음)
6162
- *
6163
- * selectedItemsMapRef와 차이:
6164
- * - selectedIdsRef: ID만 저장 { "id1", "id2" }
6165
- * - selectedItemsMapRef: 전체 객체 저장 { id1: {...}, id2: {...} }
6166
- *
6167
- * 둘 다 필요: ID만 필요한 곳은 이것, 전체 데이터 필요한 곳은 Map
6168
- */
6169
-
6170
5613
  var selectedIdsRef = React.useRef(new Set());
6171
- /**
6172
- * 선택된 항목의 실제 데이터 Map (핵심 성능 최적화!)
6173
- *
6174
- * 목적: doRenderEvent에서 filter() 순회 제거
6175
- * - 이전: markersRef.current.filter() → O(전체 마커 수)
6176
- * - 현재: Map.values() → O(선택된 항목 수)
6177
- *
6178
- * 성능 개선: 10,000개 중 1개 선택 시
6179
- * - 이전: 10,000번 체크
6180
- * - 현재: 1번 접근 (10,000배 빠름!)
6181
- */
6182
-
6183
- var selectedItemsMapRef = React.useRef(new Map()); // --------------------------------------------------------------------------
6184
- // Drag Refs
6185
- // --------------------------------------------------------------------------
5614
+ var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
6186
5615
 
6187
5616
  var draggingRef = React.useRef(false);
6188
5617
  var prevCenterOffsetRef = React.useRef(null);
6189
5618
  var accumTranslateRef = React.useRef({
6190
5619
  x: 0,
6191
5620
  y: 0
6192
- }); // --------------------------------------------------------------------------
6193
- // Performance Refs (캐싱 & 최적화)
6194
- // --------------------------------------------------------------------------
6195
-
6196
- /** 좌표 변환 결과 LRU 캐시 */
5621
+ }); // 성능 최적화 Refs
6197
5622
 
6198
5623
  var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
6199
- /** 공간 인덱스 (빠른 Hit Test) */
6200
-
6201
5624
  var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
6202
- /** 바운딩 박스 캐시 (Viewport Culling 최적화) */
6203
-
6204
5625
  var boundingBoxCacheRef = React.useRef(new Map());
6205
- /** 뷰포트 경계 캐시 (Viewport Culling) */
6206
-
6207
- var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
6208
- // 유틸리티 함수: 뷰포트 관리
6209
- // --------------------------------------------------------------------------
6210
-
6211
- /**
6212
- * 현재 뷰포트 영역 계산
6213
- */
5626
+ var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling)
6214
5627
 
6215
5628
  var updateViewport$1 = function () {
6216
5629
  updateViewport(stageRef.current, cullingMargin, viewportRef);
6217
- };
6218
- /**
6219
- * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
6220
- */
5630
+ }; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
6221
5631
 
6222
5632
 
6223
5633
  var isInViewport$1 = function (item) {
6224
- return isInViewport(item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox);
6225
- }; // --------------------------------------------------------------------------
6226
- // 유틸리티 함수: 좌표 변환 캐싱
6227
- // --------------------------------------------------------------------------
6228
-
6229
- /**
6230
- * 마커 좌표 변환 결과를 캐시하고 반환
6231
- *
6232
- * @param markerData 마커 데이터
6233
- * @returns 변환된 좌표 또는 null
6234
- */
5634
+ return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
5635
+ }; // 마커 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
6235
5636
 
6236
5637
 
6237
5638
  var getOrComputeMarkerOffset = function (markerData) {
6238
5639
  var cached = offsetCacheRef.current.get(markerData.id);
6239
5640
  if (cached && !Array.isArray(cached)) return cached;
6240
5641
  var result = computeMarkerOffset(markerData, controller);
6241
-
6242
- if (result) {
6243
- offsetCacheRef.current.set(markerData.id, result);
6244
- }
6245
-
5642
+ if (!result) return null;
5643
+ offsetCacheRef.current.set(markerData.id, result);
6246
5644
  return result;
6247
- }; // --------------------------------------------------------------------------
6248
- // 유틸리티 함수: 바운딩 박스 계산
6249
- // --------------------------------------------------------------------------
6250
-
6251
- /**
6252
- * 마커의 바운딩 박스 계산
6253
- *
6254
- * 마커의 화면 상 위치와 크기를 기반으로 바운딩 박스를 계산합니다.
6255
- * Viewport Culling에 사용되며, tailHeight를 포함하여 전체 표시 영역을 계산합니다.
6256
- *
6257
- * @param item 마커 데이터
6258
- * @returns 바운딩 박스 (minX, minY, maxX, maxY) 또는 null (좌표 변환 실패 시)
6259
- *
6260
- * @remarks
6261
- * - **boxHeight**: 마커 본체만 포함 (Hit Test 영역)
6262
- * - **tailHeight**: 마커 꼬리 높이 (Viewport Culling용, 화면에 보이는 전체 영역 포함)
6263
- * - 바운딩 박스는 캐시되어 성능 최적화
6264
- *
6265
- * @example
6266
- * ```typescript
6267
- * const bbox = computeBoundingBox(item);
6268
- * if (!bbox) return; // 계산 실패
6269
- * // bbox.minX, bbox.minY, bbox.maxX, bbox.maxY 사용
6270
- * ```
6271
- */
5645
+ }; // 마커 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
6272
5646
 
6273
5647
 
6274
5648
  var computeBoundingBox = function (item) {
6275
- // 마커: 중심점 기준 박스 크기 계산 (꼬리 포함)
6276
5649
  var offset = getOrComputeMarkerOffset(item);
6277
5650
  if (!offset) return null;
6278
5651
  var boxWidth = item.boxWidth || 50;
6279
5652
  var boxHeight = item.boxHeight || 28;
6280
- var tailHeight = item.tailHeight || 0; // 🎯 tailHeight 사용 (Viewport Culling용)
6281
-
5653
+ var tailHeight = item.tailHeight || 0;
6282
5654
  return {
6283
5655
  minX: offset.x - boxWidth / 2,
6284
5656
  minY: offset.y - boxHeight - tailHeight,
6285
5657
  maxX: offset.x + boxWidth / 2,
6286
5658
  maxY: offset.y
6287
5659
  };
6288
- }; // --------------------------------------------------------------------------
6289
- // 유틸리티 함수: 공간 인덱싱
6290
- // --------------------------------------------------------------------------
6291
-
6292
- /**
6293
- * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
6294
- *
6295
- * 모든 마커의 바운딩 박스를 Spatial Hash Grid에 삽입합니다.
6296
- * 이를 통해 클릭/호버 시 해당 위치 주변의 마커만 빠르게 조회할 수 있습니다.
6297
- *
6298
- * @remarks
6299
- * - 호출 시점: 데이터 변경 시 또는 지도 이동/줌 완료 시
6300
- * - 성능: O(n) 시간복잡도, n은 마커 개수
6301
- * - Hit Test 성능: O(1) 수준 (30,000개 → ~10개 후보만 체크)
6302
- */
5660
+ }; // 공간 인덱스 빌드 (빠른 Hit Test용)
6303
5661
 
6304
5662
 
6305
5663
  var buildSpatialIndex$1 = function () {
6306
5664
  buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
6307
- }; // --------------------------------------------------------------------------
6308
- // 렌더링 함수 결정
6309
- // --------------------------------------------------------------------------
6310
-
6311
- /**
6312
- * 외부 렌더링 함수에 전달할 유틸리티 객체
6313
- *
6314
- * 커스텀 렌더링 함수(renderBase, renderAnimation, renderEvent)에서 사용할
6315
- * 좌표 변환 등의 헬퍼 함수들을 제공합니다.
6316
- *
6317
- * @remarks
6318
- * - getOrComputeMarkerOffset: 마커 좌표 변환 (자동 캐싱)
6319
- * - getOrComputePolygonOffsets: 폴리곤 좌표 변환 (마커에서는 사용 안 함)
6320
- */
5665
+ }; // 렌더링 유틸리티 객체
6321
5666
 
6322
5667
 
6323
5668
  var renderUtils = {
@@ -6325,48 +5670,29 @@
6325
5670
  return null;
6326
5671
  },
6327
5672
  getOrComputeMarkerOffset: getOrComputeMarkerOffset
6328
- };
6329
- /** Base Layer에서 사용할 빈 Set (재사용) */
6330
-
6331
- React.useRef(new Set());
6332
- /**
6333
- * Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
6334
- *
6335
- * 🔥 최적화:
6336
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
6337
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
6338
- * 3. 클로저로 최신 데이터 참조
6339
- *
6340
- * 🎯 topOnHover 지원:
6341
- * - renderEvent가 없을 때 Base Layer에서 hover 처리 (fallback)
6342
- * - renderEvent가 있으면 Event Layer에서 처리 (성능 최적화)
6343
- */
5673
+ }; // Base Layer 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
6344
5674
 
6345
5675
  var doRenderBase = function () {
6346
5676
  var layer = baseLayerRef.current;
6347
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
6348
-
5677
+ if (!layer) return;
6349
5678
  var shape = layer.findOne('.base-render-shape');
6350
5679
 
6351
5680
  if (!shape) {
6352
- // 최초 생성 (한 번만 실행됨)
6353
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
6354
5681
  shape = new Konva__default["default"].Shape({
6355
5682
  name: 'base-render-shape',
6356
5683
  sceneFunc: function (context, shape) {
6357
5684
  var ctx = context;
6358
- var hovered = hoveredItemRef.current; // 클로저로 최신 ref 참조
5685
+ var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
6359
5686
 
6360
- var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
5687
+ var visibleItems = dataRef.current.filter(function (item) {
6361
5688
  return isInViewport$1(item);
6362
- }) : dataRef.current; // topOnHover true이고 renderEvent가 없으면 Base Layer에서 hover 처리
5689
+ }); // topOnHover 옵션: hover된 항목을 나중에 그려서 최상위에 표시
6363
5690
 
6364
5691
  if (topOnHover && !renderEvent && hovered) {
6365
- // hover된 항목 제외하고 렌더링
6366
5692
  visibleItems = visibleItems.filter(function (item) {
6367
5693
  return item.id !== hovered.id;
6368
5694
  });
6369
- } // 일반 항목 렌더링
5695
+ } // 일반 항목들 먼저 렌더링
6370
5696
 
6371
5697
 
6372
5698
  renderBase({
@@ -6375,12 +5701,10 @@
6375
5701
  selectedIds: selectedIdsRef.current,
6376
5702
  hoveredItem: hovered,
6377
5703
  utils: renderUtils
6378
- }); // hover된 항목을 최상단에 렌더링 (renderEvent가 없을 때만)
5704
+ }); // hover된 항목을 마지막에 렌더링하여 최상위에 표시
6379
5705
 
6380
5706
  if (topOnHover && !renderEvent && hovered) {
6381
- var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
6382
-
6383
- if (isHoveredInViewport) {
5707
+ if (isInViewport$1(hovered)) {
6384
5708
  renderBase({
6385
5709
  ctx: ctx,
6386
5710
  items: [hovered],
@@ -6396,72 +5720,26 @@
6396
5720
  hitStrokeWidth: 0
6397
5721
  });
6398
5722
  layer.add(shape);
6399
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
6400
-
5723
+ }
6401
5724
 
6402
5725
  layer.batchDraw();
6403
- };
6404
- /**
6405
- * Animation 레이어 렌더링 (선택된 마커 애니메이션)
6406
- *
6407
- * 선택된 마커에 대한 애니메이션 효과를 렌더링합니다.
6408
- * renderAnimation prop이 제공된 경우에만 실행됩니다.
6409
- *
6410
- * @remarks
6411
- * - **성능 최적화**: sceneFunc 내부에서 최신 items 참조
6412
- * - 선택 변경 시에만 재생성
6413
- * - 지도 이동 시에는 기존 Animation 계속 실행
6414
- */
6415
-
6416
-
6417
- var doRenderAnimation = function () {
6418
- if (!renderAnimation) return;
6419
- var layer = animationLayerRef.current;
6420
- if (!layer) return;
6421
- renderAnimation({
6422
- layer: layer,
6423
- selectedIds: selectedIdsRef.current,
6424
- items: dataRef.current,
6425
- utils: renderUtils
6426
- });
6427
- };
6428
- /**
6429
- * Event 레이어 렌더링 (hover + 선택 상태 표시)
6430
- *
6431
- * 마커의 hover 효과 및 선택 상태를 표시합니다.
6432
- * renderEvent prop이 제공된 경우에만 실행됩니다.
6433
- *
6434
- * @remarks
6435
- * - **성능 최적화**:
6436
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
6437
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
6438
- * 3. 클로저로 최신 데이터 참조
6439
- * - **topOnHover 지원**: hover된 항목을 최상단에 렌더링
6440
- * - 선택된 항목은 Map에서 O(1)로 조회하여 성능 최적화
6441
- */
5726
+ }; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
6442
5727
 
6443
5728
 
6444
5729
  var doRenderEvent = function () {
6445
5730
  var layer = eventLayerRef.current;
6446
- if (!layer) return;
6447
- if (!renderEvent) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
6448
-
5731
+ if (!layer || !renderEvent) return;
6449
5732
  var shape = layer.findOne('.event-render-shape');
6450
5733
 
6451
5734
  if (!shape) {
6452
- // 최초 생성 (한 번만 실행됨)
6453
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
6454
5735
  shape = new Konva__default["default"].Shape({
6455
5736
  name: 'event-render-shape',
6456
5737
  sceneFunc: function (context, shape) {
6457
- var ctx = context; // 클로저로 최신 ref 값 참조
6458
- // 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
6459
-
5738
+ var ctx = context;
6460
5739
  var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
6461
- var hovered = hoveredItemRef.current; // topOnHover가 true이면 hover된 항목을 최상단에 렌더링
5740
+ var hovered = hoveredItemRef.current;
6462
5741
 
6463
5742
  if (topOnHover && hovered) {
6464
- // 1. 먼저 일반 항목들 렌더링 (hover된 항목 제외)
6465
5743
  renderEvent({
6466
5744
  ctx: ctx,
6467
5745
  hoveredItem: null,
@@ -6470,13 +5748,9 @@
6470
5748
  return item.id !== hovered.id;
6471
5749
  }),
6472
5750
  selectedItem: selectedItemRef.current
6473
- }); // 2. hover된 항목을 최상단에 렌더링
6474
-
6475
- var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
5751
+ });
6476
5752
 
6477
- if (isHoveredInViewport) {
6478
- // hover된 항목이 선택되어 있다면 hoverSelectedItems에 포함시켜서
6479
- // renderEvent에서 hover 스타일만 적용되도록 함
5753
+ if (isInViewport$1(hovered)) {
6480
5754
  var hoveredIsSelected = selectedItems.some(function (item) {
6481
5755
  return item.id === hovered.id;
6482
5756
  });
@@ -6490,7 +5764,6 @@
6490
5764
  });
6491
5765
  }
6492
5766
  } else {
6493
- // topOnHover가 false이거나 hover된 항목이 없으면 일반 렌더링
6494
5767
  renderEvent({
6495
5768
  ctx: ctx,
6496
5769
  hoveredItem: hovered,
@@ -6505,28 +5778,21 @@
6505
5778
  hitStrokeWidth: 0
6506
5779
  });
6507
5780
  layer.add(shape);
6508
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
6509
-
5781
+ }
6510
5782
 
6511
5783
  layer.batchDraw();
6512
- };
6513
- /**
6514
- * 전체 즉시 렌더링 (IDLE 시 호출)
6515
- */
5784
+ }; // 전체 즉시 렌더링
6516
5785
 
6517
5786
 
6518
5787
  var renderAllImmediate = function () {
6519
5788
  updateViewport$1();
6520
5789
  buildSpatialIndex$1();
6521
5790
  doRenderBase();
6522
- doRenderAnimation();
6523
5791
  doRenderEvent();
6524
- }; // --------------------------------------------------------------------------
6525
- // 이벤트 핸들러: 지도 이벤트
6526
- // --------------------------------------------------------------------------
5792
+ }; // 지도 이벤트 핸들러 생성
6527
5793
 
6528
5794
 
6529
- var _g = createMapEventHandlers({
5795
+ var _f = createMapEventHandlers({
6530
5796
  controller: controller,
6531
5797
  containerRef: containerRef,
6532
5798
  markerRef: markerRef,
@@ -6537,73 +5803,38 @@
6537
5803
  boundingBoxCacheRef: boundingBoxCacheRef,
6538
5804
  renderAllImmediate: renderAllImmediate
6539
5805
  }),
6540
- handleIdle = _g.handleIdle,
6541
- handleZoomStart = _g.handleZoomStart,
6542
- handleZoomEnd = _g.handleZoomEnd,
6543
- handleCenterChanged = _g.handleCenterChanged,
6544
- handleDragStartShared = _g.handleDragStart,
6545
- handleDragEndShared = _g.handleDragEnd;
6546
- /**
6547
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
6548
- */
6549
-
5806
+ handleIdle = _f.handleIdle,
5807
+ handleZoomStart = _f.handleZoomStart,
5808
+ handleZoomEnd = _f.handleZoomEnd,
5809
+ handleCenterChanged = _f.handleCenterChanged,
5810
+ handleDragStartShared = _f.handleDragStart,
5811
+ handleDragEndShared = _f.handleDragEnd;
6550
5812
 
6551
5813
  var handleDragStart = function () {
6552
5814
  handleDragStartShared();
6553
5815
  draggingRef.current = true;
6554
5816
  controller.setMapCursor('grabbing');
6555
5817
  };
6556
- /**
6557
- * 드래그 종료 처리 (커서를 기본으로 복원)
6558
- */
6559
-
6560
5818
 
6561
5819
  var handleDragEnd = function () {
6562
5820
  handleDragEndShared();
6563
5821
  draggingRef.current = false;
6564
5822
  controller.setMapCursor('grab');
6565
- }; // --------------------------------------------------------------------------
6566
- // Hit Test & 상태 관리
6567
- // --------------------------------------------------------------------------
6568
-
6569
- /**
6570
- * 특정 좌표의 마커 데이터 찾기 (Spatial Index 사용)
6571
- *
6572
- * 클릭/호버 이벤트 시 해당 위치에 있는 마커를 찾습니다.
6573
- * Spatial Hash Grid를 사용하여 O(1) 수준의 빠른 Hit Test를 수행합니다.
6574
- *
6575
- * @param offset 검사할 화면 좌표 (픽셀 단위)
6576
- * @returns 찾은 마커 데이터 또는 null (없으면)
6577
- *
6578
- * @remarks
6579
- * - **topOnHover 지원**: topOnHover가 true일 때 현재 hover된 항목을 최우선으로 체크
6580
- * - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
6581
- * - **성능**: O(후보 항목 수) - 보통 ~10개 (30,000개 전체를 체크하지 않음)
6582
- * - Spatial Index를 통해 해당 위치 주변의 후보만 추출 후 정확한 Hit Test 수행
6583
- *
6584
- * @example
6585
- * ```typescript
6586
- * const clickedOffset = controller.positionToOffset(event.param.position);
6587
- * const data = findData(clickedOffset);
6588
- * if (data) {
6589
- * // 마커를 찾음
6590
- * }
6591
- * ```
6592
- */
5823
+ }; // Hit Test: 특정 좌표의 마커 찾기
6593
5824
 
6594
5825
 
6595
5826
  var findData = function (offset) {
6596
- // topOnHover true이고 현재 hover된 항목이 있으면, 그것을 먼저 체크
5827
+ // topOnHover 옵션이 켜져 있으면 hover된 항목을 최우선으로 확인
6597
5828
  if (topOnHover && hoveredItemRef.current) {
6598
5829
  var hovered = hoveredItemRef.current;
6599
5830
 
6600
5831
  if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
6601
- return hovered; // 여전히 hover된 항목 위에 있음
5832
+ return hovered;
6602
5833
  }
6603
- } // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 ~10개)
5834
+ } // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
6604
5835
 
6605
5836
 
6606
- var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 마커 체크
5837
+ var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
6607
5838
 
6608
5839
  for (var i = candidates.length - 1; i >= 0; i--) {
6609
5840
  var item = candidates[i];
@@ -6614,18 +5845,7 @@
6614
5845
  }
6615
5846
 
6616
5847
  return null;
6617
- };
6618
- /**
6619
- * Hover 상태 설정 및 레이어 렌더링
6620
- *
6621
- * @param data hover된 마커/폴리곤 데이터 또는 null
6622
- *
6623
- * 최적화: RAF 제거하여 즉시 렌더링 (16ms 지연 제거)
6624
- *
6625
- * 🎯 topOnHover 지원:
6626
- * - renderEvent가 있으면: Event Layer에서만 처리 (성능 최적화)
6627
- * - renderEvent가 없고 topOnHover=true면: Base Layer에서 처리
6628
- */
5848
+ }; // Hover 상태 설정 및 렌더링
6629
5849
 
6630
5850
 
6631
5851
  var setHovered = function (data) {
@@ -6635,38 +5855,18 @@
6635
5855
  controller.setMapCursor('grabbing');
6636
5856
  } else {
6637
5857
  controller.setMapCursor(data ? 'pointer' : 'grab');
6638
- } // 즉시 렌더링 (RAF 없이)
6639
-
5858
+ }
6640
5859
 
6641
5860
  if (renderEvent) {
6642
- // renderEvent가 있으면 Event Layer에서만 처리 (성능 최적화)
6643
5861
  doRenderEvent();
6644
5862
  } else if (topOnHover) {
6645
- // renderEvent가 없고 topOnHover가 true면 Base Layer에서 처리
6646
5863
  doRenderBase();
6647
5864
  }
6648
- };
6649
- /**
6650
- * 클릭 처리 (단일/다중 선택)
6651
- *
6652
- * 마커 클릭 시 선택 상태를 업데이트하고 렌더링을 수행합니다.
6653
- *
6654
- * @param data 클릭된 마커 데이터
6655
- *
6656
- * @remarks
6657
- * - **단일 선택**: 기존 선택 해제 후 새로 선택 (토글 가능)
6658
- * - **다중 선택**: enableMultiSelect가 true면 기존 선택 유지하며 추가/제거
6659
- * - **성능 최적화**:
6660
- * - 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
6661
- * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
6662
- * - 객체 생성 오버헤드 제거로 1,000개 이상도 부드럽게 처리
6663
- */
5865
+ }; // 클릭 처리: 선택 상태 업데이트
6664
5866
 
6665
5867
 
6666
5868
  var handleLocalClick = function (data) {
6667
- // 1. 선택 상태 업데이트
6668
5869
  if (enableMultiSelect) {
6669
- // 다중 선택: Set과 Map 동시 업데이트
6670
5870
  var newSelected = new Set(selectedIdsRef.current);
6671
5871
 
6672
5872
  if (newSelected.has(data.id)) {
@@ -6679,7 +5879,6 @@
6679
5879
 
6680
5880
  selectedIdsRef.current = newSelected;
6681
5881
  } else {
6682
- // 단일 선택: 토글
6683
5882
  var newSelected = new Set();
6684
5883
 
6685
5884
  if (!selectedIdsRef.current.has(data.id)) {
@@ -6693,135 +5892,80 @@
6693
5892
  selectedIdsRef.current = newSelected;
6694
5893
  }
6695
5894
 
6696
- if (!!renderAnimation) {
6697
- // 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
6698
- doRenderBase(); // 3. Animation Layer 렌더링 (선택된 마커 애니메이션)
6699
-
6700
- doRenderAnimation();
6701
- } // 4. Event Layer 렌더링 (hover 처리)
6702
-
6703
-
5895
+ doRenderBase();
6704
5896
  doRenderEvent();
6705
- }; // --------------------------------------------------------------------------
6706
- // 이벤트 핸들러: UI 이벤트
6707
- // --------------------------------------------------------------------------
6708
-
6709
- /**
6710
- * 클릭 이벤트 처리
6711
- *
6712
- * @param event 클릭 이벤트 파라미터
6713
- *
6714
- * @remarks
6715
- * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
6716
- * - 상호작용이 비활성화되어 있으면 스킵
6717
- * - Spatial Index를 사용하여 빠른 Hit Test 수행
6718
- */
5897
+ }; // 클릭 이벤트 핸들러
6719
5898
 
6720
5899
 
6721
5900
  var handleClick = function (event) {
6722
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6723
-
5901
+ if (disableInteractionRef.current) return;
6724
5902
  var clickedOffset = validateEvent(event, context, controller);
6725
5903
  if (!clickedOffset) return;
6726
5904
  var data = findData(clickedOffset);
6727
-
6728
- if (data) {
6729
- handleLocalClick(data);
6730
-
6731
- if (onClick) {
6732
- onClick(data, selectedIdsRef.current);
6733
- }
6734
- }
6735
- };
6736
- /**
6737
- * 마우스 이동 이벤트 처리 (hover 감지)
6738
- *
6739
- * @param event 마우스 이동 이벤트 파라미터
6740
- *
6741
- * @remarks
6742
- * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
6743
- * - 상호작용이 비활성화되어 있으면 스킵
6744
- * - hover 상태 변경 시에만 렌더링 및 콜백 호출 (최적화)
6745
- */
5905
+ if (!data) return;
5906
+ handleLocalClick(data);
5907
+ onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
5908
+ }; // 마우스 이동 이벤트 핸들러 (hover 감지)
6746
5909
 
6747
5910
 
6748
5911
  var handleMouseMove = function (event) {
6749
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6750
-
5912
+ if (disableInteractionRef.current) return;
6751
5913
  var mouseOffset = validateEvent(event, context, controller);
6752
5914
  if (!mouseOffset) return;
6753
5915
  var hoveredItem = findData(mouseOffset);
6754
5916
  var prevHovered = hoveredItemRef.current;
6755
-
6756
- if (prevHovered !== hoveredItem) {
6757
- setHovered(hoveredItem);
6758
- if (prevHovered && onMouseOut) onMouseOut(prevHovered);
6759
- if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
6760
- }
6761
- };
6762
- /**
6763
- * 마우스가 canvas를 벗어날 때 hover cleanup
6764
- */
5917
+ if (prevHovered === hoveredItem) return;
5918
+ setHovered(hoveredItem);
5919
+ if (prevHovered) onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
5920
+ if (hoveredItem) onMouseOver === null || onMouseOver === void 0 ? void 0 : onMouseOver(hoveredItem);
5921
+ }; // 마우스가 영역을 벗어날 때 hover 상태 초기화
6765
5922
 
6766
5923
 
6767
5924
  var handleMouseLeave = function () {
6768
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6769
-
5925
+ if (disableInteractionRef.current) return;
6770
5926
  var prevHovered = hoveredItemRef.current;
6771
-
6772
- if (prevHovered) {
6773
- hoveredItemRef.current = null;
6774
- controller.setMapCursor('grab');
6775
- doRenderEvent();
6776
-
6777
- if (onMouseOut) {
6778
- onMouseOut(prevHovered);
6779
- }
6780
- }
6781
- }; // --------------------------------------------------------------------------
6782
- // Lifecycle: DOM 초기화
6783
- // --------------------------------------------------------------------------
5927
+ if (!prevHovered) return;
5928
+ hoveredItemRef.current = null;
5929
+ controller.setMapCursor('grab');
5930
+ doRenderEvent();
5931
+ onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
5932
+ }; // DOM 초기화
6784
5933
 
6785
5934
 
6786
5935
  React.useEffect(function () {
6787
5936
  divElement.style.width = 'fit-content';
6788
5937
  return function () {
6789
- if (markerRef.current) {
6790
- controller.clearDrawable(markerRef.current);
6791
- markerRef.current = undefined;
6792
- }
5938
+ if (!markerRef.current) return;
5939
+ controller.clearDrawable(markerRef.current);
5940
+ markerRef.current = undefined;
6793
5941
  };
6794
- }, []); // --------------------------------------------------------------------------
6795
- // Lifecycle: 마커 생성/업데이트
6796
- // --------------------------------------------------------------------------
5942
+ }, []); // 마커 생성/업데이트
6797
5943
 
6798
5944
  React.useEffect(function () {
6799
- if (options) {
6800
- var bounds = controller.getCurrBounds();
5945
+ if (!options) return;
5946
+ var bounds = controller.getCurrBounds();
6801
5947
 
6802
- var markerOptions = tslib.__assign({
6803
- position: bounds.nw
6804
- }, options);
5948
+ var markerOptions = tslib.__assign({
5949
+ position: bounds.nw
5950
+ }, options);
6805
5951
 
6806
- if (markerRef.current) {
6807
- controller.updateMarker(markerRef.current, markerOptions);
6808
- } else {
6809
- markerRef.current = new Marker(markerOptions);
6810
- markerRef.current.element = divElement;
6811
- controller.createMarker(markerRef.current);
5952
+ if (markerRef.current) {
5953
+ controller.updateMarker(markerRef.current, markerOptions);
5954
+ return;
5955
+ }
6812
5956
 
6813
- if (divElement.parentElement) {
6814
- divElement.parentElement.style.pointerEvents = 'none';
6815
- }
5957
+ markerRef.current = new Marker(markerOptions);
5958
+ markerRef.current.element = divElement;
5959
+ controller.createMarker(markerRef.current);
6816
5960
 
6817
- if (options.zIndex !== undefined) {
6818
- controller.setMarkerZIndex(markerRef.current, options.zIndex);
6819
- }
6820
- }
5961
+ if (divElement.parentElement) {
5962
+ divElement.parentElement.style.pointerEvents = 'none';
6821
5963
  }
6822
- }, [options]); // --------------------------------------------------------------------------
6823
- // Lifecycle: Konva 초기화 및 이벤트 리스너 등록
6824
- // --------------------------------------------------------------------------
5964
+
5965
+ if (options.zIndex !== undefined) {
5966
+ controller.setMarkerZIndex(markerRef.current, options.zIndex);
5967
+ }
5968
+ }, [options]); // Konva 초기화 및 이벤트 리스너 등록
6825
5969
 
6826
5970
  React.useEffect(function () {
6827
5971
  var mapDiv = controller.mapDivElement;
@@ -6830,34 +5974,21 @@
6830
5974
  width: mapDiv.offsetWidth,
6831
5975
  height: mapDiv.offsetHeight
6832
5976
  });
6833
- stageRef.current = stage; // 레이어 최적화 설정
6834
-
5977
+ stageRef.current = stage;
6835
5978
  var baseLayer = new Konva__default["default"].Layer({
6836
- listening: false // 이벤트 리스닝 비활성화로 성능 향상
6837
-
6838
- });
6839
- var animationLayer = new Konva__default["default"].Layer({
6840
5979
  listening: false
6841
5980
  });
6842
5981
  var eventLayer = new Konva__default["default"].Layer({
6843
5982
  listening: false
6844
5983
  });
6845
5984
  baseLayerRef.current = baseLayer;
6846
- animationLayerRef.current = animationLayer;
6847
5985
  eventLayerRef.current = eventLayer;
6848
5986
  stage.add(baseLayer);
6849
-
6850
- if (renderAnimation) {
6851
- stage.add(animationLayer);
6852
- }
6853
-
6854
- stage.add(eventLayer); // 초기 뷰포트 설정
6855
-
6856
- updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
5987
+ stage.add(eventLayer);
5988
+ updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
6857
5989
 
6858
5990
  var resizeRafId = null;
6859
5991
  var resizeObserver = new ResizeObserver(function () {
6860
- // RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
6861
5992
  if (resizeRafId !== null) {
6862
5993
  cancelAnimationFrame(resizeRafId);
6863
5994
  }
@@ -6880,10 +6011,9 @@
6880
6011
  controller.addEventListener('CLICK', handleClick);
6881
6012
  controller.addEventListener('MOUSEMOVE', handleMouseMove);
6882
6013
  controller.addEventListener('DRAGSTART', handleDragStart);
6883
- controller.addEventListener('DRAGEND', handleDragEnd); // 맵 컨테이너에 mouseleave 이벤트 추가
6884
-
6014
+ controller.addEventListener('DRAGEND', handleDragEnd);
6885
6015
  mapDiv.addEventListener('mouseleave', handleMouseLeave);
6886
- renderAllImmediate(); // Context 사용 시 컴포넌트 등록 (다중 인스턴스 관리)
6016
+ renderAllImmediate(); // Context 사용 시 컴포넌트 등록
6887
6017
 
6888
6018
  var componentInstance = null;
6889
6019
 
@@ -6904,22 +6034,17 @@
6904
6034
  },
6905
6035
  isInteractionDisabled: function () {
6906
6036
  return disableInteractionRef.current;
6907
- } // 🚫 상호작용 비활성화 여부 반환
6908
-
6037
+ }
6909
6038
  };
6910
6039
  context.registerComponent(componentInstance);
6911
- } // Cleanup 함수
6912
-
6040
+ }
6913
6041
 
6914
6042
  return function () {
6915
- // RAF 정리
6916
6043
  if (resizeRafId !== null) {
6917
6044
  cancelAnimationFrame(resizeRafId);
6918
- } // 옵저버 정리
6919
-
6920
-
6921
- resizeObserver.disconnect(); // 이벤트 리스너 정리
6045
+ }
6922
6046
 
6047
+ resizeObserver.disconnect();
6923
6048
  controller.removeEventListener('IDLE', handleIdle);
6924
6049
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
6925
6050
  controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
@@ -6928,59 +6053,41 @@
6928
6053
  controller.removeEventListener('MOUSEMOVE', handleMouseMove);
6929
6054
  controller.removeEventListener('DRAGSTART', handleDragStart);
6930
6055
  controller.removeEventListener('DRAGEND', handleDragEnd);
6931
- mapDiv.removeEventListener('mouseleave', handleMouseLeave); // Context 정리
6056
+ mapDiv.removeEventListener('mouseleave', handleMouseLeave);
6932
6057
 
6933
6058
  if (context && componentInstance) {
6934
6059
  context.unregisterComponent(componentInstance);
6935
- } // Konva 리소스 정리
6936
-
6060
+ }
6937
6061
 
6938
6062
  baseLayer.destroyChildren();
6939
- animationLayer.destroyChildren();
6940
6063
  eventLayer.destroyChildren();
6941
- stage.destroy(); // 캐시 정리
6942
-
6064
+ stage.destroy();
6943
6065
  offsetCacheRef.current.clear();
6944
6066
  boundingBoxCacheRef.current.clear();
6945
6067
  spatialIndexRef.current.clear();
6946
- };
6947
- }, []); // 초기화는 한 번만
6948
- // --------------------------------------------------------------------------
6949
- // Lifecycle: disableInteraction 동기화
6950
- // --------------------------------------------------------------------------
6068
+ };
6069
+ }, []); // disableInteraction 동기화
6951
6070
 
6952
6071
  React.useEffect(function () {
6953
6072
  disableInteractionRef.current = disableInteraction;
6954
- }, [disableInteraction]); // --------------------------------------------------------------------------
6955
- // Lifecycle: 외부 selectedItems 동기화
6956
- // --------------------------------------------------------------------------
6073
+ }, [disableInteraction]); // 외부 selectedItems 동기화
6957
6074
 
6958
6075
  React.useEffect(function () {
6959
6076
  if (!stageRef.current) return;
6960
- syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef); // 렌더링
6961
-
6077
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
6962
6078
  doRenderBase();
6963
- doRenderAnimation();
6964
6079
  doRenderEvent();
6965
- }, [externalSelectedItems]); // 배열 자체를 dependency로 사용
6966
- // --------------------------------------------------------------------------
6967
- // Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
6968
- // --------------------------------------------------------------------------
6080
+ }, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
6969
6081
 
6970
6082
  React.useEffect(function () {
6971
- if (!stageRef.current) return; // Ref 동기화
6972
-
6973
- selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
6974
-
6083
+ if (!stageRef.current) return;
6084
+ selectedItemRef.current = externalSelectedItem;
6975
6085
  doRenderEvent();
6976
- }, [externalSelectedItem]); // --------------------------------------------------------------------------
6977
- // Lifecycle: 데이터 변경 시 렌더링
6978
- // --------------------------------------------------------------------------
6086
+ }, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
6979
6087
 
6980
6088
  React.useEffect(function () {
6981
- if (!stageRef.current) return; // dataRef 동기화
6982
-
6983
- dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
6089
+ if (!stageRef.current) return;
6090
+ dataRef.current = data;
6984
6091
 
6985
6092
  if (containerRef.current) {
6986
6093
  containerRef.current.style.transform = '';
@@ -6990,26 +6097,10 @@
6990
6097
  accumTranslateRef.current = {
6991
6098
  x: 0,
6992
6099
  y: 0
6993
- }; // 캐시 정리 (새 데이터이므로 기존 캐시는 무효)
6994
-
6100
+ };
6995
6101
  offsetCacheRef.current.clear();
6996
6102
  boundingBoxCacheRef.current.clear();
6997
- /**
6998
- * 선택 상태 동기화 (최적화 버전)
6999
- *
7000
- * data가 변경되면 selectedItemsMapRef도 업데이트 필요
7001
- * (참조가 바뀌므로 기존 Map의 데이터는 stale 상태)
7002
- *
7003
- * 🔥 중요: 화면 밖 데이터도 선택 상태 유지!
7004
- * - 현재 data에 있으면 최신 데이터로 업데이트
7005
- * - 없으면 기존 selectedItemsMapRef의 데이터 유지
7006
- *
7007
- * 최적화: data를 Map으로 먼저 변환하여 find() 순회 제거
7008
- * - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
7009
- */
7010
-
7011
- selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current); // 즉시 렌더링
7012
-
6103
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
7013
6104
  renderAllImmediate();
7014
6105
  }, [data]);
7015
6106
  return reactDom.createPortal(React__default["default"].createElement("div", {
@@ -7310,24 +6401,19 @@
7310
6401
  };
7311
6402
  };
7312
6403
 
7313
- // 메인 컴포넌트
7314
- // ============================================================================
7315
-
7316
6404
  var WoongCanvasPolygon = function (props) {
7317
6405
  var data = props.data,
7318
6406
  onClick = props.onClick,
7319
6407
  _a = props.enableMultiSelect,
7320
6408
  enableMultiSelect = _a === void 0 ? false : _a,
7321
- _b = props.enableViewportCulling,
7322
- enableViewportCulling = _b === void 0 ? true : _b,
7323
- _c = props.cullingMargin,
7324
- cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
7325
- _d = props.maxCacheSize,
7326
- maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
6409
+ _b = props.cullingMargin,
6410
+ cullingMargin = _b === void 0 ? DEFAULT_CULLING_MARGIN : _b,
6411
+ _c = props.maxCacheSize,
6412
+ maxCacheSize = _c === void 0 ? DEFAULT_MAX_CACHE_SIZE : _c,
7327
6413
  externalSelectedItems = props.selectedItems,
7328
6414
  externalSelectedItem = props.selectedItem,
7329
- _e = props.disableInteraction,
7330
- disableInteraction = _e === void 0 ? false : _e,
6415
+ _d = props.disableInteraction,
6416
+ disableInteraction = _d === void 0 ? false : _d,
7331
6417
  baseFillColor = props.baseFillColor,
7332
6418
  baseStrokeColor = props.baseStrokeColor,
7333
6419
  baseLineWidth = props.baseLineWidth,
@@ -7340,167 +6426,67 @@
7340
6426
  hoveredFillColor = props.hoveredFillColor,
7341
6427
  hoveredStrokeColor = props.hoveredStrokeColor,
7342
6428
  hoveredLineWidth = props.hoveredLineWidth,
7343
- options = tslib.__rest(props, ["data", "onClick", "enableMultiSelect", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
6429
+ options = tslib.__rest(props, ["data", "onClick", "enableMultiSelect", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
7344
6430
  // Hooks & Context
7345
6431
  // --------------------------------------------------------------------------
7346
6432
 
7347
6433
 
7348
6434
  var controller = useMintMapController();
7349
6435
  var context = useWoongCanvasContext();
7350
- var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
7351
- // DOM Refs
7352
- // --------------------------------------------------------------------------
6436
+ var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
7353
6437
 
7354
6438
  var divRef = React.useRef(document.createElement('div'));
7355
6439
  var divElement = divRef.current;
7356
6440
  var containerRef = React.useRef(null);
7357
- var markerRef = React.useRef(); // --------------------------------------------------------------------------
7358
- // Konva Refs
7359
- // --------------------------------------------------------------------------
6441
+ var markerRef = React.useRef(); // Konva Refs
7360
6442
 
7361
6443
  var stageRef = React.useRef(null);
7362
6444
  var baseLayerRef = React.useRef(null);
7363
- var eventLayerRef = React.useRef(null); // --------------------------------------------------------------------------
7364
- // Data Refs - 선택 및 Hover 상태 관리
7365
- // --------------------------------------------------------------------------
7366
-
7367
- /** data prop을 ref로 추적 (stale closure 방지, useEffect에서 동기화) */
7368
-
7369
- var dataRef = React.useRef(data); // --------------------------------------------------------------------------
7370
- // State Refs - 선택 및 Hover 상태 관리
7371
- // --------------------------------------------------------------------------
7372
-
7373
- /** 상호작용 비활성화 상태 (Ref로 관리하여 클로저 문제 해결) */
6445
+ var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
7374
6446
 
6447
+ var dataRef = React.useRef(data);
7375
6448
  var disableInteractionRef = React.useRef(disableInteraction);
7376
- /** 현재 Hover 중인 항목 */
7377
-
7378
6449
  var hoveredItemRef = React.useRef(null);
7379
- /** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
7380
-
7381
6450
  var selectedItemRef = React.useRef(externalSelectedItem);
7382
- /**
7383
- * 선택된 항목의 ID Set
7384
- *
7385
- * 용도:
7386
- * 1. onClick 콜백에 전달 - onClick(data, selectedIdsRef.current)
7387
- * 2. 선택 여부 빠른 체크 - selectedIdsRef.current.has(id)
7388
- * 3. 메모리 효율 - ID만 저장 (작음)
7389
- *
7390
- * selectedItemsMapRef와 차이:
7391
- * - selectedIdsRef: ID만 저장 { "id1", "id2" }
7392
- * - selectedItemsMapRef: 전체 객체 저장 { id1: {...}, id2: {...} }
7393
- *
7394
- * 둘 다 필요: ID만 필요한 곳은 이것, 전체 데이터 필요한 곳은 Map
7395
- */
7396
-
7397
6451
  var selectedIdsRef = React.useRef(new Set());
7398
- /**
7399
- * 선택된 항목의 실제 데이터 Map (핵심 성능 최적화!)
7400
- *
7401
- * 목적: doRenderEvent에서 filter() 순회 제거
7402
- * - 이전: markersRef.current.filter() → O(전체 마커 수)
7403
- * - 현재: Map.values() → O(선택된 항목 수)
7404
- *
7405
- * 성능 개선: 10,000개 중 1개 선택 시
7406
- * - 이전: 10,000번 체크
7407
- * - 현재: 1번 접근 (10,000배 빠름!)
7408
- */
7409
-
7410
- var selectedItemsMapRef = React.useRef(new Map()); // --------------------------------------------------------------------------
7411
- // Drag Refs
7412
- // --------------------------------------------------------------------------
6452
+ var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
7413
6453
 
7414
6454
  var draggingRef = React.useRef(false);
7415
6455
  var prevCenterOffsetRef = React.useRef(null);
7416
6456
  var accumTranslateRef = React.useRef({
7417
6457
  x: 0,
7418
6458
  y: 0
7419
- }); // --------------------------------------------------------------------------
7420
- // Performance Refs (캐싱 & 최적화)
7421
- // --------------------------------------------------------------------------
7422
-
7423
- /** 좌표 변환 결과 LRU 캐시 */
6459
+ }); // 성능 최적화 Refs
7424
6460
 
7425
6461
  var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
7426
- /** 공간 인덱스 (빠른 Hit Test) */
7427
-
7428
6462
  var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
7429
- /** 바운딩 박스 캐시 (Viewport Culling 최적화) */
7430
-
7431
6463
  var boundingBoxCacheRef = React.useRef(new Map());
7432
- /** 뷰포트 경계 캐시 (Viewport Culling) */
7433
-
7434
- var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
7435
- // 유틸리티 함수: 뷰포트 관리
7436
- // --------------------------------------------------------------------------
7437
-
7438
- /**
7439
- * 현재 뷰포트 영역 계산
7440
- */
6464
+ var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling)
7441
6465
 
7442
6466
  var updateViewport$1 = function () {
7443
6467
  updateViewport(stageRef.current, cullingMargin, viewportRef);
7444
- };
7445
- /**
7446
- * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
7447
- */
6468
+ }; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
7448
6469
 
7449
6470
 
7450
6471
  var isInViewport$1 = function (item) {
7451
- return isInViewport(item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox);
7452
- }; // --------------------------------------------------------------------------
7453
- // 유틸리티 함수: 좌표 변환 캐싱
7454
- // --------------------------------------------------------------------------
7455
-
7456
- /**
7457
- * 폴리곤 좌표 변환 결과를 캐시하고 반환
7458
- * @param polygonData 폴리곤 데이터
7459
- * @returns 변환된 좌표 배열 또는 null
7460
- */
6472
+ return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
6473
+ }; // 폴리곤 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
7461
6474
 
7462
6475
 
7463
6476
  var getOrComputePolygonOffsets = function (polygonData) {
7464
6477
  var cached = offsetCacheRef.current.get(polygonData.id);
7465
6478
  if (cached && Array.isArray(cached)) return cached;
7466
6479
  var result = computePolygonOffsets(polygonData, controller);
7467
-
7468
- if (result) {
7469
- offsetCacheRef.current.set(polygonData.id, result);
7470
- }
7471
-
6480
+ if (!result) return null;
6481
+ offsetCacheRef.current.set(polygonData.id, result);
7472
6482
  return result;
7473
- }; // --------------------------------------------------------------------------
7474
- // 유틸리티 함수: 바운딩 박스 계산
7475
- // --------------------------------------------------------------------------
7476
-
7477
- /**
7478
- * 폴리곤의 바운딩 박스 계산
7479
- *
7480
- * 폴리곤의 모든 좌표를 순회하여 최소/최대 X, Y 값을 계산합니다.
7481
- * Viewport Culling에 사용되며, MultiPolygon 형식을 지원합니다.
7482
- *
7483
- * @param item 폴리곤 데이터
7484
- * @returns 바운딩 박스 (minX, minY, maxX, maxY) 또는 null (좌표 변환 실패 시)
7485
- *
7486
- * @remarks
7487
- * - 성능: O(n), n은 폴리곤의 총 좌표 수
7488
- * - 바운딩 박스는 캐시되어 성능 최적화
7489
- * - MultiPolygon의 모든 좌표를 고려하여 계산
7490
- *
7491
- * @example
7492
- * ```typescript
7493
- * const bbox = computeBoundingBox(item);
7494
- * if (!bbox) return; // 계산 실패
7495
- * // bbox.minX, bbox.minY, bbox.maxX, bbox.maxY 사용
7496
- * ```
7497
- */
6483
+ }; // 폴리곤 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
7498
6484
 
7499
6485
 
7500
6486
  var computeBoundingBox = function (item) {
7501
- // 폴리곤: 모든 좌표의 최소/최대값 계산
7502
6487
  var offsets = getOrComputePolygonOffsets(item);
7503
- if (!offsets) return null;
6488
+ if (!offsets) return null; // 모든 좌표를 순회하며 최소/최대값 찾기
6489
+
7504
6490
  var minX = Infinity,
7505
6491
  minY = Infinity,
7506
6492
  maxX = -Infinity,
@@ -7530,71 +6516,39 @@
7530
6516
  maxX: maxX,
7531
6517
  maxY: maxY
7532
6518
  };
7533
- }; // --------------------------------------------------------------------------
7534
- // 유틸리티 함수: 공간 인덱싱
7535
- // --------------------------------------------------------------------------
7536
-
7537
- /**
7538
- * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
7539
- */
6519
+ }; // 공간 인덱스 빌드 (빠른 Hit Test용)
7540
6520
 
7541
6521
 
7542
6522
  var buildSpatialIndex$1 = function () {
7543
6523
  buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
7544
- }; // --------------------------------------------------------------------------
7545
- // 렌더링 함수 결정 (dataType에 따라)
7546
- // --------------------------------------------------------------------------
7547
-
7548
- /**
7549
- * 외부 렌더링 함수에 전달할 유틸리티 객체
7550
- */
6524
+ }; // 렌더링 유틸리티 객체
7551
6525
 
7552
6526
 
7553
6527
  var renderUtils = {
7554
6528
  getOrComputePolygonOffsets: getOrComputePolygonOffsets,
7555
6529
  getOrComputeMarkerOffset: function () {
7556
6530
  return null;
7557
- } // 폴리곤에서는 사용하지 않음
7558
-
7559
- };
7560
- /**
7561
- * 렌더링 함수 생성 (props 기반)
7562
- */
6531
+ }
6532
+ }; // 렌더링 함수 생성
7563
6533
 
7564
6534
  var renderBase = renderPolygonBase(baseFillColor, baseStrokeColor, baseLineWidth);
7565
- var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth);
7566
- /**
7567
- * Base 레이어 렌더링 (뷰포트 컬링 적용)
7568
- *
7569
- * 🔥 최적화:
7570
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
7571
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
7572
- * 3. 클로저로 최신 데이터 참조
7573
- *
7574
- * 🎯 topOnHover 지원:
7575
- * - renderEvent가 없을 때 Base Layer에서 hover 처리 (fallback)
7576
- * - renderEvent가 있으면 Event Layer에서 처리 (성능 최적화)
7577
- */
6535
+ var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth); // Base Layer 렌더링 (뷰포트 컬링 적용)
7578
6536
 
7579
6537
  var doRenderBase = function () {
7580
6538
  var layer = baseLayerRef.current;
7581
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
7582
-
6539
+ if (!layer) return;
7583
6540
  var shape = layer.findOne('.base-render-shape');
7584
6541
 
7585
6542
  if (!shape) {
7586
- // 최초 생성 (한 번만 실행됨)
7587
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
7588
6543
  shape = new Konva__default["default"].Shape({
7589
6544
  name: 'base-render-shape',
7590
6545
  sceneFunc: function (context, shape) {
7591
6546
  var ctx = context;
7592
- var hovered = hoveredItemRef.current; // 클로저로 최신 ref 참조
6547
+ var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
7593
6548
 
7594
- var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
6549
+ var visibleItems = dataRef.current.filter(function (item) {
7595
6550
  return isInViewport$1(item);
7596
- }) : dataRef.current; // 일반 항목 렌더링
7597
-
6551
+ });
7598
6552
  renderBase({
7599
6553
  ctx: ctx,
7600
6554
  items: visibleItems,
@@ -7608,45 +6562,24 @@
7608
6562
  hitStrokeWidth: 0
7609
6563
  });
7610
6564
  layer.add(shape);
7611
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
7612
-
6565
+ }
7613
6566
 
7614
6567
  layer.batchDraw();
7615
- };
7616
- /**
7617
- * Event 레이어 렌더링 (hover + 선택 상태 표시)
7618
- *
7619
- * 폴리곤의 hover 효과 및 선택 상태를 표시합니다.
7620
- * 자동 렌더링 방식으로 renderPolygonEvent를 사용합니다.
7621
- *
7622
- * @remarks
7623
- * - **성능 최적화**:
7624
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
7625
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
7626
- * 3. 클로저로 최신 데이터 참조
7627
- * - 선택된 항목은 Map에서 O(1)로 조회하여 성능 최적화
7628
- * - 자동 렌더링: 스타일 props(selectedFillColor, hoveredFillColor 등) 기반으로 자동 렌더링
7629
- */
6568
+ }; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
7630
6569
 
7631
6570
 
7632
6571
  var doRenderEvent = function () {
7633
6572
  var layer = eventLayerRef.current;
7634
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
7635
-
6573
+ if (!layer) return;
7636
6574
  var shape = layer.findOne('.event-render-shape');
7637
6575
 
7638
6576
  if (!shape) {
7639
- // 최초 생성 (한 번만 실행됨)
7640
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
7641
6577
  shape = new Konva__default["default"].Shape({
7642
6578
  name: 'event-render-shape',
7643
6579
  sceneFunc: function (context, shape) {
7644
- var ctx = context; // 클로저로 최신 ref 값 참조
7645
- // 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
7646
-
6580
+ var ctx = context;
7647
6581
  var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
7648
- var hovered = hoveredItemRef.current; // 일반 렌더링
7649
-
6582
+ var hovered = hoveredItemRef.current;
7650
6583
  renderEvent({
7651
6584
  ctx: ctx,
7652
6585
  hoveredItem: hovered,
@@ -7660,21 +6593,10 @@
7660
6593
  hitStrokeWidth: 0
7661
6594
  });
7662
6595
  layer.add(shape);
7663
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
7664
-
6596
+ }
7665
6597
 
7666
6598
  layer.batchDraw();
7667
- };
7668
- /**
7669
- * 전체 즉시 렌더링 (IDLE 시 호출)
7670
- *
7671
- * 뷰포트 업데이트, 공간 인덱스 빌드, 모든 레이어 렌더링을 순차적으로 수행합니다.
7672
- *
7673
- * @remarks
7674
- * - 호출 시점: 지도 이동/줌 완료 시, 데이터 변경 시, 리사이즈 시
7675
- * - 순서: 뷰포트 업데이트 → 공간 인덱스 빌드 → Base → Event 렌더링
7676
- * - Animation Layer는 사용하지 않음 (폴리곤 특성)
7677
- */
6599
+ }; // 전체 즉시 렌더링
7678
6600
 
7679
6601
 
7680
6602
  var renderAllImmediate = function () {
@@ -7682,12 +6604,10 @@
7682
6604
  buildSpatialIndex$1();
7683
6605
  doRenderBase();
7684
6606
  doRenderEvent();
7685
- }; // --------------------------------------------------------------------------
7686
- // 이벤트 핸들러: 지도 이벤트
7687
- // --------------------------------------------------------------------------
6607
+ }; // 지도 이벤트 핸들러 생성
7688
6608
 
7689
6609
 
7690
- var _f = createMapEventHandlers({
6610
+ var _e = createMapEventHandlers({
7691
6611
  controller: controller,
7692
6612
  containerRef: containerRef,
7693
6613
  markerRef: markerRef,
@@ -7698,49 +6618,32 @@
7698
6618
  boundingBoxCacheRef: boundingBoxCacheRef,
7699
6619
  renderAllImmediate: renderAllImmediate
7700
6620
  }),
7701
- handleIdle = _f.handleIdle,
7702
- handleZoomStart = _f.handleZoomStart,
7703
- handleZoomEnd = _f.handleZoomEnd,
7704
- handleCenterChanged = _f.handleCenterChanged,
7705
- handleDragStartShared = _f.handleDragStart,
7706
- handleDragEndShared = _f.handleDragEnd;
7707
- /**
7708
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
7709
- */
7710
-
6621
+ handleIdle = _e.handleIdle,
6622
+ handleZoomStart = _e.handleZoomStart,
6623
+ handleZoomEnd = _e.handleZoomEnd,
6624
+ handleCenterChanged = _e.handleCenterChanged,
6625
+ handleDragStartShared = _e.handleDragStart,
6626
+ handleDragEndShared = _e.handleDragEnd;
7711
6627
 
7712
6628
  var handleDragStart = function () {
7713
6629
  handleDragStartShared();
7714
6630
  draggingRef.current = true;
7715
6631
  controller.setMapCursor('grabbing');
7716
6632
  };
7717
- /**
7718
- * 드래그 종료 처리 (커서를 기본으로 복원)
7719
- */
7720
-
7721
6633
 
7722
6634
  var handleDragEnd = function () {
7723
6635
  handleDragEndShared();
7724
6636
  draggingRef.current = false;
7725
6637
  controller.setMapCursor('grab');
7726
- }; // --------------------------------------------------------------------------
7727
- // Hit Test & 상태 관리
7728
- // --------------------------------------------------------------------------
7729
-
7730
- /**
7731
- * 특정 좌표의 폴리곤 데이터 찾기 (Spatial Index 사용)
7732
- *
7733
- * @param offset 검사할 좌표
7734
- * @returns 찾은 폴리곤 데이터 또는 null
7735
- */
6638
+ }; // Hit Test: 특정 좌표의 폴리곤 찾기
7736
6639
 
7737
6640
 
7738
6641
  var findData = function (offset) {
7739
- // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 ~10개)
7740
- var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 폴리곤 체크
6642
+ // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
6643
+ var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
7741
6644
 
7742
6645
  for (var i = candidates.length - 1; i >= 0; i--) {
7743
- var item = candidates[i];
6646
+ var item = candidates[i]; // 정확한 Hit Test: Ray Casting 알고리즘으로 폴리곤 내부 여부 확인
7744
6647
 
7745
6648
  if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
7746
6649
  return item;
@@ -7748,19 +6651,7 @@
7748
6651
  }
7749
6652
 
7750
6653
  return null;
7751
- };
7752
- /**
7753
- * Hover 상태 설정 및 레이어 렌더링
7754
- *
7755
- * 마우스가 폴리곤 위에 올라갔을 때 hover 상태를 설정하고 즉시 렌더링합니다.
7756
- *
7757
- * @param data hover된 폴리곤 데이터 또는 null (hover 해제 시)
7758
- *
7759
- * @remarks
7760
- * - **성능 최적화**: RAF 없이 즉시 렌더링 (16ms 지연 제거)
7761
- * - Event Layer에서 hover 효과 표시
7762
- * - 커서 상태도 자동으로 업데이트됨 (pointer/grab)
7763
- */
6654
+ }; // Hover 상태 설정 및 렌더링
7764
6655
 
7765
6656
 
7766
6657
  var setHovered = function (data) {
@@ -7770,32 +6661,14 @@
7770
6661
  controller.setMapCursor('grabbing');
7771
6662
  } else {
7772
6663
  controller.setMapCursor(data ? 'pointer' : 'grab');
7773
- } // 즉시 렌더링 (RAF 없이)
7774
-
6664
+ }
7775
6665
 
7776
6666
  doRenderEvent();
7777
- };
7778
- /**
7779
- * 클릭 처리 (단일/다중 선택)
7780
- *
7781
- * 폴리곤 클릭 시 선택 상태를 업데이트하고 렌더링을 수행합니다.
7782
- *
7783
- * @param data 클릭된 폴리곤 데이터
7784
- *
7785
- * @remarks
7786
- * - **단일 선택**: 기존 선택 해제 후 새로 선택 (토글 가능)
7787
- * - **다중 선택**: enableMultiSelect가 true면 기존 선택 유지하며 추가/제거
7788
- * - **성능 최적화**:
7789
- * - 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
7790
- * - sceneFunc에서 selectedIds를 체크하여 선택된 폴리곤만 스킵
7791
- * - 객체 생성 오버헤드 제거로 1,000개 이상도 부드럽게 처리
7792
- */
6667
+ }; // 클릭 처리: 선택 상태 업데이트
7793
6668
 
7794
6669
 
7795
6670
  var handleLocalClick = function (data) {
7796
- // 1. 선택 상태 업데이트
7797
6671
  if (enableMultiSelect) {
7798
- // 다중 선택: Set과 Map 동시 업데이트
7799
6672
  var newSelected = new Set(selectedIdsRef.current);
7800
6673
 
7801
6674
  if (newSelected.has(data.id)) {
@@ -7808,7 +6681,6 @@
7808
6681
 
7809
6682
  selectedIdsRef.current = newSelected;
7810
6683
  } else {
7811
- // 단일 선택: 토글
7812
6684
  var newSelected = new Set();
7813
6685
 
7814
6686
  if (!selectedIdsRef.current.has(data.id)) {
@@ -7820,132 +6692,79 @@
7820
6692
  }
7821
6693
 
7822
6694
  selectedIdsRef.current = newSelected;
7823
- } // 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
7824
-
7825
-
7826
- doRenderBase(); // 3. Event Layer 렌더링 (hover 처리)
6695
+ }
7827
6696
 
6697
+ doRenderBase();
7828
6698
  doRenderEvent();
7829
- }; // --------------------------------------------------------------------------
7830
- // 이벤트 핸들러: UI 이벤트
7831
- // --------------------------------------------------------------------------
7832
-
7833
- /**
7834
- * 클릭 이벤트 처리
7835
- *
7836
- * @param event 클릭 이벤트 파라미터
7837
- *
7838
- * @remarks
7839
- * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
7840
- * - 상호작용이 비활성화되어 있으면 스킵
7841
- * - Spatial Index를 사용하여 빠른 Hit Test 수행
7842
- */
6699
+ }; // 클릭 이벤트 핸들러
7843
6700
 
7844
6701
 
7845
6702
  var handleClick = function (event) {
7846
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7847
-
6703
+ if (disableInteractionRef.current) return;
7848
6704
  var clickedOffset = validateEvent(event, context, controller);
7849
6705
  if (!clickedOffset) return;
7850
6706
  var data = findData(clickedOffset);
7851
-
7852
- if (data) {
7853
- handleLocalClick(data);
7854
-
7855
- if (onClick) {
7856
- onClick(data, selectedIdsRef.current);
7857
- }
7858
- }
7859
- };
7860
- /**
7861
- * 마우스 이동 이벤트 처리 (hover 감지)
7862
- *
7863
- * @param event 마우스 이동 이벤트 파라미터
7864
- *
7865
- * @remarks
7866
- * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
7867
- * - 상호작용이 비활성화되어 있으면 스킵
7868
- * - hover 상태 변경 시에만 렌더링 (최적화)
7869
- */
6707
+ if (!data) return;
6708
+ handleLocalClick(data);
6709
+ onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
6710
+ }; // 마우스 이동 이벤트 핸들러 (hover 감지)
7870
6711
 
7871
6712
 
7872
6713
  var handleMouseMove = function (event) {
7873
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7874
-
6714
+ if (disableInteractionRef.current) return;
7875
6715
  var mouseOffset = validateEvent(event, context, controller);
7876
6716
  if (!mouseOffset) return;
7877
6717
  var hoveredItem = findData(mouseOffset);
7878
6718
  var prevHovered = hoveredItemRef.current;
7879
-
7880
- if (prevHovered !== hoveredItem) {
7881
- setHovered(hoveredItem);
7882
- }
7883
- };
7884
- /**
7885
- * 마우스가 canvas를 벗어날 때 hover cleanup
7886
- *
7887
- * 맵 영역 밖으로 마우스가 나갔을 때 hover 상태를 초기화합니다.
7888
- *
7889
- * @remarks
7890
- * - 상호작용이 비활성화되어 있으면 스킵
7891
- * - hover 상태 초기화 및 커서 복원
7892
- */
6719
+ if (prevHovered === hoveredItem) return;
6720
+ setHovered(hoveredItem);
6721
+ }; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
7893
6722
 
7894
6723
 
7895
6724
  var handleMouseLeave = function () {
7896
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7897
-
6725
+ if (disableInteractionRef.current) return;
7898
6726
  var prevHovered = hoveredItemRef.current;
7899
-
7900
- if (prevHovered) {
7901
- hoveredItemRef.current = null;
7902
- controller.setMapCursor('grab');
7903
- doRenderEvent();
7904
- }
7905
- }; // --------------------------------------------------------------------------
7906
- // Lifecycle: DOM 초기화
7907
- // --------------------------------------------------------------------------
6727
+ if (!prevHovered) return;
6728
+ hoveredItemRef.current = null;
6729
+ controller.setMapCursor('grab');
6730
+ doRenderEvent();
6731
+ }; // DOM 초기화
7908
6732
 
7909
6733
 
7910
6734
  React.useEffect(function () {
7911
6735
  divElement.style.width = 'fit-content';
7912
6736
  return function () {
7913
- if (markerRef.current) {
7914
- controller.clearDrawable(markerRef.current);
7915
- markerRef.current = undefined;
7916
- }
6737
+ if (!markerRef.current) return;
6738
+ controller.clearDrawable(markerRef.current);
6739
+ markerRef.current = undefined;
7917
6740
  };
7918
- }, []); // --------------------------------------------------------------------------
7919
- // Lifecycle: 마커 생성/업데이트
7920
- // --------------------------------------------------------------------------
6741
+ }, []); // 마커 생성/업데이트
7921
6742
 
7922
6743
  React.useEffect(function () {
7923
- if (options) {
7924
- var bounds = controller.getCurrBounds();
6744
+ if (!options) return;
6745
+ var bounds = controller.getCurrBounds();
7925
6746
 
7926
- var markerOptions = tslib.__assign({
7927
- position: bounds.nw
7928
- }, options);
6747
+ var markerOptions = tslib.__assign({
6748
+ position: bounds.nw
6749
+ }, options);
7929
6750
 
7930
- if (markerRef.current) {
7931
- controller.updateMarker(markerRef.current, markerOptions);
7932
- } else {
7933
- markerRef.current = new Marker(markerOptions);
7934
- markerRef.current.element = divElement;
7935
- controller.createMarker(markerRef.current);
6751
+ if (markerRef.current) {
6752
+ controller.updateMarker(markerRef.current, markerOptions);
6753
+ return;
6754
+ }
7936
6755
 
7937
- if (divElement.parentElement) {
7938
- divElement.parentElement.style.pointerEvents = 'none';
7939
- }
6756
+ markerRef.current = new Marker(markerOptions);
6757
+ markerRef.current.element = divElement;
6758
+ controller.createMarker(markerRef.current);
7940
6759
 
7941
- if (options.zIndex !== undefined) {
7942
- controller.setMarkerZIndex(markerRef.current, options.zIndex);
7943
- }
7944
- }
6760
+ if (divElement.parentElement) {
6761
+ divElement.parentElement.style.pointerEvents = 'none';
7945
6762
  }
7946
- }, [options]); // --------------------------------------------------------------------------
7947
- // Lifecycle: Konva 초기화 및 이벤트 리스너 등록
7948
- // --------------------------------------------------------------------------
6763
+
6764
+ if (options.zIndex !== undefined) {
6765
+ controller.setMarkerZIndex(markerRef.current, options.zIndex);
6766
+ }
6767
+ }, [options]); // Konva 초기화 및 이벤트 리스너 등록
7949
6768
 
7950
6769
  React.useEffect(function () {
7951
6770
  var mapDiv = controller.mapDivElement;
@@ -7954,11 +6773,9 @@
7954
6773
  width: mapDiv.offsetWidth,
7955
6774
  height: mapDiv.offsetHeight
7956
6775
  });
7957
- stageRef.current = stage; // 레이어 최적화 설정
7958
-
6776
+ stageRef.current = stage;
7959
6777
  var baseLayer = new Konva__default["default"].Layer({
7960
- listening: false // 이벤트 리스닝 비활성화로 성능 향상
7961
-
6778
+ listening: false
7962
6779
  });
7963
6780
  var eventLayer = new Konva__default["default"].Layer({
7964
6781
  listening: false
@@ -7966,13 +6783,11 @@
7966
6783
  baseLayerRef.current = baseLayer;
7967
6784
  eventLayerRef.current = eventLayer;
7968
6785
  stage.add(baseLayer);
7969
- stage.add(eventLayer); // 초기 뷰포트 설정
7970
-
7971
- updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
6786
+ stage.add(eventLayer);
6787
+ updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
7972
6788
 
7973
6789
  var resizeRafId = null;
7974
6790
  var resizeObserver = new ResizeObserver(function () {
7975
- // RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
7976
6791
  if (resizeRafId !== null) {
7977
6792
  cancelAnimationFrame(resizeRafId);
7978
6793
  }
@@ -7995,10 +6810,9 @@
7995
6810
  controller.addEventListener('CLICK', handleClick);
7996
6811
  controller.addEventListener('MOUSEMOVE', handleMouseMove);
7997
6812
  controller.addEventListener('DRAGSTART', handleDragStart);
7998
- controller.addEventListener('DRAGEND', handleDragEnd); // 맵 컨테이너에 mouseleave 이벤트 추가
7999
-
6813
+ controller.addEventListener('DRAGEND', handleDragEnd);
8000
6814
  mapDiv.addEventListener('mouseleave', handleMouseLeave);
8001
- renderAllImmediate(); // Context 사용 시 컴포넌트 등록 (다중 인스턴스 관리)
6815
+ renderAllImmediate(); // Context 사용 시 컴포넌트 등록
8002
6816
 
8003
6817
  var componentInstance = null;
8004
6818
 
@@ -8017,22 +6831,17 @@
8017
6831
  },
8018
6832
  isInteractionDisabled: function () {
8019
6833
  return disableInteractionRef.current;
8020
- } // 🚫 상호작용 비활성화 여부 반환
8021
-
6834
+ }
8022
6835
  };
8023
6836
  context.registerComponent(componentInstance);
8024
- } // Cleanup 함수
8025
-
6837
+ }
8026
6838
 
8027
6839
  return function () {
8028
- // RAF 정리
8029
6840
  if (resizeRafId !== null) {
8030
6841
  cancelAnimationFrame(resizeRafId);
8031
- } // 옵저버 정리
8032
-
8033
-
8034
- resizeObserver.disconnect(); // 이벤트 리스너 정리
6842
+ }
8035
6843
 
6844
+ resizeObserver.disconnect();
8036
6845
  controller.removeEventListener('IDLE', handleIdle);
8037
6846
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
8038
6847
  controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
@@ -8041,57 +6850,41 @@
8041
6850
  controller.removeEventListener('MOUSEMOVE', handleMouseMove);
8042
6851
  controller.removeEventListener('DRAGSTART', handleDragStart);
8043
6852
  controller.removeEventListener('DRAGEND', handleDragEnd);
8044
- mapDiv.removeEventListener('mouseleave', handleMouseLeave); // Context 정리
6853
+ mapDiv.removeEventListener('mouseleave', handleMouseLeave);
8045
6854
 
8046
6855
  if (context && componentInstance) {
8047
6856
  context.unregisterComponent(componentInstance);
8048
- } // Konva 리소스 정리
8049
-
6857
+ }
8050
6858
 
8051
6859
  baseLayer.destroyChildren();
8052
6860
  eventLayer.destroyChildren();
8053
- stage.destroy(); // 캐시 정리
8054
-
6861
+ stage.destroy();
8055
6862
  offsetCacheRef.current.clear();
8056
6863
  boundingBoxCacheRef.current.clear();
8057
6864
  spatialIndexRef.current.clear();
8058
6865
  };
8059
- }, []); // 초기화는 한 번만
8060
- // --------------------------------------------------------------------------
8061
- // Lifecycle: disableInteraction 동기화
8062
- // --------------------------------------------------------------------------
6866
+ }, []); // disableInteraction 동기화
8063
6867
 
8064
6868
  React.useEffect(function () {
8065
6869
  disableInteractionRef.current = disableInteraction;
8066
- }, [disableInteraction]); // --------------------------------------------------------------------------
8067
- // Lifecycle: 외부 selectedItems 동기화
8068
- // --------------------------------------------------------------------------
6870
+ }, [disableInteraction]); // 외부 selectedItems 동기화
8069
6871
 
8070
6872
  React.useEffect(function () {
8071
6873
  if (!stageRef.current) return;
8072
- syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef); // 렌더링
8073
-
6874
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
8074
6875
  doRenderBase();
8075
6876
  doRenderEvent();
8076
- }, [externalSelectedItems]); // 배열 자체를 dependency로 사용
8077
- // --------------------------------------------------------------------------
8078
- // Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
8079
- // --------------------------------------------------------------------------
6877
+ }, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
8080
6878
 
8081
6879
  React.useEffect(function () {
8082
- if (!stageRef.current) return; // Ref 동기화
8083
-
8084
- selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
8085
-
6880
+ if (!stageRef.current) return;
6881
+ selectedItemRef.current = externalSelectedItem;
8086
6882
  doRenderEvent();
8087
- }, [externalSelectedItem]); // --------------------------------------------------------------------------
8088
- // Lifecycle: 데이터 변경 시 렌더링
8089
- // --------------------------------------------------------------------------
6883
+ }, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
8090
6884
 
8091
6885
  React.useEffect(function () {
8092
- if (!stageRef.current) return; // dataRef 동기화
8093
-
8094
- dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
6886
+ if (!stageRef.current) return;
6887
+ dataRef.current = data;
8095
6888
 
8096
6889
  if (containerRef.current) {
8097
6890
  containerRef.current.style.transform = '';
@@ -8101,26 +6894,10 @@
8101
6894
  accumTranslateRef.current = {
8102
6895
  x: 0,
8103
6896
  y: 0
8104
- }; // 캐시 정리 (새 데이터이므로 기존 캐시는 무효)
8105
-
6897
+ };
8106
6898
  offsetCacheRef.current.clear();
8107
6899
  boundingBoxCacheRef.current.clear();
8108
- /**
8109
- * 선택 상태 동기화 (최적화 버전)
8110
- *
8111
- * data가 변경되면 selectedItemsMapRef도 업데이트 필요
8112
- * (참조가 바뀌므로 기존 Map의 데이터는 stale 상태)
8113
- *
8114
- * 🔥 중요: 화면 밖 데이터도 선택 상태 유지!
8115
- * - 현재 data에 있으면 최신 데이터로 업데이트
8116
- * - 없으면 기존 selectedItemsMapRef의 데이터 유지
8117
- *
8118
- * 최적화: data를 Map으로 먼저 변환하여 find() 순회 제거
8119
- * - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
8120
- */
8121
-
8122
- selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current); // 즉시 렌더링
8123
-
6900
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
8124
6901
  renderAllImmediate();
8125
6902
  }, [data]);
8126
6903
  return reactDom.createPortal(React__default["default"].createElement("div", {