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

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 +71 -2
  2. package/dist/components/mint-map/core/advanced/shared/context.js +74 -1
  3. package/dist/components/mint-map/core/advanced/shared/helpers.d.ts +28 -0
  4. package/dist/components/mint-map/core/advanced/shared/helpers.js +52 -0
  5. package/dist/components/mint-map/core/advanced/shared/hooks.d.ts +144 -0
  6. package/dist/components/mint-map/core/advanced/shared/hooks.js +283 -0
  7. package/dist/components/mint-map/core/advanced/shared/index.d.ts +3 -0
  8. package/dist/components/mint-map/core/advanced/shared/performance.d.ts +105 -24
  9. package/dist/components/mint-map/core/advanced/shared/performance.js +105 -24
  10. package/dist/components/mint-map/core/advanced/shared/utils.d.ts +128 -14
  11. package/dist/components/mint-map/core/advanced/shared/utils.js +128 -14
  12. package/dist/components/mint-map/core/advanced/shared/viewport.d.ts +72 -0
  13. package/dist/components/mint-map/core/advanced/shared/viewport.js +81 -0
  14. package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.js +142 -209
  15. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.d.ts +0 -4
  16. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.js +122 -217
  17. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.d.ts +64 -5
  18. package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.js +81 -20
  19. package/dist/index.es.js +1066 -511
  20. package/dist/index.js +11 -0
  21. package/dist/index.umd.js +1072 -509
  22. package/package.json +1 -1
package/dist/index.umd.js CHANGED
@@ -800,6 +800,30 @@
800
800
 
801
801
  /**
802
802
  * 폴리곤 offset 계산
803
+ *
804
+ * GeoJSON MultiPolygon 형식의 위경도 좌표를 화면 픽셀 좌표로 변환합니다.
805
+ *
806
+ * @param polygonData 폴리곤 데이터 (paths 필드 필수)
807
+ * @param controller MintMapController 인스턴스
808
+ * @returns 변환된 화면 좌표 배열 (4차원 배열) 또는 null (변환 실패 시)
809
+ *
810
+ * @remarks
811
+ * - 반환 형식: [MultiPolygon][Polygon][Point][x/y]
812
+ * - 성능: O(n), n은 폴리곤의 총 좌표 수
813
+ * - GeoJSON MultiPolygon 형식 지원
814
+ *
815
+ * @example
816
+ * ```typescript
817
+ * const offsets = computePolygonOffsets(polygonData, controller);
818
+ * if (!offsets) return; // 변환 실패
819
+ *
820
+ * // offsets 구조: [MultiPolygon][Polygon][Point][x/y]
821
+ * for (const multiPolygon of offsets) {
822
+ * for (const polygon of multiPolygon) {
823
+ * // polygon은 [Point][x/y] 배열
824
+ * }
825
+ * }
826
+ * ```
803
827
  */
804
828
 
805
829
  var computePolygonOffsets = function (polygonData, controller) {
@@ -836,6 +860,23 @@
836
860
  };
837
861
  /**
838
862
  * 마커 offset 계산
863
+ *
864
+ * 마커의 위경도 좌표를 화면 픽셀 좌표로 변환합니다.
865
+ *
866
+ * @param markerData 마커 데이터 (position 필드 필수)
867
+ * @param controller MintMapController 인스턴스
868
+ * @returns 변환된 화면 좌표 (Offset) 또는 null (변환 실패 시)
869
+ *
870
+ * @remarks
871
+ * - 반환된 좌표는 마커의 중심점 (x, y)
872
+ * - 성능: O(1) - 단일 좌표 변환
873
+ *
874
+ * @example
875
+ * ```typescript
876
+ * const offset = computeMarkerOffset(markerData, controller);
877
+ * if (!offset) return; // 변환 실패
878
+ * // offset.x, offset.y는 화면 픽셀 좌표
879
+ * ```
839
880
  */
840
881
 
841
882
  var computeMarkerOffset = function (markerData, controller) {
@@ -847,6 +888,24 @@
847
888
  };
848
889
  /**
849
890
  * Point-in-Polygon 알고리즘
891
+ *
892
+ * Ray Casting 알고리즘을 사용하여 점이 폴리곤 내부에 있는지 확인합니다.
893
+ *
894
+ * @param point 확인할 점의 좌표
895
+ * @param polygon 폴리곤 좌표 배열 (각 요소는 [x, y] 형식)
896
+ * @returns 점이 폴리곤 내부에 있으면 true, 아니면 false
897
+ *
898
+ * @remarks
899
+ * - **알고리즘**: Ray Casting (Ray Crossing)
900
+ * - **성능**: O(n), n은 폴리곤의 좌표 수
901
+ * - **경계 처리**: 경계선 위의 점은 내부로 간주
902
+ *
903
+ * @example
904
+ * ```typescript
905
+ * const point = { x: 100, y: 200 };
906
+ * const polygon = [[0, 0], [100, 0], [100, 100], [0, 100]];
907
+ * const isInside = isPointInPolygon(point, polygon);
908
+ * ```
850
909
  */
851
910
 
