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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/dist/components/mint-map/core/advanced/shared/context.d.ts +9 -71
  2. package/dist/components/mint-map/core/advanced/shared/context.js +43 -137
  3. package/dist/components/mint-map/core/advanced/shared/helpers.d.ts +5 -13
  4. package/dist/components/mint-map/core/advanced/shared/helpers.js +8 -20
  5. package/dist/components/mint-map/core/advanced/shared/hooks.d.ts +6 -76
  6. package/dist/components/mint-map/core/advanced/shared/hooks.js +18 -112
  7. package/dist/components/mint-map/core/advanced/shared/performance.d.ts +9 -188
  8. package/dist/components/mint-map/core/advanced/shared/performance.js +53 -229
  9. package/dist/components/mint-map/core/advanced/shared/types.d.ts +18 -153
  10. package/dist/components/mint-map/core/advanced/shared/types.js +0 -1
  11. package/dist/components/mint-map/core/advanced/shared/utils.d.ts +21 -126
  12. package/dist/components/mint-map/core/advanced/shared/utils.js +43 -151
  13. package/dist/components/mint-map/core/advanced/shared/viewport.d.ts +4 -34
  14. package/dist/components/mint-map/core/advanced/shared/viewport.js +4 -34
  15. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.d.ts +22 -74
  16. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.js +122 -516
  17. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.d.ts +26 -76
  18. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.js +118 -432
  19. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.d.ts +3 -3
  20. package/dist/index.es.js +409 -1632
  21. package/dist/index.umd.js +409 -1632
  22. package/package.json +1 -1
package/dist/index.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,41 @@ 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; // 마커 중심점 기준으로 박스 영역 계산 (꼬리는 제외)
1045
953
 
1046
954
  var x = markerOffset.x - boxWidth / 2;
1047
- var y = markerOffset.y - boxHeight - tailHeight; // 🔥 꼬리만큼 위로!
955
+ var y = markerOffset.y - boxHeight - tailHeight; // 클릭 위치가 박스 영역 내부에 있는지 확인
1048
956
 
1049
957
  return clickedOffset.x >= x && clickedOffset.x <= x + boxWidth && clickedOffset.y >= y && clickedOffset.y <= y + boxHeight;
1050
- };
958
+ }; // Hex 색상을 RGBA로 변환
959
+
1051
960
  var hexToRgba = function (hexColor, alpha) {
1052
961
  if (alpha === void 0) {
1053
962
  alpha = 1;
1054
- } // NOTE: 입력된 hexColor에서 "#" 제거
1055
-
963
+ }
1056
964
 
1057
- var hex = hexColor.replace('#', ''); // NOTE: 6자리일 경우 알파 값은 사용자가 제공한 alpha 값으로 설정
965
+ var hex = hexColor.replace('#', '');
1058
966
 
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, ")");
967
+ if (hex.length !== 6) {
968
+ throw new Error('Invalid hex color format');
1064
969
  }
1065
970
 
1066
- throw new Error('Invalid hex color format');
971
+ var r = parseInt(hex.substring(0, 2), 16);
972
+ var g = parseInt(hex.substring(2, 4), 16);
973
+ var b = parseInt(hex.substring(4, 6), 16);
974
+ return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
1067
975
  };
1068
976
  var tempCanvas = document.createElement('canvas');
1069
977
  var tempCtx = tempCanvas.getContext('2d');
1070
978
  /**
1071
- * 텍스트 박스의 너비를 계산합니다.
1072
- *
1073
- * Canvas 2D Context의 measureText()를 사용하여 텍스트의 실제 너비를 계산하고,
1074
- * 패딩과 최소 너비를 고려하여 최종 너비를 반환합니다.
979
+ * 텍스트 박스 너비 계산
1075
980
  *
1076
981
  * @param params 파라미터 객체
1077
982
  * @param params.text 측정할 텍스트
1078
- * @param params.fontConfig 폰트 설정 (예: 'bold 16px Arial')
1079
- * @param params.padding 텍스트 박스에 적용할 패딩 값 (px)
983
+ * @param params.fontConfig 폰트 설정
984
+ * @param params.padding 패딩 값 (px)
1080
985
  * @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
- * ```
986
+ * @returns 계산된 텍스트 박스 너비 (px)
1096
987
  */
1097
988
 
1098
989
  var calculateTextBoxWidth = function (_a) {
@@ -1110,62 +1001,25 @@ var WoongCanvasContext = createContext(null);
1110
1001
  /**
1111
1002
  * WoongCanvasProvider 컴포넌트
1112
1003
  *
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
- * ```
1004
+ * 다중 WoongCanvas 인스턴스를 관리하고 zIndex 기반 이벤트 우선순위를 처리합니다.
1131
1005
  */
1132
1006
 
1133
1007
  var WoongCanvasProvider = function (_a) {
1134
1008
  var children = _a.children;
1135
- var controller = useMintMapController(); // Refs
1136
-
1009
+ var controller = useMintMapController();
1137
1010
  var componentsRef = useRef([]);
1138
1011
  var currentHoveredRef = useRef(null);
1139
1012
  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
- */
1013
+ var draggingRef = useRef(false); // 컴포넌트 등록 (zIndex 내림차순 정렬)
1150
1014
 
1151
1015
  var registerComponent = useCallback(function (instance) {
1152
1016
  componentsRef.current.push(instance);
1153
1017
  componentsRef.current.sort(function (a, b) {
1154
1018
  return b.zIndex - a.zIndex;
1155
1019
  });
1156
- }, []);
1157
- /**
1158
- * 컴포넌트 등록 해제
1159
- *
1160
- * 컴포넌트 인스턴스를 등록 해제합니다.
1161
- * hover 중이던 컴포넌트면 hover 상태도 초기화합니다.
1162
- *
1163
- * @template T 마커/폴리곤 데이터의 추가 속성 타입
1164
- * @param instance 등록 해제할 컴포넌트 인스턴스
1165
- */
1020
+ }, []); // 컴포넌트 등록 해제
1166
1021
 
1167
1022
  var unregisterComponent = useCallback(function (instance) {
1168
- // Hover 중이던 컴포넌트면 초기화
1169
1023
  if (currentHoveredRef.current === instance) {
1170
1024
  currentHoveredRef.current = null;
1171
1025
  currentHoveredDataRef.current = null;
@@ -1174,118 +1028,77 @@ var WoongCanvasProvider = function (_a) {
1174
1028
  componentsRef.current = componentsRef.current.filter(function (c) {
1175
1029
  return c !== instance;
1176
1030
  });
1177
- }, []);
1178
- /**
1179
- * 전역 클릭 핸들러 (zIndex 우선순위)
1180
- *
1181
- * 모든 등록된 WoongCanvas 컴포넌트 중 zIndex가 높은 컴포넌트부터 클릭 이벤트를 처리합니다.
1182
- *
1183
- * @param event 클릭 이벤트 파라미터
1184
- *
1185
- * @remarks
1186
- * - zIndex가 높은 컴포넌트부터 순회하여 첫 번째 히트만 처리
1187
- * - 상호작용이 비활성화된 컴포넌트는 스킵
1188
- * - Context가 없으면 각 컴포넌트의 로컬 핸들러가 처리
1189
- */
1031
+ }, []); // 전역 클릭 핸들러 (zIndex 우선순위)
1190
1032
 
1191
1033
  var handleGlobalClick = useCallback(function (event) {
1192
- var _a;
1034
+ var _a, _b;
1193
1035
 
1194
1036
  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]; // 🚫 상호작용이 비활성화된 컴포넌트는 스킵
1037
+ var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
1199
1038
 
1039
+ for (var _i = 0, _c = componentsRef.current; _i < _c.length; _i++) {
1040
+ var component = _c[_i];
1200
1041
  if (component.isInteractionDisabled()) continue;
1201
1042
  var data = component.findData(clickedOffset);
1043
+ if (!data) continue; // 첫 번째로 찾은 항목만 처리하고 종료 (zIndex 우선순위)
1202
1044
 
1203
- if (data) {
1204
- component.handleLocalClick(data);
1205
-
1206
- if (component.onClick) {
1207
- component.onClick(data, component.getSelectedIds());
1208
- }
1209
-
1210
- return; // 첫 번째 히트만 처리
1211
- }
1045
+ component.handleLocalClick(data);
1046
+ (_b = component.onClick) === null || _b === void 0 ? void 0 : _b.call(component, data, component.getSelectedIds());
1047
+ return;
1212
1048
  }
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
- */
1049
+ }, [controller]); // 전역 마우스 이동 핸들러 (zIndex 우선순위)
1227
1050
 
1228
1051
  var handleGlobalMouseMove = useCallback(function (event) {
1229
1052
  var _a;
1230
1053
 
1231
1054
  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
-
1055
+ var mouseOffset = controller.positionToOffset(event.param.position);
1234
1056
  var newHoveredComponent = null;
1235
- var newHoveredData = null;
1057
+ var newHoveredData = null; // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
1236
1058
 
1237
1059
  for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
1238
- var component = _b[_i]; // 🚫 상호작용이 비활성화된 컴포넌트는 스킵
1239
-
1060
+ var component = _b[_i];
1240
1061
  if (component.isInteractionDisabled()) continue;
1241
1062
  var data = component.findData(mouseOffset);
1063
+ if (!data) continue; // 첫 번째로 찾은 항목만 hover 처리 (zIndex 우선순위)
1242
1064
 
1243
- if (data) {
1244
- newHoveredComponent = component;
1245
- newHoveredData = data;
1246
- break; // 번째 히트만 처리
1247
- }
1248
- } // Hover 상태 변경 감지 (최적화: 별도 ref로 직접 비교)
1249
-
1065
+ newHoveredComponent = component;
1066
+ newHoveredData = data;
1067
+ break;
1068
+ } // hover 상태가 변경되지 않았으면 종료 (불필요한 렌더링 방지)
1250
1069
 
1251
- if (currentHoveredRef.current !== newHoveredComponent || currentHoveredDataRef.current !== newHoveredData) {
1252
- // 이전 hover 해제
1253
- if (currentHoveredRef.current) {
1254
- currentHoveredRef.current.setHovered(null);
1255
1070
 
1256
- if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
1257
- currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
1258
- }
1259
- } // 새 hover 설정
1071
+ if (currentHoveredRef.current === newHoveredComponent && currentHoveredDataRef.current === newHoveredData) {
1072
+ return;
1073
+ } // 기존 hover 항목에 mouseOut 이벤트 발생
1260
1074
 
1261
1075
 
1262
- if (newHoveredComponent && newHoveredData) {
1263
- newHoveredComponent.setHovered(newHoveredData);
1076
+ if (currentHoveredRef.current) {
1077
+ currentHoveredRef.current.setHovered(null);
1264
1078
 
1265
- if (newHoveredComponent.onMouseOver) {
1266
- newHoveredComponent.onMouseOver(newHoveredData);
1267
- }
1079
+ if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
1080
+ currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
1268
1081
  }
1082
+ } // 새 hover 항목에 mouseOver 이벤트 발생
1083
+
1269
1084
 
1270
- currentHoveredRef.current = newHoveredComponent;
1271
- currentHoveredDataRef.current = newHoveredData;
1085
+ if (newHoveredComponent && newHoveredData) {
1086
+ newHoveredComponent.setHovered(newHoveredData);
1087
+
1088
+ if (newHoveredComponent.onMouseOver) {
1089
+ newHoveredComponent.onMouseOver(newHoveredData);
1090
+ }
1272
1091
  }
1273
- }, [controller]);
1274
- /**
1275
- * 줌/드래그 시작 (마우스 이동 이벤트 무시)
1276
- */
1277
1092
 
1093
+ currentHoveredRef.current = newHoveredComponent;
1094
+ currentHoveredDataRef.current = newHoveredData;
1095
+ }, [controller]);
1278
1096
  var handleZoomStart = useCallback(function () {
1279
1097
  draggingRef.current = true;
1280
1098
  }, []);
