@mint-ui/map 1.2.0-test.35 → 1.2.0-test.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +18 -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 +43 -151
- 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 +122 -516
- 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 +409 -1632
- package/dist/index.umd.js +409 -1632
- 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,41 @@
|
|
|
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; // 마커 중심점 기준으로 박스 영역 계산 (꼬리는 제외)
|
|
1049
957
|
|
|
1050
958
|
var x = markerOffset.x - boxWidth / 2;
|
|
1051
|
-
var y = markerOffset.y - boxHeight - tailHeight; //
|
|
959
|
+
var y = markerOffset.y - boxHeight - tailHeight; // 클릭 위치가 박스 영역 내부에 있는지 확인
|
|
1052
960
|
|
|
1053
961
|
return clickedOffset.x >= x && clickedOffset.x <= x + boxWidth && clickedOffset.y >= y && clickedOffset.y <= y + boxHeight;
|
|
1054
|
-
};
|
|
962
|
+
}; // Hex 색상을 RGBA로 변환
|
|
963
|
+
|
|
1055
964
|
var hexToRgba = function (hexColor, alpha) {
|
|
1056
965
|
if (alpha === void 0) {
|
|
1057
966
|
alpha = 1;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
967
|
+
}
|
|
1060
968
|
|
|
1061
|
-
var hex = hexColor.replace('#', '');
|
|
969
|
+
var hex = hexColor.replace('#', '');
|
|
1062
970
|
|
|
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, ")");
|
|
971
|
+
if (hex.length !== 6) {
|
|
972
|
+
throw new Error('Invalid hex color format');
|
|
1068
973
|
}
|
|
1069
974
|
|
|
1070
|
-
|
|
975
|
+
var r = parseInt(hex.substring(0, 2), 16);
|
|
976
|
+
var g = parseInt(hex.substring(2, 4), 16);
|
|
977
|
+
var b = parseInt(hex.substring(4, 6), 16);
|
|
978
|
+
return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
|
|
1071
979
|
};
|
|
1072
980
|
var tempCanvas = document.createElement('canvas');
|
|
1073
981
|
var tempCtx = tempCanvas.getContext('2d');
|
|
1074
982
|
/**
|
|
1075
|
-
* 텍스트
|
|
1076
|
-
*
|
|
1077
|
-
* Canvas 2D Context의 measureText()를 사용하여 텍스트의 실제 너비를 계산하고,
|
|
1078
|
-
* 패딩과 최소 너비를 고려하여 최종 너비를 반환합니다.
|
|
983
|
+
* 텍스트 박스 너비 계산
|
|
1079
984
|
*
|
|
1080
985
|
* @param params 파라미터 객체
|
|
1081
986
|
* @param params.text 측정할 텍스트
|
|
1082
|
-
* @param params.fontConfig 폰트 설정
|
|
1083
|
-
* @param params.padding
|
|
987
|
+
* @param params.fontConfig 폰트 설정
|
|
988
|
+
* @param params.padding 패딩 값 (px)
|
|
1084
989
|
* @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
|
-
* ```
|
|
990
|
+
* @returns 계산된 텍스트 박스 너비 (px)
|
|
1100
991
|
*/
|
|
1101
992
|
|
|
1102
993
|
var calculateTextBoxWidth = function (_a) {
|
|
@@ -1114,62 +1005,25 @@
|
|
|
1114
1005
|
/**
|
|
1115
1006
|
* WoongCanvasProvider 컴포넌트
|
|
1116
1007
|
*
|
|
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
|
-
* ```
|
|
1008
|
+
* 다중 WoongCanvas 인스턴스를 관리하고 zIndex 기반 이벤트 우선순위를 처리합니다.
|
|
1135
1009
|
*/
|
|
1136
1010
|
|
|
1137
1011
|
var WoongCanvasProvider = function (_a) {
|
|
1138
1012
|
var children = _a.children;
|
|
1139
|
-
var controller = useMintMapController();
|
|
1140
|
-
|
|
1013
|
+
var controller = useMintMapController();
|
|
1141
1014
|
var componentsRef = React.useRef([]);
|
|
1142
1015
|
var currentHoveredRef = React.useRef(null);
|
|
1143
1016
|
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
|
-
*/
|
|
1017
|
+
var draggingRef = React.useRef(false); // 컴포넌트 등록 (zIndex 내림차순 정렬)
|
|
1154
1018
|
|
|
1155
1019
|
var registerComponent = React.useCallback(function (instance) {
|
|
1156
1020
|
componentsRef.current.push(instance);
|
|
1157
1021
|
componentsRef.current.sort(function (a, b) {
|
|
1158
1022
|
return b.zIndex - a.zIndex;
|
|
1159
1023
|
});
|
|
1160
|
-
}, []);
|
|
1161
|
-
/**
|
|
1162
|
-
* 컴포넌트 등록 해제
|
|
1163
|
-
*
|
|
1164
|
-
* 컴포넌트 인스턴스를 등록 해제합니다.
|
|
1165
|
-
* hover 중이던 컴포넌트면 hover 상태도 초기화합니다.
|
|
1166
|
-
*
|
|
1167
|
-
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1168
|
-
* @param instance 등록 해제할 컴포넌트 인스턴스
|
|
1169
|
-
*/
|
|
1024
|
+
}, []); // 컴포넌트 등록 해제
|
|
1170
1025
|
|
|
1171
1026
|
var unregisterComponent = React.useCallback(function (instance) {
|
|
1172
|
-
// Hover 중이던 컴포넌트면 초기화
|
|
1173
1027
|
if (currentHoveredRef.current === instance) {
|
|
1174
1028
|
currentHoveredRef.current = null;
|
|
1175
1029
|
currentHoveredDataRef.current = null;
|
|
@@ -1178,118 +1032,77 @@
|
|
|
1178
1032
|
componentsRef.current = componentsRef.current.filter(function (c) {
|
|
1179
1033
|
return c !== instance;
|
|
1180
1034
|
});
|
|
1181
|
-
}, []);
|
|
1182
|
-
/**
|
|
1183
|
-
* 전역 클릭 핸들러 (zIndex 우선순위)
|
|
1184
|
-
*
|
|
1185
|
-
* 모든 등록된 WoongCanvas 컴포넌트 중 zIndex가 높은 컴포넌트부터 클릭 이벤트를 처리합니다.
|
|
1186
|
-
*
|
|
1187
|
-
* @param event 클릭 이벤트 파라미터
|
|
1188
|
-
*
|
|
1189
|
-
* @remarks
|
|
1190
|
-
* - zIndex가 높은 컴포넌트부터 순회하여 첫 번째 히트만 처리
|
|
1191
|
-
* - 상호작용이 비활성화된 컴포넌트는 스킵
|
|
1192
|
-
* - Context가 없으면 각 컴포넌트의 로컬 핸들러가 처리
|
|
1193
|
-
*/
|
|
1035
|
+
}, []); // 전역 클릭 핸들러 (zIndex 우선순위)
|
|
1194
1036
|
|
|
1195
1037
|
var handleGlobalClick = React.useCallback(function (event) {
|
|
1196
|
-
var _a;
|
|
1038
|
+
var _a, _b;
|
|
1197
1039
|
|
|
1198
1040
|
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]; // 🚫 상호작용이 비활성화된 컴포넌트는 스킵
|
|
1041
|
+
var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
|
|
1203
1042
|
|
|
1043
|
+
for (var _i = 0, _c = componentsRef.current; _i < _c.length; _i++) {
|
|
1044
|
+
var component = _c[_i];
|
|
1204
1045
|
if (component.isInteractionDisabled()) continue;
|
|
1205
1046
|
var data = component.findData(clickedOffset);
|
|
1047
|
+
if (!data) continue; // 첫 번째로 찾은 항목만 처리하고 종료 (zIndex 우선순위)
|
|
1206
1048
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
if (component.onClick) {
|
|
1211
|
-
component.onClick(data, component.getSelectedIds());
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
return; // 첫 번째 히트만 처리
|
|
1215
|
-
}
|
|
1049
|
+
component.handleLocalClick(data);
|
|
1050
|
+
(_b = component.onClick) === null || _b === void 0 ? void 0 : _b.call(component, data, component.getSelectedIds());
|
|
1051
|
+
return;
|
|
1216
1052
|
}
|
|
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
|
-
*/
|
|
1053
|
+
}, [controller]); // 전역 마우스 이동 핸들러 (zIndex 우선순위)
|
|
1231
1054
|
|
|
1232
1055
|
var handleGlobalMouseMove = React.useCallback(function (event) {
|
|
1233
1056
|
var _a;
|
|
1234
1057
|
|
|
1235
1058
|
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
|
-
|
|
1059
|
+
var mouseOffset = controller.positionToOffset(event.param.position);
|
|
1238
1060
|
var newHoveredComponent = null;
|
|
1239
|
-
var newHoveredData = null;
|
|
1061
|
+
var newHoveredData = null; // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
|
|
1240
1062
|
|
|
1241
1063
|
for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
|
|
1242
|
-
var component = _b[_i];
|
|
1243
|
-
|
|
1064
|
+
var component = _b[_i];
|
|
1244
1065
|
if (component.isInteractionDisabled()) continue;
|
|
1245
1066
|
var data = component.findData(mouseOffset);
|
|
1067
|
+
if (!data) continue; // 첫 번째로 찾은 항목만 hover 처리 (zIndex 우선순위)
|
|
1246
1068
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
}
|
|
1252
|
-
} // Hover 상태 변경 감지 (최적화: 별도 ref로 직접 비교)
|
|
1253
|
-
|
|
1069
|
+
newHoveredComponent = component;
|
|
1070
|
+
newHoveredData = data;
|
|
1071
|
+
break;
|
|
1072
|
+
} // hover 상태가 변경되지 않았으면 종료 (불필요한 렌더링 방지)
|
|
1254
1073
|
|
|
1255
|
-
if (currentHoveredRef.current !== newHoveredComponent || currentHoveredDataRef.current !== newHoveredData) {
|
|
1256
|
-
// 이전 hover 해제
|
|
1257
|
-
if (currentHoveredRef.current) {
|
|
1258
|
-
currentHoveredRef.current.setHovered(null);
|
|
1259
1074
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
} // 새 hover 설정
|
|
1075
|
+
if (currentHoveredRef.current === newHoveredComponent && currentHoveredDataRef.current === newHoveredData) {
|
|
1076
|
+
return;
|
|
1077
|
+
} // 기존 hover 항목에 mouseOut 이벤트 발생
|
|
1264
1078
|
|
|
1265
1079
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1080
|
+
if (currentHoveredRef.current) {
|
|
1081
|
+
currentHoveredRef.current.setHovered(null);
|
|
1268
1082
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1083
|
+
if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
|
|
1084
|
+
currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
|
|
1272
1085
|
}
|
|
1086
|
+
} // 새 hover 항목에 mouseOver 이벤트 발생
|
|
1087
|
+
|
|
1273
1088
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1089
|
+
if (newHoveredComponent && newHoveredData) {
|
|
1090
|
+
newHoveredComponent.setHovered(newHoveredData);
|
|
1091
|
+
|
|
1092
|
+
if (newHoveredComponent.onMouseOver) {
|
|
1093
|
+
newHoveredComponent.onMouseOver(newHoveredData);
|
|
1094
|
+
}
|
|
1276
1095
|
}
|
|
1277
|
-
}, [controller]);
|
|
1278
|
-
/**
|
|
1279
|
-
* 줌/드래그 시작 (마우스 이동 이벤트 무시)
|
|
1280
|
-
*/
|
|
1281
1096
|
|
|
1097
|
+
currentHoveredRef.current = newHoveredComponent;
|
|
1098
|
+
currentHoveredDataRef.current = newHoveredData;
|
|
1099
|
+
}, [controller]);
|
|
1282
1100
|
var handleZoomStart = React.useCallback(function () {
|
|
1283
1101
|
draggingRef.current = true;
|
|
1284
1102
|
}, []);
|
|
1285
|
-
/**
|
|
1286
|
-
* 지도 idle (마우스 이동 이벤트 재개)
|
|
1287
|
-
*/
|
|
1288
|
-
|
|
1289
1103
|
var handleIdle = React.useCallback(function () {
|
|
1290
1104
|
draggingRef.current = false;
|
|
1291
|
-
}, []);
|
|
1292
|
-
|
|
1105
|
+
}, []);
|
|
1293
1106
|
React.useEffect(function () {
|
|
1294
1107
|
controller.addEventListener('CLICK', handleGlobalClick);
|
|
1295
1108
|
controller.addEventListener('MOUSEMOVE', handleGlobalMouseMove);
|
|
@@ -1301,8 +1114,7 @@
|
|
|
1301
1114
|
controller.removeEventListener('ZOOMSTART', handleZoomStart);
|
|
1302
1115
|
controller.removeEventListener('IDLE', handleIdle);
|
|
1303
1116
|
};
|
|
1304
|
-
}, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]);
|
|
1305
|
-
|
|
1117
|
+
}, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]);
|
|
1306
1118
|
var contextValue = React.useMemo(function () {
|
|
1307
1119
|
return {
|
|
1308
1120
|
registerComponent: registerComponent,
|
|
@@ -1316,102 +1128,40 @@
|
|
|
1316
1128
|
/**
|
|
1317
1129
|
* WoongCanvas Context Hook
|
|
1318
1130
|
*
|
|
1319
|
-
* WoongCanvasProvider로 감싸진 컴포넌트에서 Context에 접근할 수 있는 hook입니다.
|
|
1320
|
-
*
|
|
1321
1131
|
* @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
1132
|
*/
|
|
1335
1133
|
|
|
1336
1134
|
var useWoongCanvasContext = function () {
|
|
1337
|
-
|
|
1338
|
-
return context;
|
|
1135
|
+
return React.useContext(WoongCanvasContext);
|
|
1339
1136
|
};
|
|
1340
1137
|
|
|
1341
|
-
// ============================================================================
|
|
1342
|
-
// 성능 최적화 상수 (30,000개 마커/폴리곤 기준 최적화)
|
|
1343
|
-
// ============================================================================
|
|
1344
|
-
|
|
1345
1138
|
/**
|
|
1346
|
-
* 공간 인덱스 그리드 셀 크기 (
|
|
1347
|
-
*
|
|
1348
|
-
* 최적값 계산:
|
|
1349
|
-
* - 목표: 클릭 시 셀당 10~30개 항목만 체크 (빠른 Hit Test)
|
|
1350
|
-
* - 화면 크기: 1920×1080 기준
|
|
1351
|
-
* - 30,000개 항목 → 50px 셀 크기 = 약 800개 셀 = 셀당 ~37개
|
|
1139
|
+
* 공간 인덱스 그리드 셀 크기 (픽셀 단위)
|
|
1352
1140
|
*
|
|
1353
|
-
*
|
|
1354
|
-
* - 200px: 셀당 ~577개 → Hit Test O(577) ❌ 느림
|
|
1355
|
-
* - 50px: 셀당 ~37개 → Hit Test O(37) ✅ 15배 빠름!
|
|
1356
|
-
*
|
|
1357
|
-
* 트레이드오프:
|
|
1358
|
-
* - 작을수록: Hit Test 빠름, 메모리 사용량 증가
|
|
1359
|
-
* - 클수록: 메모리 효율적, Hit Test 느림
|
|
1141
|
+
* @default 50
|
|
1360
1142
|
*/
|
|
1361
1143
|
var SPATIAL_GRID_CELL_SIZE = 50;
|
|
1362
1144
|
/**
|
|
1363
|
-
* 뷰포트 컬링 여유 공간 (
|
|
1145
|
+
* 뷰포트 컬링 여유 공간 (픽셀 단위)
|
|
1364
1146
|
*
|
|
1365
|
-
*
|
|
1366
|
-
* 30,000개 중 실제 렌더링: 화면에 보이는 1,000~3,000개만
|
|
1147
|
+
* @default 100
|
|
1367
1148
|
*/
|
|
1368
1149
|
|
|
1369
1150
|
var DEFAULT_CULLING_MARGIN = 100;
|
|
1370
1151
|
/**
|
|
1371
1152
|
* LRU 캐시 최대 항목 수
|
|
1372
1153
|
*
|
|
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() 호출되므로 메모리 누적 없음
|
|
1154
|
+
* @default 30000
|
|
1384
1155
|
*/
|
|
1385
1156
|
|
|
1386
1157
|
var DEFAULT_MAX_CACHE_SIZE = 30000;
|
|
1387
1158
|
/**
|
|
1388
|
-
* LRU (Least Recently Used)
|
|
1159
|
+
* LRU Cache (Least Recently Used)
|
|
1389
1160
|
*
|
|
1390
|
-
*
|
|
1161
|
+
* 좌표 변환 결과를 캐싱하기 위한 캐시 구현
|
|
1391
1162
|
*
|
|
1392
1163
|
* @template K 캐시 키 타입
|
|
1393
1164
|
* @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
1165
|
*/
|
|
1416
1166
|
|
|
1417
1167
|
var LRUCache =
|
|
@@ -1424,63 +1174,43 @@
|
|
|
1424
1174
|
|
|
1425
1175
|
this.cache = new Map();
|
|
1426
1176
|
this.maxSize = maxSize;
|
|
1427
|
-
}
|
|
1428
|
-
/**
|
|
1429
|
-
* 캐시에서 값 조회
|
|
1430
|
-
*
|
|
1431
|
-
* @param key 조회할 키
|
|
1432
|
-
* @returns 캐시된 값 또는 undefined (캐시 미스 시)
|
|
1433
|
-
*
|
|
1434
|
-
* @remarks
|
|
1435
|
-
* - 성능: O(1) 해시 조회
|
|
1436
|
-
* - 최적화: delete+set 제거로 읽기 성능 대폭 향상
|
|
1437
|
-
*/
|
|
1177
|
+
} // 캐시에서 값 조회
|
|
1438
1178
|
|
|
1439
1179
|
|
|
1440
1180
|
LRUCache.prototype.get = function (key) {
|
|
1441
1181
|
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
|
-
*/
|
|
1182
|
+
}; // 캐시에 값 저장 (FIFO eviction)
|
|
1454
1183
|
|
|
1455
1184
|
|
|
1456
1185
|
LRUCache.prototype.set = function (key, value) {
|
|
1457
1186
|
var exists = this.cache.has(key);
|
|
1458
1187
|
|
|
1459
1188
|
if (exists) {
|
|
1460
|
-
// 기존 항목 업데이트: 단순 덮어쓰기 (크기 변화 없음)
|
|
1461
1189
|
this.cache.set(key, value);
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
if (this.cache.size >= this.maxSize) {
|
|
1465
|
-
// 가장 오래된 항목 제거 (Map의 첫 번째 항목)
|
|
1466
|
-
var firstKey = this.cache.keys().next().value;
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1467
1192
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1193
|
+
if (this.cache.size >= this.maxSize) {
|
|
1194
|
+
var firstKey = this.cache.keys().next().value;
|
|
1472
1195
|
|
|
1473
|
-
|
|
1196
|
+
if (firstKey !== undefined) {
|
|
1197
|
+
this.cache.delete(firstKey);
|
|
1198
|
+
}
|
|
1474
1199
|
}
|
|
1475
|
-
|
|
1200
|
+
|
|
1201
|
+
this.cache.set(key, value);
|
|
1202
|
+
}; // 캐시 초기화
|
|
1203
|
+
|
|
1476
1204
|
|
|
1477
1205
|
LRUCache.prototype.clear = function () {
|
|
1478
1206
|
this.cache.clear();
|
|
1479
|
-
};
|
|
1207
|
+
}; // 캐시 크기 반환
|
|
1208
|
+
|
|
1480
1209
|
|
|
1481
1210
|
LRUCache.prototype.size = function () {
|
|
1482
1211
|
return this.cache.size;
|
|
1483
|
-
};
|
|
1212
|
+
}; // 키 존재 여부 확인
|
|
1213
|
+
|
|
1484
1214
|
|
|
1485
1215
|
LRUCache.prototype.has = function (key) {
|
|
1486
1216
|
return this.cache.has(key);
|
|
@@ -1490,16 +1220,10 @@
|
|
|
1490
1220
|
}();
|
|
1491
1221
|
/**
|
|
1492
1222
|
* Spatial Hash Grid (공간 해시 그리드)
|
|
1493
|
-
* 공간 인덱싱을 위한 그리드 기반 자료구조 (개선 버전)
|
|
1494
1223
|
*
|
|
1495
|
-
*
|
|
1496
|
-
* 1. 중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전
|
|
1497
|
-
* 2. 메모리 누수 방지: 기존 항목 자동 제거
|
|
1498
|
-
* 3. 성능 최적화: 불필요한 배열 생성 최소화
|
|
1224
|
+
* 빠른 Hit Test를 위한 그리드 기반 공간 인덱싱 자료구조
|
|
1499
1225
|
*
|
|
1500
|
-
*
|
|
1501
|
-
* - 빠른 Hit Test (마우스 클릭 시 어떤 마커/폴리곤인지 찾기)
|
|
1502
|
-
* - 30,000개 항목 → 클릭 위치 주변 ~10개만 체크 (3,000배 빠름)
|
|
1226
|
+
* @template T 인덱싱할 항목 타입
|
|
1503
1227
|
*/
|
|
1504
1228
|
|
|
1505
1229
|
var SpatialHashGrid =
|
|
@@ -1513,28 +1237,24 @@
|
|
|
1513
1237
|
this.cellSize = cellSize;
|
|
1514
1238
|
this.grid = new Map();
|
|
1515
1239
|
this.itemToCells = new Map();
|
|
1516
|
-
}
|
|
1517
|
-
/**
|
|
1518
|
-
* 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
|
|
1519
|
-
*/
|
|
1240
|
+
} // 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
|
|
1520
1241
|
|
|
1521
1242
|
|
|
1522
1243
|
SpatialHashGrid.prototype.getCellKey = function (x, y) {
|
|
1244
|
+
// 좌표를 셀 크기로 나눈 몫으로 셀 인덱스 계산
|
|
1523
1245
|
var cellX = Math.floor(x / this.cellSize);
|
|
1524
1246
|
var cellY = Math.floor(y / this.cellSize);
|
|
1525
1247
|
return "".concat(cellX, ",").concat(cellY);
|
|
1526
|
-
};
|
|
1527
|
-
/**
|
|
1528
|
-
* 바운딩 박스가 걸치는 모든 셀 키 배열 반환
|
|
1529
|
-
*/
|
|
1248
|
+
}; // 바운딩 박스가 걸치는 모든 셀 키 배열 반환
|
|
1530
1249
|
|
|
1531
1250
|
|
|
1532
1251
|
SpatialHashGrid.prototype.getCellsForBounds = function (minX, minY, maxX, maxY) {
|
|
1533
|
-
var cells = [];
|
|
1252
|
+
var cells = []; // 바운딩 박스가 걸치는 셀 범위 계산
|
|
1253
|
+
|
|
1534
1254
|
var startCellX = Math.floor(minX / this.cellSize);
|
|
1535
1255
|
var startCellY = Math.floor(minY / this.cellSize);
|
|
1536
1256
|
var endCellX = Math.floor(maxX / this.cellSize);
|
|
1537
|
-
var endCellY = Math.floor(maxY / this.cellSize);
|
|
1257
|
+
var endCellY = Math.floor(maxY / this.cellSize); // 바운딩 박스가 걸치는 모든 셀을 배열에 추가
|
|
1538
1258
|
|
|
1539
1259
|
for (var x = startCellX; x <= endCellX; x++) {
|
|
1540
1260
|
for (var y = startCellY; y <= endCellY; y++) {
|
|
@@ -1543,31 +1263,15 @@
|
|
|
1543
1263
|
}
|
|
1544
1264
|
|
|
1545
1265
|
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
|
-
*/
|
|
1266
|
+
}; // 항목 추가 (바운딩 박스 기반, 중복 삽입 방지)
|
|
1563
1267
|
|
|
1564
1268
|
|
|
1565
1269
|
SpatialHashGrid.prototype.insert = function (item, minX, minY, maxX, maxY) {
|
|
1566
|
-
//
|
|
1567
|
-
this.remove(item); //
|
|
1270
|
+
// 기존 항목 제거 (중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전)
|
|
1271
|
+
this.remove(item); // 바운딩 박스가 걸치는 모든 셀에 항목 등록
|
|
1568
1272
|
|
|
1569
1273
|
var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
|
|
1570
|
-
this.itemToCells.set(item, cells);
|
|
1274
|
+
this.itemToCells.set(item, cells); // 항목과 셀의 매핑 저장 (제거 시 필요)
|
|
1571
1275
|
|
|
1572
1276
|
for (var _i = 0, cells_1 = cells; _i < cells_1.length; _i++) {
|
|
1573
1277
|
var cell = cells_1[_i];
|
|
@@ -1578,24 +1282,12 @@
|
|
|
1578
1282
|
|
|
1579
1283
|
this.grid.get(cell).push(item);
|
|
1580
1284
|
}
|
|
1581
|
-
};
|
|
1582
|
-
/**
|
|
1583
|
-
* 항목 제거
|
|
1584
|
-
*
|
|
1585
|
-
* 공간 인덱스에서 항목을 제거합니다.
|
|
1586
|
-
*
|
|
1587
|
-
* @param item 제거할 항목
|
|
1588
|
-
*
|
|
1589
|
-
* @remarks
|
|
1590
|
-
* - 메모리 누수 방지: 모든 셀에서 참조 완전 제거
|
|
1591
|
-
* - 빈 셀 정리: 항목이 없어진 셀은 자동으로 정리됨
|
|
1592
|
-
* - 성능: O(셀 개수), 보통 O(1)
|
|
1593
|
-
*/
|
|
1285
|
+
}; // 항목 제거 (모든 셀에서 참조 제거)
|
|
1594
1286
|
|
|
1595
1287
|
|
|
1596
1288
|
SpatialHashGrid.prototype.remove = function (item) {
|
|
1597
1289
|
var prevCells = this.itemToCells.get(item);
|
|
1598
|
-
if (!prevCells) return; //
|
|
1290
|
+
if (!prevCells) return; // 항목이 등록된 모든 셀에서 참조 제거 (메모리 누수 방지)
|
|
1599
1291
|
|
|
1600
1292
|
for (var _i = 0, prevCells_1 = prevCells; _i < prevCells_1.length; _i++) {
|
|
1601
1293
|
var cell = prevCells_1[_i];
|
|
@@ -1606,86 +1298,39 @@
|
|
|
1606
1298
|
|
|
1607
1299
|
if (index !== -1) {
|
|
1608
1300
|
cellItems.splice(index, 1);
|
|
1609
|
-
} // 빈 셀 정리 (메모리
|
|
1301
|
+
} // 빈 셀 정리 (메모리 효율: 사용하지 않는 셀 제거)
|
|
1610
1302
|
|
|
1611
1303
|
|
|
1612
1304
|
if (cellItems.length === 0) {
|
|
1613
1305
|
this.grid.delete(cell);
|
|
1614
1306
|
}
|
|
1615
1307
|
}
|
|
1616
|
-
}
|
|
1308
|
+
} // 항목과 셀의 매핑 제거
|
|
1309
|
+
|
|
1617
1310
|
|
|
1618
1311
|
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
|
-
*/
|
|
1312
|
+
}; // 항목 위치 업데이트 (remove + insert)
|
|
1631
1313
|
|
|
1632
1314
|
|
|
1633
1315
|
SpatialHashGrid.prototype.update = function (item, minX, minY, maxX, maxY) {
|
|
1634
1316
|
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
|
-
*/
|
|
1317
|
+
}; // 점 주변의 항목 조회 (Hit Test용)
|
|
1660
1318
|
|
|
1661
1319
|
|
|
1662
1320
|
SpatialHashGrid.prototype.queryPoint = function (x, y) {
|
|
1321
|
+
// 클릭 위치가 속한 셀의 모든 항목 조회 (O(1) 수준의 빠른 조회)
|
|
1663
1322
|
var cellKey = this.getCellKey(x, y);
|
|
1664
1323
|
var items = this.grid.get(cellKey); // 빈 배열 재사용 (메모리 할당 최소화)
|
|
1665
1324
|
|
|
1666
1325
|
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
|
-
*/
|
|
1326
|
+
}; // 영역 내 항목 조회 (Viewport Culling용)
|
|
1684
1327
|
|
|
1685
1328
|
|
|
1686
1329
|
SpatialHashGrid.prototype.queryBounds = function (minX, minY, maxX, maxY) {
|
|
1330
|
+
// 영역이 걸치는 모든 셀 찾기
|
|
1687
1331
|
var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
|
|
1688
|
-
var results = new Set();
|
|
1332
|
+
var results = new Set(); // 중복 제거를 위해 Set 사용
|
|
1333
|
+
// 각 셀의 모든 항목을 결과에 추가
|
|
1689
1334
|
|
|
1690
1335
|
for (var _i = 0, cells_2 = cells; _i < cells_2.length; _i++) {
|
|
1691
1336
|
var cell = cells_2[_i];
|
|
@@ -1694,48 +1339,24 @@
|
|
|
1694
1339
|
if (items) {
|
|
1695
1340
|
for (var _a = 0, items_1 = items; _a < items_1.length; _a++) {
|
|
1696
1341
|
var item = items_1[_a];
|
|
1697
|
-
results.add(item);
|
|
1342
|
+
results.add(item); // Set이므로 중복 자동 제거
|
|
1698
1343
|
}
|
|
1699
1344
|
}
|
|
1700
1345
|
}
|
|
1701
1346
|
|
|
1702
1347
|
return Array.from(results);
|
|
1703
|
-
};
|
|
1704
|
-
/**
|
|
1705
|
-
* 항목 존재 여부 확인
|
|
1706
|
-
*
|
|
1707
|
-
* @param item 확인할 항목
|
|
1708
|
-
* @returns 항목이 인덱스에 있으면 true, 아니면 false
|
|
1709
|
-
*
|
|
1710
|
-
* @remarks
|
|
1711
|
-
* - 성능: O(1) 해시 조회
|
|
1712
|
-
*/
|
|
1348
|
+
}; // 항목 존재 여부 확인
|
|
1713
1349
|
|
|
1714
1350
|
|
|
1715
1351
|
SpatialHashGrid.prototype.has = function (item) {
|
|
1716
1352
|
return this.itemToCells.has(item);
|
|
1717
|
-
};
|
|
1718
|
-
/**
|
|
1719
|
-
* 전체 초기화
|
|
1720
|
-
*/
|
|
1353
|
+
}; // 전체 초기화
|
|
1721
1354
|
|
|
1722
1355
|
|
|
1723
1356
|
SpatialHashGrid.prototype.clear = function () {
|
|
1724
1357
|
this.grid.clear();
|
|
1725
1358
|
this.itemToCells.clear();
|
|
1726
|
-
};
|
|
1727
|
-
/**
|
|
1728
|
-
* 통계 정보
|
|
1729
|
-
*
|
|
1730
|
-
* 공간 인덱스의 현재 상태를 반환합니다. 디버깅 및 성능 분석에 유용합니다.
|
|
1731
|
-
*
|
|
1732
|
-
* @returns 통계 정보 객체
|
|
1733
|
-
*
|
|
1734
|
-
* @remarks
|
|
1735
|
-
* - totalCells: 현재 사용 중인 셀 개수
|
|
1736
|
-
* - totalItems: 인덱스에 등록된 고유 항목 수 (정확)
|
|
1737
|
-
* - avgItemsPerCell: 셀당 평균 항목 수
|
|
1738
|
-
*/
|
|
1359
|
+
}; // 통계 정보 반환
|
|
1739
1360
|
|
|
1740
1361
|
|
|
1741
1362
|
SpatialHashGrid.prototype.stats = function () {
|
|
@@ -1756,20 +1377,9 @@
|
|
|
1756
1377
|
/**
|
|
1757
1378
|
* 현재 뷰포트 영역 계산
|
|
1758
1379
|
*
|
|
1759
|
-
* Konva Stage
|
|
1760
|
-
*
|
|
1761
|
-
* @param stage Konva Stage 인스턴스 (width, height 메서드 제공)
|
|
1380
|
+
* @param stage Konva Stage 인스턴스
|
|
1762
1381
|
* @param cullingMargin 컬링 여유 공간 (px)
|
|
1763
1382
|
* @param viewportRef 뷰포트 경계를 저장할 ref
|
|
1764
|
-
*
|
|
1765
|
-
* @remarks
|
|
1766
|
-
* - 화면 밖 cullingMargin만큼의 영역까지 포함하여 계산
|
|
1767
|
-
* - 스크롤 시 부드러운 전환을 위해 여유 공간 포함
|
|
1768
|
-
*
|
|
1769
|
-
* @example
|
|
1770
|
-
* ```typescript
|
|
1771
|
-
* updateViewport(stageRef.current, cullingMargin, viewportRef);
|
|
1772
|
-
* ```
|
|
1773
1383
|
*/
|
|
1774
1384
|
var updateViewport = function (stage, cullingMargin, viewportRef) {
|
|
1775
1385
|
if (!stage) return;
|
|
@@ -1783,35 +1393,16 @@
|
|
|
1783
1393
|
/**
|
|
1784
1394
|
* 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
|
|
1785
1395
|
*
|
|
1786
|
-
* 뷰포트 컬링을 위한 함수입니다. 바운딩 박스와 뷰포트 경계의 교차를 확인합니다.
|
|
1787
|
-
* 바운딩 박스는 캐시되어 성능을 최적화합니다.
|
|
1788
|
-
*
|
|
1789
1396
|
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1790
1397
|
* @param item 확인할 아이템
|
|
1791
|
-
* @param enableViewportCulling 뷰포트 컬링 활성화 여부
|
|
1792
1398
|
* @param viewportRef 뷰포트 경계 ref
|
|
1793
1399
|
* @param boundingBoxCacheRef 바운딩 박스 캐시 ref
|
|
1794
1400
|
* @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
|
-
* ```
|
|
1401
|
+
* @returns 뷰포트 안에 있으면 true
|
|
1811
1402
|
*/
|
|
1812
1403
|
|
|
1813
|
-
var isInViewport = function (item,
|
|
1814
|
-
if (!
|
|
1404
|
+
var isInViewport = function (item, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
|
|
1405
|
+
if (!viewportRef.current) return true;
|
|
1815
1406
|
var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
|
|
1816
1407
|
|
|
1817
1408
|
var bbox = boundingBoxCacheRef.current.get(item.id);
|
|
@@ -1831,33 +1422,9 @@
|
|
|
1831
1422
|
/**
|
|
1832
1423
|
* 지도 이벤트 핸들러 생성 함수
|
|
1833
1424
|
*
|
|
1834
|
-
* 지도 이동, 줌, 드래그 등의 이벤트를 처리하는 핸들러들을 생성합니다.
|
|
1835
|
-
*
|
|
1836
1425
|
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1837
1426
|
* @param deps 이벤트 핸들러 생성에 필요한 의존성
|
|
1838
1427
|
* @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
1428
|
*/
|
|
1862
1429
|
|
|
1863
1430
|
var createMapEventHandlers = function (deps) {
|
|
@@ -1869,14 +1436,7 @@
|
|
|
1869
1436
|
accumTranslateRef = deps.accumTranslateRef,
|
|
1870
1437
|
offsetCacheRef = deps.offsetCacheRef,
|
|
1871
1438
|
boundingBoxCacheRef = deps.boundingBoxCacheRef,
|
|
1872
|
-
renderAllImmediate = deps.renderAllImmediate;
|
|
1873
|
-
/**
|
|
1874
|
-
* 지도 이동/줌 완료 시 처리
|
|
1875
|
-
*
|
|
1876
|
-
* - 캐시 초기화: 좌표 변환 결과가 변경되었으므로 캐시 무효화
|
|
1877
|
-
* - 마커 위치 업데이트: 새로운 지도 위치에 맞게 마커 재배치
|
|
1878
|
-
* - 렌더링: 새 위치에서 전체 렌더링 수행
|
|
1879
|
-
*/
|
|
1439
|
+
renderAllImmediate = deps.renderAllImmediate; // 지도 이동/줌 완료 시 처리 (캐시 초기화 및 렌더링)
|
|
1880
1440
|
|
|
1881
1441
|
var handleIdle = function () {
|
|
1882
1442
|
prevCenterOffsetRef.current = null;
|
|
@@ -1894,45 +1454,34 @@
|
|
|
1894
1454
|
position: bounds.nw
|
|
1895
1455
|
}, options);
|
|
1896
1456
|
|
|
1897
|
-
markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (
|
|
1457
|
+
markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (transform 제거 시 잠깐 빈 화면이 보이는 것 방지)
|
|
1898
1458
|
|
|
1899
1459
|
if (containerRef.current) {
|
|
1900
1460
|
containerRef.current.style.transform = '';
|
|
1901
1461
|
containerRef.current.style.visibility = '';
|
|
1902
|
-
} // 새 위치에서 렌더링
|
|
1462
|
+
} // 새 위치에서 렌더링 (캐시는 이미 초기화됨)
|
|
1903
1463
|
|
|
1904
1464
|
|
|
1905
1465
|
renderAllImmediate();
|
|
1906
|
-
};
|
|
1907
|
-
/**
|
|
1908
|
-
* 줌 시작 시 처리 (일시적으로 숨김)
|
|
1909
|
-
*/
|
|
1466
|
+
}; // 줌 시작 시 처리 (일시적으로 숨김)
|
|
1910
1467
|
|
|
1911
1468
|
|
|
1912
1469
|
var handleZoomStart = function () {
|
|
1913
|
-
if (containerRef.current)
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
};
|
|
1917
|
-
/**
|
|
1918
|
-
* 줌 종료 시 처리 (다시 표시)
|
|
1919
|
-
*/
|
|
1470
|
+
if (!containerRef.current) return;
|
|
1471
|
+
containerRef.current.style.visibility = 'hidden';
|
|
1472
|
+
}; // 줌 종료 시 처리 (다시 표시)
|
|
1920
1473
|
|
|
1921
1474
|
|
|
1922
1475
|
var handleZoomEnd = function () {
|
|
1923
|
-
if (containerRef.current)
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
};
|
|
1927
|
-
/**
|
|
1928
|
-
* 지도 중심 변경 시 처리 (transform으로 이동 추적)
|
|
1929
|
-
*/
|
|
1476
|
+
if (!containerRef.current) return;
|
|
1477
|
+
containerRef.current.style.visibility = '';
|
|
1478
|
+
}; // 지도 중심 변경 시 처리 (transform으로 이동 추적, 캐시 유지)
|
|
1930
1479
|
|
|
1931
1480
|
|
|
1932
1481
|
var handleCenterChanged = function () {
|
|
1933
1482
|
var center = controller.getCurrBounds().getCenter();
|
|
1934
1483
|
var curr = controller.positionToOffset(center);
|
|
1935
|
-
var prev = prevCenterOffsetRef.current;
|
|
1484
|
+
var prev = prevCenterOffsetRef.current; // 첫 번째 호출 시 이전 위치 저장만 하고 종료
|
|
1936
1485
|
|
|
1937
1486
|
if (!prev) {
|
|
1938
1487
|
prevCenterOffsetRef.current = {
|
|
@@ -1940,10 +1489,12 @@
|
|
|
1940
1489
|
y: curr.y
|
|
1941
1490
|
};
|
|
1942
1491
|
return;
|
|
1943
|
-
}
|
|
1492
|
+
} // 이전 위치와 현재 위치의 차이 계산 (이동 거리)
|
|
1493
|
+
|
|
1944
1494
|
|
|
1945
1495
|
var dx = prev.x - curr.x;
|
|
1946
|
-
var dy = prev.y - curr.y;
|
|
1496
|
+
var dy = prev.y - curr.y; // 누적 이동 거리 저장 (transform으로 화면만 이동, 캐시는 유지하여 성능 최적화)
|
|
1497
|
+
|
|
1947
1498
|
accumTranslateRef.current = {
|
|
1948
1499
|
x: accumTranslateRef.current.x + dx,
|
|
1949
1500
|
y: accumTranslateRef.current.y + dy
|
|
@@ -1951,23 +1502,15 @@
|
|
|
1951
1502
|
prevCenterOffsetRef.current = {
|
|
1952
1503
|
x: curr.x,
|
|
1953
1504
|
y: curr.y
|
|
1954
|
-
};
|
|
1505
|
+
}; // CSS transform으로 컨테이너 이동 (캐시된 좌표는 그대로 유지)
|
|
1955
1506
|
|
|
1956
1507
|
if (containerRef.current) {
|
|
1957
1508
|
containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
|
|
1958
1509
|
}
|
|
1959
1510
|
};
|
|
1960
|
-
/**
|
|
1961
|
-
* 드래그 시작 처리
|
|
1962
|
-
*/
|
|
1963
|
-
|
|
1964
1511
|
|
|
1965
1512
|
var handleDragStart = function () {// 커서는 각 컴포넌트에서 처리
|
|
1966
1513
|
};
|
|
1967
|
-
/**
|
|
1968
|
-
* 드래그 종료 처리
|
|
1969
|
-
*/
|
|
1970
|
-
|
|
1971
1514
|
|
|
1972
1515
|
var handleDragEnd = function () {// 커서는 각 컴포넌트에서 처리
|
|
1973
1516
|
};
|
|
@@ -1984,26 +1527,10 @@
|
|
|
1984
1527
|
/**
|
|
1985
1528
|
* 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
|
|
1986
1529
|
*
|
|
1987
|
-
* Spatial Hash Grid에 모든 데이터의 바운딩 박스를 삽입합니다.
|
|
1988
|
-
* 이를 통해 클릭/호버 시 O(1) 수준의 빠른 Hit Test가 가능합니다.
|
|
1989
|
-
*
|
|
1990
1530
|
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1991
1531
|
* @param data 공간 인덱스에 삽입할 데이터 배열
|
|
1992
1532
|
* @param spatialIndex Spatial Hash Grid 인스턴스
|
|
1993
1533
|
* @param computeBoundingBox 바운딩 박스 계산 함수
|
|
1994
|
-
*
|
|
1995
|
-
* @remarks
|
|
1996
|
-
* - 성능: O(n) 시간복잡도, n은 데이터 개수
|
|
1997
|
-
* - 호출 시점: 데이터 변경 시 또는 지도 이동/줌 완료 시
|
|
1998
|
-
*
|
|
1999
|
-
* @example
|
|
2000
|
-
* ```typescript
|
|
2001
|
-
* buildSpatialIndex(
|
|
2002
|
-
* dataRef.current,
|
|
2003
|
-
* spatialIndexRef.current,
|
|
2004
|
-
* computeBoundingBox
|
|
2005
|
-
* );
|
|
2006
|
-
* ```
|
|
2007
1534
|
*/
|
|
2008
1535
|
|
|
2009
1536
|
var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
|
|
@@ -2019,29 +1546,13 @@
|
|
|
2019
1546
|
}
|
|
2020
1547
|
};
|
|
2021
1548
|
/**
|
|
2022
|
-
* 선택 상태 동기화
|
|
2023
|
-
*
|
|
2024
|
-
* 데이터 변경 시 선택된 항목의 참조를 최신 데이터로 업데이트합니다.
|
|
2025
|
-
* 화면 밖에 있는 선택된 항목도 선택 상태를 유지합니다.
|
|
1549
|
+
* 선택 상태 동기화 (화면 밖 데이터도 선택 상태 유지)
|
|
2026
1550
|
*
|
|
2027
1551
|
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
2028
1552
|
* @param data 최신 데이터 배열
|
|
2029
1553
|
* @param selectedIds 선택된 항목 ID Set
|
|
2030
1554
|
* @param selectedItemsMap 현재 선택된 항목 Map
|
|
2031
1555
|
* @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
1556
|
*/
|
|
2046
1557
|
|
|
2047
1558
|
var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
|
|
@@ -2069,24 +1580,10 @@
|
|
|
2069
1580
|
/**
|
|
2070
1581
|
* 외부 selectedItems를 내부 상태로 동기화
|
|
2071
1582
|
*
|
|
2072
|
-
* 외부에서 전달된 selectedItems prop을 내부 ref 상태로 동기화합니다.
|
|
2073
|
-
*
|
|
2074
1583
|
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
2075
|
-
* @param externalSelectedItems 외부에서 전달된 선택된 항목 배열
|
|
1584
|
+
* @param externalSelectedItems 외부에서 전달된 선택된 항목 배열
|
|
2076
1585
|
* @param selectedIdsRef 선택된 ID Set ref
|
|
2077
1586
|
* @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
1587
|
*/
|
|
2091
1588
|
|
|
2092
1589
|
var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef, selectedItemsMapRef) {
|
|
@@ -2102,22 +1599,17 @@
|
|
|
2102
1599
|
};
|
|
2103
1600
|
|
|
2104
1601
|
/**
|
|
2105
|
-
* 이벤트 유효성 검증
|
|
1602
|
+
* 이벤트 유효성 검증 및 좌표 변환
|
|
2106
1603
|
*
|
|
2107
1604
|
* @param event 이벤트 파라미터
|
|
2108
|
-
* @param context
|
|
1605
|
+
* @param context WoongCanvasContext 인스턴스
|
|
2109
1606
|
* @param controller MintMapController 인스턴스
|
|
2110
|
-
* @returns 유효한
|
|
2111
|
-
*
|
|
2112
|
-
* @remarks
|
|
2113
|
-
* Context가 있으면 전역 이벤트 핸들러가 처리하므로 로컬 핸들러는 스킵
|
|
1607
|
+
* @returns 유효한 화면 좌표 또는 null
|
|
2114
1608
|
*/
|
|
2115
1609
|
var validateEvent = function (event, context, controller) {
|
|
2116
|
-
var _a;
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
if (context) return null; // 이벤트 파라미터 검증
|
|
1610
|
+
var _a;
|
|
2120
1611
|
|
|
1612
|
+
if (context) return null;
|
|
2121
1613
|
if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return null;
|
|
2122
1614
|
|
|
2123
1615
|
try {
|
|
@@ -2128,22 +1620,15 @@
|
|
|
2128
1620
|
}
|
|
2129
1621
|
};
|
|
2130
1622
|
/**
|
|
2131
|
-
* Map의 values를 배열로 변환
|
|
1623
|
+
* Map의 values를 배열로 변환
|
|
2132
1624
|
*
|
|
1625
|
+
* @template T Map 값의 타입
|
|
2133
1626
|
* @param map 변환할 Map
|
|
2134
1627
|
* @returns Map의 값 배열
|
|
2135
|
-
*
|
|
2136
|
-
* @remarks
|
|
2137
|
-
* Map.values()는 IterableIterator를 반환하므로 배열 변환이 필요할 때 사용합니다.
|
|
2138
|
-
* 성능: O(n) 시간복잡도
|
|
2139
|
-
*
|
|
2140
|
-
* 최적화: Array.from을 사용하되, 크기를 미리 할당하여 메모리 재할당 최소화
|
|
2141
1628
|
*/
|
|
2142
1629
|
|
|
2143
1630
|
var mapValuesToArray = function (map) {
|
|
2144
|
-
|
|
2145
|
-
if (map.size === 0) return []; // Array.from 사용 (TypeScript 컴파일러 호환성)
|
|
2146
|
-
|
|
1631
|
+
if (map.size === 0) return [];
|
|
2147
1632
|
return Array.from(map.values());
|
|
2148
1633
|
};
|
|
2149
1634
|
|
|
@@ -6087,9 +5572,6 @@
|
|
|
6087
5572
|
}))));
|
|
6088
5573
|
}
|
|
6089
5574
|
|
|
6090
|
-
// 메인 컴포넌트
|
|
6091
|
-
// ============================================================================
|
|
6092
|
-
|
|
6093
5575
|
var WoongCanvasMarker = function (props) {
|
|
6094
5576
|
var data = props.data,
|
|
6095
5577
|
onClick = props.onClick,
|
|
@@ -6099,225 +5581,88 @@
|
|
|
6099
5581
|
enableMultiSelect = _a === void 0 ? false : _a,
|
|
6100
5582
|
_b = props.topOnHover,
|
|
6101
5583
|
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,
|
|
5584
|
+
_c = props.cullingMargin,
|
|
5585
|
+
cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
|
|
5586
|
+
_d = props.maxCacheSize,
|
|
5587
|
+
maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
|
|
6108
5588
|
externalSelectedItems = props.selectedItems,
|
|
6109
5589
|
externalSelectedItem = props.selectedItem,
|
|
6110
|
-
|
|
6111
|
-
disableInteraction =
|
|
5590
|
+
_e = props.disableInteraction,
|
|
5591
|
+
disableInteraction = _e === void 0 ? false : _e,
|
|
6112
5592
|
renderBase = props.renderBase,
|
|
6113
|
-
renderAnimation = props.renderAnimation,
|
|
6114
5593
|
renderEvent = props.renderEvent,
|
|
6115
|
-
options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "
|
|
6116
|
-
// Hooks & Context
|
|
6117
|
-
// --------------------------------------------------------------------------
|
|
6118
|
-
|
|
5594
|
+
options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderEvent"]);
|
|
6119
5595
|
|
|
6120
5596
|
var controller = useMintMapController();
|
|
6121
5597
|
var context = useWoongCanvasContext();
|
|
6122
|
-
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; //
|
|
6123
|
-
// DOM Refs
|
|
6124
|
-
// --------------------------------------------------------------------------
|
|
5598
|
+
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
|
|
6125
5599
|
|
|
6126
5600
|
var divRef = React.useRef(document.createElement('div'));
|
|
6127
5601
|
var divElement = divRef.current;
|
|
6128
5602
|
var containerRef = React.useRef(null);
|
|
6129
|
-
var markerRef = React.useRef(); //
|
|
6130
|
-
// Konva Refs
|
|
6131
|
-
// --------------------------------------------------------------------------
|
|
5603
|
+
var markerRef = React.useRef(); // Konva Refs
|
|
6132
5604
|
|
|
6133
5605
|
var stageRef = React.useRef(null);
|
|
6134
5606
|
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로 관리하여 클로저 문제 해결) */
|
|
5607
|
+
var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
|
|
6147
5608
|
|
|
5609
|
+
var dataRef = React.useRef(data);
|
|
6148
5610
|
var disableInteractionRef = React.useRef(disableInteraction);
|
|
6149
|
-
/** 현재 Hover 중인 항목 */
|
|
6150
|
-
|
|
6151
5611
|
var hoveredItemRef = React.useRef(null);
|
|
6152
|
-
/** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
|
|
6153
|
-
|
|
6154
5612
|
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
5613
|
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
|
-
// --------------------------------------------------------------------------
|
|
5614
|
+
var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
|
|
6186
5615
|
|
|
6187
5616
|
var draggingRef = React.useRef(false);
|
|
6188
5617
|
var prevCenterOffsetRef = React.useRef(null);
|
|
6189
5618
|
var accumTranslateRef = React.useRef({
|
|
6190
5619
|
x: 0,
|
|
6191
5620
|
y: 0
|
|
6192
|
-
}); //
|
|
6193
|
-
// Performance Refs (캐싱 & 최적화)
|
|
6194
|
-
// --------------------------------------------------------------------------
|
|
6195
|
-
|
|
6196
|
-
/** 좌표 변환 결과 LRU 캐시 */
|
|
5621
|
+
}); // 성능 최적화 Refs
|
|
6197
5622
|
|
|
6198
5623
|
var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
|
|
6199
|
-
/** 공간 인덱스 (빠른 Hit Test) */
|
|
6200
|
-
|
|
6201
5624
|
var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
|
|
6202
|
-
/** 바운딩 박스 캐시 (Viewport Culling 최적화) */
|
|
6203
|
-
|
|
6204
5625
|
var boundingBoxCacheRef = React.useRef(new Map());
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
|
|
6208
|
-
// 유틸리티 함수: 뷰포트 관리
|
|
6209
|
-
// --------------------------------------------------------------------------
|
|
6210
|
-
|
|
6211
|
-
/**
|
|
6212
|
-
* 현재 뷰포트 영역 계산
|
|
6213
|
-
*/
|
|
5626
|
+
var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling용)
|
|
6214
5627
|
|
|
6215
5628
|
var updateViewport$1 = function () {
|
|
6216
5629
|
updateViewport(stageRef.current, cullingMargin, viewportRef);
|
|
6217
|
-
};
|
|
6218
|
-
/**
|
|
6219
|
-
* 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
|
|
6220
|
-
*/
|
|
5630
|
+
}; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
|
|
6221
5631
|
|
|
6222
5632
|
|
|
6223
5633
|
var isInViewport$1 = function (item) {
|
|
6224
|
-
return isInViewport(item,
|
|
6225
|
-
}; //
|
|
6226
|
-
// 유틸리티 함수: 좌표 변환 캐싱
|
|
6227
|
-
// --------------------------------------------------------------------------
|
|
6228
|
-
|
|
6229
|
-
/**
|
|
6230
|
-
* 마커 좌표 변환 결과를 캐시하고 반환
|
|
6231
|
-
*
|
|
6232
|
-
* @param markerData 마커 데이터
|
|
6233
|
-
* @returns 변환된 좌표 또는 null
|
|
6234
|
-
*/
|
|
5634
|
+
return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
|
|
5635
|
+
}; // 마커 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
|
|
6235
5636
|
|
|
6236
5637
|
|
|
6237
5638
|
var getOrComputeMarkerOffset = function (markerData) {
|
|
6238
5639
|
var cached = offsetCacheRef.current.get(markerData.id);
|
|
6239
5640
|
if (cached && !Array.isArray(cached)) return cached;
|
|
6240
5641
|
var result = computeMarkerOffset(markerData, controller);
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
offsetCacheRef.current.set(markerData.id, result);
|
|
6244
|
-
}
|
|
6245
|
-
|
|
5642
|
+
if (!result) return null;
|
|
5643
|
+
offsetCacheRef.current.set(markerData.id, result);
|
|
6246
5644
|
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
|
-
*/
|
|
5645
|
+
}; // 마커 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
|
|
6272
5646
|
|
|
6273
5647
|
|
|
6274
5648
|
var computeBoundingBox = function (item) {
|
|
6275
|
-
// 마커: 중심점 기준 박스 크기 계산 (꼬리 포함)
|
|
6276
5649
|
var offset = getOrComputeMarkerOffset(item);
|
|
6277
5650
|
if (!offset) return null;
|
|
6278
5651
|
var boxWidth = item.boxWidth || 50;
|
|
6279
5652
|
var boxHeight = item.boxHeight || 28;
|
|
6280
|
-
var tailHeight = item.tailHeight || 0;
|
|
6281
|
-
|
|
5653
|
+
var tailHeight = item.tailHeight || 0;
|
|
6282
5654
|
return {
|
|
6283
5655
|
minX: offset.x - boxWidth / 2,
|
|
6284
5656
|
minY: offset.y - boxHeight - tailHeight,
|
|
6285
5657
|
maxX: offset.x + boxWidth / 2,
|
|
6286
5658
|
maxY: offset.y
|
|
6287
5659
|
};
|
|
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
|
-
*/
|
|
5660
|
+
}; // 공간 인덱스 빌드 (빠른 Hit Test용)
|
|
6303
5661
|
|
|
6304
5662
|
|
|
6305
5663
|
var buildSpatialIndex$1 = function () {
|
|
6306
5664
|
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
|
-
*/
|
|
5665
|
+
}; // 렌더링 유틸리티 객체
|
|
6321
5666
|
|
|
6322
5667
|
|
|
6323
5668
|
var renderUtils = {
|
|
@@ -6325,48 +5670,29 @@
|
|
|
6325
5670
|
return null;
|
|
6326
5671
|
},
|
|
6327
5672
|
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
|
-
*/
|
|
5673
|
+
}; // Base Layer 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
|
|
6344
5674
|
|
|
6345
5675
|
var doRenderBase = function () {
|
|
6346
5676
|
var layer = baseLayerRef.current;
|
|
6347
|
-
if (!layer) return;
|
|
6348
|
-
|
|
5677
|
+
if (!layer) return;
|
|
6349
5678
|
var shape = layer.findOne('.base-render-shape');
|
|
6350
5679
|
|
|
6351
5680
|
if (!shape) {
|
|
6352
|
-
// 최초 생성 (한 번만 실행됨)
|
|
6353
|
-
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
6354
5681
|
shape = new Konva__default["default"].Shape({
|
|
6355
5682
|
name: 'base-render-shape',
|
|
6356
5683
|
sceneFunc: function (context, shape) {
|
|
6357
5684
|
var ctx = context;
|
|
6358
|
-
var hovered = hoveredItemRef.current; //
|
|
5685
|
+
var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
|
|
6359
5686
|
|
|
6360
|
-
var visibleItems =
|
|
5687
|
+
var visibleItems = dataRef.current.filter(function (item) {
|
|
6361
5688
|
return isInViewport$1(item);
|
|
6362
|
-
})
|
|
5689
|
+
}); // topOnHover 옵션: hover된 항목을 나중에 그려서 최상위에 표시
|
|
6363
5690
|
|
|
6364
5691
|
if (topOnHover && !renderEvent && hovered) {
|
|
6365
|
-
// hover된 항목 제외하고 렌더링
|
|
6366
5692
|
visibleItems = visibleItems.filter(function (item) {
|
|
6367
5693
|
return item.id !== hovered.id;
|
|
6368
5694
|
});
|
|
6369
|
-
} // 일반
|
|
5695
|
+
} // 일반 항목들 먼저 렌더링
|
|
6370
5696
|
|
|
6371
5697
|
|
|
6372
5698
|
renderBase({
|
|
@@ -6375,12 +5701,10 @@
|
|
|
6375
5701
|
selectedIds: selectedIdsRef.current,
|
|
6376
5702
|
hoveredItem: hovered,
|
|
6377
5703
|
utils: renderUtils
|
|
6378
|
-
}); // hover된 항목을
|
|
5704
|
+
}); // hover된 항목을 마지막에 렌더링하여 최상위에 표시
|
|
6379
5705
|
|
|
6380
5706
|
if (topOnHover && !renderEvent && hovered) {
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
if (isHoveredInViewport) {
|
|
5707
|
+
if (isInViewport$1(hovered)) {
|
|
6384
5708
|
renderBase({
|
|
6385
5709
|
ctx: ctx,
|
|
6386
5710
|
items: [hovered],
|
|
@@ -6396,72 +5720,26 @@
|
|
|
6396
5720
|
hitStrokeWidth: 0
|
|
6397
5721
|
});
|
|
6398
5722
|
layer.add(shape);
|
|
6399
|
-
}
|
|
6400
|
-
|
|
5723
|
+
}
|
|
6401
5724
|
|
|
6402
5725
|
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
|
-
*/
|
|
5726
|
+
}; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
|
|
6442
5727
|
|
|
6443
5728
|
|
|
6444
5729
|
var doRenderEvent = function () {
|
|
6445
5730
|
var layer = eventLayerRef.current;
|
|
6446
|
-
if (!layer) return;
|
|
6447
|
-
if (!renderEvent) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
|
|
6448
|
-
|
|
5731
|
+
if (!layer || !renderEvent) return;
|
|
6449
5732
|
var shape = layer.findOne('.event-render-shape');
|
|
6450
5733
|
|
|
6451
5734
|
if (!shape) {
|
|
6452
|
-
// 최초 생성 (한 번만 실행됨)
|
|
6453
|
-
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
6454
5735
|
shape = new Konva__default["default"].Shape({
|
|
6455
5736
|
name: 'event-render-shape',
|
|
6456
5737
|
sceneFunc: function (context, shape) {
|
|
6457
|
-
var ctx = context;
|
|
6458
|
-
// 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
|
|
6459
|
-
|
|
5738
|
+
var ctx = context;
|
|
6460
5739
|
var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
|
|
6461
|
-
var hovered = hoveredItemRef.current;
|
|
5740
|
+
var hovered = hoveredItemRef.current;
|
|
6462
5741
|
|
|
6463
5742
|
if (topOnHover && hovered) {
|
|
6464
|
-
// 1. 먼저 일반 항목들 렌더링 (hover된 항목 제외)
|
|
6465
5743
|
renderEvent({
|
|
6466
5744
|
ctx: ctx,
|
|
6467
5745
|
hoveredItem: null,
|
|
@@ -6470,13 +5748,9 @@
|
|
|
6470
5748
|
return item.id !== hovered.id;
|
|
6471
5749
|
}),
|
|
6472
5750
|
selectedItem: selectedItemRef.current
|
|
6473
|
-
});
|
|
6474
|
-
|
|
6475
|
-
var isHoveredInViewport = enableViewportCulling ? isInViewport$1(hovered) : true;
|
|
5751
|
+
});
|
|
6476
5752
|
|
|
6477
|
-
if (
|
|
6478
|
-
// hover된 항목이 선택되어 있다면 hoverSelectedItems에 포함시켜서
|
|
6479
|
-
// renderEvent에서 hover 스타일만 적용되도록 함
|
|
5753
|
+
if (isInViewport$1(hovered)) {
|
|
6480
5754
|
var hoveredIsSelected = selectedItems.some(function (item) {
|
|
6481
5755
|
return item.id === hovered.id;
|
|
6482
5756
|
});
|
|
@@ -6490,7 +5764,6 @@
|
|
|
6490
5764
|
});
|
|
6491
5765
|
}
|
|
6492
5766
|
} else {
|
|
6493
|
-
// topOnHover가 false이거나 hover된 항목이 없으면 일반 렌더링
|
|
6494
5767
|
renderEvent({
|
|
6495
5768
|
ctx: ctx,
|
|
6496
5769
|
hoveredItem: hovered,
|
|
@@ -6505,28 +5778,21 @@
|
|
|
6505
5778
|
hitStrokeWidth: 0
|
|
6506
5779
|
});
|
|
6507
5780
|
layer.add(shape);
|
|
6508
|
-
}
|
|
6509
|
-
|
|
5781
|
+
}
|
|
6510
5782
|
|
|
6511
5783
|
layer.batchDraw();
|
|
6512
|
-
};
|
|
6513
|
-
/**
|
|
6514
|
-
* 전체 즉시 렌더링 (IDLE 시 호출)
|
|
6515
|
-
*/
|
|
5784
|
+
}; // 전체 즉시 렌더링
|
|
6516
5785
|
|
|
6517
5786
|
|
|
6518
5787
|
var renderAllImmediate = function () {
|
|
6519
5788
|
updateViewport$1();
|
|
6520
5789
|
buildSpatialIndex$1();
|
|
6521
5790
|
doRenderBase();
|
|
6522
|
-
doRenderAnimation();
|
|
6523
5791
|
doRenderEvent();
|
|
6524
|
-
}; //
|
|
6525
|
-
// 이벤트 핸들러: 지도 이벤트
|
|
6526
|
-
// --------------------------------------------------------------------------
|
|
5792
|
+
}; // 지도 이벤트 핸들러 생성
|
|
6527
5793
|
|
|
6528
5794
|
|
|
6529
|
-
var
|
|
5795
|
+
var _f = createMapEventHandlers({
|
|
6530
5796
|
controller: controller,
|
|
6531
5797
|
containerRef: containerRef,
|
|
6532
5798
|
markerRef: markerRef,
|
|
@@ -6537,73 +5803,38 @@
|
|
|
6537
5803
|
boundingBoxCacheRef: boundingBoxCacheRef,
|
|
6538
5804
|
renderAllImmediate: renderAllImmediate
|
|
6539
5805
|
}),
|
|
6540
|
-
handleIdle =
|
|
6541
|
-
handleZoomStart =
|
|
6542
|
-
handleZoomEnd =
|
|
6543
|
-
handleCenterChanged =
|
|
6544
|
-
handleDragStartShared =
|
|
6545
|
-
handleDragEndShared =
|
|
6546
|
-
/**
|
|
6547
|
-
* 드래그 시작 처리 (커서를 grabbing으로 변경)
|
|
6548
|
-
*/
|
|
6549
|
-
|
|
5806
|
+
handleIdle = _f.handleIdle,
|
|
5807
|
+
handleZoomStart = _f.handleZoomStart,
|
|
5808
|
+
handleZoomEnd = _f.handleZoomEnd,
|
|
5809
|
+
handleCenterChanged = _f.handleCenterChanged,
|
|
5810
|
+
handleDragStartShared = _f.handleDragStart,
|
|
5811
|
+
handleDragEndShared = _f.handleDragEnd;
|
|
6550
5812
|
|
|
6551
5813
|
var handleDragStart = function () {
|
|
6552
5814
|
handleDragStartShared();
|
|
6553
5815
|
draggingRef.current = true;
|
|
6554
5816
|
controller.setMapCursor('grabbing');
|
|
6555
5817
|
};
|
|
6556
|
-
/**
|
|
6557
|
-
* 드래그 종료 처리 (커서를 기본으로 복원)
|
|
6558
|
-
*/
|
|
6559
|
-
|
|
6560
5818
|
|
|
6561
5819
|
var handleDragEnd = function () {
|
|
6562
5820
|
handleDragEndShared();
|
|
6563
5821
|
draggingRef.current = false;
|
|
6564
5822
|
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
|
-
*/
|
|
5823
|
+
}; // Hit Test: 특정 좌표의 마커 찾기
|
|
6593
5824
|
|
|
6594
5825
|
|
|
6595
5826
|
var findData = function (offset) {
|
|
6596
|
-
// topOnHover
|
|
5827
|
+
// topOnHover 옵션이 켜져 있으면 hover된 항목을 최우선으로 확인
|
|
6597
5828
|
if (topOnHover && hoveredItemRef.current) {
|
|
6598
5829
|
var hovered = hoveredItemRef.current;
|
|
6599
5830
|
|
|
6600
5831
|
if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
|
|
6601
|
-
return hovered;
|
|
5832
|
+
return hovered;
|
|
6602
5833
|
}
|
|
6603
|
-
} //
|
|
5834
|
+
} // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
|
|
6604
5835
|
|
|
6605
5836
|
|
|
6606
|
-
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); //
|
|
5837
|
+
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
|
|
6607
5838
|
|
|
6608
5839
|
for (var i = candidates.length - 1; i >= 0; i--) {
|
|
6609
5840
|
var item = candidates[i];
|
|
@@ -6614,18 +5845,7 @@
|
|
|
6614
5845
|
}
|
|
6615
5846
|
|
|
6616
5847
|
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
|
-
*/
|
|
5848
|
+
}; // Hover 상태 설정 및 렌더링
|
|
6629
5849
|
|
|
6630
5850
|
|
|
6631
5851
|
var setHovered = function (data) {
|
|
@@ -6635,38 +5855,18 @@
|
|
|
6635
5855
|
controller.setMapCursor('grabbing');
|
|
6636
5856
|
} else {
|
|
6637
5857
|
controller.setMapCursor(data ? 'pointer' : 'grab');
|
|
6638
|
-
}
|
|
6639
|
-
|
|
5858
|
+
}
|
|
6640
5859
|
|
|
6641
5860
|
if (renderEvent) {
|
|
6642
|
-
// renderEvent가 있으면 Event Layer에서만 처리 (성능 최적화)
|
|
6643
5861
|
doRenderEvent();
|
|
6644
5862
|
} else if (topOnHover) {
|
|
6645
|
-
// renderEvent가 없고 topOnHover가 true면 Base Layer에서 처리
|
|
6646
5863
|
doRenderBase();
|
|
6647
5864
|
}
|
|
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
|
-
*/
|
|
5865
|
+
}; // 클릭 처리: 선택 상태 업데이트
|
|
6664
5866
|
|
|
6665
5867
|
|
|
6666
5868
|
var handleLocalClick = function (data) {
|
|
6667
|
-
// 1. 선택 상태 업데이트
|
|
6668
5869
|
if (enableMultiSelect) {
|
|
6669
|
-
// 다중 선택: Set과 Map 동시 업데이트
|
|
6670
5870
|
var newSelected = new Set(selectedIdsRef.current);
|
|
6671
5871
|
|
|
6672
5872
|
if (newSelected.has(data.id)) {
|
|
@@ -6679,7 +5879,6 @@
|
|
|
6679
5879
|
|
|
6680
5880
|
selectedIdsRef.current = newSelected;
|
|
6681
5881
|
} else {
|
|
6682
|
-
// 단일 선택: 토글
|
|
6683
5882
|
var newSelected = new Set();
|
|
6684
5883
|
|
|
6685
5884
|
if (!selectedIdsRef.current.has(data.id)) {
|
|
@@ -6693,135 +5892,80 @@
|
|
|
6693
5892
|
selectedIdsRef.current = newSelected;
|
|
6694
5893
|
}
|
|
6695
5894
|
|
|
6696
|
-
|
|
6697
|
-
// 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
|
|
6698
|
-
doRenderBase(); // 3. Animation Layer 렌더링 (선택된 마커 애니메이션)
|
|
6699
|
-
|
|
6700
|
-
doRenderAnimation();
|
|
6701
|
-
} // 4. Event Layer 렌더링 (hover 처리)
|
|
6702
|
-
|
|
6703
|
-
|
|
5895
|
+
doRenderBase();
|
|
6704
5896
|
doRenderEvent();
|
|
6705
|
-
}; //
|
|
6706
|
-
// 이벤트 핸들러: UI 이벤트
|
|
6707
|
-
// --------------------------------------------------------------------------
|
|
6708
|
-
|
|
6709
|
-
/**
|
|
6710
|
-
* 클릭 이벤트 처리
|
|
6711
|
-
*
|
|
6712
|
-
* @param event 클릭 이벤트 파라미터
|
|
6713
|
-
*
|
|
6714
|
-
* @remarks
|
|
6715
|
-
* - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
|
|
6716
|
-
* - 상호작용이 비활성화되어 있으면 스킵
|
|
6717
|
-
* - Spatial Index를 사용하여 빠른 Hit Test 수행
|
|
6718
|
-
*/
|
|
5897
|
+
}; // 클릭 이벤트 핸들러
|
|
6719
5898
|
|
|
6720
5899
|
|
|
6721
5900
|
var handleClick = function (event) {
|
|
6722
|
-
if (disableInteractionRef.current) return;
|
|
6723
|
-
|
|
5901
|
+
if (disableInteractionRef.current) return;
|
|
6724
5902
|
var clickedOffset = validateEvent(event, context, controller);
|
|
6725
5903
|
if (!clickedOffset) return;
|
|
6726
5904
|
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
|
-
*/
|
|
5905
|
+
if (!data) return;
|
|
5906
|
+
handleLocalClick(data);
|
|
5907
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
|
|
5908
|
+
}; // 마우스 이동 이벤트 핸들러 (hover 감지)
|
|
6746
5909
|
|
|
6747
5910
|
|
|
6748
5911
|
var handleMouseMove = function (event) {
|
|
6749
|
-
if (disableInteractionRef.current) return;
|
|
6750
|
-
|
|
5912
|
+
if (disableInteractionRef.current) return;
|
|
6751
5913
|
var mouseOffset = validateEvent(event, context, controller);
|
|
6752
5914
|
if (!mouseOffset) return;
|
|
6753
5915
|
var hoveredItem = findData(mouseOffset);
|
|
6754
5916
|
var prevHovered = hoveredItemRef.current;
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
}
|
|
6761
|
-
};
|
|
6762
|
-
/**
|
|
6763
|
-
* 마우스가 canvas를 벗어날 때 hover cleanup
|
|
6764
|
-
*/
|
|
5917
|
+
if (prevHovered === hoveredItem) return;
|
|
5918
|
+
setHovered(hoveredItem);
|
|
5919
|
+
if (prevHovered) onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
|
|
5920
|
+
if (hoveredItem) onMouseOver === null || onMouseOver === void 0 ? void 0 : onMouseOver(hoveredItem);
|
|
5921
|
+
}; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
|
|
6765
5922
|
|
|
6766
5923
|
|
|
6767
5924
|
var handleMouseLeave = function () {
|
|
6768
|
-
if (disableInteractionRef.current) return;
|
|
6769
|
-
|
|
5925
|
+
if (disableInteractionRef.current) return;
|
|
6770
5926
|
var prevHovered = hoveredItemRef.current;
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
if (onMouseOut) {
|
|
6778
|
-
onMouseOut(prevHovered);
|
|
6779
|
-
}
|
|
6780
|
-
}
|
|
6781
|
-
}; // --------------------------------------------------------------------------
|
|
6782
|
-
// Lifecycle: DOM 초기화
|
|
6783
|
-
// --------------------------------------------------------------------------
|
|
5927
|
+
if (!prevHovered) return;
|
|
5928
|
+
hoveredItemRef.current = null;
|
|
5929
|
+
controller.setMapCursor('grab');
|
|
5930
|
+
doRenderEvent();
|
|
5931
|
+
onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
|
|
5932
|
+
}; // DOM 초기화
|
|
6784
5933
|
|
|
6785
5934
|
|
|
6786
5935
|
React.useEffect(function () {
|
|
6787
5936
|
divElement.style.width = 'fit-content';
|
|
6788
5937
|
return function () {
|
|
6789
|
-
if (markerRef.current)
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
}
|
|
5938
|
+
if (!markerRef.current) return;
|
|
5939
|
+
controller.clearDrawable(markerRef.current);
|
|
5940
|
+
markerRef.current = undefined;
|
|
6793
5941
|
};
|
|
6794
|
-
}, []); //
|
|
6795
|
-
// Lifecycle: 마커 생성/업데이트
|
|
6796
|
-
// --------------------------------------------------------------------------
|
|
5942
|
+
}, []); // 마커 생성/업데이트
|
|
6797
5943
|
|
|
6798
5944
|
React.useEffect(function () {
|
|
6799
|
-
if (options)
|
|
6800
|
-
|
|
5945
|
+
if (!options) return;
|
|
5946
|
+
var bounds = controller.getCurrBounds();
|
|
6801
5947
|
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
5948
|
+
var markerOptions = tslib.__assign({
|
|
5949
|
+
position: bounds.nw
|
|
5950
|
+
}, options);
|
|
6805
5951
|
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
markerRef.current.element = divElement;
|
|
6811
|
-
controller.createMarker(markerRef.current);
|
|
5952
|
+
if (markerRef.current) {
|
|
5953
|
+
controller.updateMarker(markerRef.current, markerOptions);
|
|
5954
|
+
return;
|
|
5955
|
+
}
|
|
6812
5956
|
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
5957
|
+
markerRef.current = new Marker(markerOptions);
|
|
5958
|
+
markerRef.current.element = divElement;
|
|
5959
|
+
controller.createMarker(markerRef.current);
|
|
6816
5960
|
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
}
|
|
6820
|
-
}
|
|
5961
|
+
if (divElement.parentElement) {
|
|
5962
|
+
divElement.parentElement.style.pointerEvents = 'none';
|
|
6821
5963
|
}
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
5964
|
+
|
|
5965
|
+
if (options.zIndex !== undefined) {
|
|
5966
|
+
controller.setMarkerZIndex(markerRef.current, options.zIndex);
|
|
5967
|
+
}
|
|
5968
|
+
}, [options]); // Konva 초기화 및 이벤트 리스너 등록
|
|
6825
5969
|
|
|
6826
5970
|
React.useEffect(function () {
|
|
6827
5971
|
var mapDiv = controller.mapDivElement;
|
|
@@ -6830,34 +5974,21 @@
|
|
|
6830
5974
|
width: mapDiv.offsetWidth,
|
|
6831
5975
|
height: mapDiv.offsetHeight
|
|
6832
5976
|
});
|
|
6833
|
-
stageRef.current = stage;
|
|
6834
|
-
|
|
5977
|
+
stageRef.current = stage;
|
|
6835
5978
|
var baseLayer = new Konva__default["default"].Layer({
|
|
6836
|
-
listening: false // 이벤트 리스닝 비활성화로 성능 향상
|
|
6837
|
-
|
|
6838
|
-
});
|
|
6839
|
-
var animationLayer = new Konva__default["default"].Layer({
|
|
6840
5979
|
listening: false
|
|
6841
5980
|
});
|
|
6842
5981
|
var eventLayer = new Konva__default["default"].Layer({
|
|
6843
5982
|
listening: false
|
|
6844
5983
|
});
|
|
6845
5984
|
baseLayerRef.current = baseLayer;
|
|
6846
|
-
animationLayerRef.current = animationLayer;
|
|
6847
5985
|
eventLayerRef.current = eventLayer;
|
|
6848
5986
|
stage.add(baseLayer);
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
stage.add(animationLayer);
|
|
6852
|
-
}
|
|
6853
|
-
|
|
6854
|
-
stage.add(eventLayer); // 초기 뷰포트 설정
|
|
6855
|
-
|
|
6856
|
-
updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
|
|
5987
|
+
stage.add(eventLayer);
|
|
5988
|
+
updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
|
|
6857
5989
|
|
|
6858
5990
|
var resizeRafId = null;
|
|
6859
5991
|
var resizeObserver = new ResizeObserver(function () {
|
|
6860
|
-
// RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
|
|
6861
5992
|
if (resizeRafId !== null) {
|
|
6862
5993
|
cancelAnimationFrame(resizeRafId);
|
|
6863
5994
|
}
|
|
@@ -6880,10 +6011,9 @@
|
|
|
6880
6011
|
controller.addEventListener('CLICK', handleClick);
|
|
6881
6012
|
controller.addEventListener('MOUSEMOVE', handleMouseMove);
|
|
6882
6013
|
controller.addEventListener('DRAGSTART', handleDragStart);
|
|
6883
|
-
controller.addEventListener('DRAGEND', handleDragEnd);
|
|
6884
|
-
|
|
6014
|
+
controller.addEventListener('DRAGEND', handleDragEnd);
|
|
6885
6015
|
mapDiv.addEventListener('mouseleave', handleMouseLeave);
|
|
6886
|
-
renderAllImmediate(); // Context 사용 시 컴포넌트 등록
|
|
6016
|
+
renderAllImmediate(); // Context 사용 시 컴포넌트 등록
|
|
6887
6017
|
|
|
6888
6018
|
var componentInstance = null;
|
|
6889
6019
|
|
|
@@ -6904,22 +6034,17 @@
|
|
|
6904
6034
|
},
|
|
6905
6035
|
isInteractionDisabled: function () {
|
|
6906
6036
|
return disableInteractionRef.current;
|
|
6907
|
-
}
|
|
6908
|
-
|
|
6037
|
+
}
|
|
6909
6038
|
};
|
|
6910
6039
|
context.registerComponent(componentInstance);
|
|
6911
|
-
}
|
|
6912
|
-
|
|
6040
|
+
}
|
|
6913
6041
|
|
|
6914
6042
|
return function () {
|
|
6915
|
-
// RAF 정리
|
|
6916
6043
|
if (resizeRafId !== null) {
|
|
6917
6044
|
cancelAnimationFrame(resizeRafId);
|
|
6918
|
-
}
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
resizeObserver.disconnect(); // 이벤트 리스너 정리
|
|
6045
|
+
}
|
|
6922
6046
|
|
|
6047
|
+
resizeObserver.disconnect();
|
|
6923
6048
|
controller.removeEventListener('IDLE', handleIdle);
|
|
6924
6049
|
controller.removeEventListener('ZOOMSTART', handleZoomStart);
|
|
6925
6050
|
controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
|
|
@@ -6928,59 +6053,41 @@
|
|
|
6928
6053
|
controller.removeEventListener('MOUSEMOVE', handleMouseMove);
|
|
6929
6054
|
controller.removeEventListener('DRAGSTART', handleDragStart);
|
|
6930
6055
|
controller.removeEventListener('DRAGEND', handleDragEnd);
|
|
6931
|
-
mapDiv.removeEventListener('mouseleave', handleMouseLeave);
|
|
6056
|
+
mapDiv.removeEventListener('mouseleave', handleMouseLeave);
|
|
6932
6057
|
|
|
6933
6058
|
if (context && componentInstance) {
|
|
6934
6059
|
context.unregisterComponent(componentInstance);
|
|
6935
|
-
}
|
|
6936
|
-
|
|
6060
|
+
}
|
|
6937
6061
|
|
|
6938
6062
|
baseLayer.destroyChildren();
|
|
6939
|
-
animationLayer.destroyChildren();
|
|
6940
6063
|
eventLayer.destroyChildren();
|
|
6941
|
-
stage.destroy();
|
|
6942
|
-
|
|
6064
|
+
stage.destroy();
|
|
6943
6065
|
offsetCacheRef.current.clear();
|
|
6944
6066
|
boundingBoxCacheRef.current.clear();
|
|
6945
6067
|
spatialIndexRef.current.clear();
|
|
6946
|
-
};
|
|
6947
|
-
}, []); //
|
|
6948
|
-
// --------------------------------------------------------------------------
|
|
6949
|
-
// Lifecycle: disableInteraction 동기화
|
|
6950
|
-
// --------------------------------------------------------------------------
|
|
6068
|
+
};
|
|
6069
|
+
}, []); // disableInteraction 동기화
|
|
6951
6070
|
|
|
6952
6071
|
React.useEffect(function () {
|
|
6953
6072
|
disableInteractionRef.current = disableInteraction;
|
|
6954
|
-
}, [disableInteraction]); //
|
|
6955
|
-
// Lifecycle: 외부 selectedItems 동기화
|
|
6956
|
-
// --------------------------------------------------------------------------
|
|
6073
|
+
}, [disableInteraction]); // 외부 selectedItems 동기화
|
|
6957
6074
|
|
|
6958
6075
|
React.useEffect(function () {
|
|
6959
6076
|
if (!stageRef.current) return;
|
|
6960
|
-
syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
|
|
6961
|
-
|
|
6077
|
+
syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
|
|
6962
6078
|
doRenderBase();
|
|
6963
|
-
doRenderAnimation();
|
|
6964
6079
|
doRenderEvent();
|
|
6965
|
-
}, [externalSelectedItems]); //
|
|
6966
|
-
// --------------------------------------------------------------------------
|
|
6967
|
-
// Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
6968
|
-
// --------------------------------------------------------------------------
|
|
6080
|
+
}, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
6969
6081
|
|
|
6970
6082
|
React.useEffect(function () {
|
|
6971
|
-
if (!stageRef.current) return;
|
|
6972
|
-
|
|
6973
|
-
selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
|
|
6974
|
-
|
|
6083
|
+
if (!stageRef.current) return;
|
|
6084
|
+
selectedItemRef.current = externalSelectedItem;
|
|
6975
6085
|
doRenderEvent();
|
|
6976
|
-
}, [externalSelectedItem]); //
|
|
6977
|
-
// Lifecycle: 데이터 변경 시 렌더링
|
|
6978
|
-
// --------------------------------------------------------------------------
|
|
6086
|
+
}, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
|
|
6979
6087
|
|
|
6980
6088
|
React.useEffect(function () {
|
|
6981
|
-
if (!stageRef.current) return;
|
|
6982
|
-
|
|
6983
|
-
dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
|
|
6089
|
+
if (!stageRef.current) return;
|
|
6090
|
+
dataRef.current = data;
|
|
6984
6091
|
|
|
6985
6092
|
if (containerRef.current) {
|
|
6986
6093
|
containerRef.current.style.transform = '';
|
|
@@ -6990,26 +6097,10 @@
|
|
|
6990
6097
|
accumTranslateRef.current = {
|
|
6991
6098
|
x: 0,
|
|
6992
6099
|
y: 0
|
|
6993
|
-
};
|
|
6994
|
-
|
|
6100
|
+
};
|
|
6995
6101
|
offsetCacheRef.current.clear();
|
|
6996
6102
|
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
|
-
|
|
6103
|
+
selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
|
|
7013
6104
|
renderAllImmediate();
|
|
7014
6105
|
}, [data]);
|
|
7015
6106
|
return reactDom.createPortal(React__default["default"].createElement("div", {
|
|
@@ -7310,24 +6401,19 @@
|
|
|
7310
6401
|
};
|
|
7311
6402
|
};
|
|
7312
6403
|
|
|
7313
|
-
// 메인 컴포넌트
|
|
7314
|
-
// ============================================================================
|
|
7315
|
-
|
|
7316
6404
|
var WoongCanvasPolygon = function (props) {
|
|
7317
6405
|
var data = props.data,
|
|
7318
6406
|
onClick = props.onClick,
|
|
7319
6407
|
_a = props.enableMultiSelect,
|
|
7320
6408
|
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,
|
|
6409
|
+
_b = props.cullingMargin,
|
|
6410
|
+
cullingMargin = _b === void 0 ? DEFAULT_CULLING_MARGIN : _b,
|
|
6411
|
+
_c = props.maxCacheSize,
|
|
6412
|
+
maxCacheSize = _c === void 0 ? DEFAULT_MAX_CACHE_SIZE : _c,
|
|
7327
6413
|
externalSelectedItems = props.selectedItems,
|
|
7328
6414
|
externalSelectedItem = props.selectedItem,
|
|
7329
|
-
|
|
7330
|
-
disableInteraction =
|
|
6415
|
+
_d = props.disableInteraction,
|
|
6416
|
+
disableInteraction = _d === void 0 ? false : _d,
|
|
7331
6417
|
baseFillColor = props.baseFillColor,
|
|
7332
6418
|
baseStrokeColor = props.baseStrokeColor,
|
|
7333
6419
|
baseLineWidth = props.baseLineWidth,
|
|
@@ -7340,167 +6426,67 @@
|
|
|
7340
6426
|
hoveredFillColor = props.hoveredFillColor,
|
|
7341
6427
|
hoveredStrokeColor = props.hoveredStrokeColor,
|
|
7342
6428
|
hoveredLineWidth = props.hoveredLineWidth,
|
|
7343
|
-
options = tslib.__rest(props, ["data", "onClick", "enableMultiSelect", "
|
|
6429
|
+
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
6430
|
// Hooks & Context
|
|
7345
6431
|
// --------------------------------------------------------------------------
|
|
7346
6432
|
|
|
7347
6433
|
|
|
7348
6434
|
var controller = useMintMapController();
|
|
7349
6435
|
var context = useWoongCanvasContext();
|
|
7350
|
-
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; //
|
|
7351
|
-
// DOM Refs
|
|
7352
|
-
// --------------------------------------------------------------------------
|
|
6436
|
+
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
|
|
7353
6437
|
|
|
7354
6438
|
var divRef = React.useRef(document.createElement('div'));
|
|
7355
6439
|
var divElement = divRef.current;
|
|
7356
6440
|
var containerRef = React.useRef(null);
|
|
7357
|
-
var markerRef = React.useRef(); //
|
|
7358
|
-
// Konva Refs
|
|
7359
|
-
// --------------------------------------------------------------------------
|
|
6441
|
+
var markerRef = React.useRef(); // Konva Refs
|
|
7360
6442
|
|
|
7361
6443
|
var stageRef = React.useRef(null);
|
|
7362
6444
|
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로 관리하여 클로저 문제 해결) */
|
|
6445
|
+
var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
|
|
7374
6446
|
|
|
6447
|
+
var dataRef = React.useRef(data);
|
|
7375
6448
|
var disableInteractionRef = React.useRef(disableInteraction);
|
|
7376
|
-
/** 현재 Hover 중인 항목 */
|
|
7377
|
-
|
|
7378
6449
|
var hoveredItemRef = React.useRef(null);
|
|
7379
|
-
/** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
|
|
7380
|
-
|
|
7381
6450
|
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
6451
|
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
|
-
// --------------------------------------------------------------------------
|
|
6452
|
+
var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
|
|
7413
6453
|
|
|
7414
6454
|
var draggingRef = React.useRef(false);
|
|
7415
6455
|
var prevCenterOffsetRef = React.useRef(null);
|
|
7416
6456
|
var accumTranslateRef = React.useRef({
|
|
7417
6457
|
x: 0,
|
|
7418
6458
|
y: 0
|
|
7419
|
-
}); //
|
|
7420
|
-
// Performance Refs (캐싱 & 최적화)
|
|
7421
|
-
// --------------------------------------------------------------------------
|
|
7422
|
-
|
|
7423
|
-
/** 좌표 변환 결과 LRU 캐시 */
|
|
6459
|
+
}); // 성능 최적화 Refs
|
|
7424
6460
|
|
|
7425
6461
|
var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
|
|
7426
|
-
/** 공간 인덱스 (빠른 Hit Test) */
|
|
7427
|
-
|
|
7428
6462
|
var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
|
|
7429
|
-
/** 바운딩 박스 캐시 (Viewport Culling 최적화) */
|
|
7430
|
-
|
|
7431
6463
|
var boundingBoxCacheRef = React.useRef(new Map());
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
|
|
7435
|
-
// 유틸리티 함수: 뷰포트 관리
|
|
7436
|
-
// --------------------------------------------------------------------------
|
|
7437
|
-
|
|
7438
|
-
/**
|
|
7439
|
-
* 현재 뷰포트 영역 계산
|
|
7440
|
-
*/
|
|
6464
|
+
var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling용)
|
|
7441
6465
|
|
|
7442
6466
|
var updateViewport$1 = function () {
|
|
7443
6467
|
updateViewport(stageRef.current, cullingMargin, viewportRef);
|
|
7444
|
-
};
|
|
7445
|
-
/**
|
|
7446
|
-
* 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
|
|
7447
|
-
*/
|
|
6468
|
+
}; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
|
|
7448
6469
|
|
|
7449
6470
|
|
|
7450
6471
|
var isInViewport$1 = function (item) {
|
|
7451
|
-
return isInViewport(item,
|
|
7452
|
-
}; //
|
|
7453
|
-
// 유틸리티 함수: 좌표 변환 캐싱
|
|
7454
|
-
// --------------------------------------------------------------------------
|
|
7455
|
-
|
|
7456
|
-
/**
|
|
7457
|
-
* 폴리곤 좌표 변환 결과를 캐시하고 반환
|
|
7458
|
-
* @param polygonData 폴리곤 데이터
|
|
7459
|
-
* @returns 변환된 좌표 배열 또는 null
|
|
7460
|
-
*/
|
|
6472
|
+
return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
|
|
6473
|
+
}; // 폴리곤 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
|
|
7461
6474
|
|
|
7462
6475
|
|
|
7463
6476
|
var getOrComputePolygonOffsets = function (polygonData) {
|
|
7464
6477
|
var cached = offsetCacheRef.current.get(polygonData.id);
|
|
7465
6478
|
if (cached && Array.isArray(cached)) return cached;
|
|
7466
6479
|
var result = computePolygonOffsets(polygonData, controller);
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
offsetCacheRef.current.set(polygonData.id, result);
|
|
7470
|
-
}
|
|
7471
|
-
|
|
6480
|
+
if (!result) return null;
|
|
6481
|
+
offsetCacheRef.current.set(polygonData.id, result);
|
|
7472
6482
|
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
|
-
*/
|
|
6483
|
+
}; // 폴리곤 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
|
|
7498
6484
|
|
|
7499
6485
|
|
|
7500
6486
|
var computeBoundingBox = function (item) {
|
|
7501
|
-
// 폴리곤: 모든 좌표의 최소/최대값 계산
|
|
7502
6487
|
var offsets = getOrComputePolygonOffsets(item);
|
|
7503
|
-
if (!offsets) return null;
|
|
6488
|
+
if (!offsets) return null; // 모든 좌표를 순회하며 최소/최대값 찾기
|
|
6489
|
+
|
|
7504
6490
|
var minX = Infinity,
|
|
7505
6491
|
minY = Infinity,
|
|
7506
6492
|
maxX = -Infinity,
|
|
@@ -7530,71 +6516,39 @@
|
|
|
7530
6516
|
maxX: maxX,
|
|
7531
6517
|
maxY: maxY
|
|
7532
6518
|
};
|
|
7533
|
-
}; //
|
|
7534
|
-
// 유틸리티 함수: 공간 인덱싱
|
|
7535
|
-
// --------------------------------------------------------------------------
|
|
7536
|
-
|
|
7537
|
-
/**
|
|
7538
|
-
* 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
|
|
7539
|
-
*/
|
|
6519
|
+
}; // 공간 인덱스 빌드 (빠른 Hit Test용)
|
|
7540
6520
|
|
|
7541
6521
|
|
|
7542
6522
|
var buildSpatialIndex$1 = function () {
|
|
7543
6523
|
buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
|
|
7544
|
-
}; //
|
|
7545
|
-
// 렌더링 함수 결정 (dataType에 따라)
|
|
7546
|
-
// --------------------------------------------------------------------------
|
|
7547
|
-
|
|
7548
|
-
/**
|
|
7549
|
-
* 외부 렌더링 함수에 전달할 유틸리티 객체
|
|
7550
|
-
*/
|
|
6524
|
+
}; // 렌더링 유틸리티 객체
|
|
7551
6525
|
|
|
7552
6526
|
|
|
7553
6527
|
var renderUtils = {
|
|
7554
6528
|
getOrComputePolygonOffsets: getOrComputePolygonOffsets,
|
|
7555
6529
|
getOrComputeMarkerOffset: function () {
|
|
7556
6530
|
return null;
|
|
7557
|
-
}
|
|
7558
|
-
|
|
7559
|
-
};
|
|
7560
|
-
/**
|
|
7561
|
-
* 렌더링 함수 생성 (props 기반)
|
|
7562
|
-
*/
|
|
6531
|
+
}
|
|
6532
|
+
}; // 렌더링 함수 생성
|
|
7563
6533
|
|
|
7564
6534
|
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
|
-
*/
|
|
6535
|
+
var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth); // Base Layer 렌더링 (뷰포트 컬링 적용)
|
|
7578
6536
|
|
|
7579
6537
|
var doRenderBase = function () {
|
|
7580
6538
|
var layer = baseLayerRef.current;
|
|
7581
|
-
if (!layer) return;
|
|
7582
|
-
|
|
6539
|
+
if (!layer) return;
|
|
7583
6540
|
var shape = layer.findOne('.base-render-shape');
|
|
7584
6541
|
|
|
7585
6542
|
if (!shape) {
|
|
7586
|
-
// 최초 생성 (한 번만 실행됨)
|
|
7587
|
-
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
7588
6543
|
shape = new Konva__default["default"].Shape({
|
|
7589
6544
|
name: 'base-render-shape',
|
|
7590
6545
|
sceneFunc: function (context, shape) {
|
|
7591
6546
|
var ctx = context;
|
|
7592
|
-
var hovered = hoveredItemRef.current; //
|
|
6547
|
+
var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
|
|
7593
6548
|
|
|
7594
|
-
var visibleItems =
|
|
6549
|
+
var visibleItems = dataRef.current.filter(function (item) {
|
|
7595
6550
|
return isInViewport$1(item);
|
|
7596
|
-
})
|
|
7597
|
-
|
|
6551
|
+
});
|
|
7598
6552
|
renderBase({
|
|
7599
6553
|
ctx: ctx,
|
|
7600
6554
|
items: visibleItems,
|
|
@@ -7608,45 +6562,24 @@
|
|
|
7608
6562
|
hitStrokeWidth: 0
|
|
7609
6563
|
});
|
|
7610
6564
|
layer.add(shape);
|
|
7611
|
-
}
|
|
7612
|
-
|
|
6565
|
+
}
|
|
7613
6566
|
|
|
7614
6567
|
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
|
-
*/
|
|
6568
|
+
}; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
|
|
7630
6569
|
|
|
7631
6570
|
|
|
7632
6571
|
var doRenderEvent = function () {
|
|
7633
6572
|
var layer = eventLayerRef.current;
|
|
7634
|
-
if (!layer) return;
|
|
7635
|
-
|
|
6573
|
+
if (!layer) return;
|
|
7636
6574
|
var shape = layer.findOne('.event-render-shape');
|
|
7637
6575
|
|
|
7638
6576
|
if (!shape) {
|
|
7639
|
-
// 최초 생성 (한 번만 실행됨)
|
|
7640
|
-
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
7641
6577
|
shape = new Konva__default["default"].Shape({
|
|
7642
6578
|
name: 'event-render-shape',
|
|
7643
6579
|
sceneFunc: function (context, shape) {
|
|
7644
|
-
var ctx = context;
|
|
7645
|
-
// 성능 최적화: Array.from 대신 직접 변환 (메모리 할당 최소화)
|
|
7646
|
-
|
|
6580
|
+
var ctx = context;
|
|
7647
6581
|
var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
|
|
7648
|
-
var hovered = hoveredItemRef.current;
|
|
7649
|
-
|
|
6582
|
+
var hovered = hoveredItemRef.current;
|
|
7650
6583
|
renderEvent({
|
|
7651
6584
|
ctx: ctx,
|
|
7652
6585
|
hoveredItem: hovered,
|
|
@@ -7660,21 +6593,10 @@
|
|
|
7660
6593
|
hitStrokeWidth: 0
|
|
7661
6594
|
});
|
|
7662
6595
|
layer.add(shape);
|
|
7663
|
-
}
|
|
7664
|
-
|
|
6596
|
+
}
|
|
7665
6597
|
|
|
7666
6598
|
layer.batchDraw();
|
|
7667
|
-
};
|
|
7668
|
-
/**
|
|
7669
|
-
* 전체 즉시 렌더링 (IDLE 시 호출)
|
|
7670
|
-
*
|
|
7671
|
-
* 뷰포트 업데이트, 공간 인덱스 빌드, 모든 레이어 렌더링을 순차적으로 수행합니다.
|
|
7672
|
-
*
|
|
7673
|
-
* @remarks
|
|
7674
|
-
* - 호출 시점: 지도 이동/줌 완료 시, 데이터 변경 시, 리사이즈 시
|
|
7675
|
-
* - 순서: 뷰포트 업데이트 → 공간 인덱스 빌드 → Base → Event 렌더링
|
|
7676
|
-
* - Animation Layer는 사용하지 않음 (폴리곤 특성)
|
|
7677
|
-
*/
|
|
6599
|
+
}; // 전체 즉시 렌더링
|
|
7678
6600
|
|
|
7679
6601
|
|
|
7680
6602
|
var renderAllImmediate = function () {
|
|
@@ -7682,12 +6604,10 @@
|
|
|
7682
6604
|
buildSpatialIndex$1();
|
|
7683
6605
|
doRenderBase();
|
|
7684
6606
|
doRenderEvent();
|
|
7685
|
-
}; //
|
|
7686
|
-
// 이벤트 핸들러: 지도 이벤트
|
|
7687
|
-
// --------------------------------------------------------------------------
|
|
6607
|
+
}; // 지도 이벤트 핸들러 생성
|
|
7688
6608
|
|
|
7689
6609
|
|
|
7690
|
-
var
|
|
6610
|
+
var _e = createMapEventHandlers({
|
|
7691
6611
|
controller: controller,
|
|
7692
6612
|
containerRef: containerRef,
|
|
7693
6613
|
markerRef: markerRef,
|
|
@@ -7698,49 +6618,32 @@
|
|
|
7698
6618
|
boundingBoxCacheRef: boundingBoxCacheRef,
|
|
7699
6619
|
renderAllImmediate: renderAllImmediate
|
|
7700
6620
|
}),
|
|
7701
|
-
handleIdle =
|
|
7702
|
-
handleZoomStart =
|
|
7703
|
-
handleZoomEnd =
|
|
7704
|
-
handleCenterChanged =
|
|
7705
|
-
handleDragStartShared =
|
|
7706
|
-
handleDragEndShared =
|
|
7707
|
-
/**
|
|
7708
|
-
* 드래그 시작 처리 (커서를 grabbing으로 변경)
|
|
7709
|
-
*/
|
|
7710
|
-
|
|
6621
|
+
handleIdle = _e.handleIdle,
|
|
6622
|
+
handleZoomStart = _e.handleZoomStart,
|
|
6623
|
+
handleZoomEnd = _e.handleZoomEnd,
|
|
6624
|
+
handleCenterChanged = _e.handleCenterChanged,
|
|
6625
|
+
handleDragStartShared = _e.handleDragStart,
|
|
6626
|
+
handleDragEndShared = _e.handleDragEnd;
|
|
7711
6627
|
|
|
7712
6628
|
var handleDragStart = function () {
|
|
7713
6629
|
handleDragStartShared();
|
|
7714
6630
|
draggingRef.current = true;
|
|
7715
6631
|
controller.setMapCursor('grabbing');
|
|
7716
6632
|
};
|
|
7717
|
-
/**
|
|
7718
|
-
* 드래그 종료 처리 (커서를 기본으로 복원)
|
|
7719
|
-
*/
|
|
7720
|
-
|
|
7721
6633
|
|
|
7722
6634
|
var handleDragEnd = function () {
|
|
7723
6635
|
handleDragEndShared();
|
|
7724
6636
|
draggingRef.current = false;
|
|
7725
6637
|
controller.setMapCursor('grab');
|
|
7726
|
-
}; //
|
|
7727
|
-
// Hit Test & 상태 관리
|
|
7728
|
-
// --------------------------------------------------------------------------
|
|
7729
|
-
|
|
7730
|
-
/**
|
|
7731
|
-
* 특정 좌표의 폴리곤 데이터 찾기 (Spatial Index 사용)
|
|
7732
|
-
*
|
|
7733
|
-
* @param offset 검사할 좌표
|
|
7734
|
-
* @returns 찾은 폴리곤 데이터 또는 null
|
|
7735
|
-
*/
|
|
6638
|
+
}; // Hit Test: 특정 좌표의 폴리곤 찾기
|
|
7736
6639
|
|
|
7737
6640
|
|
|
7738
6641
|
var findData = function (offset) {
|
|
7739
|
-
//
|
|
7740
|
-
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); //
|
|
6642
|
+
// 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
|
|
6643
|
+
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
|
|
7741
6644
|
|
|
7742
6645
|
for (var i = candidates.length - 1; i >= 0; i--) {
|
|
7743
|
-
var item = candidates[i];
|
|
6646
|
+
var item = candidates[i]; // 정확한 Hit Test: Ray Casting 알고리즘으로 폴리곤 내부 여부 확인
|
|
7744
6647
|
|
|
7745
6648
|
if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
|
|
7746
6649
|
return item;
|
|
@@ -7748,19 +6651,7 @@
|
|
|
7748
6651
|
}
|
|
7749
6652
|
|
|
7750
6653
|
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
|
-
*/
|
|
6654
|
+
}; // Hover 상태 설정 및 렌더링
|
|
7764
6655
|
|
|
7765
6656
|
|
|
7766
6657
|
var setHovered = function (data) {
|
|
@@ -7770,32 +6661,14 @@
|
|
|
7770
6661
|
controller.setMapCursor('grabbing');
|
|
7771
6662
|
} else {
|
|
7772
6663
|
controller.setMapCursor(data ? 'pointer' : 'grab');
|
|
7773
|
-
}
|
|
7774
|
-
|
|
6664
|
+
}
|
|
7775
6665
|
|
|
7776
6666
|
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
|
-
*/
|
|
6667
|
+
}; // 클릭 처리: 선택 상태 업데이트
|
|
7793
6668
|
|
|
7794
6669
|
|
|
7795
6670
|
var handleLocalClick = function (data) {
|
|
7796
|
-
// 1. 선택 상태 업데이트
|
|
7797
6671
|
if (enableMultiSelect) {
|
|
7798
|
-
// 다중 선택: Set과 Map 동시 업데이트
|
|
7799
6672
|
var newSelected = new Set(selectedIdsRef.current);
|
|
7800
6673
|
|
|
7801
6674
|
if (newSelected.has(data.id)) {
|
|
@@ -7808,7 +6681,6 @@
|
|
|
7808
6681
|
|
|
7809
6682
|
selectedIdsRef.current = newSelected;
|
|
7810
6683
|
} else {
|
|
7811
|
-
// 단일 선택: 토글
|
|
7812
6684
|
var newSelected = new Set();
|
|
7813
6685
|
|
|
7814
6686
|
if (!selectedIdsRef.current.has(data.id)) {
|
|
@@ -7820,132 +6692,79 @@
|
|
|
7820
6692
|
}
|
|
7821
6693
|
|
|
7822
6694
|
selectedIdsRef.current = newSelected;
|
|
7823
|
-
}
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
doRenderBase(); // 3. Event Layer 렌더링 (hover 처리)
|
|
6695
|
+
}
|
|
7827
6696
|
|
|
6697
|
+
doRenderBase();
|
|
7828
6698
|
doRenderEvent();
|
|
7829
|
-
}; //
|
|
7830
|
-
// 이벤트 핸들러: UI 이벤트
|
|
7831
|
-
// --------------------------------------------------------------------------
|
|
7832
|
-
|
|
7833
|
-
/**
|
|
7834
|
-
* 클릭 이벤트 처리
|
|
7835
|
-
*
|
|
7836
|
-
* @param event 클릭 이벤트 파라미터
|
|
7837
|
-
*
|
|
7838
|
-
* @remarks
|
|
7839
|
-
* - Context가 있으면 전역 이벤트 핸들러가 처리하므로 스킵
|
|
7840
|
-
* - 상호작용이 비활성화되어 있으면 스킵
|
|
7841
|
-
* - Spatial Index를 사용하여 빠른 Hit Test 수행
|
|
7842
|
-
*/
|
|
6699
|
+
}; // 클릭 이벤트 핸들러
|
|
7843
6700
|
|
|
7844
6701
|
|
|
7845
6702
|
var handleClick = function (event) {
|
|
7846
|
-
if (disableInteractionRef.current) return;
|
|
7847
|
-
|
|
6703
|
+
if (disableInteractionRef.current) return;
|
|
7848
6704
|
var clickedOffset = validateEvent(event, context, controller);
|
|
7849
6705
|
if (!clickedOffset) return;
|
|
7850
6706
|
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
|
-
*/
|
|
6707
|
+
if (!data) return;
|
|
6708
|
+
handleLocalClick(data);
|
|
6709
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
|
|
6710
|
+
}; // 마우스 이동 이벤트 핸들러 (hover 감지)
|
|
7870
6711
|
|
|
7871
6712
|
|
|
7872
6713
|
var handleMouseMove = function (event) {
|
|
7873
|
-
if (disableInteractionRef.current) return;
|
|
7874
|
-
|
|
6714
|
+
if (disableInteractionRef.current) return;
|
|
7875
6715
|
var mouseOffset = validateEvent(event, context, controller);
|
|
7876
6716
|
if (!mouseOffset) return;
|
|
7877
6717
|
var hoveredItem = findData(mouseOffset);
|
|
7878
6718
|
var prevHovered = hoveredItemRef.current;
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
}
|
|
7883
|
-
};
|
|
7884
|
-
/**
|
|
7885
|
-
* 마우스가 canvas를 벗어날 때 hover cleanup
|
|
7886
|
-
*
|
|
7887
|
-
* 맵 영역 밖으로 마우스가 나갔을 때 hover 상태를 초기화합니다.
|
|
7888
|
-
*
|
|
7889
|
-
* @remarks
|
|
7890
|
-
* - 상호작용이 비활성화되어 있으면 스킵
|
|
7891
|
-
* - hover 상태 초기화 및 커서 복원
|
|
7892
|
-
*/
|
|
6719
|
+
if (prevHovered === hoveredItem) return;
|
|
6720
|
+
setHovered(hoveredItem);
|
|
6721
|
+
}; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
|
|
7893
6722
|
|
|
7894
6723
|
|
|
7895
6724
|
var handleMouseLeave = function () {
|
|
7896
|
-
if (disableInteractionRef.current) return;
|
|
7897
|
-
|
|
6725
|
+
if (disableInteractionRef.current) return;
|
|
7898
6726
|
var prevHovered = hoveredItemRef.current;
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
}
|
|
7905
|
-
}; // --------------------------------------------------------------------------
|
|
7906
|
-
// Lifecycle: DOM 초기화
|
|
7907
|
-
// --------------------------------------------------------------------------
|
|
6727
|
+
if (!prevHovered) return;
|
|
6728
|
+
hoveredItemRef.current = null;
|
|
6729
|
+
controller.setMapCursor('grab');
|
|
6730
|
+
doRenderEvent();
|
|
6731
|
+
}; // DOM 초기화
|
|
7908
6732
|
|
|
7909
6733
|
|
|
7910
6734
|
React.useEffect(function () {
|
|
7911
6735
|
divElement.style.width = 'fit-content';
|
|
7912
6736
|
return function () {
|
|
7913
|
-
if (markerRef.current)
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
}
|
|
6737
|
+
if (!markerRef.current) return;
|
|
6738
|
+
controller.clearDrawable(markerRef.current);
|
|
6739
|
+
markerRef.current = undefined;
|
|
7917
6740
|
};
|
|
7918
|
-
}, []); //
|
|
7919
|
-
// Lifecycle: 마커 생성/업데이트
|
|
7920
|
-
// --------------------------------------------------------------------------
|
|
6741
|
+
}, []); // 마커 생성/업데이트
|
|
7921
6742
|
|
|
7922
6743
|
React.useEffect(function () {
|
|
7923
|
-
if (options)
|
|
7924
|
-
|
|
6744
|
+
if (!options) return;
|
|
6745
|
+
var bounds = controller.getCurrBounds();
|
|
7925
6746
|
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
6747
|
+
var markerOptions = tslib.__assign({
|
|
6748
|
+
position: bounds.nw
|
|
6749
|
+
}, options);
|
|
7929
6750
|
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
markerRef.current.element = divElement;
|
|
7935
|
-
controller.createMarker(markerRef.current);
|
|
6751
|
+
if (markerRef.current) {
|
|
6752
|
+
controller.updateMarker(markerRef.current, markerOptions);
|
|
6753
|
+
return;
|
|
6754
|
+
}
|
|
7936
6755
|
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
6756
|
+
markerRef.current = new Marker(markerOptions);
|
|
6757
|
+
markerRef.current.element = divElement;
|
|
6758
|
+
controller.createMarker(markerRef.current);
|
|
7940
6759
|
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
}
|
|
7944
|
-
}
|
|
6760
|
+
if (divElement.parentElement) {
|
|
6761
|
+
divElement.parentElement.style.pointerEvents = 'none';
|
|
7945
6762
|
}
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
6763
|
+
|
|
6764
|
+
if (options.zIndex !== undefined) {
|
|
6765
|
+
controller.setMarkerZIndex(markerRef.current, options.zIndex);
|
|
6766
|
+
}
|
|
6767
|
+
}, [options]); // Konva 초기화 및 이벤트 리스너 등록
|
|
7949
6768
|
|
|
7950
6769
|
React.useEffect(function () {
|
|
7951
6770
|
var mapDiv = controller.mapDivElement;
|
|
@@ -7954,11 +6773,9 @@
|
|
|
7954
6773
|
width: mapDiv.offsetWidth,
|
|
7955
6774
|
height: mapDiv.offsetHeight
|
|
7956
6775
|
});
|
|
7957
|
-
stageRef.current = stage;
|
|
7958
|
-
|
|
6776
|
+
stageRef.current = stage;
|
|
7959
6777
|
var baseLayer = new Konva__default["default"].Layer({
|
|
7960
|
-
listening: false
|
|
7961
|
-
|
|
6778
|
+
listening: false
|
|
7962
6779
|
});
|
|
7963
6780
|
var eventLayer = new Konva__default["default"].Layer({
|
|
7964
6781
|
listening: false
|
|
@@ -7966,13 +6783,11 @@
|
|
|
7966
6783
|
baseLayerRef.current = baseLayer;
|
|
7967
6784
|
eventLayerRef.current = eventLayer;
|
|
7968
6785
|
stage.add(baseLayer);
|
|
7969
|
-
stage.add(eventLayer);
|
|
7970
|
-
|
|
7971
|
-
updateViewport$1(); // ResizeObserver (맵 크기 변경 감지)
|
|
6786
|
+
stage.add(eventLayer);
|
|
6787
|
+
updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
|
|
7972
6788
|
|
|
7973
6789
|
var resizeRafId = null;
|
|
7974
6790
|
var resizeObserver = new ResizeObserver(function () {
|
|
7975
|
-
// RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
|
|
7976
6791
|
if (resizeRafId !== null) {
|
|
7977
6792
|
cancelAnimationFrame(resizeRafId);
|
|
7978
6793
|
}
|
|
@@ -7995,10 +6810,9 @@
|
|
|
7995
6810
|
controller.addEventListener('CLICK', handleClick);
|
|
7996
6811
|
controller.addEventListener('MOUSEMOVE', handleMouseMove);
|
|
7997
6812
|
controller.addEventListener('DRAGSTART', handleDragStart);
|
|
7998
|
-
controller.addEventListener('DRAGEND', handleDragEnd);
|
|
7999
|
-
|
|
6813
|
+
controller.addEventListener('DRAGEND', handleDragEnd);
|
|
8000
6814
|
mapDiv.addEventListener('mouseleave', handleMouseLeave);
|
|
8001
|
-
renderAllImmediate(); // Context 사용 시 컴포넌트 등록
|
|
6815
|
+
renderAllImmediate(); // Context 사용 시 컴포넌트 등록
|
|
8002
6816
|
|
|
8003
6817
|
var componentInstance = null;
|
|
8004
6818
|
|
|
@@ -8017,22 +6831,17 @@
|
|
|
8017
6831
|
},
|
|
8018
6832
|
isInteractionDisabled: function () {
|
|
8019
6833
|
return disableInteractionRef.current;
|
|
8020
|
-
}
|
|
8021
|
-
|
|
6834
|
+
}
|
|
8022
6835
|
};
|
|
8023
6836
|
context.registerComponent(componentInstance);
|
|
8024
|
-
}
|
|
8025
|
-
|
|
6837
|
+
}
|
|
8026
6838
|
|
|
8027
6839
|
return function () {
|
|
8028
|
-
// RAF 정리
|
|
8029
6840
|
if (resizeRafId !== null) {
|
|
8030
6841
|
cancelAnimationFrame(resizeRafId);
|
|
8031
|
-
}
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
resizeObserver.disconnect(); // 이벤트 리스너 정리
|
|
6842
|
+
}
|
|
8035
6843
|
|
|
6844
|
+
resizeObserver.disconnect();
|
|
8036
6845
|
controller.removeEventListener('IDLE', handleIdle);
|
|
8037
6846
|
controller.removeEventListener('ZOOMSTART', handleZoomStart);
|
|
8038
6847
|
controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
|
|
@@ -8041,57 +6850,41 @@
|
|
|
8041
6850
|
controller.removeEventListener('MOUSEMOVE', handleMouseMove);
|
|
8042
6851
|
controller.removeEventListener('DRAGSTART', handleDragStart);
|
|
8043
6852
|
controller.removeEventListener('DRAGEND', handleDragEnd);
|
|
8044
|
-
mapDiv.removeEventListener('mouseleave', handleMouseLeave);
|
|
6853
|
+
mapDiv.removeEventListener('mouseleave', handleMouseLeave);
|
|
8045
6854
|
|
|
8046
6855
|
if (context && componentInstance) {
|
|
8047
6856
|
context.unregisterComponent(componentInstance);
|
|
8048
|
-
}
|
|
8049
|
-
|
|
6857
|
+
}
|
|
8050
6858
|
|
|
8051
6859
|
baseLayer.destroyChildren();
|
|
8052
6860
|
eventLayer.destroyChildren();
|
|
8053
|
-
stage.destroy();
|
|
8054
|
-
|
|
6861
|
+
stage.destroy();
|
|
8055
6862
|
offsetCacheRef.current.clear();
|
|
8056
6863
|
boundingBoxCacheRef.current.clear();
|
|
8057
6864
|
spatialIndexRef.current.clear();
|
|
8058
6865
|
};
|
|
8059
|
-
}, []); //
|
|
8060
|
-
// --------------------------------------------------------------------------
|
|
8061
|
-
// Lifecycle: disableInteraction 동기화
|
|
8062
|
-
// --------------------------------------------------------------------------
|
|
6866
|
+
}, []); // disableInteraction 동기화
|
|
8063
6867
|
|
|
8064
6868
|
React.useEffect(function () {
|
|
8065
6869
|
disableInteractionRef.current = disableInteraction;
|
|
8066
|
-
}, [disableInteraction]); //
|
|
8067
|
-
// Lifecycle: 외부 selectedItems 동기화
|
|
8068
|
-
// --------------------------------------------------------------------------
|
|
6870
|
+
}, [disableInteraction]); // 외부 selectedItems 동기화
|
|
8069
6871
|
|
|
8070
6872
|
React.useEffect(function () {
|
|
8071
6873
|
if (!stageRef.current) return;
|
|
8072
|
-
syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
|
|
8073
|
-
|
|
6874
|
+
syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
|
|
8074
6875
|
doRenderBase();
|
|
8075
6876
|
doRenderEvent();
|
|
8076
|
-
}, [externalSelectedItems]); //
|
|
8077
|
-
// --------------------------------------------------------------------------
|
|
8078
|
-
// Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
8079
|
-
// --------------------------------------------------------------------------
|
|
6877
|
+
}, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
8080
6878
|
|
|
8081
6879
|
React.useEffect(function () {
|
|
8082
|
-
if (!stageRef.current) return;
|
|
8083
|
-
|
|
8084
|
-
selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
|
|
8085
|
-
|
|
6880
|
+
if (!stageRef.current) return;
|
|
6881
|
+
selectedItemRef.current = externalSelectedItem;
|
|
8086
6882
|
doRenderEvent();
|
|
8087
|
-
}, [externalSelectedItem]); //
|
|
8088
|
-
// Lifecycle: 데이터 변경 시 렌더링
|
|
8089
|
-
// --------------------------------------------------------------------------
|
|
6883
|
+
}, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
|
|
8090
6884
|
|
|
8091
6885
|
React.useEffect(function () {
|
|
8092
|
-
if (!stageRef.current) return;
|
|
8093
|
-
|
|
8094
|
-
dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
|
|
6886
|
+
if (!stageRef.current) return;
|
|
6887
|
+
dataRef.current = data;
|
|
8095
6888
|
|
|
8096
6889
|
if (containerRef.current) {
|
|
8097
6890
|
containerRef.current.style.transform = '';
|
|
@@ -8101,26 +6894,10 @@
|
|
|
8101
6894
|
accumTranslateRef.current = {
|
|
8102
6895
|
x: 0,
|
|
8103
6896
|
y: 0
|
|
8104
|
-
};
|
|
8105
|
-
|
|
6897
|
+
};
|
|
8106
6898
|
offsetCacheRef.current.clear();
|
|
8107
6899
|
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
|
-
|
|
6900
|
+
selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
|
|
8124
6901
|
renderAllImmediate();
|
|
8125
6902
|
}, [data]);
|
|
8126
6903
|
return reactDom.createPortal(React__default["default"].createElement("div", {
|