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

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