1281
- /**
1282
- * 지도 idle (마우스 이동 이벤트 재개)
1283
- */
1284
-
1285
1099
  var handleIdle = useCallback(function () {
1286
1100
  draggingRef.current = false;
1287
- }, []); // 이벤트 리스너 등록
1288
-
1101
+ }, []);
1289
1102
  useEffect(function () {
1290
1103
  controller.addEventListener('CLICK', handleGlobalClick);
1291
1104
  controller.addEventListener('MOUSEMOVE', handleGlobalMouseMove);
@@ -1297,8 +1110,7 @@ var WoongCanvasProvider = function (_a) {
1297
1110
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
1298
1111
  controller.removeEventListener('IDLE', handleIdle);
1299
1112
  };
1300
- }, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]); // Context value 메모이제이션
1301
-
1113
+ }, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]);
1302
1114
  var contextValue = useMemo(function () {
1303
1115
  return {
1304
1116
  registerComponent: registerComponent,
@@ -1312,102 +1124,40 @@ var WoongCanvasProvider = function (_a) {
1312
1124
  /**
1313
1125
  * WoongCanvas Context Hook
1314
1126
  *
1315
- * WoongCanvasProvider로 감싸진 컴포넌트에서 Context에 접근할 수 있는 hook입니다.
1316
- *
1317
1127
  * @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
1128
  */
1331
1129
 
1332
1130
  var useWoongCanvasContext = function () {
1333
- var context = useContext(WoongCanvasContext);
1334
- return context;
1131
+ return useContext(WoongCanvasContext);
1335
1132
  };
1336
1133
 
1337
- // ============================================================================
1338
- // 성능 최적화 상수 (30,000개 마커/폴리곤 기준 최적화)
1339
- // ============================================================================
1340
-
1341
1134
  /**
1342
- * 공간 인덱스 그리드 셀 크기 (px)
1343
- *
1344
- * 최적값 계산:
1345
- * - 목표: 클릭 시 셀당 10~30개 항목만 체크 (빠른 Hit Test)
1346
- * - 화면 크기: 1920×1080 기준
1347
- * - 30,000개 항목 → 50px 셀 크기 = 약 800개 셀 = 셀당 ~37개
1135
+ * 공간 인덱스 그리드 셀 크기 (픽셀 단위)
1348
1136
  *
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 느림
1137
+ * @default 50
1356
1138
  */
1357
1139
  var SPATIAL_GRID_CELL_SIZE = 50;
1358
1140
  /**
1359
- * 뷰포트 컬링 여유 공간 (px)
1141
+ * 뷰포트 컬링 여유 공간 (픽셀 단위)
1360
1142
  *
1361
- * 화면 밖 100px까지 렌더링하여 스크롤 시 부드러운 전환
1362
- * 30,000개 중 실제 렌더링: 화면에 보이는 1,000~3,000개만
1143
+ * @default 100
1363
1144
  */
1364
1145
 
1365
1146
  var DEFAULT_CULLING_MARGIN = 100;
1366
1147
  /**
1367
1148
  * LRU 캐시 최대 항목 수
1368
1149
  *
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() 호출되므로 메모리 누적 없음
1150
+ * @default 30000
1380
1151
  */
1381
1152
 
1382
1153
  var DEFAULT_MAX_CACHE_SIZE = 30000;
1383
1154
  /**
1384
- * LRU (Least Recently Used) Cache
1155
+ * LRU Cache (Least Recently Used)
1385
1156
  *
1386
- * 메모리 제한을 위한 캐시 구현입니다. WoongCanvas 컴포넌트에서 좌표 변환 결과를 캐싱하는데 사용됩니다.
1157
+ * 좌표 변환 결과를 캐싱하기 위한 캐시 구현
1387
1158
  *
1388
1159
  * @template K 캐시 키 타입
1389
1160
  * @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
1161
  */
1412
1162
 
1413
1163
  var LRUCache =
@@ -1420,63 +1170,43 @@ function () {
1420
1170
 
1421
1171
  this.cache = new Map();
1422
1172
  this.maxSize = maxSize;
1423
- }
1424
- /**
1425
- * 캐시에서 값 조회
1426
- *
1427
- * @param key 조회할 키
1428
- * @returns 캐시된 값 또는 undefined (캐시 미스 시)
1429
- *
1430
- * @remarks
1431
- * - 성능: O(1) 해시 조회
1432
- * - 최적화: delete+set 제거로 읽기 성능 대폭 향상
1433
- */
1173
+ } // 캐시에서 값 조회
1434
1174
 
1435
1175
 
1436
1176
  LRUCache.prototype.get = function (key) {
1437
1177
  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
- */
1178
+ }; // 캐시에 값 저장 (FIFO eviction)
1450
1179
 
1451
1180
 
1452
1181
  LRUCache.prototype.set = function (key, value) {
1453
1182
  var exists = this.cache.has(key);
1454
1183
 
1455
1184
  if (exists) {
1456
- // 기존 항목 업데이트: 단순 덮어쓰기 (크기 변화 없음)
1457
1185
  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;
1186
+ return;
1187
+ }
1463
1188
 
1464
- if (firstKey !== undefined) {
1465
- this.cache.delete(firstKey);
1466
- }
1467
- }
1189
+ if (this.cache.size >= this.maxSize) {
1190
+ var firstKey = this.cache.keys().next().value;
1468
1191
 
1469
- this.cache.set(key, value);
1192
+ if (firstKey !== undefined) {
1193
+ this.cache.delete(firstKey);
1194
+ }
1470
1195
  }
1471
- };
1196
+
1197
+ this.cache.set(key, value);
1198
+ }; // 캐시 초기화
1199
+
1472
1200
 
1473
1201
  LRUCache.prototype.clear = function () {
1474
1202
  this.cache.clear();
1475
- };
1203
+ }; // 캐시 크기 반환
1204
+
1476
1205
 
1477
1206
  LRUCache.prototype.size = function () {
1478
1207
  return this.cache.size;
1479
- };
1208
+ }; // 키 존재 여부 확인
1209
+
1480
1210
 
1481
1211
  LRUCache.prototype.has = function (key) {
1482
1212
  return this.cache.has(key);
@@ -1486,16 +1216,10 @@ function () {
1486
1216
  }();
1487
1217
  /**
1488
1218
  * Spatial Hash Grid (공간 해시 그리드)
1489
- * 공간 인덱싱을 위한 그리드 기반 자료구조 (개선 버전)
1490
1219
  *
1491
- * 개선 사항:
1492
- * 1. 중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전
1493
- * 2. 메모리 누수 방지: 기존 항목 자동 제거
1494
- * 3. 성능 최적화: 불필요한 배열 생성 최소화
1220
+ * 빠른 Hit Test를 위한 그리드 기반 공간 인덱싱 자료구조
1495
1221
  *
1496
- * 사용 사례:
1497
- * - 빠른 Hit Test (마우스 클릭 시 어떤 마커/폴리곤인지 찾기)
1498
- * - 30,000개 항목 → 클릭 위치 주변 ~10개만 체크 (3,000배 빠름)
1222
+ * @template T 인덱싱할 항목 타입
1499
1223
  */
1500
1224
 
1501
1225
  var SpatialHashGrid =
@@ -1509,28 +1233,24 @@ function () {
1509
1233
  this.cellSize = cellSize;
1510
1234
  this.grid = new Map();
1511
1235
  this.itemToCells = new Map();
1512
- }
1513
- /**
1514
- * 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
1515
- */
1236
+ } // 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
1516
1237
 
1517
1238
 
1518
1239
  SpatialHashGrid.prototype.getCellKey = function (x, y) {
1240
+ // 좌표를 셀 크기로 나눈 몫으로 셀 인덱스 계산
1519
1241
  var cellX = Math.floor(x / this.cellSize);
1520
1242
  var cellY = Math.floor(y / this.cellSize);
1521
1243
  return "".concat(cellX, ",").concat(cellY);
1522
- };
1523
- /**
1524
- * 바운딩 박스가 걸치는 모든 셀 키 배열 반환
1525
- */
1244
+ }; // 바운딩 박스가 걸치는 모든 셀 키 배열 반환
1526
1245
 
1527
1246
 
1528
1247
  SpatialHashGrid.prototype.getCellsForBounds = function (minX, minY, maxX, maxY) {
1529
- var cells = [];
1248
+ var cells = []; // 바운딩 박스가 걸치는 셀 범위 계산
1249
+
1530
1250
  var startCellX = Math.floor(minX / this.cellSize);
1531
1251
  var startCellY = Math.floor(minY / this.cellSize);
1532
1252
  var endCellX = Math.floor(maxX / this.cellSize);
1533
- var endCellY = Math.floor(maxY / this.cellSize);
1253
+ var endCellY = Math.floor(maxY / this.cellSize); // 바운딩 박스가 걸치는 모든 셀을 배열에 추가
1534
1254
 
1535
1255
  for (var x = startCellX; x <= endCellX; x++) {
1536
1256
  for (var y = startCellY; y <= endCellY; y++) {
@@ -1539,31 +1259,15 @@ function () {
1539
1259
  }
1540
1260
 
1541
1261
  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
- */
1262
+ }; // 항목 추가 (바운딩 박스 기반, 중복 삽입 방지)
1559
1263
 
1560
1264
 
1561
1265
  SpatialHashGrid.prototype.insert = function (item, minX, minY, maxX, maxY) {
1562
- // 1. 기존 항목 제거 (중복 방지)
1563
- this.remove(item); // 2. 위치에 삽입
1266
+ // 기존 항목 제거 (중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전)
1267
+ this.remove(item); // 바운딩 박스가 걸치는 모든 셀에 항목 등록
1564
1268
 
1565
1269
  var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
1566
- this.itemToCells.set(item, cells);
1270
+ this.itemToCells.set(item, cells); // 항목과 셀의 매핑 저장 (제거 시 필요)
1567
1271
 
1568
1272
  for (var _i = 0, cells_1 = cells; _i < cells_1.length; _i++) {
1569
1273
  var cell = cells_1[_i];
@@ -1574,24 +1278,12 @@ function () {
1574
1278
 
1575
1279
  this.grid.get(cell).push(item);
1576
1280
  }
1577
- };
1578
- /**
1579
- * 항목 제거
1580
- *
1581
- * 공간 인덱스에서 항목을 제거합니다.
1582
- *
1583
- * @param item 제거할 항목
1584
- *
1585
- * @remarks
1586
- * - 메모리 누수 방지: 모든 셀에서 참조 완전 제거
1587
- * - 빈 셀 정리: 항목이 없어진 셀은 자동으로 정리됨
1588
- * - 성능: O(셀 개수), 보통 O(1)
1589
- */
1281
+ }; // 항목 제거 (모든 셀에서 참조 제거)
1590
1282
 
1591
1283
 
1592
1284
  SpatialHashGrid.prototype.remove = function (item) {
1593
1285
  var prevCells = this.itemToCells.get(item);
1594
- if (!prevCells) return; // 셀에서 항목 제거
1286
+ if (!prevCells) return; // 항목이 등록된 모든 셀에서 참조 제거 (메모리 누수 방지)
1595
1287
 
1596
1288
  for (var _i = 0, prevCells_1 = prevCells; _i < prevCells_1.length; _i++) {
1597
1289
  var cell = prevCells_1[_i];
@@ -1602,86 +1294,39 @@ function () {
1602
1294
 
1603
1295
  if (index !== -1) {
1604
1296
  cellItems.splice(index, 1);
1605
- } // 빈 셀 정리 (메모리 효율)
1297
+ } // 빈 셀 정리 (메모리 효율: 사용하지 않는 셀 제거)
1606
1298
 
1607
1299
 
1608
1300
  if (cellItems.length === 0) {
1609
1301
  this.grid.delete(cell);
1610
1302
  }
1611
1303
  }
1612
- }
1304
+ } // 항목과 셀의 매핑 제거
1305
+
1613
1306
 
1614
1307
  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
- */
1308
+ }; // 항목 위치 업데이트 (remove + insert)
1627
1309
 
1628
1310
 
1629
1311
  SpatialHashGrid.prototype.update = function (item, minX, minY, maxX, maxY) {
1630
1312
  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
- */
1313
+ }; // 점 주변의 항목 조회 (Hit Test용)
1656
1314
 