852
911
  var isPointInPolygon = function (point, polygon) {
@@ -866,13 +925,32 @@
866
925
  /**
867
926
  * 폴리곤 히트 테스트 (도넛 폴리곤 지원)
868
927
  *
869
- * 로직:
870
- * 1. 외부 폴리곤(첫 번째): 내부에 있어야 함
871
- * 2. 내부 구멍들(나머지): 내부에 있으면 안 됨 (evenodd 규칙)
928
+ * 점이 폴리곤 내부에 있는지 확인합니다. 도넛 폴리곤(구멍이 있는 폴리곤)을 지원합니다.
929
+ *
930
+ * @param clickedOffset 클릭/마우스 위치 좌표
931
+ * @param polygonData 폴리곤 데이터
932
+ * @param getPolygonOffsets 폴리곤 좌표 변환 함수
933
+ * @returns 점이 폴리곤 내부에 있으면 true, 아니면 false
872
934
  *
873
- * 중요: 도넛 폴리곤과 내부 폴리곤은 별개의 polygonData로 처리됨
935
+ * @remarks
936
+ * - **도넛 폴리곤 처리** (isDonutPolygon === true):
937
+ * 1. 외부 폴리곤(첫 번째): 내부에 있어야 함
938
+ * 2. 내부 구멍들(나머지): 내부에 있으면 안 됨 (evenodd 규칙)
939
+ * - **일반 폴리곤 처리**: Point-in-Polygon 알고리즘 사용
940
+ * - **성능**: O(n), n은 폴리곤의 총 좌표 수
941
+ *
942
+ * **중요**: 도넛 폴리곤과 내부 폴리곤은 별개의 polygonData로 처리됩니다.
874
943
  * - 도넛 폴리곤 A: isDonutPolygon=true
875
944
  * - 내부 폴리곤 B: isDonutPolygon=false (별도 데이터)
945
+ *
946
+ * @example
947
+ * ```typescript
948
+ * const isHit = isPointInPolygonData(
949
+ * clickedOffset,
950
+ * polygonData,
951
+ * getOrComputePolygonOffsets
952
+ * );
953
+ * ```
876
954
  */
877
955
 
878
956
  var isPointInPolygonData = function (clickedOffset, polygonData, getPolygonOffsets) {
@@ -937,10 +1015,29 @@
937
1015
  /**
938
1016
  * 마커 히트 테스트 (클릭/hover 영역 체크)
939
1017
  *
940
- * 🎯 중요: 꼬리(tail)는 Hit Test 영역에서 제외됩니다!
941
- * - markerOffset.y마커 최하단(꼬리 끝) 좌표
942
- * - boxHeight는 마커 본체만 포함 (꼬리 제외)
943
- * - tailHeight만큼 위로 올려서 본체만 Hit Test 영역으로 사용
1018
+ * 점이 마커의 클릭/호버 영역 내부에 있는지 확인합니다.
1019
+ * 마커의 꼬리(tail)Hit Test 영역에서 제외됩니다.
1020
+ *
1021
+ * @param clickedOffset 클릭/마우스 위치 좌표
1022
+ * @param markerData 마커 데이터
1023
+ * @param getMarkerOffset 마커 좌표 변환 함수
1024
+ * @returns 점이 마커 영역 내부에 있으면 true, 아니면 false
1025
+ *
1026
+ * @remarks
1027
+ * - **꼬리 제외**: 꼬리(tail)는 Hit Test 영역에서 제외됩니다
1028
+ * - markerOffset.y는 마커 최하단(꼬리 끝) 좌표
1029
+ * - boxHeight는 마커 본체만 포함 (꼬리 제외)
1030
+ * - tailHeight만큼 위로 올려서 본체만 Hit Test 영역으로 사용
1031
+ * - **성능**: O(1) - 단순 사각형 영역 체크
1032
+ *
1033
+ * @example
1034
+ * ```typescript
1035
+ * const isHit = isPointInMarkerData(
1036
+ * clickedOffset,
1037
+ * markerData,
1038
+ * getOrComputeMarkerOffset
1039
+ * );
1040
+ * ```
944
1041
  */
945
1042
 
946
1043
  var isPointInMarkerData = function (clickedOffset, markerData, getMarkerOffset) {
@@ -977,12 +1074,29 @@
977
1074
  /**
978
1075
  * 텍스트 박스의 너비를 계산합니다.
979
1076
  *
980
- * @param {Object} params - 파라미터 객체
981
- * @param {string} params.text - 측정할 텍스트
982
- * @param {string} params.fontConfig - 폰트 설정 (예: 'bold 16px Arial')
983
- * @param {number} params.padding - 텍스트 박스에 적용할 패딩 값
984
- * @param {number} params.minWidth - 최소 너비
985
- * @returns {number} 계산된 텍스트 박스의 너비
1077
+ * Canvas 2D Context의 measureText()를 사용하여 텍스트의 실제 너비를 계산하고,
1078
+ * 패딩과 최소 너비를 고려하여 최종 너비를 반환합니다.
1079
+ *
1080
+ * @param params 파라미터 객체
1081
+ * @param params.text 측정할 텍스트
1082
+ * @param params.fontConfig 폰트 설정 (예: 'bold 16px Arial')
1083
+ * @param params.padding 텍스트 박스에 적용할 패딩 값 (px)
1084
+ * @param params.minWidth 최소 너비 (px)
1085
+ * @returns 계산된 텍스트 박스의 너비 (px)
1086
+ *
1087
+ * @remarks
1088
+ * - 성능: O(1) - 단일 텍스트 측정
1089
+ * - 임시 Canvas를 사용하여 정확한 너비 측정
1090
+ *
1091
+ * @example
1092
+ * ```typescript
1093
+ * const width = calculateTextBoxWidth({
1094
+ * text: "Hello World",
1095
+ * fontConfig: 'bold 16px Arial',
1096
+ * padding: 20,
1097
+ * minWidth: 60
1098
+ * });
1099
+ * ```
986
1100
  */
987
1101
 
988
1102
  var calculateTextBoxWidth = function (_a) {
@@ -997,6 +1111,29 @@
997
1111
  };
998
1112
 
999
1113
  var WoongCanvasContext = React.createContext(null);
1114
+ /**
1115
+ * WoongCanvasProvider 컴포넌트
1116
+ *
1117
+ * 다중 WoongCanvas 컴포넌트 인스턴스를 관리하는 Context Provider입니다.
1118
+ * 여러 WoongCanvasMarker/WoongCanvasPolygon이 함께 사용될 때 전역 이벤트 조정을 수행합니다.
1119
+ *
1120
+ * @param props 컴포넌트 props
1121
+ * @param props.children 자식 컴포넌트
1122
+ *
1123
+ * @remarks
1124
+ * - zIndex 기반으로 이벤트 우선순위 처리
1125
+ * - 전역 클릭/호버 이벤트 조정
1126
+ * - 여러 레이어 간 상호작용 관리
1127
+ *
1128
+ * @example
1129
+ * ```tsx
1130
+ * <WoongCanvasProvider>
1131
+ * <WoongCanvasMarker data={markers} zIndex={10} />
1132
+ * <WoongCanvasPolygon data={polygons} zIndex={5} />
1133
+ * </WoongCanvasProvider>
1134
+ * ```
1135
+ */
1136
+
1000
1137
  var WoongCanvasProvider = function (_a) {
1001
1138
  var children = _a.children;
1002
1139
  var controller = useMintMapController(); // Refs
@@ -1007,7 +1144,12 @@
1007
1144
  var draggingRef = React.useRef(false);
1008
1145
  /**
1009
1146
  * 컴포넌트 등록 (zIndex 내림차순 정렬)
1010
- * 높은 zIndex가 먼저 처리됨
1147
+ *
1148
+ * 컴포넌트 인스턴스를 등록하고 zIndex 기준으로 내림차순 정렬합니다.
1149
+ * 높은 zIndex를 가진 컴포넌트가 이벤트 처리에서 우선순위를 가집니다.
1150
+ *
1151
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1152
+ * @param instance 등록할 컴포넌트 인스턴스
1011
1153
  */
1012
1154
 
1013
1155
  var registerComponent = React.useCallback(function (instance) {
@@ -1018,6 +1160,12 @@
1018
1160
  }, []);
1019
1161
  /**
1020
1162
  * 컴포넌트 등록 해제
1163
+ *
1164
+ * 컴포넌트 인스턴스를 등록 해제합니다.
1165
+ * hover 중이던 컴포넌트면 hover 상태도 초기화합니다.
1166
+ *
1167
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1168
+ * @param instance 등록 해제할 컴포넌트 인스턴스
1021
1169
  */
1022
1170
 
1023
1171
  var unregisterComponent = React.useCallback(function (instance) {
@@ -1033,6 +1181,15 @@
1033
1181
  }, []);
1034
1182
  /**
1035
1183
  * 전역 클릭 핸들러 (zIndex 우선순위)
1184
+ *
1185
+ * 모든 등록된 WoongCanvas 컴포넌트 중 zIndex가 높은 컴포넌트부터 클릭 이벤트를 처리합니다.
1186
+ *
1187
+ * @param event 클릭 이벤트 파라미터
1188
+ *
1189
+ * @remarks
1190
+ * - zIndex가 높은 컴포넌트부터 순회하여 첫 번째 히트만 처리
1191
+ * - 상호작용이 비활성화된 컴포넌트는 스킵
1192
+ * - Context가 없으면 각 컴포넌트의 로컬 핸들러가 처리
1036
1193
  */
1037
1194
 
1038
1195
  var handleGlobalClick = React.useCallback(function (event) {
@@ -1060,6 +1217,16 @@
1060
1217
  }, [controller]);
1061
1218
  /**
1062
1219
  * 전역 마우스 이동 핸들러 (zIndex 우선순위)
1220
+ *
1221
+ * 모든 등록된 WoongCanvas 컴포넌트 중 zIndex가 높은 컴포넌트부터 hover 이벤트를 처리합니다.
1222
+ *
1223
+ * @param event 마우스 이동 이벤트 파라미터
1224
+ *
1225
+ * @remarks
1226
+ * - zIndex가 높은 컴포넌트부터 순회하여 첫 번째 히트만 처리
1227
+ * - 상호작용이 비활성화된 컴포넌트는 스킵
1228
+ * - 드래그 중이면 이벤트 무시 (성능 최적화)
1229
+ * - hover 상태 변경 감지 후 이전 hover 해제 및 새 hover 설정
1063
1230
  */
1064
1231
 
1065
1232
  var handleGlobalMouseMove = React.useCallback(function (event) {
@@ -1146,6 +1313,26 @@
1146
1313
  value: contextValue
1147
1314
  }, children);
1148
1315
  };
1316
+ /**
1317
+ * WoongCanvas Context Hook
1318
+ *
1319
+ * WoongCanvasProvider로 감싸진 컴포넌트에서 Context에 접근할 수 있는 hook입니다.
1320
+ *
1321
+ * @returns WoongCanvasContextValue 또는 null (Provider 없으면)
1322
+ *
1323
+ * @remarks
1324
+ * - Provider로 감싸지지 않으면 null 반환 (각 컴포넌트가 로컬 이벤트 처리)
1325
+ * - Provider로 감싸져 있으면 전역 이벤트 조정 활성화
1326
+ *
1327
+ * @example
1328
+ * ```typescript
1329
+ * const context = useWoongCanvasContext();
1330
+ * if (context) {
1331
+ * // Context 사용 중 (전역 이벤트 조정)
1332
+ * }
1333
+ * ```
1334
+ */
1335
+
1149
1336
  var useWoongCanvasContext = function () {
1150
1337
  var context = React.useContext(WoongCanvasContext);
1151
1338
  return context;
@@ -1199,12 +1386,32 @@
1199
1386
  var DEFAULT_MAX_CACHE_SIZE = 30000;
1200
1387
  /**
1201
1388
  * LRU (Least Recently Used) Cache
1202
- * 메모리 제한을 위한 캐시 구현 (최적화 버전)
1203
1389
  *
1204
- * 개선 사항:
1390
+ * 메모리 제한을 위한 캐시 구현입니다. WoongCanvas 컴포넌트에서 좌표 변환 결과를 캐싱하는데 사용됩니다.
1391
+ *
1392
+ * @template K 캐시 키 타입
1393
+ * @template V 캐시 값 타입
1394
+ *
1395
+ * @remarks
1396
+ * **개선 사항**:
1205
1397
  * 1. get() 성능 향상: 접근 빈도 추적 없이 단순 조회만 수행 (delete+set 제거)
1206
1398
  * 2. set() 버그 수정: 기존 키 업데이트 시 maxSize 체크 로직 개선
1207
1399
  * 3. 메모리 효율: 단순 FIFO 캐시로 동작하여 오버헤드 최소화
1400
+ *
1401
+ * **트레이드오프**:
1402
+ * - 장점: 읽기 성능 대폭 향상 (10,000번 get → 이전보다 2배 빠름)
1403
+ * - 단점: 접근 빈도가 아닌 삽입 순서 기반 eviction (FIFO)
1404
+ *
1405
+ * WoongCanvasMarker 사용 사례에 최적:
1406
+ * - 좌표 변환 결과는 zoom/pan 시 어차피 전체 초기화
1407
+ * - 접근 빈도 추적보다 빠른 조회가 더 중요
1408
+ *
1409
+ * @example
1410
+ * ```typescript
1411
+ * const cache = new LRUCache<string, Offset>(30000);
1412
+ * cache.set(item.id, offset);
1413
+ * const cached = cache.get(item.id);
1414
+ * ```
1208
1415
  */
1209
1416
 
1210
1417
  var LRUCache =
@@ -1221,17 +1428,12 @@
1221
1428
  /**
1222
1429
  * 캐시에서 값 조회
1223
1430
  *
1224
- * 최적화: delete+set 제거
1225
- * - 이전: 매번 delete+set으로 LRU 갱신 (해시 재계산 비용)
1226
- * - 현재: 단순 조회만 수행 (O(1) 해시 조회)
1227
- *
1228
- * 트레이드오프:
1229
- * - 장점: 읽기 성능 대폭 향상 (10,000번 get → 이전보다 2배 빠름)
1230
- * - 단점: 접근 빈도가 아닌 삽입 순서 기반 eviction (FIFO)
1431
+ * @param key 조회할 키
1432
+ * @returns 캐시된 또는 undefined (캐시 미스 )
1231
1433
  *
1232
- * WoongCanvasMarker 사용 사례에 최적:
1233
- * - 좌표 변환 결과는 zoom/pan 시 어차피 전체 초기화
1234
- * - 접근 빈도 추적보다 빠른 조회가 중요
1434
+ * @remarks
1435
+ * - 성능: O(1) 해시 조회
1436
+ * - 최적화: delete+set 제거로 읽기 성능 대폭 향상
1235
1437
  */
1236
1438
 
1237
1439
 
@@ -1239,11 +1441,15 @@
1239
1441
  return this.cache.get(key);
1240
1442
  };
1241
1443
  /**
1242
- * 캐시에 값 저장 (버그 수정 + 최적화)
1444
+ * 캐시에 값 저장
1243
1445
  *
1244
- * 수정 사항:
1245
- * 1. 기존 업데이트 시 크기 체크 누락 버그 수정
1246
- * 2. 로직 명확화: 기존 항목/신규 항목 분리 처리
1446
+ * @param key 저장할 키
1447
+ * @param value 저장할
1448
+ *
1449
+ * @remarks
1450
+ * - 기존 키 업데이트: 단순 덮어쓰기 (크기 변화 없음)
1451
+ * - 신규 키 추가: 크기 체크 후 필요시 가장 오래된 항목 제거 (FIFO)
1452
+ * - 성능: O(1) 평균 시간복잡도
1247
1453
  */
1248
1454
 
1249
1455
 
@@ -1341,9 +1547,18 @@
1341
1547
  /**
1342
1548
  * 항목 추가 (바운딩 박스 기반)
1343
1549
  *
1344
- * 개선 사항:
1550
+ * 항목을 공간 인덱스에 추가합니다. 바운딩 박스가 걸치는 모든 셀에 삽입됩니다.
1551
+ *
1552
+ * @param item 추가할 항목
1553
+ * @param minX 바운딩 박스 최소 X 좌표
1554
+ * @param minY 바운딩 박스 최소 Y 좌표
1555
+ * @param maxX 바운딩 박스 최대 X 좌표
1556
+ * @param maxY 바운딩 박스 최대 Y 좌표
1557
+ *
1558
+ * @remarks
1345
1559
  * - 중복 삽입 방지: 기존 항목이 있으면 먼저 제거 후 재삽입
1346
1560
  * - 메모리 누수 방지: 이전 셀 참조 완전 제거
1561
+ * - 성능: O(1) 평균 시간복잡도
1347
1562
  */
1348
1563
 
1349
1564
 
@@ -1367,7 +1582,14 @@
1367
1582
  /**
1368
1583
  * 항목 제거
1369
1584
  *
1370
- * 추가된 메서드: 메모리 누수 방지 및 업데이트 지원
1585
+ * 공간 인덱스에서 항목을 제거합니다.
1586
+ *
1587
+ * @param item 제거할 항목
1588
+ *
1589
+ * @remarks
1590
+ * - 메모리 누수 방지: 모든 셀에서 참조 완전 제거
1591
+ * - 빈 셀 정리: 항목이 없어진 셀은 자동으로 정리됨
1592
+ * - 성능: O(셀 개수), 보통 O(1)
1371
1593
  */
1372
1594
 
1373
1595
 
@@ -1398,7 +1620,13 @@
1398
1620
  /**
1399
1621
  * 항목 위치 업데이트
1400
1622
  *
1401
- * 추가된 메서드: remove + insert의 편의 함수
1623
+ * 항목의 위치를 업데이트합니다. remove + insert의 편의 함수입니다.
1624
+ *
1625
+ * @param item 업데이트할 항목
1626
+ * @param minX 새로운 바운딩 박스 최소 X 좌표
1627
+ * @param minY 새로운 바운딩 박스 최소 Y 좌표
1628
+ * @param maxX 새로운 바운딩 박스 최대 X 좌표
1629
+ * @param maxY 새로운 바운딩 박스 최대 Y 좌표
1402
1630
  */
1403
1631
 
1404
1632
 
@@ -1408,7 +1636,26 @@
1408
1636
  /**
1409
1637
  * 점 주변의 항목 조회 (1개 셀만)
1410
1638
  *
1411
- * 성능: O(해당 셀의 항목 수) - 보통 ~10개
1639
+ * 특정 좌표가 속한 셀의 모든 항목을 반환합니다.
1640
+ *
1641
+ * @param x 조회할 X 좌표
1642
+ * @param y 조회할 Y 좌표
1643
+ * @returns 해당 셀의 항목 배열 (없으면 빈 배열)
1644
+ *
1645
+ * @remarks
1646
+ * - 성능: O(해당 셀의 항목 수) - 보통 ~10개 (30,000개 전체를 체크하지 않음)
1647
+ * - Hit Test에 최적화된 메서드
1648
+ * - 빈 배열 재사용으로 메모리 할당 최소화
1649
+ *
1650
+ * @example
1651
+ * ```typescript
1652
+ * const candidates = grid.queryPoint(mouseX, mouseY);
1653
+ * for (const item of candidates) {
1654
+ * if (isPointInItem(item, mouseX, mouseY)) {
1655
+ * return item;
1656
+ * }
1657
+ * }
1658
+ * ```
1412
1659
  */
1413
1660
 
1414
1661
 
@@ -1421,8 +1668,18 @@
1421
1668
  /**
1422
1669
  * 영역 내 항목 조회
1423
1670
  *
1424
- * 성능: O( 개수 × 셀당 평균 항목 수)
1425
- * Set으로 중복 제거 보장
1671
+ * 특정 영역(바운딩 박스)과 교차하는 모든 항목을 반환합니다.
1672
+ *
1673
+ * @param minX 영역 최소 X 좌표
1674
+ * @param minY 영역 최소 Y 좌표
1675
+ * @param maxX 영역 최대 X 좌표
1676
+ * @param maxY 영역 최대 Y 좌표
1677
+ * @returns 영역과 교차하는 항목 배열 (중복 제거됨)
1678
+ *
1679
+ * @remarks
1680
+ * - 성능: O(셀 개수 × 셀당 평균 항목 수)
1681
+ * - Set으로 중복 제거 보장 (항목이 여러 셀에 걸쳐 있어도 한 번만 반환)
1682
+ * - Viewport Culling에 유용
1426
1683
  */
1427
1684
 
1428
1685
 
@@ -1447,7 +1704,11 @@
1447
1704
  /**
1448
1705
  * 항목 존재 여부 확인
1449
1706
  *
1450
- * 추가된 메서드: 빠른 존재 여부 체크
1707
+ * @param item 확인할 항목
1708
+ * @returns 항목이 인덱스에 있으면 true, 아니면 false
1709
+ *
1710
+ * @remarks
1711
+ * - 성능: O(1) 해시 조회
1451
1712
  */
1452
1713
 
1453
1714
 
@@ -1466,7 +1727,14 @@
1466
1727
  /**
1467
1728
  * 통계 정보
1468
1729
  *
1469
- * 개선: totalItems는 실제 고유 항목 수를 정확히 반환
1730
+ * 공간 인덱스의 현재 상태를 반환합니다. 디버깅 성능 분석에 유용합니다.
1731
+ *
1732
+ * @returns 통계 정보 객체
1733
+ *
1734
+ * @remarks
1735
+ * - totalCells: 현재 사용 중인 셀 개수
1736
+ * - totalItems: 인덱스에 등록된 고유 항목 수 (정확)
1737
+ * - avgItemsPerCell: 셀당 평균 항목 수
1470
1738
  */
1471
1739
 
1472
1740
 
@@ -1485,6 +1753,400 @@
1485
1753
  return SpatialHashGrid;
1486
1754
  }();
1487
1755
 
1756
+ /**
1757
+ * 현재 뷰포트 영역 계산
1758
+ *
1759
+ * Konva Stage의 크기와 컬링 마진을 기반으로 뷰포트 경계를 계산합니다.
1760
+ *
1761
+ * @param stage Konva Stage 인스턴스 (width, height 메서드 제공)
1762
+ * @param cullingMargin 컬링 여유 공간 (px)
1763
+ * @param viewportRef 뷰포트 경계를 저장할 ref
1764
+ *
1765
+ * @remarks
1766
+ * - 화면 밖 cullingMargin만큼의 영역까지 포함하여 계산
1767
+ * - 스크롤 시 부드러운 전환을 위해 여유 공간 포함
1768
+ *
1769
+ * @example
1770
+ * ```typescript
1771
+ * updateViewport(stageRef.current, cullingMargin, viewportRef);
1772
+ * ```
1773
+ */
1774
+ var updateViewport = function (stage, cullingMargin, viewportRef) {
1775
+ if (!stage) return;
1776
+ viewportRef.current = {
1777
+ minX: -cullingMargin,
1778
+ maxX: stage.width() + cullingMargin,
1779
+ minY: -cullingMargin,
1780
+ maxY: stage.height() + cullingMargin
1781
+ };
1782
+ };
1783
+ /**
1784
+ * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
1785
+ *
1786
+ * 뷰포트 컬링을 위한 함수입니다. 바운딩 박스와 뷰포트 경계의 교차를 확인합니다.
1787
+ * 바운딩 박스는 캐시되어 성능을 최적화합니다.
1788
+ *
1789
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1790
+ * @param item 확인할 아이템
1791
+ * @param enableViewportCulling 뷰포트 컬링 활성화 여부
1792
+ * @param viewportRef 뷰포트 경계 ref
1793
+ * @param boundingBoxCacheRef 바운딩 박스 캐시 ref
1794
+ * @param computeBoundingBox 바운딩 박스 계산 함수
1795
+ * @returns 뷰포트 안에 있으면 true, 아니면 false
1796
+ *
1797
+ * @remarks
1798
+ * - 성능: O(1) (캐시 히트 시) 또는 O(바운딩 박스 계산 비용) (캐시 미스 시)
1799
+ * - 바운딩 박스는 자동으로 캐시되어 재사용됨
1800
+ *
1801
+ * @example
1802
+ * ```typescript
1803
+ * const isVisible = isInViewport(
1804
+ * item,
1805
+ * enableViewportCulling,
1806
+ * viewportRef,
1807
+ * boundingBoxCacheRef,
1808
+ * computeBoundingBox
1809
+ * );
1810
+ * ```
1811
+ */
1812
+
1813
+ var isInViewport = function (item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
1814
+ if (!enableViewportCulling || !viewportRef.current) return true;
1815
+ var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
1816
+
1817
+ var bbox = boundingBoxCacheRef.current.get(item.id);
1818
+
1819
+ if (!bbox) {
1820
+ // 바운딩 박스 계산 (공통 함수 사용)
1821
+ var computed = computeBoundingBox(item);
1822
+ if (!computed) return false;
1823
+ bbox = computed;
1824
+ boundingBoxCacheRef.current.set(item.id, bbox);
1825
+ } // 바운딩 박스와 viewport 교차 체크
1826
+
1827
+
1828
+ return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
1829
+ };
1830
+
1831
+ /**
1832
+ * 지도 이벤트 핸들러 생성 함수
1833
+ *
1834
+ * 지도 이동, 줌, 드래그 등의 이벤트를 처리하는 핸들러들을 생성합니다.
1835
+ *
1836
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1837
+ * @param deps 이벤트 핸들러 생성에 필요한 의존성
1838
+ * @returns 지도 이벤트 핸들러 객체
1839
+ *
1840
+ * @example
1841
+ * ```typescript
1842
+ * const {
1843
+ * handleIdle,
1844
+ * handleZoomStart,
1845
+ * handleZoomEnd,
1846
+ * handleCenterChanged,
1847
+ * handleDragStart,
1848
+ * handleDragEnd,
1849
+ * } = createMapEventHandlers({
1850
+ * controller,
1851
+ * containerRef,
1852
+ * markerRef,
1853
+ * options,
1854
+ * prevCenterOffsetRef,
1855
+ * accumTranslateRef,
1856
+ * offsetCacheRef,
1857
+ * boundingBoxCacheRef,
1858
+ * renderAllImmediate,
1859
+ * });
1860
+ * ```
1861
+ */
1862
+
1863
+ var createMapEventHandlers = function (deps) {
1864
+ var controller = deps.controller,
1865
+ containerRef = deps.containerRef,
1866
+ markerRef = deps.markerRef,
1867
+ options = deps.options,
1868
+ prevCenterOffsetRef = deps.prevCenterOffsetRef,
1869
+ accumTranslateRef = deps.accumTranslateRef,
1870
+ offsetCacheRef = deps.offsetCacheRef,
1871
+ boundingBoxCacheRef = deps.boundingBoxCacheRef,
1872
+ renderAllImmediate = deps.renderAllImmediate;
1873
+ /**
1874
+ * 지도 이동/줌 완료 시 처리
1875
+ *
1876
+ * - 캐시 초기화: 좌표 변환 결과가 변경되었으므로 캐시 무효화
1877
+ * - 마커 위치 업데이트: 새로운 지도 위치에 맞게 마커 재배치
1878
+ * - 렌더링: 새 위치에서 전체 렌더링 수행
1879
+ */
1880
+
1881
+ var handleIdle = function () {
1882
+ prevCenterOffsetRef.current = null;
1883
+ accumTranslateRef.current = {
1884
+ x: 0,
1885
+ y: 0
1886
+ }; // 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
1887
+
1888
+ offsetCacheRef.current.clear();
1889
+ boundingBoxCacheRef.current.clear(); // 마커 위치 업데이트
1890
+
1891
+ var bounds = controller.getCurrBounds();
1892
+
1893
+ var markerOptions = tslib.__assign({
1894
+ position: bounds.nw
1895
+ }, options);
1896
+
1897
+ markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
1898
+
1899
+ if (containerRef.current) {
1900
+ containerRef.current.style.transform = '';
1901
+ containerRef.current.style.visibility = '';
1902
+ } // 새 위치에서 렌더링
1903
+
1904
+
1905
+ renderAllImmediate();
1906
+ };
1907
+ /**
1908
+ * 줌 시작 시 처리 (일시적으로 숨김)
1909
+ */
1910
+
1911
+
1912
+ var handleZoomStart = function () {
1913
+ if (containerRef.current) {
1914
+ containerRef.current.style.visibility = 'hidden';
1915
+ }
1916
+ };
1917
+ /**
1918
+ * 줌 종료 시 처리 (다시 표시)
1919
+ */
1920
+
1921
+
1922
+ var handleZoomEnd = function () {
1923
+ if (containerRef.current) {
1924
+ containerRef.current.style.visibility = '';
1925
+ }
1926
+ };
1927
+ /**
1928
+ * 지도 중심 변경 시 처리 (transform으로 이동 추적)
1929
+ */
1930
+
1931
+
1932
+ var handleCenterChanged = function () {
1933
+ var center = controller.getCurrBounds().getCenter();
1934
+ var curr = controller.positionToOffset(center);
1935
+ var prev = prevCenterOffsetRef.current;
1936
+
1937
+ if (!prev) {
1938
+ prevCenterOffsetRef.current = {
1939
+ x: curr.x,
1940
+ y: curr.y
1941
+ };
1942
+ return;
1943
+ }
1944
+
1945
+ var dx = prev.x - curr.x;
1946
+ var dy = prev.y - curr.y;
1947
+ accumTranslateRef.current = {
1948
+ x: accumTranslateRef.current.x + dx,
1949
+ y: accumTranslateRef.current.y + dy
1950
+ };
1951
+ prevCenterOffsetRef.current = {
1952
+ x: curr.x,
1953
+ y: curr.y
1954
+ };
1955
+
1956
+ if (containerRef.current) {
1957
+ containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
1958
+ }
1959
+ };
1960
+ /**
1961
+ * 드래그 시작 처리
1962
+ */
1963
+
1964
+
1965
+ var handleDragStart = function () {// 커서는 각 컴포넌트에서 처리
1966
+ };
1967
+ /**
1968
+ * 드래그 종료 처리
1969
+ */
1970
+
1971
+
1972
+ var handleDragEnd = function () {// 커서는 각 컴포넌트에서 처리
1973
+ };
1974
+
1975
+ return {
1976
+ handleIdle: handleIdle,
1977
+ handleZoomStart: handleZoomStart,
1978
+ handleZoomEnd: handleZoomEnd,
1979
+ handleCenterChanged: handleCenterChanged,
1980
+ handleDragStart: handleDragStart,
1981
+ handleDragEnd: handleDragEnd
1982
+ };
1983
+ };
1984
+ /**
1985
+ * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
1986
+ *
1987
+ * Spatial Hash Grid에 모든 데이터의 바운딩 박스를 삽입합니다.
1988
+ * 이를 통해 클릭/호버 시 O(1) 수준의 빠른 Hit Test가 가능합니다.
1989
+ *
1990
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1991
+ * @param data 공간 인덱스에 삽입할 데이터 배열
1992
+ * @param spatialIndex Spatial Hash Grid 인스턴스
1993
+ * @param computeBoundingBox 바운딩 박스 계산 함수
1994
+ *
1995
+ * @remarks
1996
+ * - 성능: O(n) 시간복잡도, n은 데이터 개수
1997
+ * - 호출 시점: 데이터 변경 시 또는 지도 이동/줌 완료 시
1998
+ *
1999
+ * @example
2000
+ * ```typescript
2001
+ * buildSpatialIndex(
2002
+ * dataRef.current,
2003
+ * spatialIndexRef.current,
2004
+ * computeBoundingBox
2005
+ * );
2006
+ * ```
2007
+ */
2008
+
2009
+ var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
2010
+ spatialIndex.clear();
2011
+
2012
+ for (var _i = 0, data_1 = data; _i < data_1.length; _i++) {
2013
+ var item = data_1[_i];
2014
+ var bbox = computeBoundingBox(item);
2015
+
2016
+ if (bbox) {
2017
+ spatialIndex.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
2018
+ }
2019
+ }
2020
+ };
2021
+ /**
2022
+ * 선택 상태 동기화 유틸리티
2023
+ *
2024
+ * 데이터 변경 시 선택된 항목의 참조를 최신 데이터로 업데이트합니다.
2025
+ * 화면 밖에 있는 선택된 항목도 선택 상태를 유지합니다.
2026
+ *
2027
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
2028
+ * @param data 최신 데이터 배열
2029
+ * @param selectedIds 선택된 항목 ID Set
2030
+ * @param selectedItemsMap 현재 선택된 항목 Map
2031
+ * @returns 업데이트된 선택된 항목 Map
2032
+ *
2033
+ * @remarks
2034
+ * - 성능: O(n + m), n은 전체 데이터 수, m은 선택된 항목 수
2035
+ * - 화면 밖 데이터도 선택 상태 유지 (최신 데이터가 없으면 기존 데이터 유지)
2036
+ *
2037
+ * @example
2038
+ * ```typescript
2039
+ * selectedItemsMapRef.current = syncSelectedItems(
2040
+ * data,
2041
+ * selectedIdsRef.current,
2042
+ * selectedItemsMapRef.current
2043
+ * );
2044
+ * ```
2045
+ */
2046
+
2047
+ var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
2048
+ var dataMap = new Map(data.map(function (m) {
2049
+ return [m.id, m];
2050
+ }));
2051
+ var newSelectedItemsMap = new Map();
2052
+ selectedIds.forEach(function (id) {
2053
+ // 현재 data에 있으면 최신 데이터 사용
2054
+ var currentItem = dataMap.get(id);
2055
+
2056
+ if (currentItem) {
2057
+ newSelectedItemsMap.set(id, currentItem);
2058
+ } else {
2059
+ // 화면 밖이면 기존 데이터 유지
2060
+ var prevItem = selectedItemsMap.get(id);
2061
+
2062
+ if (prevItem) {
2063
+ newSelectedItemsMap.set(id, prevItem);
2064
+ }
2065
+ }
2066
+ });
2067
+ return newSelectedItemsMap;
2068
+ };
2069
+ /**
2070
+ * 외부 selectedItems를 내부 상태로 동기화
2071
+ *
2072
+ * 외부에서 전달된 selectedItems prop을 내부 ref 상태로 동기화합니다.
2073
+ *
2074
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
2075
+ * @param externalSelectedItems 외부에서 전달된 선택된 항목 배열 (undefined면 동기화 안 함)
2076
+ * @param selectedIdsRef 선택된 ID Set ref
2077
+ * @param selectedItemsMapRef 선택된 항목 Map ref
2078
+ *
2079
+ * @remarks
2080
+ * - externalSelectedItems가 undefined면 외부 제어가 아니므로 아무 작업도 하지 않음
2081
+ * - 성능: O(m), m은 externalSelectedItems의 길이
2082
+ *
2083
+ * @example
2084
+ * ```typescript
2085
+ * useEffect(() => {
2086
+ * syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
2087
+ * // 렌더링...
2088
+ * }, [externalSelectedItems]);
2089
+ * ```
2090
+ */
2091
+
2092
+ var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef, selectedItemsMapRef) {
2093
+ if (externalSelectedItems === undefined) return;
2094
+ var newSelectedIds = new Set();
2095
+ var newSelectedItemsMap = new Map();
2096
+ externalSelectedItems.forEach(function (item) {
2097
+ newSelectedIds.add(item.id);
2098
+ newSelectedItemsMap.set(item.id, item);
2099
+ });
2100
+ selectedIdsRef.current = newSelectedIds;
2101
+ selectedItemsMapRef.current = newSelectedItemsMap;
2102
+ };
2103
+
2104
+ /**
2105
+ * 이벤트 유효성 검증 헬퍼
2106
+ *
2107
+ * @param event 이벤트 파라미터
2108
+ * @param context Context가 있는지 여부
2109
+ * @param controller MintMapController 인스턴스
2110
+ * @returns 유효한 offset 또는 null
2111
+ *
2112
+ * @remarks
2113
+ * Context가 있으면 전역 이벤트 핸들러가 처리하므로 로컬 핸들러는 스킵
2114
+ */
2115
+ var validateEvent = function (event, context, controller) {
2116
+ var _a; // Context가 있으면 전역 핸들러가 처리
2117
+
2118
+
2119
+ if (context) return null; // 이벤트 파라미터 검증
2120
+
2121
+ if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return null;
2122
+
2123
+ try {
2124
+ return controller.positionToOffset(event.param.position);
2125
+ } catch (error) {
2126
+ console.error('[WoongCanvas] validateEvent error:', error);
2127
+ return null;
2128
+ }
2129
+ };
2130
+ /**
2131
+ * Map의 values를 배열로 변환 (최적화 버전)
2132
+ *
2133
+ * @param map 변환할 Map
2134
+ * @returns Map의 값 배열
2135
+ *
2136
+ * @remarks
2137
+ * Map.values()는 IterableIterator를 반환하므로 배열 변환이 필요할 때 사용합니다.
2138
+ * 성능: O(n) 시간복잡도
2139
+ *
2140
+ * 최적화: Array.from을 사용하되, 크기를 미리 할당하여 메모리 재할당 최소화
2141
+ */
2142
+
2143
+ var mapValuesToArray = function (map) {
2144
+ // Map이 비어있으면 빈 배열 반환 (메모리 할당 최소화)
2145
+ if (map.size === 0) return []; // Array.from 사용 (TypeScript 컴파일러 호환성)
2146
+
2147
+ return Array.from(map.values());
2148
+ };
2149
+
1488
2150
  var cn$3 = classNames__default["default"].bind(styles$1);
