@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.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import { __extends, __awaiter, __generator, __spreadArray, __assign, __rest } from 'tslib';
1
+ import { __extends, __assign, __awaiter, __generator, __spreadArray, __rest } from 'tslib';
2
2
  import React, { createContext, useContext, useRef, useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import classNames from 'classnames/bind';
4
4
  import styleInject from 'style-inject';
@@ -796,6 +796,30 @@ function () {
796
796
 
797
797
  /**
798
798
  * 폴리곤 offset 계산
799
+ *
800
+ * GeoJSON MultiPolygon 형식의 위경도 좌표를 화면 픽셀 좌표로 변환합니다.
801
+ *
802
+ * @param polygonData 폴리곤 데이터 (paths 필드 필수)
803
+ * @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
+ * ```
799
823
  */
800
824
 
801
825
  var computePolygonOffsets = function (polygonData, controller) {
@@ -832,6 +856,23 @@ var computePolygonOffsets = function (polygonData, controller) {
832
856
  };
833
857
  /**
834
858
  * 마커 offset 계산
859
+ *
860
+ * 마커의 위경도 좌표를 화면 픽셀 좌표로 변환합니다.
861
+ *
862
+ * @param markerData 마커 데이터 (position 필드 필수)
863
+ * @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
+ * ```
835
876
  */
836
877
 
837
878
  var computeMarkerOffset = function (markerData, controller) {
@@ -843,6 +884,24 @@ var computeMarkerOffset = function (markerData, controller) {
843
884
  };
844
885
  /**
845
886
  * Point-in-Polygon 알고리즘
887
+ *
888
+ * Ray Casting 알고리즘을 사용하여 점이 폴리곤 내부에 있는지 확인합니다.
889
+ *
890
+ * @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
+ * ```
846
905
  */
847
906
 
848
907
  var isPointInPolygon = function (point, polygon) {
@@ -862,13 +921,32 @@ var isPointInPolygon = function (point, polygon) {
862
921
  /**
863
922
  * 폴리곤 히트 테스트 (도넛 폴리곤 지원)
864
923
  *
865
- * 로직:
866
- * 1. 외부 폴리곤(첫 번째): 내부에 있어야 함
867
- * 2. 내부 구멍들(나머지): 내부에 있으면 안 됨 (evenodd 규칙)
924
+ * 점이 폴리곤 내부에 있는지 확인합니다. 도넛 폴리곤(구멍이 있는 폴리곤)을 지원합니다.
925
+ *
926
+ * @param clickedOffset 클릭/마우스 위치 좌표
927
+ * @param polygonData 폴리곤 데이터
928
+ * @param getPolygonOffsets 폴리곤 좌표 변환 함수
929
+ * @returns 점이 폴리곤 내부에 있으면 true, 아니면 false
868
930
  *
869
- * 중요: 도넛 폴리곤과 내부 폴리곤은 별개의 polygonData로 처리됨
931
+ * @remarks
932
+ * - **도넛 폴리곤 처리** (isDonutPolygon === true):
933
+ * 1. 외부 폴리곤(첫 번째): 내부에 있어야 함
934
+ * 2. 내부 구멍들(나머지): 내부에 있으면 안 됨 (evenodd 규칙)
935
+ * - **일반 폴리곤 처리**: Point-in-Polygon 알고리즘 사용
936
+ * - **성능**: O(n), n은 폴리곤의 총 좌표 수
937
+ *
938
+ * **중요**: 도넛 폴리곤과 내부 폴리곤은 별개의 polygonData로 처리됩니다.
870
939
  * - 도넛 폴리곤 A: isDonutPolygon=true
871
940
  * - 내부 폴리곤 B: isDonutPolygon=false (별도 데이터)
941
+ *
942
+ * @example
943
+ * ```typescript
944
+ * const isHit = isPointInPolygonData(
945
+ * clickedOffset,
946
+ * polygonData,
947
+ * getOrComputePolygonOffsets
948
+ * );
949
+ * ```
872
950
  */
873
951
 
874
952
  var isPointInPolygonData = function (clickedOffset, polygonData, getPolygonOffsets) {
@@ -933,10 +1011,29 @@ var isPointInPolygonData = function (clickedOffset, polygonData, getPolygonOffse
933
1011
  /**
934
1012
  * 마커 히트 테스트 (클릭/hover 영역 체크)
935
1013
  *
936
- * 🎯 중요: 꼬리(tail)는 Hit Test 영역에서 제외됩니다!
937
- * - markerOffset.y마커 최하단(꼬리 끝) 좌표
938
- * - boxHeight는 마커 본체만 포함 (꼬리 제외)
939
- * - tailHeight만큼 위로 올려서 본체만 Hit Test 영역으로 사용
1014
+ * 점이 마커의 클릭/호버 영역 내부에 있는지 확인합니다.
1015
+ * 마커의 꼬리(tail)Hit Test 영역에서 제외됩니다.
1016
+ *
1017
+ * @param clickedOffset 클릭/마우스 위치 좌표
1018
+ * @param markerData 마커 데이터
1019
+ * @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
+ * ```
940
1037
  */
941
1038
 
942
1039
  var isPointInMarkerData = function (clickedOffset, markerData, getMarkerOffset) {
@@ -973,12 +1070,29 @@ var tempCtx = tempCanvas.getContext('2d');
973
1070
  /**
974
1071
  * 텍스트 박스의 너비를 계산합니다.
975
1072
  *
976
- * @param {Object} params - 파라미터 객체
977
- * @param {string} params.text - 측정할 텍스트
978
- * @param {string} params.fontConfig - 폰트 설정 (예: 'bold 16px Arial')
979
- * @param {number} params.padding - 텍스트 박스에 적용할 패딩 값
980
- * @param {number} params.minWidth - 최소 너비
981
- * @returns {number} 계산된 텍스트 박스의 너비
1073
+ * Canvas 2D Context의 measureText()를 사용하여 텍스트의 실제 너비를 계산하고,
1074
+ * 패딩과 최소 너비를 고려하여 최종 너비를 반환합니다.
1075
+ *
1076
+ * @param params 파라미터 객체
1077
+ * @param params.text 측정할 텍스트
1078
+ * @param params.fontConfig 폰트 설정 (예: 'bold 16px Arial')
1079
+ * @param params.padding 텍스트 박스에 적용할 패딩 값 (px)
1080
+ * @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
+ * ```
982
1096
  */
983
1097
 
984
1098
  var calculateTextBoxWidth = function (_a) {
@@ -993,6 +1107,29 @@ var calculateTextBoxWidth = function (_a) {
993
1107
  };
994
1108
 
995
1109
  var WoongCanvasContext = createContext(null);
1110
+ /**
1111
+ * WoongCanvasProvider 컴포넌트
1112
+ *
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
+ * ```
1131
+ */
1132
+
996
1133
  var WoongCanvasProvider = function (_a) {
997
1134
  var children = _a.children;
998
1135
  var controller = useMintMapController(); // Refs
@@ -1003,7 +1140,12 @@ var WoongCanvasProvider = function (_a) {
1003
1140
  var draggingRef = useRef(false);
1004
1141
  /**
1005
1142
  * 컴포넌트 등록 (zIndex 내림차순 정렬)
1006
- * 높은 zIndex가 먼저 처리됨
1143
+ *
1144
+ * 컴포넌트 인스턴스를 등록하고 zIndex 기준으로 내림차순 정렬합니다.
1145
+ * 높은 zIndex를 가진 컴포넌트가 이벤트 처리에서 우선순위를 가집니다.
1146
+ *
1147
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1148
+ * @param instance 등록할 컴포넌트 인스턴스
1007
1149
  */
1008
1150
 
1009
1151
  var registerComponent = useCallback(function (instance) {
@@ -1014,6 +1156,12 @@ var WoongCanvasProvider = function (_a) {
1014
1156
  }, []);
1015
1157
  /**
1016
1158
  * 컴포넌트 등록 해제
1159
+ *
1160
+ * 컴포넌트 인스턴스를 등록 해제합니다.
1161
+ * hover 중이던 컴포넌트면 hover 상태도 초기화합니다.
1162
+ *
1163
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1164
+ * @param instance 등록 해제할 컴포넌트 인스턴스
1017
1165
  */
1018
1166
 
1019
1167
  var unregisterComponent = useCallback(function (instance) {
@@ -1029,6 +1177,15 @@ var WoongCanvasProvider = function (_a) {
1029
1177
  }, []);
1030
1178
  /**
1031
1179
  * 전역 클릭 핸들러 (zIndex 우선순위)
1180
+ *
1181
+ * 모든 등록된 WoongCanvas 컴포넌트 중 zIndex가 높은 컴포넌트부터 클릭 이벤트를 처리합니다.
1182
+ *
1183
+ * @param event 클릭 이벤트 파라미터
1184
+ *
1185
+ * @remarks
1186
+ * - zIndex가 높은 컴포넌트부터 순회하여 첫 번째 히트만 처리
1187
+ * - 상호작용이 비활성화된 컴포넌트는 스킵
1188
+ * - Context가 없으면 각 컴포넌트의 로컬 핸들러가 처리
1032
1189
  */
1033
1190
 
1034
1191
  var handleGlobalClick = useCallback(function (event) {
@@ -1056,6 +1213,16 @@ var WoongCanvasProvider = function (_a) {
1056
1213
  }, [controller]);
1057
1214
  /**
1058
1215
  * 전역 마우스 이동 핸들러 (zIndex 우선순위)
1216
+ *
1217
+ * 모든 등록된 WoongCanvas 컴포넌트 중 zIndex가 높은 컴포넌트부터 hover 이벤트를 처리합니다.
1218
+ *
1219
+ * @param event 마우스 이동 이벤트 파라미터
1220
+ *
1221
+ * @remarks
1222
+ * - zIndex가 높은 컴포넌트부터 순회하여 첫 번째 히트만 처리
1223
+ * - 상호작용이 비활성화된 컴포넌트는 스킵
1224
+ * - 드래그 중이면 이벤트 무시 (성능 최적화)
1225
+ * - hover 상태 변경 감지 후 이전 hover 해제 및 새 hover 설정
1059
1226
  */
1060
1227
 
1061
1228
  var handleGlobalMouseMove = useCallback(function (event) {
@@ -1142,6 +1309,26 @@ var WoongCanvasProvider = function (_a) {
1142
1309
  value: contextValue
1143
1310
  }, children);
1144
1311
  };
1312
+ /**
1313
+ * WoongCanvas Context Hook
1314
+ *
1315
+ * WoongCanvasProvider로 감싸진 컴포넌트에서 Context에 접근할 수 있는 hook입니다.
1316
+ *
1317
+ * @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
+ */
1331
+
1145
1332
  var useWoongCanvasContext = function () {
1146
1333
  var context = useContext(WoongCanvasContext);
1147
1334
  return context;
@@ -1195,12 +1382,32 @@ var DEFAULT_CULLING_MARGIN = 100;
1195
1382
  var DEFAULT_MAX_CACHE_SIZE = 30000;
1196
1383
  /**
1197
1384
  * LRU (Least Recently Used) Cache
1198
- * 메모리 제한을 위한 캐시 구현 (최적화 버전)
1199
1385
  *
1200
- * 개선 사항:
1386
+ * 메모리 제한을 위한 캐시 구현입니다. WoongCanvas 컴포넌트에서 좌표 변환 결과를 캐싱하는데 사용됩니다.
1387
+ *
1388
+ * @template K 캐시 키 타입
1389
+ * @template V 캐시 값 타입
1390
+ *
1391
+ * @remarks
1392
+ * **개선 사항**:
1201
1393
  * 1. get() 성능 향상: 접근 빈도 추적 없이 단순 조회만 수행 (delete+set 제거)
1202
1394
  * 2. set() 버그 수정: 기존 키 업데이트 시 maxSize 체크 로직 개선
1203
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
+ * ```
1204
1411
  */
1205
1412
 
1206
1413
  var LRUCache =
@@ -1217,17 +1424,12 @@ function () {
1217
1424
  /**
1218
1425
  * 캐시에서 값 조회
1219
1426
  *
1220
- * 최적화: delete+set 제거
1221
- * - 이전: 매번 delete+set으로 LRU 갱신 (해시 재계산 비용)
1222
- * - 현재: 단순 조회만 수행 (O(1) 해시 조회)
1223
- *
1224
- * 트레이드오프:
1225
- * - 장점: 읽기 성능 대폭 향상 (10,000번 get → 이전보다 2배 빠름)
1226
- * - 단점: 접근 빈도가 아닌 삽입 순서 기반 eviction (FIFO)
1427
+ * @param key 조회할 키
1428
+ * @returns 캐시된 또는 undefined (캐시 미스 )
1227
1429
  *
1228
- * WoongCanvasMarker 사용 사례에 최적:
1229
- * - 좌표 변환 결과는 zoom/pan 시 어차피 전체 초기화
1230
- * - 접근 빈도 추적보다 빠른 조회가 중요
1430
+ * @remarks
1431
+ * - 성능: O(1) 해시 조회
1432
+ * - 최적화: delete+set 제거로 읽기 성능 대폭 향상
1231
1433
  */
1232
1434
 
1233
1435
 
@@ -1235,11 +1437,15 @@ function () {
1235
1437
  return this.cache.get(key);
1236
1438
  };
1237
1439
  /**
1238
- * 캐시에 값 저장 (버그 수정 + 최적화)
1440
+ * 캐시에 값 저장
1239
1441
  *
1240
- * 수정 사항:
1241
- * 1. 기존 업데이트 시 크기 체크 누락 버그 수정
1242
- * 2. 로직 명확화: 기존 항목/신규 항목 분리 처리
1442
+ * @param key 저장할 키
1443
+ * @param value 저장할
1444
+ *
1445
+ * @remarks
1446
+ * - 기존 키 업데이트: 단순 덮어쓰기 (크기 변화 없음)
1447
+ * - 신규 키 추가: 크기 체크 후 필요시 가장 오래된 항목 제거 (FIFO)
1448
+ * - 성능: O(1) 평균 시간복잡도
1243
1449
  */
1244
1450
 
1245
1451
 
@@ -1337,9 +1543,18 @@ function () {
1337
1543
  /**
1338
1544
  * 항목 추가 (바운딩 박스 기반)
1339
1545
  *
1340
- * 개선 사항:
1546
+ * 항목을 공간 인덱스에 추가합니다. 바운딩 박스가 걸치는 모든 셀에 삽입됩니다.
1547
+ *
1548
+ * @param item 추가할 항목
1549
+ * @param minX 바운딩 박스 최소 X 좌표
1550
+ * @param minY 바운딩 박스 최소 Y 좌표
1551
+ * @param maxX 바운딩 박스 최대 X 좌표
1552
+ * @param maxY 바운딩 박스 최대 Y 좌표
1553
+ *
1554
+ * @remarks
1341
1555
  * - 중복 삽입 방지: 기존 항목이 있으면 먼저 제거 후 재삽입
1342
1556
  * - 메모리 누수 방지: 이전 셀 참조 완전 제거
1557
+ * - 성능: O(1) 평균 시간복잡도
1343
1558
  */
1344
1559
 
1345
1560
 
@@ -1363,7 +1578,14 @@ function () {
1363
1578
  /**
1364
1579
  * 항목 제거
1365
1580
  *
1366
- * 추가된 메서드: 메모리 누수 방지 및 업데이트 지원
1581
+ * 공간 인덱스에서 항목을 제거합니다.
1582
+ *
1583
+ * @param item 제거할 항목
1584
+ *
1585
+ * @remarks
1586
+ * - 메모리 누수 방지: 모든 셀에서 참조 완전 제거
1587
+ * - 빈 셀 정리: 항목이 없어진 셀은 자동으로 정리됨
1588
+ * - 성능: O(셀 개수), 보통 O(1)
1367
1589
  */
1368
1590
 
1369
1591
 
@@ -1394,7 +1616,13 @@ function () {
1394
1616
  /**
1395
1617
  * 항목 위치 업데이트
1396
1618
  *
1397
- * 추가된 메서드: remove + insert의 편의 함수
1619
+ * 항목의 위치를 업데이트합니다. remove + insert의 편의 함수입니다.
1620
+ *
1621
+ * @param item 업데이트할 항목
1622
+ * @param minX 새로운 바운딩 박스 최소 X 좌표
1623
+ * @param minY 새로운 바운딩 박스 최소 Y 좌표
1624
+ * @param maxX 새로운 바운딩 박스 최대 X 좌표
1625
+ * @param maxY 새로운 바운딩 박스 최대 Y 좌표
1398
1626
  */
1399
1627
 
1400
1628
 
@@ -1404,7 +1632,26 @@ function () {
1404
1632
  /**
1405
1633
  * 점 주변의 항목 조회 (1개 셀만)
1406
1634
  *
1407
- * 성능: O(해당 셀의 항목 수) - 보통 ~10개
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
+ * ```
1408
1655
  */
1409
1656
 
1410
1657
 
@@ -1417,8 +1664,18 @@ function () {
1417
1664
  /**
1418
1665
  * 영역 내 항목 조회
1419
1666
  *
1420
- * 성능: O( 개수 × 셀당 평균 항목 수)
1421
- * Set으로 중복 제거 보장
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에 유용
1422
1679
  */
1423
1680
 
1424
1681
 
@@ -1443,7 +1700,11 @@ function () {
1443
1700
  /**
1444
1701
  * 항목 존재 여부 확인
1445
1702
  *
1446
- * 추가된 메서드: 빠른 존재 여부 체크
1703
+ * @param item 확인할 항목
1704
+ * @returns 항목이 인덱스에 있으면 true, 아니면 false
1705
+ *
1706
+ * @remarks
1707
+ * - 성능: O(1) 해시 조회
1447
1708
  */
1448
1709
 
1449
1710
 
@@ -1462,7 +1723,14 @@ function () {
1462
1723
  /**
1463
1724
  * 통계 정보
1464
1725
  *
1465
- * 개선: totalItems는 실제 고유 항목 수를 정확히 반환
1726
+ * 공간 인덱스의 현재 상태를 반환합니다. 디버깅 성능 분석에 유용합니다.
1727
+ *
1728
+ * @returns 통계 정보 객체
1729
+ *
1730
+ * @remarks
1731
+ * - totalCells: 현재 사용 중인 셀 개수
1732
+ * - totalItems: 인덱스에 등록된 고유 항목 수 (정확)
1733
+ * - avgItemsPerCell: 셀당 평균 항목 수
1466
1734
  */
1467
1735
 
1468
1736
 
@@ -1481,6 +1749,400 @@ function () {
1481
1749
  return SpatialHashGrid;
1482
1750
  }();
1483
1751
 
1752
+ /**
1753
+ * 현재 뷰포트 영역 계산
1754
+ *
1755
+ * Konva Stage의 크기와 컬링 마진을 기반으로 뷰포트 경계를 계산합니다.
1756
+ *
1757
+ * @param stage Konva Stage 인스턴스 (width, height 메서드 제공)
1758
+ * @param cullingMargin 컬링 여유 공간 (px)
1759
+ * @param viewportRef 뷰포트 경계를 저장할 ref
1760
+ *
1761
+ * @remarks
1762
+ * - 화면 밖 cullingMargin만큼의 영역까지 포함하여 계산
1763
+ * - 스크롤 시 부드러운 전환을 위해 여유 공간 포함
1764
+ *
1765
+ * @example
1766
+ * ```typescript
1767
+ * updateViewport(stageRef.current, cullingMargin, viewportRef);
1768
+ * ```
1769
+ */
1770
+ var updateViewport = function (stage, cullingMargin, viewportRef) {
1771
+ if (!stage) return;
1772
+ viewportRef.current = {
1773
+ minX: -cullingMargin,
1774
+ maxX: stage.width() + cullingMargin,
1775
+ minY: -cullingMargin,
1776
+ maxY: stage.height() + cullingMargin
1777
+ };
1778
+ };
1779
+ /**
1780
+ * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
1781
+ *
1782
+ * 뷰포트 컬링을 위한 함수입니다. 바운딩 박스와 뷰포트 경계의 교차를 확인합니다.
1783
+ * 바운딩 박스는 캐시되어 성능을 최적화합니다.
1784
+ *
1785
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1786
+ * @param item 확인할 아이템
1787
+ * @param enableViewportCulling 뷰포트 컬링 활성화 여부
1788
+ * @param viewportRef 뷰포트 경계 ref
1789
+ * @param boundingBoxCacheRef 바운딩 박스 캐시 ref
1790
+ * @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
+ * ```
1807
+ */
1808
+
1809
+ var isInViewport = function (item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
1810
+ if (!enableViewportCulling || !viewportRef.current) return true;
1811
+ var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
1812
+
1813
+ var bbox = boundingBoxCacheRef.current.get(item.id);
1814
+
1815
+ if (!bbox) {
1816
+ // 바운딩 박스 계산 (공통 함수 사용)
1817
+ var computed = computeBoundingBox(item);
1818
+ if (!computed) return false;
1819
+ bbox = computed;
1820
+ boundingBoxCacheRef.current.set(item.id, bbox);
1821
+ } // 바운딩 박스와 viewport 교차 체크
1822
+
1823
+
1824
+ return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
1825
+ };
1826
+
1827
+ /**
1828
+ * 지도 이벤트 핸들러 생성 함수
1829
+ *
1830
+ * 지도 이동, 줌, 드래그 등의 이벤트를 처리하는 핸들러들을 생성합니다.
1831
+ *
1832
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1833
+ * @param deps 이벤트 핸들러 생성에 필요한 의존성
1834
+ * @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
+ */
1858
+
1859
+ var createMapEventHandlers = function (deps) {
1860
+ var controller = deps.controller,
1861
+ containerRef = deps.containerRef,
1862
+ markerRef = deps.markerRef,
1863
+ options = deps.options,
1864
+ prevCenterOffsetRef = deps.prevCenterOffsetRef,
1865
+ accumTranslateRef = deps.accumTranslateRef,
1866
+ offsetCacheRef = deps.offsetCacheRef,
1867
+ boundingBoxCacheRef = deps.boundingBoxCacheRef,
1868
+ renderAllImmediate = deps.renderAllImmediate;
1869
+ /**
1870
+ * 지도 이동/줌 완료 시 처리
1871
+ *
1872
+ * - 캐시 초기화: 좌표 변환 결과가 변경되었으므로 캐시 무효화
1873
+ * - 마커 위치 업데이트: 새로운 지도 위치에 맞게 마커 재배치
1874
+ * - 렌더링: 새 위치에서 전체 렌더링 수행
1875
+ */
1876
+
1877
+ var handleIdle = function () {
1878
+ prevCenterOffsetRef.current = null;
1879
+ accumTranslateRef.current = {
1880
+ x: 0,
1881
+ y: 0
1882
+ }; // 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
1883
+
1884
+ offsetCacheRef.current.clear();
1885
+ boundingBoxCacheRef.current.clear(); // 마커 위치 업데이트
1886
+
1887
+ var bounds = controller.getCurrBounds();
1888
+
1889
+ var markerOptions = __assign({
1890
+ position: bounds.nw
1891
+ }, options);
1892
+
1893
+ markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
1894
+
1895
+ if (containerRef.current) {
1896
+ containerRef.current.style.transform = '';
1897
+ containerRef.current.style.visibility = '';
1898
+ } // 새 위치에서 렌더링
1899
+
1900
+
1901
+ renderAllImmediate();
1902
+ };
1903
+ /**
1904
+ * 줌 시작 시 처리 (일시적으로 숨김)
1905
+ */
1906
+
1907
+
1908
+ var handleZoomStart = function () {
1909
+ if (containerRef.current) {
1910
+ containerRef.current.style.visibility = 'hidden';
1911
+ }
1912
+ };
1913
+ /**
1914
+ * 줌 종료 시 처리 (다시 표시)
1915
+ */
1916
+
1917
+
1918
+ var handleZoomEnd = function () {
1919
+ if (containerRef.current) {
1920
+ containerRef.current.style.visibility = '';
1921
+ }
1922
+ };
1923
+ /**
1924
+ * 지도 중심 변경 시 처리 (transform으로 이동 추적)
1925
+ */
1926
+
1927
+
1928
+ var handleCenterChanged = function () {
1929
+ var center = controller.getCurrBounds().getCenter();
1930
+ var curr = controller.positionToOffset(center);
1931
+ var prev = prevCenterOffsetRef.current;
1932
+
1933
+ if (!prev) {
1934
+ prevCenterOffsetRef.current = {
1935
+ x: curr.x,
1936
+ y: curr.y
1937
+ };
1938
+ return;
1939
+ }
1940
+
1941
+ var dx = prev.x - curr.x;
1942
+ var dy = prev.y - curr.y;
1943
+ accumTranslateRef.current = {
1944
+ x: accumTranslateRef.current.x + dx,
1945
+ y: accumTranslateRef.current.y + dy
1946
+ };
1947
+ prevCenterOffsetRef.current = {
1948
+ x: curr.x,
1949
+ y: curr.y
1950
+ };
1951
+
1952
+ if (containerRef.current) {
1953
+ containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
1954
+ }
1955
+ };
1956
+ /**
1957
+ * 드래그 시작 처리
1958
+ */
1959
+
1960
+
1961
+ var handleDragStart = function () {// 커서는 각 컴포넌트에서 처리
1962
+ };
1963
+ /**
1964
+ * 드래그 종료 처리
1965
+ */
1966
+
1967
+
1968
+ var handleDragEnd = function () {// 커서는 각 컴포넌트에서 처리
1969
+ };
1970
+
1971
+ return {
1972
+ handleIdle: handleIdle,
1973
+ handleZoomStart: handleZoomStart,
1974
+ handleZoomEnd: handleZoomEnd,
1975
+ handleCenterChanged: handleCenterChanged,
1976
+ handleDragStart: handleDragStart,
1977
+ handleDragEnd: handleDragEnd
1978
+ };
1979
+ };
1980
+ /**
1981
+ * 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
1982
+ *
1983
+ * Spatial Hash Grid에 모든 데이터의 바운딩 박스를 삽입합니다.
1984
+ * 이를 통해 클릭/호버 시 O(1) 수준의 빠른 Hit Test가 가능합니다.
1985
+ *
1986
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
1987
+ * @param data 공간 인덱스에 삽입할 데이터 배열
1988
+ * @param spatialIndex Spatial Hash Grid 인스턴스
1989
+ * @param computeBoundingBox 바운딩 박스 계산 함수
1990
+ *
1991
+ * @remarks
1992
+ * - 성능: O(n) 시간복잡도, n은 데이터 개수
1993
+ * - 호출 시점: 데이터 변경 시 또는 지도 이동/줌 완료 시
1994
+ *
1995
+ * @example
1996
+ * ```typescript
1997
+ * buildSpatialIndex(
1998
+ * dataRef.current,
1999
+ * spatialIndexRef.current,
2000
+ * computeBoundingBox
2001
+ * );
2002
+ * ```
2003
+ */
2004
+
2005
+ var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
2006
+ spatialIndex.clear();
2007
+
2008
+ for (var _i = 0, data_1 = data; _i < data_1.length; _i++) {
2009
+ var item = data_1[_i];
2010
+ var bbox = computeBoundingBox(item);
2011
+
2012
+ if (bbox) {
2013
+ spatialIndex.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
2014
+ }
2015
+ }
2016
+ };
2017
+ /**
2018
+ * 선택 상태 동기화 유틸리티
2019
+ *
2020
+ * 데이터 변경 시 선택된 항목의 참조를 최신 데이터로 업데이트합니다.
2021
+ * 화면 밖에 있는 선택된 항목도 선택 상태를 유지합니다.
2022
+ *
2023
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
2024
+ * @param data 최신 데이터 배열
2025
+ * @param selectedIds 선택된 항목 ID Set
2026
+ * @param selectedItemsMap 현재 선택된 항목 Map
2027
+ * @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
+ */
2042
+
2043
+ var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
2044
+ var dataMap = new Map(data.map(function (m) {
2045
+ return [m.id, m];
2046
+ }));
2047
+ var newSelectedItemsMap = new Map();
2048
+ selectedIds.forEach(function (id) {
2049
+ // 현재 data에 있으면 최신 데이터 사용
2050
+ var currentItem = dataMap.get(id);
2051
+
2052
+ if (currentItem) {
2053
+ newSelectedItemsMap.set(id, currentItem);
2054
+ } else {
2055
+ // 화면 밖이면 기존 데이터 유지
2056
+ var prevItem = selectedItemsMap.get(id);
2057
+
2058
+ if (prevItem) {
2059
+ newSelectedItemsMap.set(id, prevItem);
2060
+ }
2061
+ }
2062
+ });
2063
+ return newSelectedItemsMap;
2064
+ };
2065
+ /**
2066
+ * 외부 selectedItems를 내부 상태로 동기화
2067
+ *
2068
+ * 외부에서 전달된 selectedItems prop을 내부 ref 상태로 동기화합니다.
2069
+ *
2070
+ * @template T 마커/폴리곤 데이터의 추가 속성 타입
2071
+ * @param externalSelectedItems 외부에서 전달된 선택된 항목 배열 (undefined면 동기화 안 함)
2072
+ * @param selectedIdsRef 선택된 ID Set ref
2073
+ * @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
+ */
2087
+
2088
+ var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef, selectedItemsMapRef) {
2089
+ if (externalSelectedItems === undefined) return;
2090
+ var newSelectedIds = new Set();
2091
+ var newSelectedItemsMap = new Map();
2092
+ externalSelectedItems.forEach(function (item) {
2093
+ newSelectedIds.add(item.id);
2094
+ newSelectedItemsMap.set(item.id, item);
2095
+ });
2096
+ selectedIdsRef.current = newSelectedIds;
2097
+ selectedItemsMapRef.current = newSelectedItemsMap;
2098
+ };
2099
+
2100
+ /**
2101
+ * 이벤트 유효성 검증 헬퍼
2102
+ *
2103
+ * @param event 이벤트 파라미터
2104
+ * @param context Context가 있는지 여부
2105
+ * @param controller MintMapController 인스턴스
2106
+ * @returns 유효한 offset 또는 null
2107
+ *
2108
+ * @remarks
2109
+ * Context가 있으면 전역 이벤트 핸들러가 처리하므로 로컬 핸들러는 스킵
2110
+ */
2111
+ var validateEvent = function (event, context, controller) {
2112
+ var _a; // Context가 있으면 전역 핸들러가 처리
2113
+
2114
+
2115
+ if (context) return null; // 이벤트 파라미터 검증
2116
+
2117
+ if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return null;
2118
+
2119
+ try {
2120
+ return controller.positionToOffset(event.param.position);
2121
+ } catch (error) {
2122
+ console.error('[WoongCanvas] validateEvent error:', error);
2123
+ return null;
2124
+ }
2125
+ };
2126
+ /**
2127
+ * Map의 values를 배열로 변환 (최적화 버전)
2128
+ *
2129
+ * @param map 변환할 Map
2130
+ * @returns Map의 값 배열
2131
+ *
2132
+ * @remarks
2133
+ * Map.values()는 IterableIterator를 반환하므로 배열 변환이 필요할 때 사용합니다.
2134
+ * 성능: O(n) 시간복잡도
2135
+ *
2136
+ * 최적화: Array.from을 사용하되, 크기를 미리 할당하여 메모리 재할당 최소화
2137
+ */
2138
+
2139
+ var mapValuesToArray = function (map) {
2140
+ // Map이 비어있으면 빈 배열 반환 (메모리 할당 최소화)
2141
+ if (map.size === 0) return []; // Array.from 사용 (TypeScript 컴파일러 호환성)
2142
+
2143
+ return Array.from(map.values());
2144
+ };
2145
+
1484
2146
  var cn$3 = classNames.bind(styles$1);
1485
2147
  function MintMapCore(_a) {
1486
2148
  var _this = this;
@@ -5546,37 +6208,16 @@ var WoongCanvasMarker = function (props) {
5546
6208
  * 현재 뷰포트 영역 계산
5547
6209
  */
5548
6210
 
5549
- var updateViewport = function () {
5550
- if (!stageRef.current) return;
5551
- var stage = stageRef.current;
5552
- viewportRef.current = {
5553
- minX: -cullingMargin,
5554
- maxX: stage.width() + cullingMargin,
5555
- minY: -cullingMargin,
5556
- maxY: stage.height() + cullingMargin
5557
- };
6211
+ var updateViewport$1 = function () {
6212
+ updateViewport(stageRef.current, cullingMargin, viewportRef);
5558
6213
  };
5559
6214
  /**
5560
6215
  * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
5561
6216
  */
5562
6217
 
5563
6218
 
5564
- var isInViewport = function (item) {
5565
- if (!enableViewportCulling || !viewportRef.current) return true;
5566
- var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
5567
-
5568
- var bbox = boundingBoxCacheRef.current.get(item.id);
5569
-
5570
- if (!bbox) {
5571
- // 바운딩 박스 계산 (공통 함수 사용)
5572
- var computed = computeBoundingBox(item);
5573
- if (!computed) return false;
5574
- bbox = computed;
5575
- boundingBoxCacheRef.current.set(item.id, bbox);
5576
- } // 바운딩 박스와 viewport 교차 체크
5577
-
5578
-
5579
- return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
6219
+ var isInViewport$1 = function (item) {
6220
+ return isInViewport(item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox);
5580
6221
  }; // --------------------------------------------------------------------------
5581
6222
  // 유틸리티 함수: 좌표 변환 캐싱
5582
6223
  // --------------------------------------------------------------------------
@@ -5606,12 +6247,23 @@ var WoongCanvasMarker = function (props) {
5606
6247
  /**
5607
6248
  * 마커의 바운딩 박스 계산
5608
6249
  *
5609
- * 🎯 마커의 경우:
5610
- * - boxHeight: 본체만 (Hit Test 영역)
5611
- * - tailHeight: 꼬리 높이 (Viewport Culling용, 화면에 보이는 전체 영역)
6250
+ * 마커의 화면 상 위치와 크기를 기반으로 바운딩 박스를 계산합니다.
6251
+ * Viewport Culling에 사용되며, tailHeight를 포함하여 전체 표시 영역을 계산합니다.
5612
6252
  *
5613
6253
  * @param item 마커 데이터
5614
- * @returns 바운딩 박스 또는 null
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
+ * ```
5615
6267
  */
5616
6268
 
5617
6269
 
@@ -5635,29 +6287,32 @@ var WoongCanvasMarker = function (props) {
5635
6287
 
5636
6288
  /**
5637
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개 후보만 체크)
5638
6298
  */
5639
6299
 
5640
6300
 
5641
- var buildSpatialIndex = function () {
5642
- var spatial = spatialIndexRef.current;
5643
- spatial.clear();
5644
- var currentData = dataRef.current;
5645
-
5646
- for (var _i = 0, currentData_1 = currentData; _i < currentData_1.length; _i++) {
5647
- var item = currentData_1[_i]; // 바운딩 박스 계산 (공통 함수 사용)
5648
-
5649
- var bbox = computeBoundingBox(item);
5650
-
5651
- if (bbox) {
5652
- spatial.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
5653
- }
5654
- }
6301
+ var buildSpatialIndex$1 = function () {
6302
+ buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
5655
6303
  }; // --------------------------------------------------------------------------
5656
- // 렌더링 함수 결정 (dataType에 따라)
6304
+ // 렌더링 함수 결정
5657
6305
  // --------------------------------------------------------------------------
5658
6306
 
5659
6307
  /**
5660
6308
  * 외부 렌더링 함수에 전달할 유틸리티 객체
6309
+ *
6310
+ * 커스텀 렌더링 함수(renderBase, renderAnimation, renderEvent)에서 사용할
6311
+ * 좌표 변환 등의 헬퍼 함수들을 제공합니다.
6312
+ *
6313
+ * @remarks
6314
+ * - getOrComputeMarkerOffset: 마커 좌표 변환 (자동 캐싱)
6315
+ * - getOrComputePolygonOffsets: 폴리곤 좌표 변환 (마커에서는 사용 안 함)
5661
6316
  */
5662
6317
 
5663
6318
 
@@ -5699,7 +6354,7 @@ var WoongCanvasMarker = function (props) {
5699
6354
  var hovered = hoveredItemRef.current; // 클로저로 최신 ref 값 참조
5700
6355
 
5701
6356
  var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
5702
- return isInViewport(item);
6357
+ return isInViewport$1(item);
5703
6358
  }) : dataRef.current; // topOnHover가 true이고 renderEvent가 없으면 Base Layer에서 hover 처리
5704
6359
 
5705
6360
  if (topOnHover && !renderEvent && hovered) {
@@ -5719,7 +6374,7 @@ var WoongCanvasMarker = function (props) {
5719
6374
  }); // hover된 항목을 최상단에 렌더링 (renderEvent가 없을 때만)
5720
6375
 
5721
6376
  if (topOnHover && !renderEvent && hovered) {
5722
- var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
6377
+ var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
5723
6378
 
5724
6379
  if (isHoveredInViewport) {
5725
6380
  renderBase({
@@ -5745,7 +6400,11 @@ var WoongCanvasMarker = function (props) {
5745
6400
  /**
5746
6401
  * Animation 레이어 렌더링 (선택된 마커 애니메이션)
5747
6402
  *
5748
- * 🔥 최적화: sceneFunc 내부에서 최신 items 참조
6403
+ * 선택된 마커에 대한 애니메이션 효과를 렌더링합니다.
6404
+ * renderAnimation prop이 제공된 경우에만 실행됩니다.
6405
+ *
6406
+ * @remarks
6407
+ * - **성능 최적화**: sceneFunc 내부에서 최신 items 참조
5749
6408
  * - 선택 변경 시에만 재생성
5750
6409
  * - 지도 이동 시에는 기존 Animation 계속 실행
5751
6410
  */
@@ -5765,10 +6424,16 @@ var WoongCanvasMarker = function (props) {
5765
6424
  /**
5766
6425
  * Event 레이어 렌더링 (hover + 선택 상태 표시)
5767
6426
  *
5768
- * 🔥 최적화:
5769
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
5770
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
5771
- * 3. 클로저로 최신 데이터 참조
6427
+ * 마커의 hover 효과 및 선택 상태를 표시합니다.
6428
+ * renderEvent prop이 제공된 경우에만 실행됩니다.
6429
+ *
6430
+ * @remarks
6431
+ * - **성능 최적화**:
6432
+ * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
6433
+ * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
6434
+ * 3. 클로저로 최신 데이터 참조
6435
+ * - **topOnHover 지원**: hover된 항목을 최상단에 렌더링
6436
+ * - 선택된 항목은 Map에서 O(1)로 조회하여 성능 최적화
5772
6437
  */
5773
6438
 
5774
6439
 
@@ -5786,8 +6451,9 @@ var WoongCanvasMarker = function (props) {
5786
6451
  name: 'event-render-shape',
5787
6452
  sceneFunc: function (context, shape) {
5788
6453
  var ctx = context; // 클로저로 최신 ref 값 참조
6454
+ // 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
5789
6455
 
5790
- var selectedItems = Array.from(selectedItemsMapRef.current.values());
6456
+ var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
5791
6457
  var hovered = hoveredItemRef.current; // topOnHover가 true이면 hover된 항목을 최상단에 렌더링
5792
6458
 
5793
6459
  if (topOnHover && hovered) {
@@ -5802,7 +6468,7 @@ var WoongCanvasMarker = function (props) {
5802
6468
  selectedItem: selectedItemRef.current
5803
6469
  }); // 2. hover된 항목을 최상단에 렌더링
5804
6470
 
5805
- var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
6471
+ var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
5806
6472
 
5807
6473
  if (isHoveredInViewport) {
5808
6474
  // hover된 항목이 선택되어 있다면 hoverSelectedItems에 포함시켜서
@@ -5846,8 +6512,8 @@ var WoongCanvasMarker = function (props) {
5846
6512
 
5847
6513
 
5848
6514
  var renderAllImmediate = function () {
5849
- updateViewport();
5850
- buildSpatialIndex();
6515
+ updateViewport$1();
6516
+ buildSpatialIndex$1();
5851
6517
  doRenderBase();
5852
6518
  doRenderAnimation();
5853
6519
  doRenderEvent();
@@ -5855,89 +6521,43 @@ var WoongCanvasMarker = function (props) {
5855
6521
  // 이벤트 핸들러: 지도 이벤트
5856
6522
  // --------------------------------------------------------------------------
5857
6523
 
5858
- /**
5859
- * 지도 이동/줌 완료 시 처리
5860
- */
5861
-
5862
-
5863
- var handleIdle = function () {
5864
- prevCenterOffsetRef.current = null;
5865
- accumTranslateRef.current = {
5866
- x: 0,
5867
- y: 0
5868
- }; // 2. 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
5869
-
5870
- offsetCacheRef.current.clear();
5871
- boundingBoxCacheRef.current.clear(); // 3. 마커 위치 업데이트
5872
-
5873
- var bounds = controller.getCurrBounds();
5874
-
5875
- var markerOptions = __assign({
5876
- position: bounds.nw
5877
- }, options);
5878
-
5879
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
5880
6524
 
5881
- if (containerRef.current) {
5882
- containerRef.current.style.transform = '';
5883
- containerRef.current.style.visibility = '';
5884
- } // 5. 새 위치에서 렌더링
5885
-
5886
-
5887
- renderAllImmediate();
5888
- };
5889
- /**
5890
- * 줌 시작 시 처리 (일시적으로 숨김)
5891
- */
5892
-
5893
-
5894
- var handleZoomStart = function () {
5895
- if (containerRef.current) {
5896
- containerRef.current.style.visibility = 'hidden';
5897
- }
5898
- };
6525
+ var _g = createMapEventHandlers({
6526
+ controller: controller,
6527
+ containerRef: containerRef,
6528
+ markerRef: markerRef,
6529
+ options: options,
6530
+ prevCenterOffsetRef: prevCenterOffsetRef,
6531
+ accumTranslateRef: accumTranslateRef,
6532
+ offsetCacheRef: offsetCacheRef,
6533
+ boundingBoxCacheRef: boundingBoxCacheRef,
6534
+ renderAllImmediate: renderAllImmediate
6535
+ }),
6536
+ handleIdle = _g.handleIdle,
6537
+ handleZoomStart = _g.handleZoomStart,
6538
+ handleZoomEnd = _g.handleZoomEnd,
6539
+ handleCenterChanged = _g.handleCenterChanged,
6540
+ handleDragStartShared = _g.handleDragStart,
6541
+ handleDragEndShared = _g.handleDragEnd;
5899
6542
  /**
5900
- * 종료 처리 (다시 표시)
5901
- */
5902
-
5903
-
5904
- var handleZoomEnd = function () {
5905
- if (containerRef.current) {
5906
- containerRef.current.style.visibility = '';
5907
- }
5908
- };
5909
- /**
5910
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
5911
- */
5912
-
5913
-
5914
- var handleCenterChanged = function () {
5915
- var center = controller.getCurrBounds().getCenter();
5916
- var curr = controller.positionToOffset(center);
5917
- var prev = prevCenterOffsetRef.current;
5918
-
5919
- if (!prev) {
5920
- prevCenterOffsetRef.current = {
5921
- x: curr.x,
5922
- y: curr.y
5923
- };
5924
- return;
5925
- }
6543
+ * 드래그 시작 처리 (커서를 grabbing으로 변경)
6544
+ */
5926
6545
 
5927
- var dx = prev.x - curr.x;
5928
- var dy = prev.y - curr.y;
5929
- accumTranslateRef.current = {
5930
- x: accumTranslateRef.current.x + dx,
5931
- y: accumTranslateRef.current.y + dy
5932
- };
5933
- prevCenterOffsetRef.current = {
5934
- x: curr.x,
5935
- y: curr.y
5936
- };
5937
6546
 
5938
- if (containerRef.current) {
5939
- containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
5940
- }
6547
+ var handleDragStart = function () {
6548
+ handleDragStartShared();
6549
+ draggingRef.current = true;
6550
+ controller.setMapCursor('grabbing');
6551
+ };
6552
+ /**
6553
+ * 드래그 종료 처리 (커서를 기본으로 복원)
6554
+ */
6555
+
6556
+
6557
+ var handleDragEnd = function () {
6558
+ handleDragEndShared();
6559
+ draggingRef.current = false;
6560
+ controller.setMapCursor('grab');
5941
6561
  }; // --------------------------------------------------------------------------
5942
6562
  // Hit Test & 상태 관리
5943
6563
  // --------------------------------------------------------------------------
@@ -5945,12 +6565,26 @@ var WoongCanvasMarker = function (props) {
5945
6565
  /**
5946
6566
  * 특정 좌표의 마커 데이터 찾기 (Spatial Index 사용)
5947
6567
  *
5948
- * topOnHover가 true일 때:
5949
- * - 현재 hover된 항목을 최우선으로 체크
5950
- * - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
6568
+ * 클릭/호버 이벤트 시 해당 위치에 있는 마커를 찾습니다.
6569
+ * Spatial Hash Grid를 사용하여 O(1) 수준의 빠른 Hit Test를 수행합니다.
5951
6570
  *
5952
- * @param offset 검사할 좌표
5953
- * @returns 찾은 마커 데이터 또는 null
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
+ * ```
5954
6588
  */
5955
6589
 
5956
6590
 
@@ -6011,11 +6645,17 @@ var WoongCanvasMarker = function (props) {
6011
6645
  /**
6012
6646
  * 클릭 처리 (단일/다중 선택)
6013
6647
  *
6014
- * @param data 클릭된 마커/폴리곤 데이터
6648
+ * 마커 클릭 선택 상태를 업데이트하고 렌더링을 수행합니다.
6015
6649
  *
6016
- * 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
6017
- * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
6018
- * - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
6650
+ * @param data 클릭된 마커 데이터
6651
+ *
6652
+ * @remarks
6653
+ * - **단일 선택**: 기존 선택 해제 후 새로 선택 (토글 가능)
6654
+ * - **다중 선택**: enableMultiSelect가 true면 기존 선택 유지하며 추가/제거
6655
+ * - **성능 최적화**:
6656
+ * - 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
6657
+ * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
6658
+ * - 객체 생성 오버헤드 제거로 1,000개 이상도 부드럽게 처리
6019
6659
  */
6020
6660
 
6021
6661
 
@@ -6064,75 +6704,57 @@ var WoongCanvasMarker = function (props) {
6064
6704
 
6065
6705
  /**
6066
6706
  * 클릭 이벤트 처리
6707
+ *
6708
+ * @param event 클릭 이벤트 파라미터
6709
+ *
6710
+ * @remarks
6711
+ * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
6712
+ * - 상호작용이 비활성화되어 있으면 스킵
6713
+ * - Spatial Index를 사용하여 빠른 Hit Test 수행
6067
6714
  */
6068
6715
 
6069
6716
 
6070
6717
  var handleClick = function (event) {
6071
- var _a;
6072
-
6073
6718
  if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6074
6719
 
6075
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
6076
-
6077
- try {
6078
- var clickedOffset = controller.positionToOffset(event.param.position);
6079
- var data_1 = findData(clickedOffset);
6720
+ var clickedOffset = validateEvent(event, context, controller);
6721
+ if (!clickedOffset) return;
6722
+ var data = findData(clickedOffset);
6080
6723
 
6081
- if (data_1) {
6082
- handleLocalClick(data_1);
6724
+ if (data) {
6725
+ handleLocalClick(data);
6083
6726
 
6084
- if (onClick) {
6085
- onClick(data_1, selectedIdsRef.current);
6086
- }
6727
+ if (onClick) {
6728
+ onClick(data, selectedIdsRef.current);
6087
6729
  }
6088
- } catch (error) {
6089
- console.error('[WoongCanvasMarker] handleClick error:', error);
6090
6730
  }
6091
6731
  };
6092
6732
  /**
6093
6733
  * 마우스 이동 이벤트 처리 (hover 감지)
6734
+ *
6735
+ * @param event 마우스 이동 이벤트 파라미터
6736
+ *
6737
+ * @remarks
6738
+ * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
6739
+ * - 상호작용이 비활성화되어 있으면 스킵
6740
+ * - hover 상태 변경 시에만 렌더링 및 콜백 호출 (최적화)
6094
6741
  */
6095
6742
 
6096
6743
 
6097
6744
  var handleMouseMove = function (event) {
6098
- var _a;
6099
-
6100
6745
  if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
6101
6746
 
6102
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
6103
-
6104
- try {
6105
- var mouseOffset = controller.positionToOffset(event.param.position);
6106
- var hoveredItem = findData(mouseOffset);
6107
- var prevHovered = hoveredItemRef.current;
6747
+ var mouseOffset = validateEvent(event, context, controller);
6748
+ if (!mouseOffset) return;
6749
+ var hoveredItem = findData(mouseOffset);
6750
+ var prevHovered = hoveredItemRef.current;
6108
6751
 
6109
- if (prevHovered !== hoveredItem) {
6110
- setHovered(hoveredItem);
6111
- if (prevHovered && onMouseOut) onMouseOut(prevHovered);
6112
- if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
6113
- }
6114
- } catch (error) {
6115
- console.error('[WoongCanvasMarker] handleMouseMove error:', error);
6752
+ if (prevHovered !== hoveredItem) {
6753
+ setHovered(hoveredItem);
6754
+ if (prevHovered && onMouseOut) onMouseOut(prevHovered);
6755
+ if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
6116
6756
  }
6117
6757
  };
6118
- /**
6119
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
6120
- */
6121
-
6122
-
6123
- var handleDragStart = function () {
6124
- draggingRef.current = true;
6125
- controller.setMapCursor('grabbing');
6126
- };
6127
- /**
6128
- * 드래그 종료 처리 (커서를 기본으로 복원)
6129
- */
6130
-
6131
-
6132
- var handleDragEnd = function () {
6133
- draggingRef.current = false;
6134
- controller.setMapCursor('grab');
6135
- };
6136
6758
  /**
6137
6759
  * 마우스가 canvas를 벗어날 때 hover cleanup
6138
6760
  */
@@ -6227,7 +6849,7 @@ var WoongCanvasMarker = function (props) {
6227
6849
 
6228
6850
  stage.add(eventLayer); // 초기 뷰포트 설정
6229
6851
 
6230
- updateViewport(); // ResizeObserver (맵 크기 변경 감지)
6852
+ updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
6231
6853
 
6232
6854
  var resizeRafId = null;
6233
6855
  var resizeObserver = new ResizeObserver(function () {
@@ -6241,7 +6863,7 @@ var WoongCanvasMarker = function (props) {
6241
6863
  stage.height(mapDiv.offsetHeight);
6242
6864
  offsetCacheRef.current.clear();
6243
6865
  boundingBoxCacheRef.current.clear();
6244
- updateViewport();
6866
+ updateViewport$1();
6245
6867
  renderAllImmediate();
6246
6868
  resizeRafId = null;
6247
6869
  });
@@ -6330,18 +6952,8 @@ var WoongCanvasMarker = function (props) {
6330
6952
  // --------------------------------------------------------------------------
6331
6953
 
6332
6954
  useEffect(function () {
6333
- if (!stageRef.current) return; // externalSelectedItems가 undefined면 외부 제어 안 함
6334
-
6335
- if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
6336
-
6337
- var newSelectedIds = new Set();
6338
- var newSelectedItemsMap = new Map();
6339
- externalSelectedItems.forEach(function (item) {
6340
- newSelectedIds.add(item.id);
6341
- newSelectedItemsMap.set(item.id, item);
6342
- });
6343
- selectedIdsRef.current = newSelectedIds;
6344
- selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
6955
+ if (!stageRef.current) return;
6956
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef); // 렌더링
6345
6957
 
6346
6958
  doRenderBase();
6347
6959
  doRenderAnimation();
@@ -6392,27 +7004,7 @@ var WoongCanvasMarker = function (props) {
6392
7004
  * - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
6393
7005
  */
6394
7006
 
6395
- var dataMap = new Map(data.map(function (m) {
6396
- return [m.id, m];
6397
- }));
6398
- var newSelectedItemsMap = new Map();
6399
- selectedIdsRef.current.forEach(function (id) {
6400
- // 현재 data에 있으면 최신 데이터 사용
6401
- var currentItem = dataMap.get(id);
6402
-
6403
- if (currentItem) {
6404
- newSelectedItemsMap.set(id, currentItem);
6405
- } else {
6406
- // 화면 밖이면 기존 데이터 유지
6407
- var prevItem = selectedItemsMapRef.current.get(id);
6408
-
6409
- if (prevItem) {
6410
- newSelectedItemsMap.set(id, prevItem);
6411
- }
6412
- }
6413
- }); // selectedIdsRef는 그대로 유지 (화면 밖 항목도 선택 상태 유지)
6414
-
6415
- selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
7007
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current); // 즉시 렌더링
6416
7008
 
6417
7009
  renderAllImmediate();
6418
7010
  }, [data]);
@@ -6430,10 +7022,35 @@ var WoongCanvasMarker = function (props) {
6430
7022
  * 폴리곤 렌더링 유틸리티
6431
7023
  *
6432
7024
  * 이 파일은 폴리곤 렌더링을 위한 헬퍼 함수와 팩토리 함수를 제공합니다.
7025
+ * GeoJSON MultiPolygon 형식을 지원하며, 도넛 폴리곤(구멍이 있는 폴리곤)도 처리할 수 있습니다.
6433
7026
  */
6434
7027
 
6435
7028
  /**
6436
7029
  * 폴리곤 그리기 헬퍼 함수 (도넛 폴리곤 지원)
7030
+ *
7031
+ * Canvas 2D Context를 사용하여 폴리곤을 그립니다.
7032
+ * 도넛 폴리곤의 경우 evenodd fill rule을 사용하여 구멍을 처리합니다.
7033
+ *
7034
+ * @param params 폴리곤 그리기 파라미터
7035
+ *
7036
+ * @remarks
7037
+ * - **도넛 폴리곤 처리**:
7038
+ * - 외부 폴리곤과 내부 구멍들을 같은 path에 추가
7039
+ * - `fill('evenodd')`를 사용하여 구멍 뚫기
7040
+ * - **일반 폴리곤 처리**: 각 폴리곤 그룹을 개별적으로 그리기
7041
+ * - **성능**: O(n), n은 폴리곤의 총 좌표 수
7042
+ *
7043
+ * @example
7044
+ * ```typescript
7045
+ * drawPolygon({
7046
+ * ctx,
7047
+ * polygonOffsets: [[[[100, 200], [200, 200], [200, 100], [100, 100]]]],
7048
+ * isDonutPolygon: false,
7049
+ * fillColor: 'rgba(255, 0, 0, 0.5)',
7050
+ * strokeColor: 'rgba(255, 0, 0, 1)',
7051
+ * lineWidth: 2
7052
+ * });
7053
+ * ```
6437
7054
  */
6438
7055
  var drawPolygon = function (_a) {
6439
7056
  var ctx = _a.ctx,
@@ -6447,13 +7064,16 @@ var drawPolygon = function (_a) {
6447
7064
  var multiPolygon = polygonOffsets_1[_i];
6448
7065
 
6449
7066
  if (isDonutPolygon) {
6450
- ctx.beginPath(); // 1. 외부 폴리곤 그리기
7067
+ // 도넛 폴리곤 처리: 외부 폴리곤 + 내부 구멍들을 같은 path에 추가
7068
+ ctx.beginPath(); // 1. 외부 폴리곤 그리기 (첫 번째 폴리곤)
6451
7069
 
6452
- if (multiPolygon[0] && multiPolygon[0].length > 0) {
6453
- ctx.moveTo(multiPolygon[0][0][0], multiPolygon[0][0][1]);
7070
+ var outerPolygon = multiPolygon[0];
7071
+
7072
+ if (outerPolygon && outerPolygon.length > 0) {
7073
+ ctx.moveTo(outerPolygon[0][0], outerPolygon[0][1]);
6454
7074
 
6455
- for (var i = 1; i < multiPolygon[0].length; i++) {
6456
- ctx.lineTo(multiPolygon[0][i][0], multiPolygon[0][i][1]);
7075
+ for (var i = 1; i < outerPolygon.length; i++) {
7076
+ ctx.lineTo(outerPolygon[i][0], outerPolygon[i][1]);
6457
7077
  }
6458
7078
 
6459
7079
  ctx.closePath();
@@ -6480,7 +7100,7 @@ var drawPolygon = function (_a) {
6480
7100
  ctx.lineWidth = lineWidth;
6481
7101
  ctx.stroke();
6482
7102
  } else {
6483
- // 일반 폴리곤
7103
+ // 일반 폴리곤 처리: 각 폴리곤 그룹을 개별적으로 그리기
6484
7104
  for (var _b = 0, multiPolygon_1 = multiPolygon; _b < multiPolygon_1.length; _b++) {
6485
7105
  var polygonGroup = multiPolygon_1[_b];
6486
7106
  if (!polygonGroup.length) continue;
@@ -6493,7 +7113,8 @@ var drawPolygon = function (_a) {
6493
7113
  ctx.lineTo(point[0], point[1]);
6494
7114
  }
6495
7115
 
6496
- ctx.closePath();
7116
+ ctx.closePath(); // 스타일 설정 및 렌더링
7117
+
6497
7118
  ctx.fillStyle = fillColor;
6498
7119
  ctx.strokeStyle = strokeColor;
6499
7120
  ctx.lineWidth = lineWidth;
@@ -6504,19 +7125,30 @@ var drawPolygon = function (_a) {
6504
7125
  }
6505
7126
  };
6506
7127
  /**
6507
- * 폴리곤 Base 렌더링 함수
7128
+ * 폴리곤 Base 렌더링 함수 팩토리
6508
7129
  *
7130
+ * Base Layer에서 사용할 렌더링 함수를 생성합니다.
7131
+ * 선택되지 않은 폴리곤만 렌더링하며, 선택된 항목은 Event Layer에서 처리됩니다.
7132
+ *
7133
+ * @template T 폴리곤 데이터의 추가 속성 타입
6509
7134
  * @param baseFillColor 기본 폴리곤 채우기 색상
6510
7135
  * @param baseStrokeColor 기본 폴리곤 테두리 색상
6511
7136
  * @param baseLineWidth 기본 폴리곤 테두리 두께
6512
7137
  * @returns Base Layer 렌더링 함수
6513
7138
  *
7139
+ * @remarks
7140
+ * - 선택된 항목은 Event Layer에서 그려지므로 Base Layer에서는 스킵
7141
+ * - 성능: O(n), n은 렌더링할 폴리곤 개수
7142
+ * - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
7143
+ *
6514
7144
  * @example
7145
+ * ```typescript
6515
7146
  * const renderBase = renderPolygonBase(
6516
7147
  * 'rgba(255, 100, 100, 0.5)',
6517
7148
  * 'rgba(200, 50, 50, 0.8)',
6518
7149
  * 2
6519
7150
  * );
7151
+ * ```
6520
7152
  */
6521
7153
 
6522
7154
  var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth) {
@@ -6527,12 +7159,15 @@ var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth)
6527
7159
  utils = _a.utils;
6528
7160
 
6529
7161
  for (var _i = 0, items_1 = items; _i < items_1.length; _i++) {
6530
- var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림
7162
+ var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림 (중복 렌더링 방지)
7163
+
7164
+ if (selectedIds.has(item.id)) continue; // paths가 없으면 스킵
7165
+
7166
+ if (!item.paths) continue; // 좌표 변환 (자동 캐싱)
6531
7167
 
6532
- if (selectedIds.has(item.id)) continue;
6533
- if (!item.paths) continue;
6534
7168
  var polygonOffsets = utils.getOrComputePolygonOffsets(item);
6535
- if (!polygonOffsets) continue;
7169
+ if (!polygonOffsets) continue; // 폴리곤 그리기
7170
+
6536
7171
  drawPolygon({
6537
7172
  ctx: ctx,
6538
7173
  polygonOffsets: polygonOffsets,
@@ -6545,8 +7180,12 @@ var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth)
6545
7180
  };
6546
7181
  };
6547
7182
  /**
6548
- * 폴리곤 Event 렌더링 함수
7183
+ * 폴리곤 Event 렌더링 함수 팩토리
7184
+ *
7185
+ * Event Layer에서 사용할 렌더링 함수를 생성합니다.
7186
+ * 선택된 항목, hover된 항목, 마지막 선택된 항목을 각각 다른 스타일로 렌더링합니다.
6549
7187
  *
7188
+ * @template T 폴리곤 데이터의 추가 속성 타입
6550
7189
  * @param baseFillColor 기본 폴리곤 채우기 색상 (필수, fallback용)
6551
7190
  * @param baseStrokeColor 기본 폴리곤 테두리 색상 (필수, fallback용)
6552
7191
  * @param baseLineWidth 기본 폴리곤 테두리 두께 (필수, fallback용)
@@ -6561,7 +7200,14 @@ var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth)
6561
7200
  * @param hoveredLineWidth Hover 시 폴리곤 테두리 두께 (선택, 기본값: selectedLineWidth)
6562
7201
  * @returns Event Layer 렌더링 함수
6563
7202
  *
7203
+ * @remarks
7204
+ * - **렌더링 순서**: 선택된 항목 → 마지막 선택된 항목 → hover된 항목 (최상단)
7205
+ * - **성능**: O(m), m은 선택된 항목 수 + hover된 항목 수
7206
+ * - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
7207
+ * - hover된 항목이 선택되어 있으면 active 스타일 적용
7208
+ *
6564
7209
  * @example
7210
+ * ```typescript
6565
7211
  * const renderEvent = renderPolygonEvent(
6566
7212
  * 'rgba(255, 100, 100, 0.5)', // baseFillColor
6567
7213
  * 'rgba(200, 50, 50, 0.8)', // baseStrokeColor
@@ -6570,6 +7216,7 @@ var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth)
6570
7216
  * 'rgba(255, 152, 0, 1)', // selectedStrokeColor
6571
7217
  * 4 // selectedLineWidth
6572
7218
  * );
7219
+ * ```
6573
7220
  */
6574
7221
 
6575
7222
  var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth) {
@@ -6597,13 +7244,19 @@ var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth
6597
7244
  hoveredItem = _a.hoveredItem,
6598
7245
  utils = _a.utils,
6599
7246
  selectedItems = _a.selectedItems,
6600
- selectedItem = _a.selectedItem; // 1. 선택된 항목들 그리기 (마지막 선택 항목과 호버된 항목 제외)
7247
+ selectedItem = _a.selectedItem; // 성능 최적화: selectedItems를 Set으로 변환하여 O(1) 조회 (매번 some() 체크 방지)
7248
+
7249
+ var selectedIdsSet = selectedItems ? new Set(selectedItems.map(function (item) {
7250
+ return item.id;
7251
+ })) : new Set();
7252
+ var hoveredItemId = hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id;
7253
+ var selectedItemId = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id; // 1. 선택된 항목들 그리기 (마지막 선택 항목과 호버된 항목 제외)
6601
7254
 
6602
7255
  if (selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.length) {
6603
7256
  for (var _i = 0, selectedItems_1 = selectedItems; _i < selectedItems_1.length; _i++) {
6604
7257
  var item = selectedItems_1[_i]; // 마지막 선택 항목과 호버된 항목은 나중에 따로 그림
6605
7258
 
6606
- if (item.id === (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) || (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id) === item.id) continue;
7259
+ if (item.id === selectedItemId || item.id === hoveredItemId) continue;
6607
7260
  if (!item.paths) continue;
6608
7261
  var polygonOffsets = utils.getOrComputePolygonOffsets(item);
6609
7262
  if (!polygonOffsets) continue;
@@ -6619,7 +7272,7 @@ var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth
6619
7272
  } // 2. 마지막 선택된 항목 그리기 (호버되지 않은 경우)
6620
7273
 
6621
7274
 
6622
- if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) && (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id) !== selectedItem.id) {
7275
+ if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) && hoveredItemId !== selectedItemId) {
6623
7276
  var polygonOffsets = utils.getOrComputePolygonOffsets(selectedItem);
6624
7277
 
6625
7278
  if (polygonOffsets) {
@@ -6637,10 +7290,10 @@ var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth
6637
7290
 
6638
7291
  if (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.paths) {
6639
7292
  var polygonOffsets = utils.getOrComputePolygonOffsets(hoveredItem);
6640
- if (!polygonOffsets) return;
6641
- var isSelected = selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.some(function (item) {
6642
- return item.id === hoveredItem.id;
6643
- });
7293
+ if (!polygonOffsets) return; // 좌표 변환 실패 시 스킵 (return은 렌더링 함수 종료)
7294
+ // 성능 최적화: Set을 사용하여 O(1) 조회 (이전: O(m) some() 체크)
7295
+
7296
+ var isSelected = selectedIdsSet.has(hoveredItem.id);
6644
7297
  drawPolygon({
6645
7298
  ctx: ctx,
6646
7299
  polygonOffsets: polygonOffsets,
@@ -6659,8 +7312,6 @@ var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth
6659
7312
  var WoongCanvasPolygon = function (props) {
6660
7313
  var data = props.data,
6661
7314
  onClick = props.onClick,
6662
- onMouseOver = props.onMouseOver,
6663
- onMouseOut = props.onMouseOut,
6664
7315
  _a = props.enableMultiSelect,
6665
7316
  enableMultiSelect = _a === void 0 ? false : _a,
6666
7317
  _b = props.enableViewportCulling,
@@ -6685,7 +7336,7 @@ var WoongCanvasPolygon = function (props) {
6685
7336
  hoveredFillColor = props.hoveredFillColor,
6686
7337
  hoveredStrokeColor = props.hoveredStrokeColor,
6687
7338
  hoveredLineWidth = props.hoveredLineWidth,
6688
- options = __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"]); // --------------------------------------------------------------------------
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"]); // --------------------------------------------------------------------------
6689
7340
  // Hooks & Context
6690
7341
  // --------------------------------------------------------------------------
6691
7342
 
@@ -6784,37 +7435,16 @@ var WoongCanvasPolygon = function (props) {
6784
7435
  * 현재 뷰포트 영역 계산
6785
7436
  */
6786
7437
 
6787
- var updateViewport = function () {
6788
- if (!stageRef.current) return;
6789
- var stage = stageRef.current;
6790
- viewportRef.current = {
6791
- minX: -cullingMargin,
6792
- maxX: stage.width() + cullingMargin,
6793
- minY: -cullingMargin,
6794
- maxY: stage.height() + cullingMargin
6795
- };
7438
+ var updateViewport$1 = function () {
7439
+ updateViewport(stageRef.current, cullingMargin, viewportRef);
6796
7440
  };
6797
7441
  /**
6798
7442
  * 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
6799
7443
  */
6800
7444
 
6801
7445
 
6802
- var isInViewport = function (item) {
6803
- if (!enableViewportCulling || !viewportRef.current) return true;
6804
- var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
6805
-
6806
- var bbox = boundingBoxCacheRef.current.get(item.id);
6807
-
6808
- if (!bbox) {
6809
- // 바운딩 박스 계산 (공통 함수 사용)
6810
- var computed = computeBoundingBox(item);
6811
- if (!computed) return false;
6812
- bbox = computed;
6813
- boundingBoxCacheRef.current.set(item.id, bbox);
6814
- } // 바운딩 박스와 viewport 교차 체크
6815
-
6816
-
6817
- return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
7446
+ var isInViewport$1 = function (item) {
7447
+ return isInViewport(item, enableViewportCulling, viewportRef, boundingBoxCacheRef, computeBoundingBox);
6818
7448
  }; // --------------------------------------------------------------------------
6819
7449
  // 유틸리티 함수: 좌표 변환 캐싱
6820
7450
  // --------------------------------------------------------------------------
@@ -6843,8 +7473,23 @@ var WoongCanvasPolygon = function (props) {
6843
7473
  /**
6844
7474
  * 폴리곤의 바운딩 박스 계산
6845
7475
  *
7476
+ * 폴리곤의 모든 좌표를 순회하여 최소/최대 X, Y 값을 계산합니다.
7477
+ * Viewport Culling에 사용되며, MultiPolygon 형식을 지원합니다.
7478
+ *
6846
7479
  * @param item 폴리곤 데이터
6847
- * @returns 바운딩 박스 또는 null
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
+ * ```
6848
7493
  */
6849
7494
 
6850
7495
 
@@ -6890,20 +7535,8 @@ var WoongCanvasPolygon = function (props) {
6890
7535
  */
6891
7536
 
6892
7537
 
6893
- var buildSpatialIndex = function () {
6894
- var spatial = spatialIndexRef.current;
6895
- spatial.clear();
6896
- var currentData = dataRef.current;
6897
-
6898
- for (var _i = 0, currentData_1 = currentData; _i < currentData_1.length; _i++) {
6899
- var item = currentData_1[_i]; // 바운딩 박스 계산 (공통 함수 사용)
6900
-
6901
- var bbox = computeBoundingBox(item);
6902
-
6903
- if (bbox) {
6904
- spatial.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
6905
- }
6906
- }
7538
+ var buildSpatialIndex$1 = function () {
7539
+ buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
6907
7540
  }; // --------------------------------------------------------------------------
6908
7541
  // 렌더링 함수 결정 (dataType에 따라)
6909
7542
  // --------------------------------------------------------------------------
@@ -6926,11 +7559,8 @@ var WoongCanvasPolygon = function (props) {
6926
7559
 
6927
7560
  var renderBase = renderPolygonBase(baseFillColor, baseStrokeColor, baseLineWidth);
6928
7561
  var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth);
6929
- /** Base Layer에서 사용할 빈 Set (재사용) */
6930
-
6931
- useRef(new Set());
6932
7562
  /**
6933
- * Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
7563
+ * Base 레이어 렌더링 (뷰포트 컬링 적용)
6934
7564
  *
6935
7565
  * 🔥 최적화:
6936
7566
  * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
@@ -6958,7 +7588,7 @@ var WoongCanvasPolygon = function (props) {
6958
7588
  var hovered = hoveredItemRef.current; // 클로저로 최신 ref 값 참조
6959
7589
 
6960
7590
  var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
6961
- return isInViewport(item);
7591
+ return isInViewport$1(item);
6962
7592
  }) : dataRef.current; // 일반 항목 렌더링
6963
7593
 
6964
7594
  renderBase({
@@ -6982,10 +7612,16 @@ var WoongCanvasPolygon = function (props) {
6982
7612
  /**
6983
7613
  * Event 레이어 렌더링 (hover + 선택 상태 표시)
6984
7614
  *
6985
- * 🔥 최적화:
6986
- * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
6987
- * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
6988
- * 3. 클로저로 최신 데이터 참조
7615
+ * 폴리곤의 hover 효과 및 선택 상태를 표시합니다.
7616
+ * 자동 렌더링 방식으로 renderPolygonEvent를 사용합니다.
7617
+ *
7618
+ * @remarks
7619
+ * - **성능 최적화**:
7620
+ * 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
7621
+ * 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
7622
+ * 3. 클로저로 최신 데이터 참조
7623
+ * - 선택된 항목은 Map에서 O(1)로 조회하여 성능 최적화
7624
+ * - 자동 렌더링: 스타일 props(selectedFillColor, hoveredFillColor 등) 기반으로 자동 렌더링
6989
7625
  */
6990
7626
 
6991
7627
 
@@ -7002,8 +7638,9 @@ var WoongCanvasPolygon = function (props) {
7002
7638
  name: 'event-render-shape',
7003
7639
  sceneFunc: function (context, shape) {
7004
7640
  var ctx = context; // 클로저로 최신 ref 값 참조
7641
+ // 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
7005
7642
 
7006
- var selectedItems = Array.from(selectedItemsMapRef.current.values());
7643
+ var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
7007
7644
  var hovered = hoveredItemRef.current; // 일반 렌더링
7008
7645
 
7009
7646
  renderEvent({
@@ -7026,101 +7663,62 @@ var WoongCanvasPolygon = function (props) {
7026
7663
  };
7027
7664
  /**
7028
7665
  * 전체 즉시 렌더링 (IDLE 시 호출)
7666
+ *
7667
+ * 뷰포트 업데이트, 공간 인덱스 빌드, 모든 레이어 렌더링을 순차적으로 수행합니다.
7668
+ *
7669
+ * @remarks
7670
+ * - 호출 시점: 지도 이동/줌 완료 시, 데이터 변경 시, 리사이즈 시
7671
+ * - 순서: 뷰포트 업데이트 → 공간 인덱스 빌드 → Base → Event 렌더링
7672
+ * - Animation Layer는 사용하지 않음 (폴리곤 특성)
7029
7673
  */
7030
7674
 
7031
7675
 
7032
7676
  var renderAllImmediate = function () {
7033
- updateViewport();
7034
- buildSpatialIndex();
7677
+ updateViewport$1();
7678
+ buildSpatialIndex$1();
7035
7679
  doRenderBase();
7036
7680
  doRenderEvent();
7037
7681
  }; // --------------------------------------------------------------------------
7038
7682
  // 이벤트 핸들러: 지도 이벤트
7039
7683
  // --------------------------------------------------------------------------
7040
7684
 
7041
- /**
7042
- * 지도 이동/줌 완료 시 처리
7043
- */
7044
-
7045
-
7046
- var handleIdle = function () {
7047
- prevCenterOffsetRef.current = null;
7048
- accumTranslateRef.current = {
7049
- x: 0,
7050
- y: 0
7051
- }; // 2. 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
7052
-
7053
- offsetCacheRef.current.clear();
7054
- boundingBoxCacheRef.current.clear(); // 3. 마커 위치 업데이트
7055
-
7056
- var bounds = controller.getCurrBounds();
7057
-
7058
- var markerOptions = __assign({
7059
- position: bounds.nw
7060
- }, options);
7061
-
7062
- markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
7063
-
7064
- if (containerRef.current) {
7065
- containerRef.current.style.transform = '';
7066
- containerRef.current.style.visibility = '';
7067
- } // 5. 새 위치에서 렌더링
7068
-
7069
-
7070
- renderAllImmediate();
7071
- };
7072
- /**
7073
- * 줌 시작 시 처리 (일시적으로 숨김)
7074
- */
7075
-
7076
7685
 
7077
- var handleZoomStart = function () {
7078
- if (containerRef.current) {
7079
- containerRef.current.style.visibility = 'hidden';
7080
- }
7081
- };
7686
+ var _f = createMapEventHandlers({
7687
+ controller: controller,
7688
+ containerRef: containerRef,
7689
+ markerRef: markerRef,
7690
+ options: options,
7691
+ prevCenterOffsetRef: prevCenterOffsetRef,
7692
+ accumTranslateRef: accumTranslateRef,
7693
+ offsetCacheRef: offsetCacheRef,
7694
+ boundingBoxCacheRef: boundingBoxCacheRef,
7695
+ renderAllImmediate: renderAllImmediate
7696
+ }),
7697
+ handleIdle = _f.handleIdle,
7698
+ handleZoomStart = _f.handleZoomStart,
7699
+ handleZoomEnd = _f.handleZoomEnd,
7700
+ handleCenterChanged = _f.handleCenterChanged,
7701
+ handleDragStartShared = _f.handleDragStart,
7702
+ handleDragEndShared = _f.handleDragEnd;
7082
7703
  /**
7083
- * 종료 처리 (다시 표시)
7704
+ * 드래그 시작 처리 (커서를 grabbing으로 변경)
7084
7705
  */
7085
7706
 
7086
7707
 
7087
- var handleZoomEnd = function () {
7088
- if (containerRef.current) {
7089
- containerRef.current.style.visibility = '';
7090
- }
7708
+ var handleDragStart = function () {
7709
+ handleDragStartShared();
7710
+ draggingRef.current = true;
7711
+ controller.setMapCursor('grabbing');
7091
7712
  };
7092
7713
  /**
7093
- * 지도 중심 변경 시 처리 (transform으로 이동 추적)
7714
+ * 드래그 종료 처리 (커서를 기본으로 복원)
7094
7715
  */
7095
7716
 
7096
7717
 
7097
- var handleCenterChanged = function () {
7098
- var center = controller.getCurrBounds().getCenter();
7099
- var curr = controller.positionToOffset(center);
7100
- var prev = prevCenterOffsetRef.current;
7101
-
7102
- if (!prev) {
7103
- prevCenterOffsetRef.current = {
7104
- x: curr.x,
7105
- y: curr.y
7106
- };
7107
- return;
7108
- }
7109
-
7110
- var dx = prev.x - curr.x;
7111
- var dy = prev.y - curr.y;
7112
- accumTranslateRef.current = {
7113
- x: accumTranslateRef.current.x + dx,
7114
- y: accumTranslateRef.current.y + dy
7115
- };
7116
- prevCenterOffsetRef.current = {
7117
- x: curr.x,
7118
- y: curr.y
7119
- };
7120
-
7121
- if (containerRef.current) {
7122
- containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
7123
- }
7718
+ var handleDragEnd = function () {
7719
+ handleDragEndShared();
7720
+ draggingRef.current = false;
7721
+ controller.setMapCursor('grab');
7124
7722
  }; // --------------------------------------------------------------------------
7125
7723
  // Hit Test & 상태 관리
7126
7724
  // --------------------------------------------------------------------------
@@ -7150,13 +7748,14 @@ var WoongCanvasPolygon = function (props) {
7150
7748
  /**
7151
7749
  * Hover 상태 설정 및 레이어 렌더링
7152
7750
  *
7153
- * @param data hover 마커/폴리곤 데이터 또는 null
7751
+ * 마우스가 폴리곤 위에 올라갔을 때 hover 상태를 설정하고 즉시 렌더링합니다.
7154
7752
  *
7155
- * 최적화: RAF 제거하여 즉시 렌더링 (16ms 지연 제거)
7753
+ * @param data hover된 폴리곤 데이터 또는 null (hover 해제 )
7156
7754
  *
7157
- * 🎯 topOnHover 지원:
7158
- * - renderEvent가 있으면: Event Layer에서만 처리 (성능 최적화)
7159
- * - renderEvent가 없고 topOnHover=true면: Base Layer에서 처리
7755
+ * @remarks
7756
+ * - **성능 최적화**: RAF 없이 즉시 렌더링 (16ms 지연 제거)
7757
+ * - Event Layer에서 hover 효과 표시
7758
+ * - 커서 상태도 자동으로 업데이트됨 (pointer/grab)
7160
7759
  */
7161
7760
 
7162
7761
 
@@ -7175,11 +7774,17 @@ var WoongCanvasPolygon = function (props) {
7175
7774
  /**
7176
7775
  * 클릭 처리 (단일/다중 선택)
7177
7776
  *
7178
- * @param data 클릭된 마커/폴리곤 데이터
7777
+ * 폴리곤 클릭 선택 상태를 업데이트하고 렌더링을 수행합니다.
7778
+ *
7779
+ * @param data 클릭된 폴리곤 데이터
7179
7780
  *
7180
- * 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
7181
- * - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
7182
- * - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
7781
+ * @remarks
7782
+ * - **단일 선택**: 기존 선택 해제 후 새로 선택 (토글 가능)
7783
+ * - **다중 선택**: enableMultiSelect가 true면 기존 선택 유지하며 추가/제거
7784
+ * - **성능 최적화**:
7785
+ * - 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
7786
+ * - sceneFunc에서 selectedIds를 체크하여 선택된 폴리곤만 스킵
7787
+ * - 객체 생성 오버헤드 제거로 1,000개 이상도 부드럽게 처리
7183
7788
  */
7184
7789
 
7185
7790
 
@@ -7223,77 +7828,63 @@ var WoongCanvasPolygon = function (props) {
7223
7828
 
7224
7829
  /**
7225
7830
  * 클릭 이벤트 처리
7831
+ *
7832
+ * @param event 클릭 이벤트 파라미터
7833
+ *
7834
+ * @remarks
7835
+ * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
7836
+ * - 상호작용이 비활성화되어 있으면 스킵
7837
+ * - Spatial Index를 사용하여 빠른 Hit Test 수행
7226
7838
  */
7227
7839
 
7228
7840
 
7229
7841
  var handleClick = function (event) {
7230
- var _a;
7231
-
7232
7842
  if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7233
7843
 
7234
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
7844
+ var clickedOffset = validateEvent(event, context, controller);
7845
+ if (!clickedOffset) return;
7846
+ var data = findData(clickedOffset);
7235
7847
 
7236
- try {
7237
- var clickedOffset = controller.positionToOffset(event.param.position);
7238
- var data_1 = findData(clickedOffset);
7239
-
7240
- if (data_1) {
7241
- handleLocalClick(data_1);
7848
+ if (data) {
7849
+ handleLocalClick(data);
7242
7850
 
7243
- if (onClick) {
7244
- onClick(data_1, selectedIdsRef.current);
7245
- }
7851
+ if (onClick) {
7852
+ onClick(data, selectedIdsRef.current);
7246
7853
  }
7247
- } catch (error) {
7248
- console.error('[WoongCanvasPolygon] handleClick error:', error);
7249
7854
  }
7250
7855
  };
7251
7856
  /**
7252
7857
  * 마우스 이동 이벤트 처리 (hover 감지)
7858
+ *
7859
+ * @param event 마우스 이동 이벤트 파라미터
7860
+ *
7861
+ * @remarks
7862
+ * - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
7863
+ * - 상호작용이 비활성화되어 있으면 스킵
7864
+ * - hover 상태 변경 시에만 렌더링 (최적화)
7253
7865
  */
7254
7866
 
7255
7867
 
7256
7868
  var handleMouseMove = function (event) {
7257
- var _a;
7258
-
7259
7869
  if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
7260
7870
 
7261
- if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
7262
-
7263
- try {
7264
- var mouseOffset = controller.positionToOffset(event.param.position);
7265
- var hoveredItem = findData(mouseOffset);
7266
- var prevHovered = hoveredItemRef.current;
7871
+ var mouseOffset = validateEvent(event, context, controller);
7872
+ if (!mouseOffset) return;
7873
+ var hoveredItem = findData(mouseOffset);
7874
+ var prevHovered = hoveredItemRef.current;
7267
7875
 
7268
- if (prevHovered !== hoveredItem) {
7269
- setHovered(hoveredItem);
7270
- if (prevHovered && onMouseOut) onMouseOut(prevHovered);
7271
- if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
7272
- }
7273
- } catch (error) {
7274
- console.error('[WoongCanvasPolygon] handleMouseMove error:', error);
7876
+ if (prevHovered !== hoveredItem) {
7877
+ setHovered(hoveredItem);
7275
7878
  }
7276
7879
  };
7277
- /**
7278
- * 드래그 시작 처리 (커서를 grabbing으로 변경)
7279
- */
7280
-
7281
-
7282
- var handleDragStart = function () {
7283
- draggingRef.current = true;
7284
- controller.setMapCursor('grabbing');
7285
- };
7286
- /**
7287
- * 드래그 종료 처리 (커서를 기본으로 복원)
7288
- */
7289
-
7290
-
7291
- var handleDragEnd = function () {
7292
- draggingRef.current = false;
7293
- controller.setMapCursor('grab');
7294
- };
7295
7880
  /**
7296
7881
  * 마우스가 canvas를 벗어날 때 hover cleanup
7882
+ *
7883
+ * 맵 영역 밖으로 마우스가 나갔을 때 hover 상태를 초기화합니다.
7884
+ *
7885
+ * @remarks
7886
+ * - 상호작용이 비활성화되어 있으면 스킵
7887
+ * - hover 상태 초기화 및 커서 복원
7297
7888
  */
7298
7889
 
7299
7890
 
@@ -7306,10 +7897,6 @@ var WoongCanvasPolygon = function (props) {
7306
7897
  hoveredItemRef.current = null;
7307
7898
  controller.setMapCursor('grab');
7308
7899
  doRenderEvent();
7309
-
7310
- if (onMouseOut) {
7311
- onMouseOut(prevHovered);
7312
- }
7313
7900
  }
7314
7901
  }; // --------------------------------------------------------------------------
7315
7902
  // Lifecycle: DOM 초기화
@@ -7377,7 +7964,7 @@ var WoongCanvasPolygon = function (props) {
7377
7964
  stage.add(baseLayer);
7378
7965
  stage.add(eventLayer); // 초기 뷰포트 설정
7379
7966
 
7380
- updateViewport(); // ResizeObserver (맵 크기 변경 감지)
7967
+ updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
7381
7968
 
7382
7969
  var resizeRafId = null;
7383
7970
  var resizeObserver = new ResizeObserver(function () {
@@ -7391,7 +7978,7 @@ var WoongCanvasPolygon = function (props) {
7391
7978
  stage.height(mapDiv.offsetHeight);
7392
7979
  offsetCacheRef.current.clear();
7393
7980
  boundingBoxCacheRef.current.clear();
7394
- updateViewport();
7981
+ updateViewport$1();
7395
7982
  renderAllImmediate();
7396
7983
  resizeRafId = null;
7397
7984
  });
@@ -7418,8 +8005,6 @@ var WoongCanvasPolygon = function (props) {
7418
8005
  return findData(offset) !== null;
7419
8006
  },
7420
8007
  onClick: onClick,
7421
- onMouseOver: onMouseOver,
7422
- onMouseOut: onMouseOut,
7423
8008
  findData: findData,
7424
8009
  setHovered: setHovered,
7425
8010
  handleLocalClick: handleLocalClick,
@@ -7479,18 +8064,8 @@ var WoongCanvasPolygon = function (props) {
7479
8064
  // --------------------------------------------------------------------------
7480
8065
 
7481
8066
  useEffect(function () {
7482
- if (!stageRef.current) return; // externalSelectedItems가 undefined면 외부 제어 안 함
7483
-
7484
- if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
7485
-
7486
- var newSelectedIds = new Set();
7487
- var newSelectedItemsMap = new Map();
7488
- externalSelectedItems.forEach(function (item) {
7489
- newSelectedIds.add(item.id);
7490
- newSelectedItemsMap.set(item.id, item);
7491
- });
7492
- selectedIdsRef.current = newSelectedIds;
7493
- selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
8067
+ if (!stageRef.current) return;
8068
+ syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef); // 렌더링
7494
8069
 
7495
8070
  doRenderBase();
7496
8071
  doRenderEvent();
@@ -7540,27 +8115,7 @@ var WoongCanvasPolygon = function (props) {
7540
8115
  * - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
7541
8116
  */
7542
8117
 
7543
- var dataMap = new Map(data.map(function (m) {
7544
- return [m.id, m];
7545
- }));
7546
- var newSelectedItemsMap = new Map();
7547
- selectedIdsRef.current.forEach(function (id) {
7548
- // 현재 data에 있으면 최신 데이터 사용
7549
- var currentItem = dataMap.get(id);
7550
-
7551
- if (currentItem) {
7552
- newSelectedItemsMap.set(id, currentItem);
7553
- } else {
7554
- // 화면 밖이면 기존 데이터 유지
7555
- var prevItem = selectedItemsMapRef.current.get(id);
7556
-
7557
- if (prevItem) {
7558
- newSelectedItemsMap.set(id, prevItem);
7559
- }
7560
- }
7561
- }); // selectedIdsRef는 그대로 유지 (화면 밖 항목도 선택 상태 유지)
7562
-
7563
- selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
8118
+ selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current); // 즉시 렌더링
7564
8119
 
7565
8120
  renderAllImmediate();
7566
8121
  }, [data]);
@@ -10405,4 +10960,4 @@ function MintMap(_a) {
10405
10960
  }), loading));
10406
10961
  }
10407
10962
 
10408
- export { AnimationPlayer, Bounds, CanvasDataType, CanvasMarker, CanvasMarkerClaude, CanvasMarkerHanquf, CircleMarker, DEFAULT_CULLING_MARGIN, DEFAULT_MAX_CACHE_SIZE, Drawable, GeoCalulator, GoogleMintMapController, LRUCache, MapBuildingProjection, MapCanvasMarkerWrapper, MapCanvasWrapper, MapControlWrapper, MapEvent, MapLoadingWithImage, MapMarkerWrapper, MapPolygonWrapper, MapPolylineWrapper, MapUIEvent, Marker, MintMap, MintMapCanvasRenderer, MintMapController, MintMapCore, MintMapProvider, NaverMintMapController, Offset, PointLoading, Polygon, PolygonCalculator, PolygonMarker, Polyline, Position, SPATIAL_GRID_CELL_SIZE, SVGCircle, SVGPolygon, SVGRect, Spacing, SpatialHashGrid, Status, WoongCanvasMarker, WoongCanvasPolygon, WoongCanvasProvider, calculateTextBoxWidth, computeMarkerOffset, computePolygonOffsets, getClusterInfo, getMapOfType, hexToRgba, isPointInMarkerData, isPointInPolygon, isPointInPolygonData, log, useMarkerMoving, useMintMapController, useWoongCanvasContext, waiting };
10963
+ export { AnimationPlayer, Bounds, CanvasDataType, CanvasMarker, CanvasMarkerClaude, CanvasMarkerHanquf, CircleMarker, DEFAULT_CULLING_MARGIN, DEFAULT_MAX_CACHE_SIZE, Drawable, GeoCalulator, GoogleMintMapController, LRUCache, MapBuildingProjection, MapCanvasMarkerWrapper, MapCanvasWrapper, MapControlWrapper, MapEvent, MapLoadingWithImage, MapMarkerWrapper, MapPolygonWrapper, MapPolylineWrapper, MapUIEvent, Marker, MintMap, MintMapCanvasRenderer, MintMapController, MintMapCore, MintMapProvider, NaverMintMapController, Offset, PointLoading, Polygon, PolygonCalculator, PolygonMarker, Polyline, Position, SPATIAL_GRID_CELL_SIZE, SVGCircle, SVGPolygon, SVGRect, Spacing, SpatialHashGrid, Status, WoongCanvasMarker, WoongCanvasPolygon, WoongCanvasProvider, buildSpatialIndex, calculateTextBoxWidth, computeMarkerOffset, computePolygonOffsets, createMapEventHandlers, getClusterInfo, getMapOfType, hexToRgba, isInViewport, isPointInMarkerData, isPointInPolygon, isPointInPolygonData, log, mapValuesToArray, syncExternalSelectedItems, syncSelectedItems, updateViewport, useMarkerMoving, useMintMapController, useWoongCanvasContext, validateEvent, waiting };