1657
1315
 
1658
1316
  SpatialHashGrid.prototype.queryPoint = function (x, y) {
1317
+ // 클릭 위치가 속한 셀의 모든 항목 조회 (O(1) 수준의 빠른 조회)
1659
1318
  var cellKey = this.getCellKey(x, y);
1660
1319
  var items = this.grid.get(cellKey); // 빈 배열 재사용 (메모리 할당 최소화)
1661
1320
 
1662
1321
  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
- */
1322
+ }; // 영역 내 항목 조회 (Viewport Culling용)
1680
1323
 
1681
1324
 
1682
1325
  SpatialHashGrid.prototype.queryBounds = function (minX, minY, maxX, maxY) {
1326
+ // 영역이 걸치는 모든 셀 찾기
1683
1327
  var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
1684
- var results = new Set();
1328
+ var results = new Set(); // 중복 제거를 위해 Set 사용
1329
+ // 각 셀의 모든 항목을 결과에 추가
1685
1330
 
1686
1331
  for (var _i = 0, cells_2 = cells; _i < cells_2.length; _i++) {
1687
1332
  var cell = cells_2[_i];
@@ -1690,48 +1335,24 @@ function () {
1690
1335
  if (items) {
1691
1336
  for (var _a = 0, items_1 = items; _a < items_1.length; _a++) {
1692
1337
  var item = items_1[_a];
1693
- results.add(item);
1338
+ results.add(item); // Set이므로 중복 자동 제거
1694
1339
  }
1695
1340
  }
1696
1341
  }
1697
1342
 
1698
1343
  return Array.from(results);
1699
- };
1700
- /**
1701
- * 항목 존재 여부 확인
1702
- *
1703
- * @param item 확인할 항목
1704
- * @returns 항목이 인덱스에 있으면 true, 아니면 false
1705
- *
1706
- * @remarks
1707
- * - 성능: O(1) 해시 조회
1708
- */
1344
+ }; // 항목 존재 여부 확인
1709
1345
 
1710
1346
 
1711
1347
  SpatialHashGrid.prototype.has = function (item) {
1712
1348
  return this.itemToCells.has(item);
1713
- };
1714
- /**
1715
- * 전체 초기화
1716
- */
1349
+ }; // 전체 초기화
1717
1350
 
1718
1351
 
1719
1352
  SpatialHashGrid.prototype.clear = function () {
1720
1353
  this.grid.clear();
1721
1354
  this.itemToCells.clear();
1722
- };
1723
- /**
1724
- * 통계 정보
1725
- *
1726
- * 공간 인덱스의 현재 상태를 반환합니다. 디버깅 및 성능 분석에 유용합니다.
1727
- *
1728
- * @returns 통계 정보 객체
1729
- *
1730
- * @remarks
1731
- * - totalCells: 현재 사용 중인 셀 개수
1732
- * - totalItems: 인덱스에 등록된 고유 항목 수 (정확)
1733
- * - avgItemsPerCell: 셀당 평균 항목 수
1734
- */
1355
+ }; // 통계 정보 반환
1735
1356
 
1736
1357
 
1737
1358
  SpatialHashGrid.prototype.stats = function () {
@@ -1752,20 +1373,9 @@ function () {
1752
1373
  /**
1753
1374
  * 현재 뷰포트 영역 계산
1754
1375
  *
1755
- * Konva Stage 크기와 컬링 마진을 기반으로 뷰포트 경계를 계산합니다.
1756
- *
1757
- * @param stage Konva Stage 인스턴스 (width, height 메서드 제공)
1376
+ * @param stage Konva Stage 인스턴스
1758
1377
  * @param cullingMargin 컬링 여유 공간 (px)
1759
1378
  * @param viewportRef 뷰포트 경계를 저장할 ref
1760
- *
1761
- * @remarks
1762
- * - 화면 밖 cullingMargin만큼의 영역까지 포함하여 계산
1763
- * - 스크롤 시 부드러운 전환을 위해 여유 공간 포함
1764
- *
1765
- * @example
1766
- * ```typescript
1767
- * updateViewport(stageRef.current, cullingMargin, viewportRef);
1768
- * ```
1769
1379
  */
1770
1380
  var updateViewport = function (stage, cullingMargin, viewportRef) {
1771
1381
  if (!stage) return;
@@ -1779,35 +1389,16 @@ var updateViewport = function (stage, cullingMargin, viewportRef) {
1779
1389
  /**
1780
1390
  * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
1781
1391
  *
1782
- * 뷰포트 컬링을 위한 함수입니다. 바운딩 박스와 뷰포트 경계의 교차를 확인합니다.
1783
- * 바운딩 박스는 캐시되어 성능을 최적화합니다.
1784
- *
1785
1392
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
1786
1393
  * @param item 확인할 아이템
1787
- * @param enableViewportCulling 뷰포트 컬링 활성화 여부
1788
1394
  * @param viewportRef 뷰포트 경계 ref
1789
1395
  * @param boundingBoxCacheRef 바운딩 박스 캐시 ref
1790
1396
  * @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
- * ```
1397
+ * @returns 뷰포트 안에 있으면 true
1807
1398
  */
1808
1399
 
1809
- var isInViewport = function (item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
1810
- if (!enableViewportCulling || !viewportRef.current) return true;
1400
+ var isInViewport = function (item, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
1401
+ if (!viewportRef.current) return true;
1811
1402
  var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
1812
1403
 
1813
1404
  var bbox = boundingBoxCacheRef.current.get(item.id);
@@ -1827,33 +1418,9 @@ var isInViewport = function (item, enableViewportCulling, viewportRef, boundingB
1827
1418
  /**
1828
1419
  * 지도 이벤트 핸들러 생성 함수
1829
1420
  *
1830
- * 지도 이동, 줌, 드래그 등의 이벤트를 처리하는 핸들러들을 생성합니다.
1831
- *
1832
1421
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
1833
1422
  * @param deps 이벤트 핸들러 생성에 필요한 의존성
1834
1423
  * @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
1424
  */
1858
1425
 
1859
1426
  var createMapEventHandlers = function (deps) {
@@ -1865,14 +1432,7 @@ var createMapEventHandlers = function (deps) {
1865
1432
  accumTranslateRef = deps.accumTranslateRef,
1866
1433
  offsetCacheRef = deps.offsetCacheRef,
1867
1434
  boundingBoxCacheRef = deps.boundingBoxCacheRef,
1868
- renderAllImmediate = deps.renderAllImmediate;
1869
- /**
1870
- * 지도 이동/줌 완료 시 처리
1871
- *
1872
- * - 캐시 초기화: 좌표 변환 결과가 변경되었으므로 캐시 무효화
1873
- * - 마커 위치 업데이트: 새로운 지도 위치에 맞게 마커 재배치
1874
- * - 렌더링: 새 위치에서 전체 렌더링 수행
1875
- */
1435
+ renderAllImmediate = deps.renderAllImmediate; // 지도 이동/줌 완료 시 처리 (캐시 초기화 및 렌더링)
1876
1436
 
1877
1437
  var handleIdle = function () {
1878
1438
  prevCenterOffsetRef.current = null;
@@ -1890,45 +1450,34 @@ var createMapEventHandlers = function (deps) {
1890
1450
  position: bounds.nw
1891
1451
  }, options);
1892
1452
 
1893
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
1453
+ markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (transform 제거 시 잠깐 빈 화면이 보이는 것 방지)
1894
1454
 
1895
1455
  if (containerRef.current) {
1896
1456
  containerRef.current.style.transform = '';
1897
1457
  containerRef.current.style.visibility = '';
1898
- } // 새 위치에서 렌더링
1458
+ } // 새 위치에서 렌더링 (캐시는 이미 초기화됨)
1899
1459
 
1900
1460
 
1901
1461
  renderAllImmediate();
1902
- };
1903
- /**
1904
- * 줌 시작 시 처리 (일시적으로 숨김)
1905
- */
1462
+ }; // 줌 시작 시 처리 (일시적으로 숨김)
1906
1463
 
1907
1464
 
1908
1465
  var handleZoomStart = function () {
1909
- if (containerRef.current) {
1910
- containerRef.current.style.visibility = 'hidden';
1911
- }
1912
- };
1913
- /**
1914
- * 줌 종료 시 처리 (다시 표시)
1915
- */
1466
+ if (!containerRef.current) return;
1467
+ containerRef.current.style.visibility = 'hidden';
1468
+ }; // 줌 종료 시 처리 (다시 표시)
1916
1469
 
1917
1470
 
1918
1471
  var handleZoomEnd = function () {
1919
- if (containerRef.current) {
1920
- containerRef.current.style.visibility = '';
1921
- }
1922
- };
1923
- /**
1924
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
1925
- */
1472
+ if (!containerRef.current) return;
1473
+ containerRef.current.style.visibility = '';
1474
+ }; // 지도 중심 변경 시 처리 (transform으로 이동 추적, 캐시 유지)
1926
1475
 
1927
1476
 
1928
1477
  var handleCenterChanged = function () {
1929
1478
  var center = controller.getCurrBounds().getCenter();
1930
1479
  var curr = controller.positionToOffset(center);
1931
- var prev = prevCenterOffsetRef.current;
1480
+ var prev = prevCenterOffsetRef.current; // 첫 번째 호출 시 이전 위치 저장만 하고 종료
1932
1481
 
1933
1482
  if (!prev) {
1934
1483
  prevCenterOffsetRef.current = {
@@ -1936,10 +1485,12 @@ var createMapEventHandlers = function (deps) {
1936
1485
  y: curr.y
1937
1486
  };
1938
1487
  return;
1939
- }
1488
+ } // 이전 위치와 현재 위치의 차이 계산 (이동 거리)
1489
+
1940
1490
 
1941
1491
  var dx = prev.x - curr.x;
1942
- var dy = prev.y - curr.y;
1492
+ var dy = prev.y - curr.y; // 누적 이동 거리 저장 (transform으로 화면만 이동, 캐시는 유지하여 성능 최적화)
1493
+
1943
1494
  accumTranslateRef.current = {
1944
1495
  x: accumTranslateRef.current.x + dx,
1945
1496
  y: accumTranslateRef.current.y + dy
@@ -1947,23 +1498,15 @@ var createMapEventHandlers = function (deps) {
1947
1498
  prevCenterOffsetRef.current = {
1948
1499
  x: curr.x,
1949
1500
  y: curr.y
1950
- };
1501
+ }; // CSS transform으로 컨테이너 이동 (캐시된 좌표는 그대로 유지)
1951
1502
 
1952
1503
  if (containerRef.current) {
1953
1504
  containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
1954
1505
  }
1955
1506
  };
1956
- /**
1957
- * 드래그 시작 처리
1958
- */
1959
-
1960
1507
 
1961
1508
  var handleDragStart = function () {// 커서는 각 컴포넌트에서 처리
1962
1509
  };
1963
- /**
1964
- * 드래그 종료 처리
1965
- */
1966
-
1967
1510
 
1968
1511
  var handleDragEnd = function () {// 커서는 각 컴포넌트에서 처리
1969
1512
  };
@@ -1980,26 +1523,10 @@ var createMapEventHandlers = function (deps) {
1980
1523
  /**
1981
1524
  * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
1982
1525
  *
1983
- * Spatial Hash Grid에 모든 데이터의 바운딩 박스를 삽입합니다.
1984
- * 이를 통해 클릭/호버 시 O(1) 수준의 빠른 Hit Test가 가능합니다.
1985
- *
1986
1526
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
1987
1527
  * @param data 공간 인덱스에 삽입할 데이터 배열
1988
1528
  * @param spatialIndex Spatial Hash Grid 인스턴스
1989
1529
  * @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
- * ```
2003
1530
  */
2004
1531
 
2005
1532
  var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
@@ -2015,29 +1542,13 @@ var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
2015
1542
  }
2016
1543
  };
2017
1544
  /**
2018
- * 선택 상태 동기화 유틸리티
2019
- *
2020
- * 데이터 변경 시 선택된 항목의 참조를 최신 데이터로 업데이트합니다.
2021
- * 화면 밖에 있는 선택된 항목도 선택 상태를 유지합니다.
1545
+ * 선택 상태 동기화 (화면 밖 데이터도 선택 상태 유지)
2022
1546
  *
2023
1547
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
2024
1548
  * @param data 최신 데이터 배열
2025
1549
  * @param selectedIds 선택된 항목 ID Set
2026
1550
  * @param selectedItemsMap 현재 선택된 항목 Map
2027
1551
  * @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
1552
  */
2042
1553
 
2043
1554
  var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
@@ -2065,24 +1576,10 @@ var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
2065
1576
  /**
2066
1577
  * 외부 selectedItems를 내부 상태로 동기화
2067
1578
  *
2068
- * 외부에서 전달된 selectedItems prop을 내부 ref 상태로 동기화합니다.
2069
- *
2070
1579
  * @template T 마커/폴리곤 데이터의 추가 속성 타입
2071
- * @param externalSelectedItems 외부에서 전달된 선택된 항목 배열 (undefined면 동기화 안 함)
1580
+ * @param externalSelectedItems 외부에서 전달된 선택된 항목 배열
2072
1581
  * @param selectedIdsRef 선택된 ID Set ref
2073
1582
  * @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
1583
  */
2087
1584
 
2088
1585
  var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef, selectedItemsMapRef) {
@@ -2098,22 +1595,17 @@ var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef,
2098
1595
  };
2099
1596
 
2100
1597
  /**
2101
- * 이벤트 유효성 검증 헬퍼
1598
+ * 이벤트 유효성 검증 및 좌표 변환
2102
1599
  *
2103
1600
  * @param event 이벤트 파라미터
2104
- * @param context Context가 있는지 여부
1601
+ * @param context WoongCanvasContext 인스턴스
2105
1602
  * @param controller MintMapController 인스턴스
2106
- * @returns 유효한 offset 또는 null
2107
- *
2108
- * @remarks
2109
- * Context가 있으면 전역 이벤트 핸들러가 처리하므로 로컬 핸들러는 스킵
1603
+ * @returns 유효한 화면 좌표 또는 null
2110
1604
  */
2111
1605
  var validateEvent = function (event, context, controller) {
2112
- var _a; // Context가 있으면 전역 핸들러가 처리
2113
-
2114
-
2115
- if (context) return null; // 이벤트 파라미터 검증
1606
+ var _a;
2116
1607
 
1608
+ if (context) return null;
2117
1609
  if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return null;
2118
1610
 
2119
1611
  try {
@@ -2124,22 +1616,15 @@ var validateEvent = function (event, context, controller) {
2124
1616
  }
2125
1617
  };
2126
1618
  /**
2127
- * Map의 values를 배열로 변환 (최적화 버전)
1619
+ * Map의 values를 배열로 변환
2128
1620
  *
1621
+ * @template T Map 값의 타입
2129
1622
  * @param map 변환할 Map
2130
1623
  * @returns Map의 값 배열
2131
- *
2132
- * @remarks
2133
- * Map.values()는 IterableIterator를 반환하므로 배열 변환이 필요할 때 사용합니다.
2134
- * 성능: O(n) 시간복잡도
2135
- *
2136
- * 최적화: Array.from을 사용하되, 크기를 미리 할당하여 메모리 재할당 최소화
2137
1624
  */
2138
1625
 
2139
1626
  var mapValuesToArray = function (map) {
2140
- // Map이 비어있으면 배열 반환 (메모리 할당 최소화)
2141
- if (map.size === 0) return []; // Array.from 사용 (TypeScript 컴파일러 호환성)
2142
-
1627
+ if (map.size === 0) return [];
2143
1628
  return Array.from(map.values());
2144
1629
  };
2145
1630
 
@@ -6083,9 +5568,6 @@ function LoadingImage(_a) {
6083
5568
  }))));