1489
2151
  function MintMapCore(_a) {
1490
2152
  var _this = this;
@@ -5550,37 +6212,16 @@
5550
6212
  * 현재 뷰포트 영역 계산
5551
6213
  */
5552
6214
 
5553
- var updateViewport = function () {
5554
- if (!stageRef.current) return;
5555
- var stage = stageRef.current;
5556
- viewportRef.current = {
5557
- minX: -cullingMargin,
5558
- maxX: stage.width() + cullingMargin,
5559
- minY: -cullingMargin,
5560
- maxY: stage.height() + cullingMargin
5561
- };
6215
+ var updateViewport$1 = function () {
6216
+ updateViewport(stageRef.current, cullingMargin, viewportRef);
5562
6217
  };
5563
6218
  /**
5564
6219
  * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
5565
6220
  */
5566
6221
 
5567
6222
 
5568
- var isInViewport = function (item) {
5569
- if (!enableViewportCulling || !viewportRef.current) return true;
5570
- var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
5571
-
5572
- var bbox = boundingBoxCacheRef.current.get(item.id);
5573
-
5574
- if (!bbox) {
5575
- // 바운딩 박스 계산 (공통 함수 사용)
5576
- var computed = computeBoundingBox(item);
5577
- if (!computed) return false;
5578
- bbox = computed;
5579
- boundingBoxCacheRef.current.set(item.id, bbox);
5580
- } // 바운딩 박스와 viewport 교차 체크
5581
-
5582
-
5583
- return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
6223
+ var isInViewport$1 = function (item) {
6224
+ return isInViewport(item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox);
5584
6225
  }; // --------------------------------------------------------------------------
5585
6226
  // 유틸리티 함수: 좌표 변환 캐싱
5586
6227
  // --------------------------------------------------------------------------
@@ -5610,12 +6251,23 @@
5610
6251
  /**
5611
6252
  * 마커의 바운딩 박스 계산
5612
6253
  *
5613
- * 🎯 마커의 경우:
5614
- * - boxHeight: 본체만 (Hit Test 영역)
5615
- * - tailHeight: 꼬리 높이 (Viewport Culling용, 화면에 보이는 전체 영역)
6254
+ * 마커의 화면 상 위치와 크기를 기반으로 바운딩 박스를 계산합니다.
6255
+ * Viewport Culling에 사용되며, tailHeight를 포함하여 전체 표시 영역을 계산합니다.
5616
6256
  *
5617
6257
  * @param item 마커 데이터
5618
- * @returns 바운딩 박스 또는 null
6258
+ * @returns 바운딩 박스 (minX, minY, maxX, maxY) 또는 null (좌표 변환 실패 시)
6259
+ *
6260
+ * @remarks
6261
+ * - **boxHeight**: 마커 본체만 포함 (Hit Test 영역)
6262
+ * - **tailHeight**: 마커 꼬리 높이 (Viewport Culling용, 화면에 보이는 전체 영역 포함)
6263
+ * - 바운딩 박스는 캐시되어 성능 최적화
6264
+ *
6265
+ * @example
6266
+ * ```typescript
6267
+ * const bbox = computeBoundingBox(item);
6268
+ * if (!bbox) return; // 계산 실패
6269
+ * // bbox.minX, bbox.minY, bbox.maxX, bbox.maxY 사용
6270
+ * ```
5619
6271
  */
5620
6272
 
5621
6273
 
@@ -5639,29 +6291,32 @@
5639
6291
 
5640
6292
  /**
5641
6293
  * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
6294
+ *
6295
+ * 모든 마커의 바운딩 박스를 Spatial Hash Grid에 삽입합니다.
6296
+ * 이를 통해 클릭/호버 시 해당 위치 주변의 마커만 빠르게 조회할 수 있습니다.
6297
+ *
6298
+ * @remarks
6299
+ * - 호출 시점: 데이터 변경 시 또는 지도 이동/줌 완료 시
6300
+ * - 성능: O(n) 시간복잡도, n은 마커 개수
6301
+ * - Hit Test 성능: O(1) 수준 (30,000개 → ~10개 후보만 체크)
5642
6302
  */
5643
6303
 
5644
6304
 
5645
- var buildSpatialIndex = function () {
5646
- var spatial = spatialIndexRef.current;
5647
- spatial.clear();
5648
- var currentData = dataRef.current;
5649
-
5650
- for (var _i = 0, currentData_1 = currentData; _i < currentData_1.length; _i++) {
5651
- var item = currentData_1[_i]; // 바운딩 박스 계산 (공통 함수 사용)
5652
-
5653
- var bbox = computeBoundingBox(item);
5654
-
5655
- if (bbox) {
5656
- spatial.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
5657
- }
5658
- }
6305
+ var buildSpatialIndex$1 = function () {
6306
+ buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
5659
6307
  }; // --------------------------------------------------------------------------
5660
- // 렌더링 함수 결정 (dataType에 따라)
6308
+ // 렌더링 함수 결정
5661
6309
  // --------------------------------------------------------------------------
5662
6310
 
5663
6311
  /**
5664
6312
  * 외부 렌더링 함수에 전달할 유틸리티 객체
6313
+ *
6314
+ * 커스텀 렌더링 함수(renderBase, renderAnimation, renderEvent)에서 사용할
6315
+ * 좌표 변환 등의 헬퍼 함수들을 제공합니다.
6316
+ *
6317
+ * @remarks
6318
+ * - getOrComputeMarkerOffset: 마커 좌표 변환 (자동 캐싱)
6319
+ * - getOrComputePolygonOffsets: 폴리곤 좌표 변환 (마커에서는 사용 안 함)
5665
6320
  */
5666
6321
 
5667
6322
 
@@ -5703,7 +6358,7 @@
5703
6358
  var hovered = hoveredItemRef.current; // 클로저로 최신 ref 값 참조
5704
6359
 
5705
6360
  var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
5706
- return isInViewport(item);
6361
+ return isInViewport$1(item);
5707
6362
  }) : dataRef.current; // topOnHover가 true이고 renderEvent가 없으면 Base Layer에서 hover 처리
5708
6363
 
5709
6364
  if (topOnHover && !renderEvent && hovered) {
@@ -5723,7 +6378,7 @@
5723
6378
  }); // hover된 항목을 최상단에 렌더링 (renderEvent가 없을 때만)
5724
6379
 
5725
6380
  if (topOnHover && !renderEvent && hovered) {
5726
- var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
6381
+ var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
5727
6382
 
5728
6383
  if (isHoveredInViewport) {
5729
6384
  renderBase({
@@ -5749,7 +6404,11 @@
5749
6404
  /**
5750
6405
  * Animation 레이어 렌더링 (선택된 마커 애니메이션)
5751
6406
  *
5752
- * 🔥 최적화: sceneFunc 내부에서 최신 items 참조
6407
+ * 선택된 마커에 대한 애니메이션 효과를 렌더링합니다.
6408
+ * renderAnimation prop이 제공된 경우에만 실행됩니다.
6409
+ *
6410
+ * @remarks
6411
+ * - **성능 최적화**: sceneFunc 내부에서 최신 items 참조
5753
6412
  * - 선택 변경 시에만 재생성
5754
6413
  * - 지도 이동 시에는 기존 Animation 계속 실행
5755
6414
  */
@@ -5769,10 +6428,16 @@
5769
6428
  /**
5770
6429
  * Event 레이어 렌더링 (hover + 선택 상태 표시)
5771
6430
  *
5772
- * 🔥 최적화:
5773
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
5774
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
5775
- * 3. 클로저로 최신 데이터 참조
6431
+ * 마커의 hover 효과 및 선택 상태를 표시합니다.
6432
+ * renderEvent prop이 제공된 경우에만 실행됩니다.
6433
+ *
6434
+ * @remarks
6435
+ * - **성능 최적화**:
6436
+ * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
6437
+ * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
6438
+ * 3. 클로저로 최신 데이터 참조
6439
+ * - **topOnHover 지원**: hover된 항목을 최상단에 렌더링
6440
+ * - 선택된 항목은 Map에서 O(1)로 조회하여 성능 최적화
5776
6441
  */
5777
6442
 
5778
6443
 
@@ -5790,8 +6455,9 @@
5790
6455
  name: 'event-render-shape',
5791
6456
  sceneFunc: function (context, shape) {
5792
6457
  var ctx = context; // 클로저로 최신 ref 값 참조
6458
+ // 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
5793
6459
 
5794
- var selectedItems = Array.from(selectedItemsMapRef.current.values());
6460
+ var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
5795
6461
  var hovered = hoveredItemRef.current; // topOnHover가 true이면 hover된 항목을 최상단에 렌더링
5796
6462
 
5797
6463
  if (topOnHover && hovered) {
@@ -5806,7 +6472,7 @@
5806
6472
  selectedItem: selectedItemRef.current
5807
6473
  }); // 2. hover된 항목을 최상단에 렌더링
5808
6474
 
5809
- var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
6475
+ var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
5810
6476
 
5811
6477
  if (isHoveredInViewport) {
5812
6478
  // hover된 항목이 선택되어 있다면 hoverSelectedItems에 포함시켜서
@@ -5850,8 +6516,8 @@
5850
6516
 
5851
6517
 
5852
6518
  var renderAllImmediate = function () {
5853
- updateViewport();
5854
- buildSpatialIndex();
6519
+ updateViewport$1();
6520
+ buildSpatialIndex$1();
5855
6521
  doRenderBase();
5856
6522
  doRenderAnimation();
5857
6523
  doRenderEvent();
@@ -5859,89 +6525,43 @@
5859
6525
  // 이벤트 핸들러: 지도 이벤트
5860
6526
  // --------------------------------------------------------------------------
5861
6527
 
5862
- /**
5863
- * 지도 이동/줌 완료 시 처리
5864
- */
5865
-
5866
-
5867
- var handleIdle = function () {
5868
- prevCenterOffsetRef.current = null;
5869
- accumTranslateRef.current = {
5870
- x: 0,
5871
- y: 0
5872
- }; // 2. 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
5873
-
5874
- offsetCacheRef.current.clear();
5875
- boundingBoxCacheRef.current.clear(); // 3. 마커 위치 업데이트
5876
-
5877
- var bounds = controller.getCurrBounds();
5878
-
5879
- var markerOptions = tslib.__assign({
5880
- position: bounds.nw
5881
- }, options);
5882
-
5883
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
5884
6528
 
5885
- if (containerRef.current) {
5886
- containerRef.current.style.transform = '';
5887
- containerRef.current.style.visibility = '';
5888
- } // 5. 새 위치에서 렌더링
5889
-
5890
-
5891
- renderAllImmediate();
5892
- };
5893
- /**
5894
- * 줌 시작 시 처리 (일시적으로 숨김)
5895
- */
5896
-
5897
-
5898
- var handleZoomStart = function () {
5899
- if (containerRef.current) {
5900
- containerRef.current.style.visibility = 'hidden';
5901
- }
5902
- };
6529
+ var _g = createMapEventHandlers({
6530
+ controller: controller,
6531
+ containerRef: containerRef,
6532
+ markerRef: markerRef,
6533
+ options: options,
6534
+ prevCenterOffsetRef: prevCenterOffsetRef,
6535
+ accumTranslateRef: accumTranslateRef,
6536
+ offsetCacheRef: offsetCacheRef,
6537
+ boundingBoxCacheRef: boundingBoxCacheRef,
6538
+ renderAllImmediate: renderAllImmediate
6539
+ }),
6540
+ handleIdle = _g.handleIdle,
6541
+ handleZoomStart = _g.handleZoomStart,
6542
+ handleZoomEnd = _g.handleZoomEnd,
6543
+ handleCenterChanged = _g.handleCenterChanged,
6544
+ handleDragStartShared = _g.handleDragStart,
6545
+ handleDragEndShared = _g.handleDragEnd;
5903
6546
  /**
5904
- * 종료 처리 (다시 표시)
5905
- */
5906
-
5907
-
5908
- var handleZoomEnd = function () {
5909
- if (containerRef.current) {
5910
- containerRef.current.style.visibility = '';
5911
- }
5912
- };
5913
- /**
5914
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
5915
- */
5916
-
5917
-
5918
- var handleCenterChanged = function () {
5919
- var center = controller.getCurrBounds().getCenter();
5920
- var curr = controller.positionToOffset(center);
5921
- var prev = prevCenterOffsetRef.current;
5922
-
5923
- if (!prev) {
5924
- prevCenterOffsetRef.current = {
5925
- x: curr.x,
5926
- y: curr.y
5927
- };
5928
- return;
5929
- }
6547
+ * 드래그 시작 처리 (커서를 grabbing으로 변경)
6548
+ */
5930
6549
 
5931
- var dx = prev.x - curr.x;
5932
- var dy = prev.y - curr.y;
5933
- accumTranslateRef.current = {
5934
- x: accumTranslateRef.current.x + dx,
5935
- y: accumTranslateRef.current.y + dy
5936
- };
5937
- prevCenterOffsetRef.current = {
5938
- x: curr.x,
5939
- y: curr.y
5940
- };
5941
6550
 
5942
- if (containerRef.current) {
5943
- containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
5944
- }
6551
+ var handleDragStart = function () {
6552
+ handleDragStartShared();
6553
+ draggingRef.current = true;
6554
+ controller.setMapCursor('grabbing');
6555
+ };
6556
+ /**
6557
+ * 드래그 종료 처리 (커서를 기본으로 복원)
6558
+ */
6559
+
6560
+
6561
+ var handleDragEnd = function () {
6562
+ handleDragEndShared();
6563
+ draggingRef.current = false;
6564
+ controller.setMapCursor('grab');
5945
6565
  }; // --------------------------------------------------------------------------
5946
6566
  // Hit Test & 상태 관리
5947
6567
  // --------------------------------------------------------------------------
@@ -5949,12 +6569,26 @@
5949
6569
  /**
5950
6570
  * 특정 좌표의 마커 데이터 찾기 (Spatial Index 사용)
5951
6571
  *
5952
- * topOnHover가 true일 때:
5953
- * - 현재 hover된 항목을 최우선으로 체크
5954
- * - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
6572
+ * 클릭/호버 이벤트 시 해당 위치에 있는 마커를 찾습니다.
6573
+ * Spatial Hash Grid를 사용하여 O(1) 수준의 빠른 Hit Test를 수행합니다.
5955
6574
  *
5956
- * @param offset 검사할 좌표
5957
- * @returns 찾은 마커 데이터 또는 null
6575
+ * @param offset 검사할 화면 좌표 (픽셀 단위)
6576
+ * @returns 찾은 마커 데이터 또는 null (없으면)
6577
+ *
6578
+ * @remarks
6579
+ * - **topOnHover 지원**: topOnHover가 true일 때 현재 hover된 항목을 최우선으로 체크
6580
+ * - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
6581
+ * - **성능**: O(후보 항목 수) - 보통 ~10개 (30,000개 전체를 체크하지 않음)
6582
+ * - Spatial Index를 통해 해당 위치 주변의 후보만 추출 후 정확한 Hit Test 수행
6583
+ *
6584
+ * @example
6585
+ * ```typescript
6586
+ * const clickedOffset = controller.positionToOffset(event.param.position);
6587
+ * const data = findData(clickedOffset);
6588
+ * if (data) {
6589
+ * // 마커를 찾음
6590
+ * }
6591
+ * ```
5958
6592
  */
5959
6593
 
5960
6594
 
@@ -6015,11 +6649,17 @@
6015
6649
  /**
6016
6650
  * 클릭 처리 (단일/다중 선택)
6017
6651
  *
6018
- * @param data 클릭된 마커/폴리곤 데이터
6652
+ * 마커 클릭 선택 상태를 업데이트하고 렌더링을 수행합니다.
6019
6653
  *
6020
- * 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
6021
- * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
6022
- * - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
6654
+ * @param data 클릭된 마커 데이터
6655
+ *
6656
+ * @remarks
6657
+ * - **단일 선택**: 기존 선택 해제 후 새로 선택 (토글 가능)
6658
+ * - **다중 선택**: enableMultiSelect가 true면 기존 선택 유지하며 추가/제거
6659
+ * - **성능 최적화**:
6660
+ * - 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
6661
+ * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
6662
+ * - 객체 생성 오버헤드 제거로 1,000개 이상도 부드럽게 처리
6023
6663
  */
6024
6664
 
6025
6665
 
@@ -6068,75 +6708,57 @@
6068
6708
 
6069
6709
  /**
6070
6710
  * 클릭 이벤트 처리
6711
+ *
6712
+ * @param event 클릭 이벤트 파라미터
6713
+ *
6714
+ * @remarks
6715
+ * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
6716
+ * - 상호작용이 비활성화되어 있으면 스킵
6717
+ * - Spatial Index를 사용하여 빠른 Hit Test 수행
6071
6718
  */
6072
6719
 
6073
6720
 
6074
6721
  var handleClick = function (event) {
6075
- var _a;
6076
-
6077
6722
  if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6078
6723
 
6079
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
6080
-
6081
- try {
6082
- var clickedOffset = controller.positionToOffset(event.param.position);
6083
- var data_1 = findData(clickedOffset);
6724
+ var clickedOffset = validateEvent(event, context, controller);
6725
+ if (!clickedOffset) return;
6726
+ var data = findData(clickedOffset);
6084
6727
 
6085
- if (data_1) {
6086
- handleLocalClick(data_1);
6728
+ if (data) {
6729
+ handleLocalClick(data);
6087
6730
 
6088
- if (onClick) {
6089
- onClick(data_1, selectedIdsRef.current);
6090
- }
6731
+ if (onClick) {
6732
+ onClick(data, selectedIdsRef.current);
6091
6733
  }
6092
- } catch (error) {
6093
- console.error('[WoongCanvasMarker] handleClick error:', error);
6094
6734
  }
6095
6735
  };
6096
6736
  /**
6097
6737
  * 마우스 이동 이벤트 처리 (hover 감지)
6738
+ *
6739
+ * @param event 마우스 이동 이벤트 파라미터
6740
+ *
6741
+ * @remarks
6742
+ * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
6743
+ * - 상호작용이 비활성화되어 있으면 스킵
6744
+ * - hover 상태 변경 시에만 렌더링 및 콜백 호출 (최적화)
6098
6745
  */
6099
6746
 
6100
6747
 
6101
6748
  var handleMouseMove = function (event) {
6102
- var _a;
6103
-
6104
6749
  if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6105
6750
 
6106
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
6107
-
6108
- try {
6109
- var mouseOffset = controller.positionToOffset(event.param.position);
6110
- var hoveredItem = findData(mouseOffset);
6111
- var prevHovered = hoveredItemRef.current;
6751
+ var mouseOffset = validateEvent(event, context, controller);
6752
+ if (!mouseOffset) return;
6753
+ var hoveredItem = findData(mouseOffset);
6754
+ var prevHovered = hoveredItemRef.current;
6112
6755
 
6113
- if (prevHovered !== hoveredItem) {
6114
- setHovered(hoveredItem);
6115
- if (prevHovered && onMouseOut) onMouseOut(prevHovered);
6116
- if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
6117
- }
6118
- } catch (error) {
6119
- console.error('[WoongCanvasMarker] handleMouseMove error:', error);
6756
+ if (prevHovered !== hoveredItem) {
6757
+ setHovered(hoveredItem);
6758
+ if (prevHovered && onMouseOut) onMouseOut(prevHovered);
6759
+ if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
6120
6760
  }
6121
6761
  };
6122
- /**
6123
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
6124
- */
6125
-
6126
-
6127
- var handleDragStart = function () {
6128
- draggingRef.current = true;
6129
- controller.setMapCursor('grabbing');
6130
- };
6131
- /**
6132
- * 드래그 종료 처리 (커서를 기본으로 복원)
6133
- */
6134
-
6135
-
6136
- var handleDragEnd = function () {
6137
- draggingRef.current = false;
6138
- controller.setMapCursor('grab');
6139
- };
6140
6762
  /**
6141
6763
  * 마우스가 canvas를 벗어날 때 hover cleanup
6142
6764
  */
@@ -6231,7 +6853,7 @@
6231
6853
 
6232
6854
  stage.add(eventLayer); // 초기 뷰포트 설정
6233
6855
 
6234
- updateViewport(); // ResizeObserver (맵 크기 변경 감지)
6856
+ updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
6235
6857
 
6236
6858
  var resizeRafId = null;
6237
6859
  var resizeObserver = new ResizeObserver(function () {
@@ -6245,7 +6867,7 @@
6245
6867
  stage.height(mapDiv.offsetHeight);
6246
6868
  offsetCacheRef.current.clear();
6247
6869
  boundingBoxCacheRef.current.clear();
6248
- updateViewport();
6870
+ updateViewport$1();
6249
6871
  renderAllImmediate();
6250
6872
  resizeRafId = null;
6251
6873
  });
@@ -6334,18 +6956,8 @@
6334
6956
  // --------------------------------------------------------------------------
6335
6957
 
6336
6958
  React.useEffect(function () {
6337
- if (!stageRef.current) return; // externalSelectedItems가 undefined면 외부 제어 안 함
6338
-
6339
- if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
6340
-
6341
- var newSelectedIds = new Set();
6342
- var newSelectedItemsMap = new Map();
6343
- externalSelectedItems.forEach(function (item) {
6344
- newSelectedIds.add(item.id);
6345
- newSelectedItemsMap.set(item.id, item);
6346
- });
6347
- selectedIdsRef.current = newSelectedIds;
6348
- selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
6959
+ if (!stageRef.current) return;
6960
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef); // 렌더링
6349
6961
 
6350
6962
  doRenderBase();
6351
6963
  doRenderAnimation();
@@ -6396,27 +7008,7 @@
6396
7008
  * - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
6397
7009
  */
6398
7010
 
6399
- var dataMap = new Map(data.map(function (m) {
6400
- return [m.id, m];
6401
- }));
6402
- var newSelectedItemsMap = new Map();
6403
- selectedIdsRef.current.forEach(function (id) {
6404
- // 현재 data에 있으면 최신 데이터 사용
6405
- var currentItem = dataMap.get(id);
6406
-
6407
- if (currentItem) {
6408
- newSelectedItemsMap.set(id, currentItem);
6409
- } else {
6410
- // 화면 밖이면 기존 데이터 유지
6411
- var prevItem = selectedItemsMapRef.current.get(id);
6412
-
6413
- if (prevItem) {
6414
- newSelectedItemsMap.set(id, prevItem);
6415
- }
6416
- }
6417
- }); // selectedIdsRef는 그대로 유지 (화면 밖 항목도 선택 상태 유지)
6418
-
6419
- selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
7011
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current); // 즉시 렌더링
6420
7012
 
6421
7013
  renderAllImmediate();
6422
7014
  }, [data]);
@@ -6434,10 +7026,35 @@
6434
7026
  * 폴리곤 렌더링 유틸리티
6435
7027
  *
6436
7028
  * 이 파일은 폴리곤 렌더링을 위한 헬퍼 함수와 팩토리 함수를 제공합니다.
7029
+ * GeoJSON MultiPolygon 형식을 지원하며, 도넛 폴리곤(구멍이 있는 폴리곤)도 처리할 수 있습니다.
6437
7030
  */
6438
7031
 
6439
7032
  /**
6440
7033
  * 폴리곤 그리기 헬퍼 함수 (도넛 폴리곤 지원)
7034
+ *
7035
+ * Canvas 2D Context를 사용하여 폴리곤을 그립니다.
7036
+ * 도넛 폴리곤의 경우 evenodd fill rule을 사용하여 구멍을 처리합니다.
7037
+ *
7038
+ * @param params 폴리곤 그리기 파라미터
7039
+ *
7040
+ * @remarks
7041
+ * - **도넛 폴리곤 처리**:
7042
+ * - 외부 폴리곤과 내부 구멍들을 같은 path에 추가
7043
+ * - `fill('evenodd')`를 사용하여 구멍 뚫기
7044
+ * - **일반 폴리곤 처리**: 각 폴리곤 그룹을 개별적으로 그리기
7045
+ * - **성능**: O(n), n은 폴리곤의 총 좌표 수
7046
+ *
7047
+ * @example
7048
+ * ```typescript
7049
+ * drawPolygon({
7050
+ * ctx,
7051
+ * polygonOffsets: [[[[100, 200], [200, 200], [200, 100], [100, 100]]]],
7052
+ * isDonutPolygon: false,
7053
+ * fillColor: 'rgba(255, 0, 0, 0.5)',
7054
+ * strokeColor: 'rgba(255, 0, 0, 1)',
7055
+ * lineWidth: 2
7056
+ * });
7057
+ * ```
6441
7058
  */
6442
7059
  var drawPolygon = function (_a) {
6443
7060
  var ctx = _a.ctx,
@@ -6451,13 +7068,16 @@
6451
7068
  var multiPolygon = polygonOffsets_1[_i];
6452
7069
 
6453
7070
  if (isDonutPolygon) {
6454
- ctx.beginPath(); // 1. 외부 폴리곤 그리기
7071
+ // 도넛 폴리곤 처리: 외부 폴리곤 + 내부 구멍들을 같은 path에 추가
7072
+ ctx.beginPath(); // 1. 외부 폴리곤 그리기 (첫 번째 폴리곤)
6455
7073
 
6456
- if (multiPolygon[0] && multiPolygon[0].length > 0) {
6457
- ctx.moveTo(multiPolygon[0][0][0], multiPolygon[0][0][1]);
7074
+ var outerPolygon = multiPolygon[0];
7075
+
7076
+ if (outerPolygon && outerPolygon.length > 0) {
7077
+ ctx.moveTo(outerPolygon[0][0], outerPolygon[0][1]);
6458
7078
 
6459
- for (var i = 1; i < multiPolygon[0].length; i++) {
6460
- ctx.lineTo(multiPolygon[0][i][0], multiPolygon[0][i][1]);
7079
+ for (var i = 1; i < outerPolygon.length; i++) {
7080
+ ctx.lineTo(outerPolygon[i][0], outerPolygon[i][1]);
6461
7081
  }
6462
7082
 
6463
7083
  ctx.closePath();
@@ -6484,7 +7104,7 @@
6484
7104
  ctx.lineWidth = lineWidth;
6485
7105
  ctx.stroke();
6486
7106
  } else {
6487
- // 일반 폴리곤
7107
+ // 일반 폴리곤 처리: 각 폴리곤 그룹을 개별적으로 그리기
6488
7108
  for (var _b = 0, multiPolygon_1 = multiPolygon; _b < multiPolygon_1.length; _b++) {
6489
7109
  var polygonGroup = multiPolygon_1[_b];
6490
7110
  if (!polygonGroup.length) continue;
@@ -6497,7 +7117,8 @@
6497
7117
  ctx.lineTo(point[0], point[1]);
6498
7118
  }
6499
7119
 
6500
- ctx.closePath();
7120
+ ctx.closePath(); // 스타일 설정 및 렌더링
7121
+
6501
7122
  ctx.fillStyle = fillColor;
6502
7123
  ctx.strokeStyle = strokeColor;
6503
7124
  ctx.lineWidth = lineWidth;
@@ -6508,19 +7129,30 @@
6508
7129
  }
6509
7130
  };
6510
7131
  /**
6511
- * 폴리곤 Base 렌더링 함수
7132
+ * 폴리곤 Base 렌더링 함수 팩토리
6512
7133
  *
7134
+ * Base Layer에서 사용할 렌더링 함수를 생성합니다.
7135
+ * 선택되지 않은 폴리곤만 렌더링하며, 선택된 항목은 Event Layer에서 처리됩니다.
7136
+ *
7137
+ * @template T 폴리곤 데이터의 추가 속성 타입
6513
7138
  * @param baseFillColor 기본 폴리곤 채우기 색상
6514
7139
  * @param baseStrokeColor 기본 폴리곤 테두리 색상
6515
7140
  * @param baseLineWidth 기본 폴리곤 테두리 두께
6516
7141
  * @returns Base Layer 렌더링 함수
6517
7142
  *
7143
+ * @remarks
7144
+ * - 선택된 항목은 Event Layer에서 그려지므로 Base Layer에서는 스킵
7145
+ * - 성능: O(n), n은 렌더링할 폴리곤 개수
7146
+ * - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
7147
+ *
6518
7148
  * @example
7149
+ * ```typescript
6519
7150
  * const renderBase = renderPolygonBase(
6520
7151
  * 'rgba(255, 100, 100, 0.5)',
6521
7152
  * 'rgba(200, 50, 50, 0.8)',
6522
7153
  * 2
6523
7154
  * );
7155
+ * ```
6524
7156
  */
6525
7157
 
6526
7158
  var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth) {
@@ -6531,12 +7163,15 @@
6531
7163
  utils = _a.utils;
6532
7164
 
6533
7165
  for (var _i = 0, items_1 = items; _i < items_1.length; _i++) {
6534
- var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림
7166
+ var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림 (중복 렌더링 방지)
7167
+
7168
+ if (selectedIds.has(item.id)) continue; // paths가 없으면 스킵
7169
+
7170
+ if (!item.paths) continue; // 좌표 변환 (자동 캐싱)
6535
7171
 
6536
- if (selectedIds.has(item.id)) continue;
6537
- if (!item.paths) continue;
6538
7172
  var polygonOffsets = utils.getOrComputePolygonOffsets(item);
6539
- if (!polygonOffsets) continue;
7173
+ if (!polygonOffsets) continue; // 폴리곤 그리기
7174
+
6540
7175
  drawPolygon({
6541
7176
  ctx: ctx,
6542
7177
  polygonOffsets: polygonOffsets,
@@ -6549,8 +7184,12 @@
6549
7184
  };
6550
7185
  };
6551
7186
  /**
6552
- * 폴리곤 Event 렌더링 함수
7187
+ * 폴리곤 Event 렌더링 함수 팩토리
7188
+ *
7189
+ * Event Layer에서 사용할 렌더링 함수를 생성합니다.
7190
+ * 선택된 항목, hover된 항목, 마지막 선택된 항목을 각각 다른 스타일로 렌더링합니다.
6553
7191
  *
7192
+ * @template T 폴리곤 데이터의 추가 속성 타입
6554
7193
  * @param baseFillColor 기본 폴리곤 채우기 색상 (필수, fallback용)
6555
7194
  * @param baseStrokeColor 기본 폴리곤 테두리 색상 (필수, fallback용)
6556
7195
  * @param baseLineWidth 기본 폴리곤 테두리 두께 (필수, fallback용)
@@ -6565,7 +7204,14 @@
6565
7204
  * @param hoveredLineWidth Hover 시 폴리곤 테두리 두께 (선택, 기본값: selectedLineWidth)
6566
7205
  * @returns Event Layer 렌더링 함수
6567
7206
  *
7207
+ * @remarks
7208
+ * - **렌더링 순서**: 선택된 항목 → 마지막 선택된 항목 → hover된 항목 (최상단)
7209
+ * - **성능**: O(m), m은 선택된 항목 수 + hover된 항목 수
7210
+ * - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
7211
+ * - hover된 항목이 선택되어 있으면 active 스타일 적용
7212
+ *
6568
7213
  * @example
7214
+ * ```typescript
6569
7215
  * const renderEvent = renderPolygonEvent(
6570
7216
  * 'rgba(255, 100, 100, 0.5)', // baseFillColor
6571
7217
  * 'rgba(200, 50, 50, 0.8)', // baseStrokeColor
@@ -6574,6 +7220,7 @@
6574
7220
  * 'rgba(255, 152, 0, 1)', // selectedStrokeColor
6575
7221
  * 4 // selectedLineWidth
6576
7222
  * );
7223
+ * ```
6577
7224
  */
6578
7225
 
6579
7226
  var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth) {
@@ -6601,13 +7248,19 @@
6601
7248
  hoveredItem = _a.hoveredItem,
6602
7249
  utils = _a.utils,
6603
7250
  selectedItems = _a.selectedItems,
6604
- selectedItem = _a.selectedItem; // 1. 선택된 항목들 그리기 (마지막 선택 항목과 호버된 항목 제외)
7251
+ selectedItem = _a.selectedItem; // 성능 최적화: selectedItems를 Set으로 변환하여 O(1) 조회 (매번 some() 체크 방지)
7252
+
7253
+ var selectedIdsSet = selectedItems ? new Set(selectedItems.map(function (item) {
7254
+ return item.id;
7255
+ })) : new Set();
7256
+ var hoveredItemId = hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id;
7257
+ var selectedItemId = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id; // 1. 선택된 항목들 그리기 (마지막 선택 항목과 호버된 항목 제외)
6605
7258
 
6606
7259
  if (selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.length) {
6607
7260
  for (var _i = 0, selectedItems_1 = selectedItems; _i < selectedItems_1.length; _i++) {
6608
7261
  var item = selectedItems_1[_i]; // 마지막 선택 항목과 호버된 항목은 나중에 따로 그림
6609
7262
 
6610
- if (item.id === (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) || (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id) === item.id) continue;
7263
+ if (item.id === selectedItemId || item.id === hoveredItemId) continue;
6611
7264
  if (!item.paths) continue;
6612
7265
  var polygonOffsets = utils.getOrComputePolygonOffsets(item);
6613
7266
  if (!polygonOffsets) continue;
@@ -6623,7 +7276,7 @@
6623
7276
  } // 2. 마지막 선택된 항목 그리기 (호버되지 않은 경우)
6624
7277
 
6625
7278
 
6626
- if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) && (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id) !== selectedItem.id) {
7279
+ if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) && hoveredItemId !== selectedItemId) {
6627
7280
  var polygonOffsets = utils.getOrComputePolygonOffsets(selectedItem);
6628
7281
 
6629
7282
  if (polygonOffsets) {
@@ -6641,10 +7294,10 @@
6641
7294
 
6642
7295
  if (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.paths) {
6643
7296
  var polygonOffsets = utils.getOrComputePolygonOffsets(hoveredItem);
6644
- if (!polygonOffsets) return;
6645
- var isSelected = selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.some(function (item) {
6646
- return item.id === hoveredItem.id;
6647
- });
7297
+ if (!polygonOffsets) return; // 좌표 변환 실패 시 스킵 (return은 렌더링 함수 종료)
7298
+ // 성능 최적화: Set을 사용하여 O(1) 조회 (이전: O(m) some() 체크)
7299
+
7300
+ var isSelected = selectedIdsSet.has(hoveredItem.id);
6648
7301
  drawPolygon({
6649
7302
  ctx: ctx,
6650
7303
  polygonOffsets: polygonOffsets,
@@ -6663,8 +7316,6 @@
6663
7316
  var WoongCanvasPolygon = function (props) {
6664
7317
  var data = props.data,
6665
7318
  onClick = props.onClick,
6666
- onMouseOver = props.onMouseOver,
6667
- onMouseOut = props.onMouseOut,
6668
7319
  _a = props.enableMultiSelect,
6669
7320
  enableMultiSelect = _a === void 0 ? false : _a,
6670
7321
  _b = props.enableViewportCulling,
@@ -6689,7 +7340,7 @@
6689
7340
  hoveredFillColor = props.hoveredFillColor,
6690
7341
  hoveredStrokeColor = props.hoveredStrokeColor,
6691
7342
  hoveredLineWidth = props.hoveredLineWidth,
6692
- options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
7343
+ options = tslib.__rest(props, ["data", "onClick", "enableMultiSelect", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
6693
7344
  // Hooks & Context
6694
7345
  // --------------------------------------------------------------------------
6695
7346
 
@@ -6788,37 +7439,16 @@
6788
7439
  * 현재 뷰포트 영역 계산
6789
7440
  */
6790
7441
 
6791
- var updateViewport = function () {
6792
- if (!stageRef.current) return;
6793
- var stage = stageRef.current;
6794
- viewportRef.current = {
6795
- minX: -cullingMargin,
6796
- maxX: stage.width() + cullingMargin,
6797
- minY: -cullingMargin,
6798
- maxY: stage.height() + cullingMargin
6799
- };
7442
+ var updateViewport$1 = function () {
7443
+ updateViewport(stageRef.current, cullingMargin, viewportRef);
6800
7444
  };
6801
7445
  /**
6802
7446
  * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
6803
7447
  */
6804
7448
 
6805
7449
 
6806
- var isInViewport = function (item) {
6807
- if (!enableViewportCulling || !viewportRef.current) return true;
6808
- var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
6809
-
6810
- var bbox = boundingBoxCacheRef.current.get(item.id);
6811
-
6812
- if (!bbox) {
6813
- // 바운딩 박스 계산 (공통 함수 사용)
6814
- var computed = computeBoundingBox(item);
6815
- if (!computed) return false;
6816
- bbox = computed;
6817
- boundingBoxCacheRef.current.set(item.id, bbox);
6818
- } // 바운딩 박스와 viewport 교차 체크
6819
-
6820
-
6821
- return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
7450
+ var isInViewport$1 = function (item) {
7451
+ return isInViewport(item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox);
6822
7452
  }; // --------------------------------------------------------------------------
6823
7453
  // 유틸리티 함수: 좌표 변환 캐싱
6824
7454
  // --------------------------------------------------------------------------
@@ -6847,8 +7477,23 @@
6847
7477
  /**
6848
7478
  * 폴리곤의 바운딩 박스 계산
6849
7479
  *
7480
+ * 폴리곤의 모든 좌표를 순회하여 최소/최대 X, Y 값을 계산합니다.
7481
+ * Viewport Culling에 사용되며, MultiPolygon 형식을 지원합니다.
7482
+ *
6850
7483
  * @param item 폴리곤 데이터
6851
- * @returns 바운딩 박스 또는 null
7484
+ * @returns 바운딩 박스 (minX, minY, maxX, maxY) 또는 null (좌표 변환 실패 시)
7485
+ *
7486
+ * @remarks
7487
+ * - 성능: O(n), n은 폴리곤의 총 좌표 수
7488
+ * - 바운딩 박스는 캐시되어 성능 최적화
7489
+ * - MultiPolygon의 모든 좌표를 고려하여 계산
7490
+ *
7491
+ * @example
7492
+ * ```typescript
7493
+ * const bbox = computeBoundingBox(item);
7494
+ * if (!bbox) return; // 계산 실패
7495
+ * // bbox.minX, bbox.minY, bbox.maxX, bbox.maxY 사용
7496
+ * ```
6852
7497
  */
6853
7498
 
6854
7499
 
@@ -6894,20 +7539,8 @@
6894
7539
  */
6895
7540
 
6896
7541
 
6897
- var buildSpatialIndex = function () {
6898
- var spatial = spatialIndexRef.current;
6899
- spatial.clear();
6900
- var currentData = dataRef.current;
6901
-
6902
- for (var _i = 0, currentData_1 = currentData; _i < currentData_1.length; _i++) {
6903
- var item = currentData_1[_i]; // 바운딩 박스 계산 (공통 함수 사용)
6904
-
6905
- var bbox = computeBoundingBox(item);
6906
-
6907
- if (bbox) {
6908
- spatial.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
6909
- }
6910
- }
7542
+ var buildSpatialIndex$1 = function () {
7543
+ buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
6911
7544
  }; // --------------------------------------------------------------------------
6912
7545
  // 렌더링 함수 결정 (dataType에 따라)
6913
7546
  // --------------------------------------------------------------------------
@@ -6930,11 +7563,8 @@
6930
7563
 
6931
7564
  var renderBase = renderPolygonBase(baseFillColor, baseStrokeColor, baseLineWidth);
6932
7565
  var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth);
6933
- /** Base Layer에서 사용할 빈 Set (재사용) */
6934
-
6935
- React.useRef(new Set());
6936
7566
  /**
6937
- * Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
7567
+ * Base 레이어 렌더링 (뷰포트 컬링 적용)
6938
7568
  *
6939
7569
  * 🔥 최적화:
6940
7570
  * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
@@ -6962,7 +7592,7 @@
6962
7592
  var hovered = hoveredItemRef.current; // 클로저로 최신 ref 값 참조
6963
7593
 
6964
7594
  var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
6965
- return isInViewport(item);
7595
+ return isInViewport$1(item);
6966
7596
  }) : dataRef.current; // 일반 항목 렌더링
6967
7597
 
6968
7598
  renderBase({
@@ -6986,10 +7616,16 @@
6986
7616
  /**
6987
7617
  * Event 레이어 렌더링 (hover + 선택 상태 표시)
6988
7618
  *
6989
- * 🔥 최적화:
6990
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
6991
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
6992
- * 3. 클로저로 최신 데이터 참조
7619
+ * 폴리곤의 hover 효과 및 선택 상태를 표시합니다.
7620
+ * 자동 렌더링 방식으로 renderPolygonEvent를 사용합니다.
7621
+ *
7622
+ * @remarks
7623
+ * - **성능 최적화**:
7624
+ * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
7625
+ * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
7626
+ * 3. 클로저로 최신 데이터 참조
7627
+ * - 선택된 항목은 Map에서 O(1)로 조회하여 성능 최적화
7628
+ * - 자동 렌더링: 스타일 props(selectedFillColor, hoveredFillColor 등) 기반으로 자동 렌더링
6993
7629
  */
6994
7630
 
6995
7631
 
@@ -7006,8 +7642,9 @@
7006
7642
  name: 'event-render-shape',
7007
7643
  sceneFunc: function (context, shape) {
7008
7644
  var ctx = context; // 클로저로 최신 ref 값 참조
7645
+ // 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
7009
7646
 
7010
- var selectedItems = Array.from(selectedItemsMapRef.current.values());
7647
+ var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
7011
7648
  var hovered = hoveredItemRef.current; // 일반 렌더링
7012
7649
 
7013
7650
  renderEvent({
@@ -7030,101 +7667,62 @@
7030
7667
  };
7031
7668
  /**
7032
7669
  * 전체 즉시 렌더링 (IDLE 시 호출)
7670
+ *
7671
+ * 뷰포트 업데이트, 공간 인덱스 빌드, 모든 레이어 렌더링을 순차적으로 수행합니다.
7672
+ *
7673
+ * @remarks
7674
+ * - 호출 시점: 지도 이동/줌 완료 시, 데이터 변경 시, 리사이즈 시
7675
+ * - 순서: 뷰포트 업데이트 → 공간 인덱스 빌드 → Base → Event 렌더링
7676
+ * - Animation Layer는 사용하지 않음 (폴리곤 특성)
7033
7677
  */
7034
7678
 
7035
7679
 
7036
7680
  var renderAllImmediate = function () {
7037
- updateViewport();
7038
- buildSpatialIndex();
7681
+ updateViewport$1();
7682
+ buildSpatialIndex$1();
7039
7683
  doRenderBase();
7040
7684
  doRenderEvent();
7041
7685
  }; // --------------------------------------------------------------------------
7042
7686
  // 이벤트 핸들러: 지도 이벤트
7043
7687
  // --------------------------------------------------------------------------
7044
7688
 
7045
- /**
7046
- * 지도 이동/줌 완료 시 처리
7047
- */
7048
-
7049
-
7050
- var handleIdle = function () {
7051
- prevCenterOffsetRef.current = null;
7052
- accumTranslateRef.current = {
7053
- x: 0,
7054
- y: 0
7055
- }; // 2. 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
7056
-
7057
- offsetCacheRef.current.clear();
7058
- boundingBoxCacheRef.current.clear(); // 3. 마커 위치 업데이트
7059
-
7060
- var bounds = controller.getCurrBounds();
7061
-
7062
- var markerOptions = tslib.__assign({
7063
- position: bounds.nw
7064
- }, options);
7065
-
7066
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
7067
-
7068
- if (containerRef.current) {
7069
- containerRef.current.style.transform = '';
7070
- containerRef.current.style.visibility = '';
7071
- } // 5. 새 위치에서 렌더링
7072
-
7073
-
7074
- renderAllImmediate();
7075
- };
7076
- /**
7077
- * 줌 시작 시 처리 (일시적으로 숨김)
7078
- */
7079
-
7080
7689
 
7081
- var handleZoomStart = function () {
7082
- if (containerRef.current) {
7083
- containerRef.current.style.visibility = 'hidden';
7084
- }
7085
- };
7690
+ var _f = createMapEventHandlers({
7691
+ controller: controller,
7692
+ containerRef: containerRef,
7693
+ markerRef: markerRef,
7694
+ options: options,
7695
+ prevCenterOffsetRef: prevCenterOffsetRef,
7696
+ accumTranslateRef: accumTranslateRef,
7697
+ offsetCacheRef: offsetCacheRef,
7698
+ boundingBoxCacheRef: boundingBoxCacheRef,
7699
+ renderAllImmediate: renderAllImmediate
7700
+ }),
7701
+ handleIdle = _f.handleIdle,
7702
+ handleZoomStart = _f.handleZoomStart,
7703
+ handleZoomEnd = _f.handleZoomEnd,
7704
+ handleCenterChanged = _f.handleCenterChanged,
7705
+ handleDragStartShared = _f.handleDragStart,
7706
+ handleDragEndShared = _f.handleDragEnd;
7086
7707
  /**
7087
- * 종료 처리 (다시 표시)
7708
+ * 드래그 시작 처리 (커서를 grabbing으로 변경)
7088
7709
  */
7089
7710
 
7090
7711
 
7091
- var handleZoomEnd = function () {
7092
- if (containerRef.current) {
7093
- containerRef.current.style.visibility = '';
7094
- }
7712
+ var handleDragStart = function () {
7713
+ handleDragStartShared();
7714
+ draggingRef.current = true;
7715
+ controller.setMapCursor('grabbing');
7095
7716
  };
7096
7717
  /**
7097
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
7718
+ * 드래그 종료 처리 (커서를 기본으로 복원)
7098
7719
  */
7099
7720
 
7100
7721
 
7101
- var handleCenterChanged = function () {
7102
- var center = controller.getCurrBounds().getCenter();
7103
- var curr = controller.positionToOffset(center);
7104
- var prev = prevCenterOffsetRef.current;
7105
-
7106
- if (!prev) {
7107
- prevCenterOffsetRef.current = {
7108
- x: curr.x,
7109
- y: curr.y
7110
- };
7111
- return;
7112
- }
7113
-
7114
- var dx = prev.x - curr.x;
7115
- var dy = prev.y - curr.y;
7116
- accumTranslateRef.current = {
7117
- x: accumTranslateRef.current.x + dx,
7118
- y: accumTranslateRef.current.y + dy
7119
- };
7120
- prevCenterOffsetRef.current = {
7121
- x: curr.x,
7122
- y: curr.y
7123
- };
7124
-
7125
- if (containerRef.current) {
7126
- containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
7127
- }
7722
+ var handleDragEnd = function () {
7723
+ handleDragEndShared();
7724
+ draggingRef.current = false;
7725
+ controller.setMapCursor('grab');
7128
7726
  }; // --------------------------------------------------------------------------
7129
7727
  // Hit Test & 상태 관리
7130
7728
  // --------------------------------------------------------------------------
@@ -7154,13 +7752,14 @@
7154
7752
  /**
7155
7753
  * Hover 상태 설정 및 레이어 렌더링
7156
7754
  *
7157
- * @param data hover 마커/폴리곤 데이터 또는 null
7755
+ * 마우스가 폴리곤 위에 올라갔을 때 hover 상태를 설정하고 즉시 렌더링합니다.
7158
7756
  *
7159
- * 최적화: RAF 제거하여 즉시 렌더링 (16ms 지연 제거)
7757
+ * @param data hover된 폴리곤 데이터 또는 null (hover 해제 )
7160
7758
  *
7161
- * 🎯 topOnHover 지원:
7162
- * - renderEvent가 있으면: Event Layer에서만 처리 (성능 최적화)
7163
- * - renderEvent가 없고 topOnHover=true면: Base Layer에서 처리
7759
+ * @remarks
7760
+ * - **성능 최적화**: RAF 없이 즉시 렌더링 (16ms 지연 제거)
7761
+ * - Event Layer에서 hover 효과 표시
7762
+ * - 커서 상태도 자동으로 업데이트됨 (pointer/grab)
7164
7763
  */
7165
7764
 
7166
7765
 
@@ -7179,11 +7778,17 @@
7179
7778
  /**
7180
7779
  * 클릭 처리 (단일/다중 선택)
7181
7780
  *
7182
- * @param data 클릭된 마커/폴리곤 데이터
7781
+ * 폴리곤 클릭 선택 상태를 업데이트하고 렌더링을 수행합니다.
7782
+ *
7783
+ * @param data 클릭된 폴리곤 데이터
7183
7784
  *
7184
- * 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
7185
- * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
7186
- * - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
7785
+ * @remarks
7786
+ * - **단일 선택**: 기존 선택 해제 후 새로 선택 (토글 가능)
7787
+ * - **다중 선택**: enableMultiSelect가 true면 기존 선택 유지하며 추가/제거
7788
+ * - **성능 최적화**:
7789
+ * - 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
7790
+ * - sceneFunc에서 selectedIds를 체크하여 선택된 폴리곤만 스킵
7791
+ * - 객체 생성 오버헤드 제거로 1,000개 이상도 부드럽게 처리
7187
7792
  */
7188
7793
 
7189
7794
 
@@ -7227,77 +7832,63 @@
7227
7832
 
7228
7833
  /**
7229
7834
  * 클릭 이벤트 처리
7835
+ *
7836
+ * @param event 클릭 이벤트 파라미터
7837
+ *
7838
+ * @remarks
7839
+ * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
7840
+ * - 상호작용이 비활성화되어 있으면 스킵
7841
+ * - Spatial Index를 사용하여 빠른 Hit Test 수행
7230
7842
  */
7231
7843
 
7232
7844
 
7233
7845
  var handleClick = function (event) {
7234
- var _a;
7235
-
7236
7846
  if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7237
7847
 
7238
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
7848
+ var clickedOffset = validateEvent(event, context, controller);
7849
+ if (!clickedOffset) return;
7850
+ var data = findData(clickedOffset);
7239
7851
 
7240
- try {
7241
- var clickedOffset = controller.positionToOffset(event.param.position);
7242
- var data_1 = findData(clickedOffset);
7243
-
7244
- if (data_1) {
7245
- handleLocalClick(data_1);
7852
+ if (data) {
7853
+ handleLocalClick(data);
7246
7854
 
7247
- if (onClick) {
7248
- onClick(data_1, selectedIdsRef.current);
7249
- }
7855
+ if (onClick) {
7856
+ onClick(data, selectedIdsRef.current);
7250
7857
  }
7251
- } catch (error) {
7252
- console.error('[WoongCanvasPolygon] handleClick error:', error);
7253
7858
  }
7254
7859
  };
7255
7860
  /**
7256
7861
  * 마우스 이동 이벤트 처리 (hover 감지)
7862
+ *
7863
+ * @param event 마우스 이동 이벤트 파라미터
7864
+ *
7865
+ * @remarks
7866
+ * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
7867
+ * - 상호작용이 비활성화되어 있으면 스킵
7868
+ * - hover 상태 변경 시에만 렌더링 (최적화)
7257
7869
  */
7258
7870
 
7259
7871
 
7260
7872
  var handleMouseMove = function (event) {
7261
- var _a;
7262
-
7263
7873
  if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7264
7874
 
7265
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
7266
-
7267
- try {
7268
- var mouseOffset = controller.positionToOffset(event.param.position);
7269
- var hoveredItem = findData(mouseOffset);
7270
- var prevHovered = hoveredItemRef.current;
7875
+ var mouseOffset = validateEvent(event, context, controller);
7876
+ if (!mouseOffset) return;
7877
+ var hoveredItem = findData(mouseOffset);
7878
+ var prevHovered = hoveredItemRef.current;
7271
7879
 
7272
- if (prevHovered !== hoveredItem) {
7273
- setHovered(hoveredItem);
7274
- if (prevHovered && onMouseOut) onMouseOut(prevHovered);
7275
- if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
7276
- }
7277
- } catch (error) {
7278
- console.error('[WoongCanvasPolygon] handleMouseMove error:', error);
7880
+ if (prevHovered !== hoveredItem) {
7881
+ setHovered(hoveredItem);
7279
7882
  }
7280
7883
  };
7281
- /**
7282
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
7283
- */
7284
-
7285
-
7286
- var handleDragStart = function () {
7287
- draggingRef.current = true;
7288
- controller.setMapCursor('grabbing');
7289
- };
7290
- /**
7291
- * 드래그 종료 처리 (커서를 기본으로 복원)
7292
- */
7293
-
7294
-
7295
- var handleDragEnd = function () {
7296
- draggingRef.current = false;
7297
- controller.setMapCursor('grab');
7298
- };
7299
7884
  /**
7300
7885
  * 마우스가 canvas를 벗어날 때 hover cleanup
7886
+ *
7887
+ * 맵 영역 밖으로 마우스가 나갔을 때 hover 상태를 초기화합니다.
7888
+ *
7889
+ * @remarks
7890
+ * - 상호작용이 비활성화되어 있으면 스킵
7891
+ * - hover 상태 초기화 및 커서 복원
7301
7892
  */
7302
7893
 
7303
7894
 
@@ -7310,10 +7901,6 @@
7310
7901
  hoveredItemRef.current = null;
7311
7902
  controller.setMapCursor('grab');
7312
7903
  doRenderEvent();
7313
-
7314
- if (onMouseOut) {
7315
- onMouseOut(prevHovered);
7316
- }
7317
7904
  }
7318
7905
  }; // --------------------------------------------------------------------------
7319
7906
  // Lifecycle: DOM 초기화
@@ -7381,7 +7968,7 @@
7381
7968
  stage.add(baseLayer);
7382
7969
  stage.add(eventLayer); // 초기 뷰포트 설정
7383
7970
 
7384
- updateViewport(); // ResizeObserver (맵 크기 변경 감지)
7971
+ updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
7385
7972
 
7386
7973
  var resizeRafId = null;
7387
7974
  var resizeObserver = new ResizeObserver(function () {
@@ -7395,7 +7982,7 @@
7395
7982
  stage.height(mapDiv.offsetHeight);
7396
7983
  offsetCacheRef.current.clear();
7397
7984
  boundingBoxCacheRef.current.clear();
7398
- updateViewport();
7985
+ updateViewport$1();
7399
7986
  renderAllImmediate();
7400
7987
  resizeRafId = null;
7401
7988
  });
@@ -7422,8 +8009,6 @@
7422
8009
  return findData(offset) !== null;
7423
8010
  },
7424
8011
  onClick: onClick,
7425
- onMouseOver: onMouseOver,
7426
- onMouseOut: onMouseOut,
7427
8012
  findData: findData,
7428
8013
  setHovered: setHovered,
7429
8014
  handleLocalClick: handleLocalClick,
@@ -7483,18 +8068,8 @@
7483
8068
  // --------------------------------------------------------------------------
7484
8069
 
7485
8070
  React.useEffect(function () {
7486
- if (!stageRef.current) return; // externalSelectedItems가 undefined면 외부 제어 안 함
7487
-
7488
- if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
7489
-
7490
- var newSelectedIds = new Set();
7491
- var newSelectedItemsMap = new Map();
7492
- externalSelectedItems.forEach(function (item) {
7493
- newSelectedIds.add(item.id);
7494
- newSelectedItemsMap.set(item.id, item);
7495
- });
7496
- selectedIdsRef.current = newSelectedIds;
7497
- selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
8071
+ if (!stageRef.current) return;
8072
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef); // 렌더링
7498
8073
 
7499
8074
  doRenderBase();
7500
8075
  doRenderEvent();
@@ -7544,27 +8119,7 @@
7544
8119
  * - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
7545
8120
  */
7546
8121
 
7547
- var dataMap = new Map(data.map(function (m) {
7548
- return [m.id, m];
7549
- }));
7550
- var newSelectedItemsMap = new Map();
7551
- selectedIdsRef.current.forEach(function (id) {
7552
- // 현재 data에 있으면 최신 데이터 사용
7553
- var currentItem = dataMap.get(id);
7554
-
7555
- if (currentItem) {
7556
- newSelectedItemsMap.set(id, currentItem);
7557
- } else {
7558
- // 화면 밖이면 기존 데이터 유지
7559
- var prevItem = selectedItemsMapRef.current.get(id);
7560
-
7561
- if (prevItem) {
7562
- newSelectedItemsMap.set(id, prevItem);
7563
- }
7564
- }
7565
- }); // selectedIdsRef는 그대로 유지 (화면 밖 항목도 선택 상태 유지)
7566
-
7567
- selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
8122
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current); // 즉시 렌더링
7568
8123
 
7569
8124
  renderAllImmediate();
7570
8125
  }, [data]);
@@ -10455,19 +11010,27 @@
10455
11010
  exports.WoongCanvasMarker = WoongCanvasMarker;
10456
11011
  exports.WoongCanvasPolygon = WoongCanvasPolygon;
10457
11012
  exports.WoongCanvasProvider = WoongCanvasProvider;
11013
+ exports.buildSpatialIndex = buildSpatialIndex;
10458
11014
  exports.calculateTextBoxWidth = calculateTextBoxWidth;
10459
11015
  exports.computeMarkerOffset = computeMarkerOffset;
10460
11016
  exports.computePolygonOffsets = computePolygonOffsets;
11017
+ exports.createMapEventHandlers = createMapEventHandlers;
10461
11018
  exports.getClusterInfo = getClusterInfo;
10462
11019
  exports.getMapOfType = getMapOfType;
10463
11020
  exports.hexToRgba = hexToRgba;
11021
+ exports.isInViewport = isInViewport;
10464
11022
  exports.isPointInMarkerData = isPointInMarkerData;
10465
11023
  exports.isPointInPolygon = isPointInPolygon;
10466
11024
  exports.isPointInPolygonData = isPointInPolygonData;
10467
11025
  exports.log = log;
11026
+ exports.mapValuesToArray = mapValuesToArray;
11027
+ exports.syncExternalSelectedItems = syncExternalSelectedItems;
11028
+ exports.syncSelectedItems = syncSelectedItems;
11029
+ exports.updateViewport = updateViewport;
10468
11030
  exports.useMarkerMoving = useMarkerMoving;
10469
11031
  exports.useMintMapController = useMintMapController;
10470
11032
  exports.useWoongCanvasContext = useWoongCanvasContext;
11033
+ exports.validateEvent = validateEvent;
10471
11034
  exports.waiting = waiting;
10472
11035
 
10473
11036
  Object.defineProperty(exports, '__esModule', { value: true });