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