6084
5569
  }
6085
5570
 
6086
- // 메인 컴포넌트
6087
- // ============================================================================
6088
-
6089
5571
  var WoongCanvasMarker = function (props) {
6090
5572
  var data = props.data,
6091
5573
  onClick = props.onClick,
@@ -6095,225 +5577,88 @@ var WoongCanvasMarker = function (props) {
6095
5577
  enableMultiSelect = _a === void 0 ? false : _a,
6096
5578
  _b = props.topOnHover,
6097
5579
  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,
5580
+ _c = props.cullingMargin,
5581
+ cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
5582
+ _d = props.maxCacheSize,
5583
+ maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
6104
5584
  externalSelectedItems = props.selectedItems,
6105
5585
  externalSelectedItem = props.selectedItem,
6106
- _f = props.disableInteraction,
6107
- disableInteraction = _f === void 0 ? false : _f,
5586
+ _e = props.disableInteraction,
5587
+ disableInteraction = _e === void 0 ? false : _e,
6108
5588
  renderBase = props.renderBase,
6109
- renderAnimation = props.renderAnimation,
6110
5589
  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
-
5590
+ options = __rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderEvent"]);
6115
5591
 
6116
5592
  var controller = useMintMapController();
6117
5593
  var context = useWoongCanvasContext();
6118
- var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
6119
- // DOM Refs
6120
- // --------------------------------------------------------------------------
5594
+ var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
6121
5595
 
6122
5596
  var divRef = useRef(document.createElement('div'));
6123
5597
  var divElement = divRef.current;
6124
5598
  var containerRef = useRef(null);
6125
- var markerRef = useRef(); // --------------------------------------------------------------------------
6126
- // Konva Refs
6127
- // --------------------------------------------------------------------------
5599
+ var markerRef = useRef(); // Konva Refs
6128
5600
 
6129
5601
  var stageRef = useRef(null);
6130
5602
  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로 관리하여 클로저 문제 해결) */
5603
+ var eventLayerRef = useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
6143
5604
 
5605
+ var dataRef = useRef(data);
6144
5606
  var disableInteractionRef = useRef(disableInteraction);
6145
- /** 현재 Hover 중인 항목 */
6146
-
6147
5607
  var hoveredItemRef = useRef(null);
6148
- /** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
6149
-
6150
5608
  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
5609
  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
- // --------------------------------------------------------------------------
5610
+ var selectedItemsMapRef = useRef(new Map()); // 드래그 상태 Refs
6182
5611
 
6183
5612
  var draggingRef = useRef(false);
6184
5613
  var prevCenterOffsetRef = useRef(null);
6185
5614
  var accumTranslateRef = useRef({
6186
5615
  x: 0,
6187
5616
  y: 0
6188
- }); // --------------------------------------------------------------------------
6189
- // Performance Refs (캐싱 & 최적화)
6190
- // --------------------------------------------------------------------------
6191
-
6192
- /** 좌표 변환 결과 LRU 캐시 */
5617
+ }); // 성능 최적화 Refs
6193
5618
 
6194
5619
  var offsetCacheRef = useRef(new LRUCache(maxCacheSize));
6195
- /** 공간 인덱스 (빠른 Hit Test) */
6196
-
6197
5620
  var spatialIndexRef = useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
6198
- /** 바운딩 박스 캐시 (Viewport Culling 최적화) */
6199
-
6200
5621
  var boundingBoxCacheRef = useRef(new Map());
6201
- /** 뷰포트 경계 캐시 (Viewport Culling) */
6202
-
6203
- var viewportRef = useRef(null); // --------------------------------------------------------------------------
6204
- // 유틸리티 함수: 뷰포트 관리
6205
- // --------------------------------------------------------------------------
6206
-
6207
- /**
6208
- * 현재 뷰포트 영역 계산
6209
- */
5622
+ var viewportRef = useRef(null); // 뷰포트 영역 계산 (Viewport Culling)
6210
5623
 
6211
5624
  var updateViewport$1 = function () {
6212
5625
  updateViewport(stageRef.current, cullingMargin, viewportRef);
6213
- };
6214
- /**
6215
- * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
6216
- */
5626
+ }; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
6217
5627
 
6218
5628
 
6219
5629
  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
- */
5630
+ return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
5631
+ }; // 마커 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
6231
5632
 
6232
5633
 
6233
5634
  var getOrComputeMarkerOffset = function (markerData) {
6234
5635
  var cached = offsetCacheRef.current.get(markerData.id);
6235
5636
  if (cached && !Array.isArray(cached)) return cached;
6236
5637
  var result = computeMarkerOffset(markerData, controller);
6237
-
6238
- if (result) {
6239
- offsetCacheRef.current.set(markerData.id, result);
6240
- }
6241
-
5638
+ if (!result) return null;
5639
+ offsetCacheRef.current.set(markerData.id, result);
6242
5640
  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
- */
5641
+ }; // 마커 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
6268
5642
 
6269
5643
 
6270
5644
  var computeBoundingBox = function (item) {
6271
- // 마커: 중심점 기준 박스 크기 계산 (꼬리 포함)
6272
5645
  var offset = getOrComputeMarkerOffset(item);
6273
5646
  if (!offset) return null;
6274
5647
  var boxWidth = item.boxWidth || 50;
6275
5648
  var boxHeight = item.boxHeight || 28;
6276
- var tailHeight = item.tailHeight || 0; // 🎯 tailHeight 사용 (Viewport Culling용)
6277
-
5649
+ var tailHeight = item.tailHeight || 0;
6278
5650
  return {
6279
5651
  minX: offset.x - boxWidth / 2,
6280
5652
  minY: offset.y - boxHeight - tailHeight,
6281
5653
  maxX: offset.x + boxWidth / 2,
6282
5654
  maxY: offset.y
6283
5655
  };
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
- */
5656
+ }; // 공간 인덱스 빌드 (빠른 Hit Test용)
6299
5657
 
6300
5658
 
6301
5659
  var buildSpatialIndex$1 = function () {
6302
5660
  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
- */
5661
+ }; // 렌더링 유틸리티 객체
6317
5662
 
6318
5663
 
6319
5664
  var renderUtils = {
@@ -6321,48 +5666,29 @@ var WoongCanvasMarker = function (props) {
6321
5666
  return null;
6322
5667
  },
6323
5668
  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
- */
5669
+ }; // Base Layer 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
6340
5670
 
