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