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