6341
5671
  var doRenderBase = function () {
6342
5672
  var layer = baseLayerRef.current;
6343
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
6344
-
5673
+ if (!layer) return;
6345
5674
  var shape = layer.findOne('.base-render-shape');
6346
5675
 
6347
5676
  if (!shape) {
6348
- // 최초 생성 (한 번만 실행됨)
6349
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
6350
5677
  shape = new Konva.Shape({
6351
5678
  name: 'base-render-shape',
6352
5679
  sceneFunc: function (context, shape) {
6353
5680
  var ctx = context;
6354
- var hovered = hoveredItemRef.current; // 클로저로 최신 ref 참조
5681
+ var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
6355
5682
 
6356
- var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
5683
+ var visibleItems = dataRef.current.filter(function (item) {
6357
5684
  return isInViewport$1(item);
6358
- }) : dataRef.current; // topOnHover true이고 renderEvent가 없으면 Base Layer에서 hover 처리
5685
+ }); // topOnHover 옵션: hover된 항목을 나중에 그려서 최상위에 표시
6359
5686
 
6360
5687
  if (topOnHover && !renderEvent && hovered) {
6361
- // hover된 항목 제외하고 렌더링
6362
5688
  visibleItems = visibleItems.filter(function (item) {
6363
5689
  return item.id !== hovered.id;
6364
5690
  });
6365
- } // 일반 항목 렌더링
5691
+ } // 일반 항목들 먼저 렌더링
6366
5692
 
6367
5693
 
6368
5694
  renderBase({
@@ -6371,12 +5697,10 @@ var WoongCanvasMarker = function (props) {
6371
5697
  selectedIds: selectedIdsRef.current,
6372
5698
  hoveredItem: hovered,
6373
5699
  utils: renderUtils
6374
- }); // hover된 항목을 최상단에 렌더링 (renderEvent가 없을 때만)
5700
+ }); // hover된 항목을 마지막에 렌더링하여 최상위에 표시
6375
5701
 
6376
5702
  if (topOnHover && !renderEvent && hovered) {
6377
- var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
6378
-
6379
- if (isHoveredInViewport) {
5703
+ if (isInViewport$1(hovered)) {
6380
5704
  renderBase({
6381
5705
  ctx: ctx,
6382
5706
  items: [hovered],
@@ -6392,72 +5716,26 @@ var WoongCanvasMarker = function (props) {
6392
5716
  hitStrokeWidth: 0
6393
5717
  });
6394
5718
  layer.add(shape);
6395
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
6396
-
5719
+ }
6397
5720
 
6398
5721
  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
- */
5722
+ }; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
6438
5723
 
6439
5724
 
6440
5725
  var doRenderEvent = function () {
6441
5726
  var layer = eventLayerRef.current;
6442
- if (!layer) return;
6443
- if (!renderEvent) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
6444
-
5727
+ if (!layer || !renderEvent) return;
6445
5728
  var shape = layer.findOne('.event-render-shape');
6446
5729
 
6447
5730
  if (!shape) {
6448
- // 최초 생성 (한 번만 실행됨)
6449
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
6450
5731
  shape = new Konva.Shape({
6451
5732
  name: 'event-render-shape',
6452
5733
  sceneFunc: function (context, shape) {
6453
- var ctx = context; // 클로저로 최신 ref 값 참조
6454
- // 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
6455
-
5734
+ var ctx = context;
6456
5735
  var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
6457
- var hovered = hoveredItemRef.current; // topOnHover가 true이면 hover된 항목을 최상단에 렌더링
5736
+ var hovered = hoveredItemRef.current;
6458
5737
 
6459
5738
  if (topOnHover && hovered) {
6460
- // 1. 먼저 일반 항목들 렌더링 (hover된 항목 제외)
6461
5739
  renderEvent({
6462
5740
  ctx: ctx,
6463
5741
  hoveredItem: null,
@@ -6466,13 +5744,9 @@ var WoongCanvasMarker = function (props) {
6466
5744
  return item.id !== hovered.id;
6467
5745
  }),
6468
5746
  selectedItem: selectedItemRef.current
6469
- }); // 2. hover된 항목을 최상단에 렌더링
6470
-
6471
- var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
5747
+ });
6472
5748
 
6473
- if (isHoveredInViewport) {
6474
- // hover된 항목이 선택되어 있다면 hoverSelectedItems에 포함시켜서
6475
- // renderEvent에서 hover 스타일만 적용되도록 함
5749
+ if (isInViewport$1(hovered)) {
6476
5750
  var hoveredIsSelected = selectedItems.some(function (item) {
6477
5751
  return item.id === hovered.id;
6478
5752
  });
@@ -6486,7 +5760,6 @@ var WoongCanvasMarker = function (props) {
6486
5760
  });
6487
5761
  }
6488
5762
  } else {
6489
- // topOnHover가 false이거나 hover된 항목이 없으면 일반 렌더링
6490
5763
  renderEvent({
6491
5764
  ctx: ctx,
6492
5765
  hoveredItem: hovered,
@@ -6501,28 +5774,21 @@ var WoongCanvasMarker = function (props) {
6501
5774
  hitStrokeWidth: 0
6502
5775
  });
6503
5776
  layer.add(shape);
6504
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
6505
-
5777
+ }
6506
5778
 
6507
5779
  layer.batchDraw();
6508
- };
6509
- /**
6510
- * 전체 즉시 렌더링 (IDLE 시 호출)
6511
- */
5780
+ }; // 전체 즉시 렌더링
6512
5781
 
6513
5782
 
6514
5783
  var renderAllImmediate = function () {
6515
5784
  updateViewport$1();
6516
5785
  buildSpatialIndex$1();
6517
5786
  doRenderBase();
6518
- doRenderAnimation();
6519
5787
  doRenderEvent();
6520
- }; // --------------------------------------------------------------------------
6521
- // 이벤트 핸들러: 지도 이벤트
6522
- // --------------------------------------------------------------------------
5788
+ }; // 지도 이벤트 핸들러 생성
6523
5789
 
6524
5790
 
6525
- var _g = createMapEventHandlers({
5791
+ var _f = createMapEventHandlers({
6526
5792
  controller: controller,
6527
5793
  containerRef: containerRef,
6528
5794
  markerRef: markerRef,
@@ -6533,73 +5799,38 @@ var WoongCanvasMarker = function (props) {
6533
5799
  boundingBoxCacheRef: boundingBoxCacheRef,
6534
5800
  renderAllImmediate: renderAllImmediate
6535
5801
  }),
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
-
5802
+ handleIdle = _f.handleIdle,
5803
+ handleZoomStart = _f.handleZoomStart,
5804
+ handleZoomEnd = _f.handleZoomEnd,
5805
+ handleCenterChanged = _f.handleCenterChanged,
5806
+ handleDragStartShared = _f.handleDragStart,
5807
+ handleDragEndShared = _f.handleDragEnd;
6546
5808
 
6547
5809
  var handleDragStart = function () {
6548
5810
  handleDragStartShared();
6549
5811
  draggingRef.current = true;
6550
5812
  controller.setMapCursor('grabbing');
6551
5813
  };
6552
- /**
6553
- * 드래그 종료 처리 (커서를 기본으로 복원)
6554
- */
6555
-
6556
5814
 
6557
5815
  var handleDragEnd = function () {
6558
5816
  handleDragEndShared();
6559
5817
  draggingRef.current = false;
6560
5818
  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
- */
5819
+ }; // Hit Test: 특정 좌표의 마커 찾기
6589
5820
 
6590
5821
 
6591
5822
  var findData = function (offset) {
6592
- // topOnHover true이고 현재 hover된 항목이 있으면, 그것을 먼저 체크
5823
+ // topOnHover 옵션이 켜져 있으면 hover된 항목을 최우선으로 확인
6593
5824
  if (topOnHover && hoveredItemRef.current) {
6594
5825
  var hovered = hoveredItemRef.current;
6595
5826
 
6596
5827
  if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
6597
- return hovered; // 여전히 hover된 항목 위에 있음
5828
+ return hovered;
6598
5829
  }
6599
- } // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 ~10개)
5830
+ } // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
6600
5831
 
6601
5832
 
6602
- var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 마커 체크
5833
+ var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
6603
5834
 
6604
5835
  for (var i = candidates.length - 1; i >= 0; i--) {
6605
5836
  var item = candidates[i];
@@ -6610,18 +5841,7 @@ var WoongCanvasMarker = function (props) {
6610
5841
  }
6611
5842
 
6612
5843
  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
- */
5844
+ }; // Hover 상태 설정 및 렌더링
6625
5845
 
6626
5846
 
6627
5847
  var setHovered = function (data) {
@@ -6631,38 +5851,18 @@ var WoongCanvasMarker = function (props) {
6631
5851
  controller.setMapCursor('grabbing');
6632
5852
  } else {
6633
5853
  controller.setMapCursor(data ? 'pointer' : 'grab');
6634
- } // 즉시 렌더링 (RAF 없이)
6635
-
5854
+ }
6636
5855
 
6637
5856
  if (renderEvent) {
6638
- // renderEvent가 있으면 Event Layer에서만 처리 (성능 최적화)
6639
5857
  doRenderEvent();
6640
5858
  } else if (topOnHover) {
6641
- // renderEvent가 없고 topOnHover가 true면 Base Layer에서 처리
6642
5859
  doRenderBase();
6643
5860
  }
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
- */
5861
+ }; // 클릭 처리: 선택 상태 업데이트
6660
5862
 
6661
5863
 
6662
5864
  var handleLocalClick = function (data) {
6663
- // 1. 선택 상태 업데이트
6664
5865
  if (enableMultiSelect) {
6665
- // 다중 선택: Set과 Map 동시 업데이트
6666
5866
  var newSelected = new Set(selectedIdsRef.current);
6667
5867
 
6668
5868
  if (newSelected.has(data.id)) {
@@ -6675,7 +5875,6 @@ var WoongCanvasMarker = function (props) {
6675
5875
 
6676
5876
  selectedIdsRef.current = newSelected;
6677
5877
  } else {
6678
- // 단일 선택: 토글
6679
5878
  var newSelected = new Set();
6680
5879
 
6681
5880
  if (!selectedIdsRef.current.has(data.id)) {
@@ -6689,135 +5888,80 @@ var WoongCanvasMarker = function (props) {
6689
5888
  selectedIdsRef.current = newSelected;
6690
5889
  }
6691
5890
 
6692
- if (!!renderAnimation) {
6693
- // 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
6694
- doRenderBase(); // 3. Animation Layer 렌더링 (선택된 마커 애니메이션)
6695
-
6696
- doRenderAnimation();
6697
- } // 4. Event Layer 렌더링 (hover 처리)
6698
-
6699
-
5891
+ doRenderBase();
6700
5892
  doRenderEvent();
6701
- }; // --------------------------------------------------------------------------
6702
- // 이벤트 핸들러: UI 이벤트
6703
- // --------------------------------------------------------------------------
6704
-
6705
- /**
6706
- * 클릭 이벤트 처리
6707
- *
6708
- * @param event 클릭 이벤트 파라미터
6709
- *
6710
- * @remarks
6711
- * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
6712
- * - 상호작용이 비활성화되어 있으면 스킵
6713
- * - Spatial Index를 사용하여 빠른 Hit Test 수행
6714
- */
5893
+ }; // 클릭 이벤트 핸들러
6715
5894
 
6716
5895
 
6717
5896
  var handleClick = function (event) {
6718
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6719
-
5897
+ if (disableInteractionRef.current) return;
6720
5898
  var clickedOffset = validateEvent(event, context, controller);
6721
5899
  if (!clickedOffset) return;
6722
5900
  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
- */
5901
+ if (!data) return;
5902
+ handleLocalClick(data);
5903
+ onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
5904
+ }; // 마우스 이동 이벤트 핸들러 (hover 감지)
6742
5905
 
6743
5906
 
6744
5907
  var handleMouseMove = function (event) {
6745
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6746
-
5908
+ if (disableInteractionRef.current) return;
6747
5909
  var mouseOffset = validateEvent(event, context, controller);
6748
5910
  if (!mouseOffset) return;
6749
5911
  var hoveredItem = findData(mouseOffset);
6750
5912
  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
- */
5913
+ if (prevHovered === hoveredItem) return;
5914
+ setHovered(hoveredItem);
5915
+ if (prevHovered) onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
5916
+ if (hoveredItem) onMouseOver === null || onMouseOver === void 0 ? void 0 : onMouseOver(hoveredItem);
5917
+ }; // 마우스가 영역을 벗어날 때 hover 상태 초기화
6761
5918
 
6762
5919
 
6763
5920
  var handleMouseLeave = function () {
6764
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6765
-
5921
+ if (disableInteractionRef.current) return;
6766
5922
  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
- // --------------------------------------------------------------------------
5923
+ if (!prevHovered) return;
5924
+ hoveredItemRef.current = null;
5925
+ controller.setMapCursor('grab');
5926
+ doRenderEvent();
5927
+ onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
5928
+ }; // DOM 초기화
6780
5929
 
6781
5930
 
6782
5931
  useEffect(function () {
6783
5932
  divElement.style.width = 'fit-content';
6784
5933
  return function () {
6785
- if (markerRef.current) {
6786
- controller.clearDrawable(markerRef.current);
6787
- markerRef.current = undefined;
6788
- }
5934
+ if (!markerRef.current) return;
5935
+ controller.clearDrawable(markerRef.current);
5936
+ markerRef.current = undefined;
6789
5937
  };
6790
- }, []); // --------------------------------------------------------------------------
6791
- // Lifecycle: 마커 생성/업데이트
6792
- // --------------------------------------------------------------------------
5938
+ }, []); // 마커 생성/업데이트
6793
5939
 
6794
5940
  useEffect(function () {
6795
- if (options) {
6796
- var bounds = controller.getCurrBounds();
5941
+ if (!options) return;
5942
+ var bounds = controller.getCurrBounds();
6797
5943
 
6798
- var markerOptions = __assign({
6799
- position: bounds.nw
6800
- }, options);
5944
+ var markerOptions = __assign({
5945
+ position: bounds.nw
5946
+ }, options);
6801
5947
 
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);
5948
+ if (markerRef.current) {
5949
+ controller.updateMarker(markerRef.current, markerOptions);
5950
+ return;
5951
+ }
6808
5952
 
6809
- if (divElement.parentElement) {
6810
- divElement.parentElement.style.pointerEvents = 'none';
6811
- }
5953
+ markerRef.current = new Marker(markerOptions);
5954
+ markerRef.current.element = divElement;
5955
+ controller.createMarker(markerRef.current);
6812
5956
 
6813
- if (options.zIndex !== undefined) {
6814
- controller.setMarkerZIndex(markerRef.current, options.zIndex);
6815
- }
6816
- }
5957
+ if (divElement.parentElement) {
5958
+ divElement.parentElement.style.pointerEvents = 'none';
6817
5959
  }
6818
- }, [options]); // --------------------------------------------------------------------------
6819
- // Lifecycle: Konva 초기화 및 이벤트 리스너 등록
6820
- // --------------------------------------------------------------------------
5960
+
5961
+ if (options.zIndex !== undefined) {
5962
+ controller.setMarkerZIndex(markerRef.current, options.zIndex);
5963
+ }
5964
+ }, [options]); // Konva 초기화 및 이벤트 리스너 등록
6821
5965
 
6822
5966
  useEffect(function () {
6823
5967
  var mapDiv = controller.mapDivElement;
@@ -6826,34 +5970,21 @@ var WoongCanvasMarker = function (props) {
6826
5970
  width: mapDiv.offsetWidth,
6827
5971
  height: mapDiv.offsetHeight
6828
5972
  });
6829
- stageRef.current = stage; // 레이어 최적화 설정
6830
-
5973
+ stageRef.current = stage;
6831
5974
  var baseLayer = new Konva.Layer({
6832
- listening: false // 이벤트 리스닝 비활성화로 성능 향상
6833
-
6834
- });
6835
- var animationLayer = new Konva.Layer({
6836
5975
  listening: false
6837
5976
  });
6838
5977
  var eventLayer = new Konva.Layer({
6839
5978
  listening: false
6840
5979
  });
6841
5980
  baseLayerRef.current = baseLayer;
6842
- animationLayerRef.current = animationLayer;
6843
5981
  eventLayerRef.current = eventLayer;
6844
5982
  stage.add(baseLayer);
6845
-
6846
- if (renderAnimation) {
6847
- stage.add(animationLayer);
6848
- }
6849
-
6850
- stage.add(eventLayer); // 초기 뷰포트 설정
6851
-
6852
- updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
5983
+ stage.add(eventLayer);
5984
+ updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
6853
5985
 
6854
5986
  var resizeRafId = null;
6855
5987
  var resizeObserver = new ResizeObserver(function () {
6856
- // RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
6857
5988
  if (resizeRafId !== null) {
6858
5989
  cancelAnimationFrame(resizeRafId);
6859
5990
  }
@@ -6876,10 +6007,9 @@ var WoongCanvasMarker = function (props) {
6876
6007
  controller.addEventListener('CLICK', handleClick);
6877
6008
  controller.addEventListener('MOUSEMOVE', handleMouseMove);
6878
6009
  controller.addEventListener('DRAGSTART', handleDragStart);
6879
- controller.addEventListener('DRAGEND', handleDragEnd); // 맵 컨테이너에 mouseleave 이벤트 추가
6880
-
6010
+ controller.addEventListener('DRAGEND', handleDragEnd);
6881
6011
  mapDiv.addEventListener('mouseleave', handleMouseLeave);
6882
- renderAllImmediate(); // Context 사용 시 컴포넌트 등록 (다중 인스턴스 관리)
6012
+ renderAllImmediate(); // Context 사용 시 컴포넌트 등록
6883
6013
 
6884
6014
  var componentInstance = null;
6885
6015
 
@@ -6900,22 +6030,17 @@ var WoongCanvasMarker = function (props) {
6900
6030
  },
6901
6031
  isInteractionDisabled: function () {
6902
6032
  return disableInteractionRef.current;
6903
- } // 🚫 상호작용 비활성화 여부 반환
6904
-
6033
+ }
6905
6034
  };
6906
6035
  context.registerComponent(componentInstance);
6907
- } // Cleanup 함수
6908
-
6036
+ }
6909
6037
 
6910
6038
  return function () {
6911
- // RAF 정리
6912
6039
  if (resizeRafId !== null) {
6913
6040
  cancelAnimationFrame(resizeRafId);
6914
- } // 옵저버 정리
6915
-
6916
-
6917
- resizeObserver.disconnect(); // 이벤트 리스너 정리
6041
+ }
6918
6042
 
6043
+ resizeObserver.disconnect();
6919
6044
  controller.removeEventListener('IDLE', handleIdle);
6920
6045
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
6921
6046
  controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
@@ -6924,59 +6049,41 @@ var WoongCanvasMarker = function (props) {
6924
6049
  controller.removeEventListener('MOUSEMOVE', handleMouseMove);
6925
6050
  controller.removeEventListener('DRAGSTART', handleDragStart);
6926
6051
  controller.removeEventListener('DRAGEND', handleDragEnd);
6927
- mapDiv.removeEventListener('mouseleave', handleMouseLeave); // Context 정리
6052
+ mapDiv.removeEventListener('mouseleave', handleMouseLeave);
6928
6053
 
6929
6054
  if (context && componentInstance) {
6930
6055
  context.unregisterComponent(componentInstance);
6931
- } // Konva 리소스 정리
6932
-
6056
+ }
6933
6057
 
6934
6058
  baseLayer.destroyChildren();
6935
- animationLayer.destroyChildren();
6936
6059
  eventLayer.destroyChildren();
6937
- stage.destroy(); // 캐시 정리
6938
-
6060
+ stage.destroy();
6939
6061
  offsetCacheRef.current.clear();
6940
6062
  boundingBoxCacheRef.current.clear();
6941
6063
  spatialIndexRef.current.clear();
6942
- };
6943
- }, []); // 초기화는 한 번만
6944
- // --------------------------------------------------------------------------
6945
- // Lifecycle: disableInteraction 동기화
6946
- // --------------------------------------------------------------------------
6064
+ };
6065
+ }, []); // disableInteraction 동기화
6947
6066
 
6948
6067
  useEffect(function () {
6949
6068
  disableInteractionRef.current = disableInteraction;
6950
- }, [disableInteraction]); // --------------------------------------------------------------------------
6951
- // Lifecycle: 외부 selectedItems 동기화
6952
- // --------------------------------------------------------------------------
6069
+ }, [disableInteraction]); // 외부 selectedItems 동기화
6953
6070
 
6954
6071
  useEffect(function () {
6955
6072
  if (!stageRef.current) return;
6956
- syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef); // 렌더링
6957
-
6073
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
6958
6074
  doRenderBase();
6959
- doRenderAnimation();
6960
6075
  doRenderEvent();
6961
- }, [externalSelectedItems]); // 배열 자체를 dependency로 사용
6962
- // --------------------------------------------------------------------------
6963
- // Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
6964
- // --------------------------------------------------------------------------
6076
+ }, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
6965
6077
 
6966
6078
  useEffect(function () {
6967
- if (!stageRef.current) return; // Ref 동기화
6968
-
6969
- selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
6970
-
6079
+ if (!stageRef.current) return;
6080
+ selectedItemRef.current = externalSelectedItem;
6971
6081
  doRenderEvent();
6972
- }, [externalSelectedItem]); // --------------------------------------------------------------------------
6973
- // Lifecycle: 데이터 변경 시 렌더링
6974
- // --------------------------------------------------------------------------
6082
+ }, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
6975
6083
 
6976
6084
  useEffect(function () {
6977
- if (!stageRef.current) return; // dataRef 동기화
6978
-
6979
- dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
6085
+ if (!stageRef.current) return;
6086
+ dataRef.current = data;
6980
6087
 
6981
6088
  if (containerRef.current) {
6982
6089
  containerRef.current.style.transform = '';
@@ -6986,26 +6093,10 @@ var WoongCanvasMarker = function (props) {
6986
6093
  accumTranslateRef.current = {
6987
6094
  x: 0,
6988
6095
  y: 0
6989
- }; // 캐시 정리 (새 데이터이므로 기존 캐시는 무효)
6990
-
6096
+ };
6991
6097
  offsetCacheRef.current.clear();
6992
6098
  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
-
6099
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
7009
6100
  renderAllImmediate();
7010
6101
  }, [data]);
7011
6102
  return createPortal(React.createElement("div", {
@@ -7306,24 +6397,19 @@ var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth
7306
6397
  };
7307
6398
  };
7308
6399
 
7309
- // 메인 컴포넌트
7310
- // ============================================================================
7311
-
7312
6400
  var WoongCanvasPolygon = function (props) {
7313
6401
  var data = props.data,
7314
6402
  onClick = props.onClick,
7315
6403
  _a = props.enableMultiSelect,
7316
6404
  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,
6405
+ _b = props.cullingMargin,
6406
+ cullingMargin = _b === void 0 ? DEFAULT_CULLING_MARGIN : _b,
6407
+ _c = props.maxCacheSize,
6408
+ maxCacheSize = _c === void 0 ? DEFAULT_MAX_CACHE_SIZE : _c,
7323
6409
  externalSelectedItems = props.selectedItems,
7324
6410
  externalSelectedItem = props.selectedItem,
7325
- _e = props.disableInteraction,
7326
- disableInteraction = _e === void 0 ? false : _e,
6411
+ _d = props.disableInteraction,
6412
+ disableInteraction = _d === void 0 ? false : _d,
7327
6413
  baseFillColor = props.baseFillColor,
7328
6414
  baseStrokeColor = props.baseStrokeColor,
7329
6415
  baseLineWidth = props.baseLineWidth,
@@ -7336,167 +6422,67 @@ var WoongCanvasPolygon = function (props) {
7336
6422
  hoveredFillColor = props.hoveredFillColor,
7337
6423
  hoveredStrokeColor = props.hoveredStrokeColor,
7338
6424
  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"]); // --------------------------------------------------------------------------
6425
+ options = __rest(props, ["data", "onClick", "enableMultiSelect", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
7340
6426
  // Hooks & Context
7341
6427
  // --------------------------------------------------------------------------
7342
6428
 
7343
6429
 
7344
6430
  var controller = useMintMapController();
7345
6431
  var context = useWoongCanvasContext();
7346
- var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
7347
- // DOM Refs
7348
- // --------------------------------------------------------------------------
6432
+ var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
7349
6433
 
7350
6434
  var divRef = useRef(document.createElement('div'));
7351
6435
  var divElement = divRef.current;
7352
6436
  var containerRef = useRef(null);
7353
- var markerRef = useRef(); // --------------------------------------------------------------------------
7354
- // Konva Refs
7355
- // --------------------------------------------------------------------------
6437
+ var markerRef = useRef(); // Konva Refs
7356
6438
 
7357
6439
  var stageRef = useRef(null);
7358
6440
  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로 관리하여 클로저 문제 해결) */
6441
+ var eventLayerRef = useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
7370
6442
 
6443
+ var dataRef = useRef(data);
7371
6444
  var disableInteractionRef = useRef(disableInteraction);
7372
- /** 현재 Hover 중인 항목 */
7373
-
7374
6445
  var hoveredItemRef = useRef(null);
7375
- /** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
7376
-
7377
6446
  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
6447
  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
- // --------------------------------------------------------------------------
6448
+ var selectedItemsMapRef = useRef(new Map()); // 드래그 상태 Refs
7409
6449
 
7410
6450
  var draggingRef = useRef(false);
7411
6451
  var prevCenterOffsetRef = useRef(null);
7412
6452
  var accumTranslateRef = useRef({
7413
6453
  x: 0,
7414
6454
  y: 0
7415
- }); // --------------------------------------------------------------------------
7416
- // Performance Refs (캐싱 & 최적화)
7417
- // --------------------------------------------------------------------------
7418
-
7419
- /** 좌표 변환 결과 LRU 캐시 */
6455
+ }); // 성능 최적화 Refs
7420
6456
 
7421
6457
  var offsetCacheRef = useRef(new LRUCache(maxCacheSize));
7422
- /** 공간 인덱스 (빠른 Hit Test) */
7423
-
7424
6458
  var spatialIndexRef = useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
7425
- /** 바운딩 박스 캐시 (Viewport Culling 최적화) */
7426
-
7427
6459
  var boundingBoxCacheRef = useRef(new Map());
7428
- /** 뷰포트 경계 캐시 (Viewport Culling) */
7429
-
7430
- var viewportRef = useRef(null); // --------------------------------------------------------------------------
7431
- // 유틸리티 함수: 뷰포트 관리
7432
- // --------------------------------------------------------------------------
7433
-
7434
- /**
7435
- * 현재 뷰포트 영역 계산
7436
- */
6460
+ var viewportRef = useRef(null); // 뷰포트 영역 계산 (Viewport Culling)
7437
6461
 
7438
6462
  var updateViewport$1 = function () {
7439
6463
  updateViewport(stageRef.current, cullingMargin, viewportRef);
7440
- };
7441
- /**
7442
- * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
7443
- */
6464
+ }; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
7444
6465
 
7445
6466
 
7446
6467
  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
- */
6468
+ return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
6469
+ }; // 폴리곤 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
7457
6470
 
7458
6471
 
7459
6472
  var getOrComputePolygonOffsets = function (polygonData) {
7460
6473
  var cached = offsetCacheRef.current.get(polygonData.id);
7461
6474
  if (cached && Array.isArray(cached)) return cached;
7462
6475
  var result = computePolygonOffsets(polygonData, controller);
7463
-
7464
- if (result) {
7465
- offsetCacheRef.current.set(polygonData.id, result);
7466
- }
7467
-
6476
+ if (!result) return null;
6477
+ offsetCacheRef.current.set(polygonData.id, result);
7468
6478
  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
- */
6479
+ }; // 폴리곤 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
7494
6480
 
7495
6481
 
7496
6482
  var computeBoundingBox = function (item) {
7497
- // 폴리곤: 모든 좌표의 최소/최대값 계산
7498
6483
  var offsets = getOrComputePolygonOffsets(item);
7499
- if (!offsets) return null;
6484
+ if (!offsets) return null; // 모든 좌표를 순회하며 최소/최대값 찾기
6485
+
7500
6486
  var minX = Infinity,
7501
6487
  minY = Infinity,
7502
6488
  maxX = -Infinity,
@@ -7526,71 +6512,39 @@ var WoongCanvasPolygon = function (props) {
7526
6512
  maxX: maxX,
7527
6513
  maxY: maxY
7528
6514
  };
7529
- }; // --------------------------------------------------------------------------
7530
- // 유틸리티 함수: 공간 인덱싱
7531
- // --------------------------------------------------------------------------
7532
-
7533
- /**
7534
- * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
7535
- */
6515
+ }; // 공간 인덱스 빌드 (빠른 Hit Test용)
7536
6516
 
7537
6517
 
7538
6518
  var buildSpatialIndex$1 = function () {
7539
6519
  buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
7540
- }; // --------------------------------------------------------------------------
7541
- // 렌더링 함수 결정 (dataType에 따라)
7542
- // --------------------------------------------------------------------------
7543
-
7544
- /**
7545
- * 외부 렌더링 함수에 전달할 유틸리티 객체
7546
- */
6520
+ }; // 렌더링 유틸리티 객체
7547
6521
 
7548
6522
 
7549
6523
  var renderUtils = {
7550
6524
  getOrComputePolygonOffsets: getOrComputePolygonOffsets,
7551
6525
  getOrComputeMarkerOffset: function () {
7552
6526
  return null;
7553
- } // 폴리곤에서는 사용하지 않음
7554
-
7555
- };
7556
- /**
7557
- * 렌더링 함수 생성 (props 기반)
7558
- */
6527
+ }
6528
+ }; // 렌더링 함수 생성
7559
6529
 
7560
6530
  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
- */
6531
+ var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth); // Base Layer 렌더링 (뷰포트 컬링 적용)
7574
6532
 
7575
6533
  var doRenderBase = function () {
7576
6534
  var layer = baseLayerRef.current;
7577
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
7578
-
6535
+ if (!layer) return;
7579
6536
  var shape = layer.findOne('.base-render-shape');
7580
6537
 
7581
6538
  if (!shape) {
7582
- // 최초 생성 (한 번만 실행됨)
7583
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
7584
6539
  shape = new Konva.Shape({
7585
6540
  name: 'base-render-shape',
7586
6541
  sceneFunc: function (context, shape) {
7587
6542
  var ctx = context;
7588
- var hovered = hoveredItemRef.current; // 클로저로 최신 ref 참조
6543
+ var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
7589
6544
 
7590
- var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
6545
+ var visibleItems = dataRef.current.filter(function (item) {
7591
6546
  return isInViewport$1(item);
7592
- }) : dataRef.current; // 일반 항목 렌더링
7593
-
6547
+ });
7594
6548
  renderBase({
7595
6549
  ctx: ctx,
7596
6550
  items: visibleItems,
@@ -7604,45 +6558,24 @@ var WoongCanvasPolygon = function (props) {
7604
6558
  hitStrokeWidth: 0
7605
6559
  });
7606
6560
  layer.add(shape);
7607
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
7608
-
6561
+ }
7609
6562
 
7610
6563
  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
- */
6564
+ }; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
7626
6565
 
7627
6566
 
7628
6567
  var doRenderEvent = function () {
7629
6568
  var layer = eventLayerRef.current;
7630
- if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
7631
-
6569
+ if (!layer) return;
7632
6570
  var shape = layer.findOne('.event-render-shape');
7633
6571
 
7634
6572
  if (!shape) {
7635
- // 최초 생성 (한 번만 실행됨)
7636
- // sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
7637
6573
  shape = new Konva.Shape({
7638
6574
  name: 'event-render-shape',
7639
6575
  sceneFunc: function (context, shape) {
7640
- var ctx = context; // 클로저로 최신 ref 값 참조
7641
- // 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
7642
-
6576
+ var ctx = context;
7643
6577
  var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
7644
- var hovered = hoveredItemRef.current; // 일반 렌더링
7645
-
6578
+ var hovered = hoveredItemRef.current;
7646
6579
  renderEvent({
7647
6580
  ctx: ctx,
7648
6581
  hoveredItem: hovered,
@@ -7656,21 +6589,10 @@ var WoongCanvasPolygon = function (props) {
7656
6589
  hitStrokeWidth: 0
7657
6590
  });
7658
6591
  layer.add(shape);
7659
- } // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
7660
-
6592
+ }
7661
6593
 
7662
6594
  layer.batchDraw();
7663
- };
7664
- /**
7665
- * 전체 즉시 렌더링 (IDLE 시 호출)
7666
- *
7667
- * 뷰포트 업데이트, 공간 인덱스 빌드, 모든 레이어 렌더링을 순차적으로 수행합니다.
7668
- *
7669
- * @remarks
7670
- * - 호출 시점: 지도 이동/줌 완료 시, 데이터 변경 시, 리사이즈 시
7671
- * - 순서: 뷰포트 업데이트 → 공간 인덱스 빌드 → Base → Event 렌더링
7672
- * - Animation Layer는 사용하지 않음 (폴리곤 특성)
7673
- */
6595
+ }; // 전체 즉시 렌더링
7674
6596
 
7675
6597
 
7676
6598
  var renderAllImmediate = function () {
@@ -7678,12 +6600,10 @@ var WoongCanvasPolygon = function (props) {
7678
6600
  buildSpatialIndex$1();
7679
6601
  doRenderBase();
7680
6602
  doRenderEvent();
7681
- }; // --------------------------------------------------------------------------
7682
- // 이벤트 핸들러: 지도 이벤트
7683
- // --------------------------------------------------------------------------
6603
+ }; // 지도 이벤트 핸들러 생성
7684
6604
 
7685
6605
 
7686
- var _f = createMapEventHandlers({
6606
+ var _e = createMapEventHandlers({
7687
6607
  controller: controller,
7688
6608
  containerRef: containerRef,
7689
6609
  markerRef: markerRef,
@@ -7694,49 +6614,32 @@ var WoongCanvasPolygon = function (props) {
7694
6614
  boundingBoxCacheRef: boundingBoxCacheRef,
7695
6615
  renderAllImmediate: renderAllImmediate
7696
6616
  }),
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
-
6617
+ handleIdle = _e.handleIdle,
6618
+ handleZoomStart = _e.handleZoomStart,
6619
+ handleZoomEnd = _e.handleZoomEnd,
6620
+ handleCenterChanged = _e.handleCenterChanged,
6621
+ handleDragStartShared = _e.handleDragStart,
6622
+ handleDragEndShared = _e.handleDragEnd;
7707
6623
 
7708
6624
  var handleDragStart = function () {
7709
6625
  handleDragStartShared();
7710
6626
  draggingRef.current = true;
7711
6627
  controller.setMapCursor('grabbing');
7712
6628
  };
7713
- /**
7714
- * 드래그 종료 처리 (커서를 기본으로 복원)
7715
- */
7716
-
7717
6629
 
7718
6630
  var handleDragEnd = function () {
7719
6631
  handleDragEndShared();
7720
6632
  draggingRef.current = false;
7721
6633
  controller.setMapCursor('grab');
7722
- }; // --------------------------------------------------------------------------
7723
- // Hit Test & 상태 관리
7724
- // --------------------------------------------------------------------------
7725
-
7726
- /**
7727
- * 특정 좌표의 폴리곤 데이터 찾기 (Spatial Index 사용)
7728
- *
7729
- * @param offset 검사할 좌표
7730
- * @returns 찾은 폴리곤 데이터 또는 null
7731
- */
6634
+ }; // Hit Test: 특정 좌표의 폴리곤 찾기
7732
6635
 
7733
6636
 
7734
6637
  var findData = function (offset) {
7735
- // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 ~10개)
7736
- var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 폴리곤 체크
6638
+ // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
6639
+ var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
7737
6640
 
7738
6641
  for (var i = candidates.length - 1; i >= 0; i--) {
7739
- var item = candidates[i];
6642
+ var item = candidates[i]; // 정확한 Hit Test: Ray Casting 알고리즘으로 폴리곤 내부 여부 확인
7740
6643
 
7741
6644
  if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
7742
6645
  return item;
@@ -7744,19 +6647,7 @@ var WoongCanvasPolygon = function (props) {
7744
6647
  }
7745
6648
 
7746
6649
  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
- */
6650
+ }; // Hover 상태 설정 및 렌더링
7760
6651
 
7761
6652
 
7762
6653
  var setHovered = function (data) {
@@ -7766,32 +6657,14 @@ var WoongCanvasPolygon = function (props) {
7766
6657
  controller.setMapCursor('grabbing');
7767
6658
  } else {
7768
6659
  controller.setMapCursor(data ? 'pointer' : 'grab');
7769
- } // 즉시 렌더링 (RAF 없이)
7770
-
6660
+ }
7771
6661
 
7772
6662
  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
- */
6663
+ }; // 클릭 처리: 선택 상태 업데이트
7789
6664
 
7790
6665
 
7791
6666
  var handleLocalClick = function (data) {
7792
- // 1. 선택 상태 업데이트
7793
6667
  if (enableMultiSelect) {
7794
- // 다중 선택: Set과 Map 동시 업데이트
7795
6668
  var newSelected = new Set(selectedIdsRef.current);
7796
6669
 
7797
6670
  if (newSelected.has(data.id)) {
@@ -7804,7 +6677,6 @@ var WoongCanvasPolygon = function (props) {
7804
6677
 
7805
6678
  selectedIdsRef.current = newSelected;
7806
6679
  } else {
7807
- // 단일 선택: 토글
7808
6680
  var newSelected = new Set();
7809
6681
 
7810
6682
  if (!selectedIdsRef.current.has(data.id)) {
@@ -7816,132 +6688,79 @@ var WoongCanvasPolygon = function (props) {
7816
6688
  }
7817
6689
 
7818
6690
  selectedIdsRef.current = newSelected;
7819
- } // 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
7820
-
7821
-
7822
- doRenderBase(); // 3. Event Layer 렌더링 (hover 처리)
6691
+ }
7823
6692
 
6693
+ doRenderBase();
7824
6694
  doRenderEvent();
7825
- }; // --------------------------------------------------------------------------
7826
- // 이벤트 핸들러: UI 이벤트
7827
- // --------------------------------------------------------------------------
7828
-
7829
- /**
7830
- * 클릭 이벤트 처리
7831
- *
7832
- * @param event 클릭 이벤트 파라미터
7833
- *
7834
- * @remarks
7835
- * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
7836
- * - 상호작용이 비활성화되어 있으면 스킵
7837
- * - Spatial Index를 사용하여 빠른 Hit Test 수행
7838
- */
6695
+ }; // 클릭 이벤트 핸들러
7839
6696
 
7840
6697
 
7841
6698
  var handleClick = function (event) {
7842
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7843
-
6699
+ if (disableInteractionRef.current) return;
7844
6700
  var clickedOffset = validateEvent(event, context, controller);
7845
6701
  if (!clickedOffset) return;
7846
6702
  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
- */
6703
+ if (!data) return;
6704
+ handleLocalClick(data);
6705
+ onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
6706
+ }; // 마우스 이동 이벤트 핸들러 (hover 감지)
7866
6707
 
7867
6708
 
7868
6709
  var handleMouseMove = function (event) {
7869
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7870
-
6710
+ if (disableInteractionRef.current) return;
7871
6711
  var mouseOffset = validateEvent(event, context, controller);
7872
6712
  if (!mouseOffset) return;
7873
6713
  var hoveredItem = findData(mouseOffset);
7874
6714
  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
- */
6715
+ if (prevHovered === hoveredItem) return;
6716
+ setHovered(hoveredItem);
6717
+ }; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
7889
6718
 
7890
6719
 
7891
6720
  var handleMouseLeave = function () {
7892
- if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7893
-
6721
+ if (disableInteractionRef.current) return;
7894
6722
  var prevHovered = hoveredItemRef.current;
7895
-
7896
- if (prevHovered) {
7897
- hoveredItemRef.current = null;
7898
- controller.setMapCursor('grab');
7899
- doRenderEvent();
7900
- }
7901
- }; // --------------------------------------------------------------------------
7902
- // Lifecycle: DOM 초기화
7903
- // --------------------------------------------------------------------------
6723
+ if (!prevHovered) return;
6724
+ hoveredItemRef.current = null;
6725
+ controller.setMapCursor('grab');
6726
+ doRenderEvent();
6727
+ }; // DOM 초기화
7904
6728
 
7905
6729
 
7906
6730
  useEffect(function () {
7907
6731
  divElement.style.width = 'fit-content';
7908
6732
  return function () {
7909
- if (markerRef.current) {
7910
- controller.clearDrawable(markerRef.current);
7911
- markerRef.current = undefined;
7912
- }
6733
+ if (!markerRef.current) return;
6734
+ controller.clearDrawable(markerRef.current);
6735
+ markerRef.current = undefined;
7913
6736
  };
7914
- }, []); // --------------------------------------------------------------------------
7915
- // Lifecycle: 마커 생성/업데이트
7916
- // --------------------------------------------------------------------------
6737
+ }, []); // 마커 생성/업데이트
7917
6738
 
7918
6739
  useEffect(function () {
7919
- if (options) {
7920
- var bounds = controller.getCurrBounds();
6740
+ if (!options) return;
6741
+ var bounds = controller.getCurrBounds();
7921
6742
 
7922
- var markerOptions = __assign({
7923
- position: bounds.nw
7924
- }, options);
6743
+ var markerOptions = __assign({
6744
+ position: bounds.nw
6745
+ }, options);
7925
6746
 
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);
6747
+ if (markerRef.current) {
6748
+ controller.updateMarker(markerRef.current, markerOptions);
6749
+ return;
6750
+ }
7932
6751
 
7933
- if (divElement.parentElement) {
7934
- divElement.parentElement.style.pointerEvents = 'none';
7935
- }
6752
+ markerRef.current = new Marker(markerOptions);
6753
+ markerRef.current.element = divElement;
6754
+ controller.createMarker(markerRef.current);
7936
6755
 
7937
- if (options.zIndex !== undefined) {
7938
- controller.setMarkerZIndex(markerRef.current, options.zIndex);
7939
- }
7940
- }
6756
+ if (divElement.parentElement) {
6757
+ divElement.parentElement.style.pointerEvents = 'none';
7941
6758
  }
7942
- }, [options]); // --------------------------------------------------------------------------
7943
- // Lifecycle: Konva 초기화 및 이벤트 리스너 등록
7944
- // --------------------------------------------------------------------------
6759
+
6760
+ if (options.zIndex !== undefined) {
6761
+ controller.setMarkerZIndex(markerRef.current, options.zIndex);
6762
+ }
6763
+ }, [options]); // Konva 초기화 및 이벤트 리스너 등록
7945
6764
 
7946
6765
  useEffect(function () {
7947
6766
  var mapDiv = controller.mapDivElement;
@@ -7950,11 +6769,9 @@ var WoongCanvasPolygon = function (props) {
7950
6769
  width: mapDiv.offsetWidth,
7951
6770
  height: mapDiv.offsetHeight
7952
6771
  });
7953
- stageRef.current = stage; // 레이어 최적화 설정
7954
-
6772
+ stageRef.current = stage;
7955
6773
  var baseLayer = new Konva.Layer({
7956
- listening: false // 이벤트 리스닝 비활성화로 성능 향상
7957
-
6774
+ listening: false
7958
6775
  });
7959
6776
  var eventLayer = new Konva.Layer({
7960
6777
  listening: false
@@ -7962,13 +6779,11 @@ var WoongCanvasPolygon = function (props) {
7962
6779
  baseLayerRef.current = baseLayer;
7963
6780
  eventLayerRef.current = eventLayer;
7964
6781
  stage.add(baseLayer);
7965
- stage.add(eventLayer); // 초기 뷰포트 설정
7966
-
7967
- updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
6782
+ stage.add(eventLayer);
6783
+ updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
7968
6784
 
7969
6785
  var resizeRafId = null;
7970
6786
  var resizeObserver = new ResizeObserver(function () {
7971
- // RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
7972
6787
  if (resizeRafId !== null) {
7973
6788
  cancelAnimationFrame(resizeRafId);
7974
6789
  }
@@ -7991,10 +6806,9 @@ var WoongCanvasPolygon = function (props) {
7991
6806
  controller.addEventListener('CLICK', handleClick);
7992
6807
  controller.addEventListener('MOUSEMOVE', handleMouseMove);
7993
6808
  controller.addEventListener('DRAGSTART', handleDragStart);
7994
- controller.addEventListener('DRAGEND', handleDragEnd); // 맵 컨테이너에 mouseleave 이벤트 추가
7995
-
6809
+ controller.addEventListener('DRAGEND', handleDragEnd);
7996
6810
  mapDiv.addEventListener('mouseleave', handleMouseLeave);
7997
- renderAllImmediate(); // Context 사용 시 컴포넌트 등록 (다중 인스턴스 관리)
6811
+ renderAllImmediate(); // Context 사용 시 컴포넌트 등록
7998
6812
 
7999
6813
  var componentInstance = null;
8000
6814
 
@@ -8013,22 +6827,17 @@ var WoongCanvasPolygon = function (props) {
8013
6827
  },
8014
6828
  isInteractionDisabled: function () {
8015
6829
  return disableInteractionRef.current;
8016
- } // 🚫 상호작용 비활성화 여부 반환
8017
-
6830
+ }
8018
6831
  };
8019
6832
  context.registerComponent(componentInstance);
8020
- } // Cleanup 함수
8021
-
6833
+ }
8022
6834
 
8023
6835
  return function () {
8024
- // RAF 정리
8025
6836
  if (resizeRafId !== null) {
8026
6837
  cancelAnimationFrame(resizeRafId);
8027
- } // 옵저버 정리
8028
-
8029
-
8030
- resizeObserver.disconnect(); // 이벤트 리스너 정리
6838
+ }
8031
6839
 
6840
+ resizeObserver.disconnect();
8032
6841
  controller.removeEventListener('IDLE', handleIdle);
8033
6842
  controller.removeEventListener('ZOOMSTART', handleZoomStart);
8034
6843
  controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
@@ -8037,57 +6846,41 @@ var WoongCanvasPolygon = function (props) {
8037
6846
  controller.removeEventListener('MOUSEMOVE', handleMouseMove);
8038
6847
  controller.removeEventListener('DRAGSTART', handleDragStart);
8039
6848
  controller.removeEventListener('DRAGEND', handleDragEnd);
8040
- mapDiv.removeEventListener('mouseleave', handleMouseLeave); // Context 정리
6849
+ mapDiv.removeEventListener('mouseleave', handleMouseLeave);
8041
6850
 
8042
6851
  if (context && componentInstance) {
8043
6852
  context.unregisterComponent(componentInstance);
8044
- } // Konva 리소스 정리
8045
-
6853
+ }
8046
6854
 
8047
6855
  baseLayer.destroyChildren();
8048
6856
  eventLayer.destroyChildren();
8049
- stage.destroy(); // 캐시 정리
8050
-
6857
+ stage.destroy();
8051
6858
  offsetCacheRef.current.clear();
8052
6859
  boundingBoxCacheRef.current.clear();
8053
6860
  spatialIndexRef.current.clear();
8054
6861
  };
8055
- }, []); // 초기화는 한 번만
8056
- // --------------------------------------------------------------------------
8057
- // Lifecycle: disableInteraction 동기화
8058
- // --------------------------------------------------------------------------
6862
+ }, []); // disableInteraction 동기화
8059
6863
 
8060
6864
  useEffect(function () {
8061
6865
  disableInteractionRef.current = disableInteraction;
8062
- }, [disableInteraction]); // --------------------------------------------------------------------------
8063
- // Lifecycle: 외부 selectedItems 동기화
8064
- // --------------------------------------------------------------------------
6866
+ }, [disableInteraction]); // 외부 selectedItems 동기화
8065
6867
 
8066
6868
  useEffect(function () {
8067
6869
  if (!stageRef.current) return;
8068
- syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef); // 렌더링
8069
-
6870
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
8070
6871
  doRenderBase();
8071
6872
  doRenderEvent();
8072
- }, [externalSelectedItems]); // 배열 자체를 dependency로 사용
8073
- // --------------------------------------------------------------------------
8074
- // Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
8075
- // --------------------------------------------------------------------------
6873
+ }, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
8076
6874
 
8077
6875
  useEffect(function () {
8078
- if (!stageRef.current) return; // Ref 동기화
8079
-
8080
- selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
8081
-
6876
+ if (!stageRef.current) return;
6877
+ selectedItemRef.current = externalSelectedItem;
8082
6878
  doRenderEvent();
8083
- }, [externalSelectedItem]); // --------------------------------------------------------------------------
8084
- // Lifecycle: 데이터 변경 시 렌더링
8085
- // --------------------------------------------------------------------------
6879
+ }, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
8086
6880
 
8087
6881
  useEffect(function () {
8088
- if (!stageRef.current) return; // dataRef 동기화
8089
-
8090
- dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
6882
+ if (!stageRef.current) return;
6883
+ dataRef.current = data;
8091
6884
 
8092
6885
  if (containerRef.current) {
8093
6886
  containerRef.current.style.transform = '';
@@ -8097,26 +6890,10 @@ var WoongCanvasPolygon = function (props) {
8097
6890
  accumTranslateRef.current = {
8098
6891
  x: 0,
8099
6892
  y: 0
8100
- }; // 캐시 정리 (새 데이터이므로 기존 캐시는 무효)
8101
-
6893
+ };
8102
6894
  offsetCacheRef.current.clear();
8103
6895
  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
-
6896
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
8120
6897
  renderAllImmediate();
8121
6898
  }, [data]);
8122
6899
  return createPortal(React.createElement("div", {