@mint-ui/map 1.2.0-test.34 → 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 +19 -12
- package/dist/components/mint-map/core/advanced/shared/context.js +54 -75
- package/dist/components/mint-map/core/advanced/shared/helpers.d.ts +20 -0
- package/dist/components/mint-map/core/advanced/shared/helpers.js +40 -0
- package/dist/components/mint-map/core/advanced/shared/hooks.d.ts +74 -0
- package/dist/components/mint-map/core/advanced/shared/hooks.js +189 -0
- package/dist/components/mint-map/core/advanced/shared/index.d.ts +3 -0
- package/dist/components/mint-map/core/advanced/shared/performance.d.ts +12 -110
- package/dist/components/mint-map/core/advanced/shared/performance.js +56 -151
- 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 +36 -27
- package/dist/components/mint-map/core/advanced/shared/utils.js +58 -52
- package/dist/components/mint-map/core/advanced/shared/viewport.d.ts +42 -0
- package/dist/components/mint-map/core/advanced/shared/viewport.js +51 -0
- package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.d.ts +22 -74
- package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.js +156 -617
- package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.d.ts +26 -76
- package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.js +152 -551
- package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.d.ts +67 -8
- package/dist/components/mint-map/core/advanced/woongCanvasPolygon/renderer.js +81 -20
- package/dist/index.es.js +917 -1575
- package/dist/index.js +11 -0
- package/dist/index.umd.js +923 -1573
- package/package.json +1 -1
package/dist/index.umd.js
CHANGED
|
@@ -656,7 +656,6 @@
|
|
|
656
656
|
|
|
657
657
|
/**
|
|
658
658
|
* 캔버스 데이터 타입 Enum
|
|
659
|
-
* 마커인지 폴리곤인지 구분하는 상수
|
|
660
659
|
*/
|
|
661
660
|
exports.CanvasDataType = void 0;
|
|
662
661
|
|
|
@@ -799,7 +798,11 @@
|
|
|
799
798
|
}();
|
|
800
799
|
|
|
801
800
|
/**
|
|
802
|
-
* 폴리곤
|
|
801
|
+
* 폴리곤 좌표 변환 (위경도 → 화면 좌표)
|
|
802
|
+
*
|
|
803
|
+
* @param polygonData 폴리곤 데이터
|
|
804
|
+
* @param controller MintMapController 인스턴스
|
|
805
|
+
* @returns 변환된 화면 좌표 배열 (4차원 배열) 또는 null
|
|
803
806
|
*/
|
|
804
807
|
|
|
805
808
|
var computePolygonOffsets = function (polygonData, controller) {
|
|
@@ -809,7 +812,7 @@
|
|
|
809
812
|
return null;
|
|
810
813
|
}
|
|
811
814
|
|
|
812
|
-
var result = [];
|
|
815
|
+
var result = []; // GeoJSON MultiPolygon 구조: [MultiPolygon][PolygonGroup][Coordinate][lng, lat]
|
|
813
816
|
|
|
814
817
|
for (var _i = 0, _a = paths.coordinates; _i < _a.length; _i++) {
|
|
815
818
|
var multiPolygon = _a[_i];
|
|
@@ -820,7 +823,8 @@
|
|
|
820
823
|
var polygonOffsets = [];
|
|
821
824
|
|
|
822
825
|
for (var _c = 0, polygonGroup_1 = polygonGroup; _c < polygonGroup_1.length; _c++) {
|
|
823
|
-
var coord = polygonGroup_1[_c];
|
|
826
|
+
var coord = polygonGroup_1[_c]; // GeoJSON은 [lng, lat] 순서이지만 Position은 [lat, lng] 순서
|
|
827
|
+
|
|
824
828
|
var pos = new Position(coord[1], coord[0]);
|
|
825
829
|
var offset = controller.positionToOffset(pos);
|
|
826
830
|
polygonOffsets.push([offset.x, offset.y]);
|
|
@@ -835,30 +839,37 @@
|
|
|
835
839
|
return result;
|
|
836
840
|
};
|
|
837
841
|
/**
|
|
838
|
-
* 마커
|
|
842
|
+
* 마커 좌표 변환 (위경도 → 화면 좌표)
|
|
843
|
+
*
|
|
844
|
+
* @param markerData 마커 데이터
|
|
845
|
+
* @param controller MintMapController 인스턴스
|
|
846
|
+
* @returns 변환된 화면 좌표 또는 null
|
|
839
847
|
*/
|
|
840
848
|
|
|
841
849
|
var computeMarkerOffset = function (markerData, controller) {
|
|
842
|
-
if (!markerData.position)
|
|
843
|
-
return null;
|
|
844
|
-
}
|
|
845
|
-
|
|
850
|
+
if (!markerData.position) return null;
|
|
846
851
|
return controller.positionToOffset(markerData.position);
|
|
847
852
|
};
|
|
848
853
|
/**
|
|
849
|
-
* Point-in-Polygon 알고리즘
|
|
854
|
+
* Point-in-Polygon 알고리즘 (Ray Casting)
|
|
855
|
+
*
|
|
856
|
+
* @param point 확인할 점의 좌표
|
|
857
|
+
* @param polygon 폴리곤 좌표 배열
|
|
858
|
+
* @returns 점이 폴리곤 내부에 있으면 true
|
|
850
859
|
*/
|
|
851
860
|
|
|
852
861
|
var isPointInPolygon = function (point, polygon) {
|
|
862
|
+
// Ray Casting 알고리즘: 점에서 오른쪽으로 무한히 뻗은 선과 폴리곤 변의 교차 횟수로 판단
|
|
853
863
|
var inside = false;
|
|
854
864
|
|
|
855
865
|
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
856
866
|
var xi = polygon[i][0],
|
|
857
867
|
yi = polygon[i][1];
|
|
858
868
|
var xj = polygon[j][0],
|
|
859
|
-
yj = polygon[j][1];
|
|
869
|
+
yj = polygon[j][1]; // 점의 y 좌표가 변의 양 끝점 사이에 있고, 교차점의 x 좌표가 점의 x 좌표보다 큰지 확인
|
|
870
|
+
|
|
860
871
|
var intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
|
|
861
|
-
if (intersect) inside = !inside;
|
|
872
|
+
if (intersect) inside = !inside; // 교차할 때마다 inside 상태 토글
|
|
862
873
|
}
|
|
863
874
|
|
|
864
875
|
return inside;
|
|
@@ -866,23 +877,20 @@
|
|
|
866
877
|
/**
|
|
867
878
|
* 폴리곤 히트 테스트 (도넛 폴리곤 지원)
|
|
868
879
|
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
* 중요: 도넛 폴리곤과 내부 폴리곤은 별개의 polygonData로 처리됨
|
|
874
|
-
* - 도넛 폴리곤 A: isDonutPolygon=true
|
|
875
|
-
* - 내부 폴리곤 B: isDonutPolygon=false (별도 데이터)
|
|
880
|
+
* @param clickedOffset 클릭/마우스 위치 좌표
|
|
881
|
+
* @param polygonData 폴리곤 데이터
|
|
882
|
+
* @param getPolygonOffsets 폴리곤 좌표 변환 함수
|
|
883
|
+
* @returns 점이 폴리곤 내부에 있으면 true
|
|
876
884
|
*/
|
|
877
885
|
|
|
878
886
|
var isPointInPolygonData = function (clickedOffset, polygonData, getPolygonOffsets) {
|
|
879
887
|
var polygonOffsets = getPolygonOffsets(polygonData);
|
|
880
|
-
if (!polygonOffsets) return false; //
|
|
888
|
+
if (!polygonOffsets) return false; // 도넛 폴리곤 처리: 외부 폴리곤 내부에 있으면서 구멍(hole) 내부에 있지 않아야 함
|
|
881
889
|
|
|
882
890
|
if (polygonData.isDonutPolygon) {
|
|
883
891
|
for (var _i = 0, polygonOffsets_1 = polygonOffsets; _i < polygonOffsets_1.length; _i++) {
|
|
884
892
|
var multiPolygon = polygonOffsets_1[_i];
|
|
885
|
-
if (multiPolygon.length === 0) continue; //
|
|
893
|
+
if (multiPolygon.length === 0) continue; // 구멍이 없는 경우 일반 폴리곤과 동일
|
|
886
894
|
|
|
887
895
|
if (multiPolygon.length === 1) {
|
|
888
896
|
if (isPointInPolygon(clickedOffset, multiPolygon[0])) {
|
|
@@ -890,33 +898,30 @@
|
|
|
890
898
|
}
|
|
891
899
|
|
|
892
900
|
continue;
|
|
893
|
-
} //
|
|
901
|
+
} // 외부 폴리곤 내부에 있는지 확인
|
|
894
902
|
|
|
895
903
|
|
|
896
904
|
var outerPolygon = multiPolygon[0];
|
|
897
905
|
|
|
898
906
|
if (!isPointInPolygon(clickedOffset, outerPolygon)) {
|
|
899
|
-
continue;
|
|
900
|
-
} //
|
|
907
|
+
continue;
|
|
908
|
+
} // 구멍 내부에 있으면 false (도넛의 빈 공간)
|
|
901
909
|
|
|
902
910
|
|
|
903
911
|
for (var i = 1; i < multiPolygon.length; i++) {
|
|
904
912
|
var hole = multiPolygon[i];
|
|
905
913
|
|
|
906
914
|
if (isPointInPolygon(clickedOffset, hole)) {
|
|
907
|
-
// ❌ 구멍 안에 있음 → 이 도넛 폴리곤은 히트 안 됨
|
|
908
|
-
// 다른 multiPolygon 체크하지 않고 바로 false 반환
|
|
909
|
-
// (도넛 폴리곤의 구멍 안은 무조건 클릭 불가)
|
|
910
915
|
return false;
|
|
911
916
|
}
|
|
912
|
-
} //
|
|
917
|
+
} // 외부 폴리곤 내부에 있으면서 모든 구멍 밖에 있으면 true
|
|
913
918
|
|
|
914
919
|
|
|
915
920
|
return true;
|
|
916
921
|
}
|
|
917
922
|
|
|
918
923
|
return false;
|
|
919
|
-
} // 일반 폴리곤 처리
|
|
924
|
+
} // 일반 폴리곤 처리
|
|
920
925
|
|
|
921
926
|
|
|
922
927
|
for (var _a = 0, polygonOffsets_2 = polygonOffsets; _a < polygonOffsets_2.length; _a++) {
|
|
@@ -935,12 +940,12 @@
|
|
|
935
940
|
return false;
|
|
936
941
|
};
|
|
937
942
|
/**
|
|
938
|
-
* 마커 히트 테스트 (
|
|
943
|
+
* 마커 히트 테스트 (꼬리 제외)
|
|
939
944
|
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
945
|
+
* @param clickedOffset 클릭/마우스 위치 좌표
|
|
946
|
+
* @param markerData 마커 데이터
|
|
947
|
+
* @param getMarkerOffset 마커 좌표 변환 함수
|
|
948
|
+
* @returns 점이 마커 영역 내부에 있으면 true
|
|
944
949
|
*/
|
|
945
950
|
|
|
946
951
|
var isPointInMarkerData = function (clickedOffset, markerData, getMarkerOffset) {
|
|
@@ -948,41 +953,41 @@
|
|
|
948
953
|
if (!markerOffset) return false;
|
|
949
954
|
var boxWidth = markerData.boxWidth || 50;
|
|
950
955
|
var boxHeight = markerData.boxHeight || 28;
|
|
951
|
-
var tailHeight = markerData.tailHeight || 0; //
|
|
956
|
+
var tailHeight = markerData.tailHeight || 0; // 마커 중심점 기준으로 박스 영역 계산 (꼬리는 제외)
|
|
952
957
|
|
|
953
958
|
var x = markerOffset.x - boxWidth / 2;
|
|
954
|
-
var y = markerOffset.y - boxHeight - tailHeight; //
|
|
959
|
+
var y = markerOffset.y - boxHeight - tailHeight; // 클릭 위치가 박스 영역 내부에 있는지 확인
|
|
955
960
|
|
|
956
961
|
return clickedOffset.x >= x && clickedOffset.x <= x + boxWidth && clickedOffset.y >= y && clickedOffset.y <= y + boxHeight;
|
|
957
|
-
};
|
|
962
|
+
}; // Hex 색상을 RGBA로 변환
|
|
963
|
+
|
|
958
964
|
var hexToRgba = function (hexColor, alpha) {
|
|
959
965
|
if (alpha === void 0) {
|
|
960
966
|
alpha = 1;
|
|
961
|
-
}
|
|
962
|
-
|
|
967
|
+
}
|
|
963
968
|
|
|
964
|
-
var hex = hexColor.replace('#', '');
|
|
969
|
+
var hex = hexColor.replace('#', '');
|
|
965
970
|
|
|
966
|
-
if (hex.length
|
|
967
|
-
|
|
968
|
-
var g = parseInt(hex.substring(2, 4), 16);
|
|
969
|
-
var b = parseInt(hex.substring(4, 6), 16);
|
|
970
|
-
return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
|
|
971
|
+
if (hex.length !== 6) {
|
|
972
|
+
throw new Error('Invalid hex color format');
|
|
971
973
|
}
|
|
972
974
|
|
|
973
|
-
|
|
975
|
+
var r = parseInt(hex.substring(0, 2), 16);
|
|
976
|
+
var g = parseInt(hex.substring(2, 4), 16);
|
|
977
|
+
var b = parseInt(hex.substring(4, 6), 16);
|
|
978
|
+
return "rgba(".concat(r, ", ").concat(g, ", ").concat(b, ", ").concat(alpha, ")");
|
|
974
979
|
};
|
|
975
980
|
var tempCanvas = document.createElement('canvas');
|
|
976
981
|
var tempCtx = tempCanvas.getContext('2d');
|
|
977
982
|
/**
|
|
978
|
-
* 텍스트
|
|
983
|
+
* 텍스트 박스 너비 계산
|
|
979
984
|
*
|
|
980
|
-
* @param
|
|
981
|
-
* @param
|
|
982
|
-
* @param
|
|
983
|
-
* @param
|
|
984
|
-
* @param
|
|
985
|
-
* @returns
|
|
985
|
+
* @param params 파라미터 객체
|
|
986
|
+
* @param params.text 측정할 텍스트
|
|
987
|
+
* @param params.fontConfig 폰트 설정
|
|
988
|
+
* @param params.padding 패딩 값 (px)
|
|
989
|
+
* @param params.minWidth 최소 너비 (px)
|
|
990
|
+
* @returns 계산된 텍스트 박스 너비 (px)
|
|
986
991
|
*/
|
|
987
992
|
|
|
988
993
|
var calculateTextBoxWidth = function (_a) {
|
|
@@ -997,31 +1002,28 @@
|
|
|
997
1002
|
};
|
|
998
1003
|
|
|
999
1004
|
var WoongCanvasContext = React.createContext(null);
|
|
1005
|
+
/**
|
|
1006
|
+
* WoongCanvasProvider 컴포넌트
|
|
1007
|
+
*
|
|
1008
|
+
* 다중 WoongCanvas 인스턴스를 관리하고 zIndex 기반 이벤트 우선순위를 처리합니다.
|
|
1009
|
+
*/
|
|
1010
|
+
|
|
1000
1011
|
var WoongCanvasProvider = function (_a) {
|
|
1001
1012
|
var children = _a.children;
|
|
1002
|
-
var controller = useMintMapController();
|
|
1003
|
-
|
|
1013
|
+
var controller = useMintMapController();
|
|
1004
1014
|
var componentsRef = React.useRef([]);
|
|
1005
1015
|
var currentHoveredRef = React.useRef(null);
|
|
1006
1016
|
var currentHoveredDataRef = React.useRef(null);
|
|
1007
|
-
var draggingRef = React.useRef(false);
|
|
1008
|
-
/**
|
|
1009
|
-
* 컴포넌트 등록 (zIndex 내림차순 정렬)
|
|
1010
|
-
* 높은 zIndex가 먼저 처리됨
|
|
1011
|
-
*/
|
|
1017
|
+
var draggingRef = React.useRef(false); // 컴포넌트 등록 (zIndex 내림차순 정렬)
|
|
1012
1018
|
|
|
1013
1019
|
var registerComponent = React.useCallback(function (instance) {
|
|
1014
1020
|
componentsRef.current.push(instance);
|
|
1015
1021
|
componentsRef.current.sort(function (a, b) {
|
|
1016
1022
|
return b.zIndex - a.zIndex;
|
|
1017
1023
|
});
|
|
1018
|
-
}, []);
|
|
1019
|
-
/**
|
|
1020
|
-
* 컴포넌트 등록 해제
|
|
1021
|
-
*/
|
|
1024
|
+
}, []); // 컴포넌트 등록 해제
|
|
1022
1025
|
|
|
1023
1026
|
var unregisterComponent = React.useCallback(function (instance) {
|
|
1024
|
-
// Hover 중이던 컴포넌트면 초기화
|
|
1025
1027
|
if (currentHoveredRef.current === instance) {
|
|
1026
1028
|
currentHoveredRef.current = null;
|
|
1027
1029
|
currentHoveredDataRef.current = null;
|
|
@@ -1030,99 +1032,77 @@
|
|
|
1030
1032
|
componentsRef.current = componentsRef.current.filter(function (c) {
|
|
1031
1033
|
return c !== instance;
|
|
1032
1034
|
});
|
|
1033
|
-
}, []);
|
|
1034
|
-
/**
|
|
1035
|
-
* 전역 클릭 핸들러 (zIndex 우선순위)
|
|
1036
|
-
*/
|
|
1035
|
+
}, []); // 전역 클릭 핸들러 (zIndex 우선순위)
|
|
1037
1036
|
|
|
1038
1037
|
var handleGlobalClick = React.useCallback(function (event) {
|
|
1039
|
-
var _a;
|
|
1038
|
+
var _a, _b;
|
|
1040
1039
|
|
|
1041
1040
|
if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
|
|
1042
|
-
var clickedOffset = controller.positionToOffset(event.param.position); // zIndex
|
|
1043
|
-
|
|
1044
|
-
for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
|
|
1045
|
-
var component = _b[_i]; // 🚫 상호작용이 비활성화된 컴포넌트는 스킵
|
|
1041
|
+
var clickedOffset = controller.positionToOffset(event.param.position); // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
|
|
1046
1042
|
|
|
1043
|
+
for (var _i = 0, _c = componentsRef.current; _i < _c.length; _i++) {
|
|
1044
|
+
var component = _c[_i];
|
|
1047
1045
|
if (component.isInteractionDisabled()) continue;
|
|
1048
1046
|
var data = component.findData(clickedOffset);
|
|
1047
|
+
if (!data) continue; // 첫 번째로 찾은 항목만 처리하고 종료 (zIndex 우선순위)
|
|
1049
1048
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (component.onClick) {
|
|
1054
|
-
component.onClick(data, component.getSelectedIds());
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
return; // 첫 번째 히트만 처리
|
|
1058
|
-
}
|
|
1049
|
+
component.handleLocalClick(data);
|
|
1050
|
+
(_b = component.onClick) === null || _b === void 0 ? void 0 : _b.call(component, data, component.getSelectedIds());
|
|
1051
|
+
return;
|
|
1059
1052
|
}
|
|
1060
|
-
}, [controller]);
|
|
1061
|
-
/**
|
|
1062
|
-
* 전역 마우스 이동 핸들러 (zIndex 우선순위)
|
|
1063
|
-
*/
|
|
1053
|
+
}, [controller]); // 전역 마우스 이동 핸들러 (zIndex 우선순위)
|
|
1064
1054
|
|
|
1065
1055
|
var handleGlobalMouseMove = React.useCallback(function (event) {
|
|
1066
1056
|
var _a;
|
|
1067
1057
|
|
|
1068
1058
|
if (draggingRef.current || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
|
|
1069
|
-
var mouseOffset = controller.positionToOffset(event.param.position);
|
|
1070
|
-
|
|
1059
|
+
var mouseOffset = controller.positionToOffset(event.param.position);
|
|
1071
1060
|
var newHoveredComponent = null;
|
|
1072
|
-
var newHoveredData = null;
|
|
1061
|
+
var newHoveredData = null; // zIndex 내림차순으로 정렬된 컴포넌트 순회 (높은 zIndex가 먼저 처리)
|
|
1073
1062
|
|
|
1074
1063
|
for (var _i = 0, _b = componentsRef.current; _i < _b.length; _i++) {
|
|
1075
|
-
var component = _b[_i];
|
|
1076
|
-
|
|
1064
|
+
var component = _b[_i];
|
|
1077
1065
|
if (component.isInteractionDisabled()) continue;
|
|
1078
1066
|
var data = component.findData(mouseOffset);
|
|
1067
|
+
if (!data) continue; // 첫 번째로 찾은 항목만 hover 처리 (zIndex 우선순위)
|
|
1079
1068
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
} // Hover 상태 변경 감지 (최적화: 별도 ref로 직접 비교)
|
|
1086
|
-
|
|
1069
|
+
newHoveredComponent = component;
|
|
1070
|
+
newHoveredData = data;
|
|
1071
|
+
break;
|
|
1072
|
+
} // hover 상태가 변경되지 않았으면 종료 (불필요한 렌더링 방지)
|
|
1087
1073
|
|
|
1088
|
-
if (currentHoveredRef.current !== newHoveredComponent || currentHoveredDataRef.current !== newHoveredData) {
|
|
1089
|
-
// 이전 hover 해제
|
|
1090
|
-
if (currentHoveredRef.current) {
|
|
1091
|
-
currentHoveredRef.current.setHovered(null);
|
|
1092
1074
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
} // 새 hover 설정
|
|
1075
|
+
if (currentHoveredRef.current === newHoveredComponent && currentHoveredDataRef.current === newHoveredData) {
|
|
1076
|
+
return;
|
|
1077
|
+
} // 기존 hover 항목에 mouseOut 이벤트 발생
|
|
1097
1078
|
|
|
1098
1079
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1080
|
+
if (currentHoveredRef.current) {
|
|
1081
|
+
currentHoveredRef.current.setHovered(null);
|
|
1101
1082
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1083
|
+
if (currentHoveredRef.current.onMouseOut && currentHoveredDataRef.current) {
|
|
1084
|
+
currentHoveredRef.current.onMouseOut(currentHoveredDataRef.current);
|
|
1105
1085
|
}
|
|
1086
|
+
} // 새 hover 항목에 mouseOver 이벤트 발생
|
|
1106
1087
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1088
|
+
|
|
1089
|
+
if (newHoveredComponent && newHoveredData) {
|
|
1090
|
+
newHoveredComponent.setHovered(newHoveredData);
|
|
1091
|
+
|
|
1092
|
+
if (newHoveredComponent.onMouseOver) {
|
|
1093
|
+
newHoveredComponent.onMouseOver(newHoveredData);
|
|
1094
|
+
}
|
|
1109
1095
|
}
|
|
1110
|
-
}, [controller]);
|
|
1111
|
-
/**
|
|
1112
|
-
* 줌/드래그 시작 (마우스 이동 이벤트 무시)
|
|
1113
|
-
*/
|
|
1114
1096
|
|
|
1097
|
+
currentHoveredRef.current = newHoveredComponent;
|
|
1098
|
+
currentHoveredDataRef.current = newHoveredData;
|
|
1099
|
+
}, [controller]);
|
|
1115
1100
|
var handleZoomStart = React.useCallback(function () {
|
|
1116
1101
|
draggingRef.current = true;
|
|
1117
1102
|
}, []);
|
|
1118
|
-
/**
|
|
1119
|
-
* 지도 idle (마우스 이동 이벤트 재개)
|
|
1120
|
-
*/
|
|
1121
|
-
|
|
1122
1103
|
var handleIdle = React.useCallback(function () {
|
|
1123
1104
|
draggingRef.current = false;
|
|
1124
|
-
}, []);
|
|
1125
|
-
|
|
1105
|
+
}, []);
|
|
1126
1106
|
React.useEffect(function () {
|
|
1127
1107
|
controller.addEventListener('CLICK', handleGlobalClick);
|
|
1128
1108
|
controller.addEventListener('MOUSEMOVE', handleGlobalMouseMove);
|
|
@@ -1134,8 +1114,7 @@
|
|
|
1134
1114
|
controller.removeEventListener('ZOOMSTART', handleZoomStart);
|
|
1135
1115
|
controller.removeEventListener('IDLE', handleIdle);
|
|
1136
1116
|
};
|
|
1137
|
-
}, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]);
|
|
1138
|
-
|
|
1117
|
+
}, [controller, handleGlobalClick, handleGlobalMouseMove, handleZoomStart, handleIdle]);
|
|
1139
1118
|
var contextValue = React.useMemo(function () {
|
|
1140
1119
|
return {
|
|
1141
1120
|
registerComponent: registerComponent,
|
|
@@ -1146,65 +1125,43 @@
|
|
|
1146
1125
|
value: contextValue
|
|
1147
1126
|
}, children);
|
|
1148
1127
|
};
|
|
1128
|
+
/**
|
|
1129
|
+
* WoongCanvas Context Hook
|
|
1130
|
+
*
|
|
1131
|
+
* @returns WoongCanvasContextValue 또는 null (Provider 없으면)
|
|
1132
|
+
*/
|
|
1133
|
+
|
|
1149
1134
|
var useWoongCanvasContext = function () {
|
|
1150
|
-
|
|
1151
|
-
return context;
|
|
1135
|
+
return React.useContext(WoongCanvasContext);
|
|
1152
1136
|
};
|
|
1153
1137
|
|
|
1154
|
-
// ============================================================================
|
|
1155
|
-
// 성능 최적화 상수 (30,000개 마커/폴리곤 기준 최적화)
|
|
1156
|
-
// ============================================================================
|
|
1157
|
-
|
|
1158
1138
|
/**
|
|
1159
|
-
* 공간 인덱스 그리드 셀 크기 (
|
|
1160
|
-
*
|
|
1161
|
-
* 최적값 계산:
|
|
1162
|
-
* - 목표: 클릭 시 셀당 10~30개 항목만 체크 (빠른 Hit Test)
|
|
1163
|
-
* - 화면 크기: 1920×1080 기준
|
|
1164
|
-
* - 30,000개 항목 → 50px 셀 크기 = 약 800개 셀 = 셀당 ~37개
|
|
1165
|
-
*
|
|
1166
|
-
* 성능 비교 (30,000개 기준):
|
|
1167
|
-
* - 200px: 셀당 ~577개 → Hit Test O(577) ❌ 느림
|
|
1168
|
-
* - 50px: 셀당 ~37개 → Hit Test O(37) ✅ 15배 빠름!
|
|
1139
|
+
* 공간 인덱스 그리드 셀 크기 (픽셀 단위)
|
|
1169
1140
|
*
|
|
1170
|
-
*
|
|
1171
|
-
* - 작을수록: Hit Test 빠름, 메모리 사용량 증가
|
|
1172
|
-
* - 클수록: 메모리 효율적, Hit Test 느림
|
|
1141
|
+
* @default 50
|
|
1173
1142
|
*/
|
|
1174
1143
|
var SPATIAL_GRID_CELL_SIZE = 50;
|
|
1175
1144
|
/**
|
|
1176
|
-
* 뷰포트 컬링 여유 공간 (
|
|
1145
|
+
* 뷰포트 컬링 여유 공간 (픽셀 단위)
|
|
1177
1146
|
*
|
|
1178
|
-
*
|
|
1179
|
-
* 30,000개 중 실제 렌더링: 화면에 보이는 1,000~3,000개만
|
|
1147
|
+
* @default 100
|
|
1180
1148
|
*/
|
|
1181
1149
|
|
|
1182
1150
|
var DEFAULT_CULLING_MARGIN = 100;
|
|
1183
1151
|
/**
|
|
1184
1152
|
* LRU 캐시 최대 항목 수
|
|
1185
1153
|
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
* 최적값 계산:
|
|
1189
|
-
* - 전체 항목: 30,000개
|
|
1190
|
-
* - 캐시 크기: 30,000개 → 100% 히트율 (메모리: ~2.4MB)
|
|
1191
|
-
*
|
|
1192
|
-
* 메모리 사용량 (항목당 ~80 bytes):
|
|
1193
|
-
* - 10,000개: ~800KB → 캐시 히트율 33% ❌
|
|
1194
|
-
* - 30,000개: ~2.4MB → 캐시 히트율 100% ✅
|
|
1195
|
-
*
|
|
1196
|
-
* zoom/pan 시 어차피 clear() 호출되므로 메모리 누적 없음
|
|
1154
|
+
* @default 30000
|
|
1197
1155
|
*/
|
|
1198
1156
|
|
|
1199
1157
|
var DEFAULT_MAX_CACHE_SIZE = 30000;
|
|
1200
1158
|
/**
|
|
1201
|
-
* LRU (Least Recently Used)
|
|
1202
|
-
*
|
|
1159
|
+
* LRU Cache (Least Recently Used)
|
|
1160
|
+
*
|
|
1161
|
+
* 좌표 변환 결과를 캐싱하기 위한 캐시 구현
|
|
1203
1162
|
*
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1206
|
-
* 2. set() 버그 수정: 기존 키 업데이트 시 maxSize 체크 로직 개선
|
|
1207
|
-
* 3. 메모리 효율: 단순 FIFO 캐시로 동작하여 오버헤드 최소화
|
|
1163
|
+
* @template K 캐시 키 타입
|
|
1164
|
+
* @template V 캐시 값 타입
|
|
1208
1165
|
*/
|
|
1209
1166
|
|
|
1210
1167
|
var LRUCache =
|
|
@@ -1217,64 +1174,43 @@
|
|
|
1217
1174
|
|
|
1218
1175
|
this.cache = new Map();
|
|
1219
1176
|
this.maxSize = maxSize;
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* 캐시에서 값 조회
|
|
1223
|
-
*
|
|
1224
|
-
* 최적화: delete+set 제거
|
|
1225
|
-
* - 이전: 매번 delete+set으로 LRU 갱신 (해시 재계산 비용)
|
|
1226
|
-
* - 현재: 단순 조회만 수행 (O(1) 해시 조회)
|
|
1227
|
-
*
|
|
1228
|
-
* 트레이드오프:
|
|
1229
|
-
* - 장점: 읽기 성능 대폭 향상 (10,000번 get → 이전보다 2배 빠름)
|
|
1230
|
-
* - 단점: 접근 빈도가 아닌 삽입 순서 기반 eviction (FIFO)
|
|
1231
|
-
*
|
|
1232
|
-
* WoongCanvasMarker 사용 사례에 최적:
|
|
1233
|
-
* - 좌표 변환 결과는 zoom/pan 시 어차피 전체 초기화
|
|
1234
|
-
* - 접근 빈도 추적보다 빠른 조회가 더 중요
|
|
1235
|
-
*/
|
|
1177
|
+
} // 캐시에서 값 조회
|
|
1236
1178
|
|
|
1237
1179
|
|
|
1238
1180
|
LRUCache.prototype.get = function (key) {
|
|
1239
1181
|
return this.cache.get(key);
|
|
1240
|
-
};
|
|
1241
|
-
/**
|
|
1242
|
-
* 캐시에 값 저장 (버그 수정 + 최적화)
|
|
1243
|
-
*
|
|
1244
|
-
* 수정 사항:
|
|
1245
|
-
* 1. 기존 키 업데이트 시 크기 체크 누락 버그 수정
|
|
1246
|
-
* 2. 로직 명확화: 기존 항목/신규 항목 분리 처리
|
|
1247
|
-
*/
|
|
1182
|
+
}; // 캐시에 값 저장 (FIFO eviction)
|
|
1248
1183
|
|
|
1249
1184
|
|
|
1250
1185
|
LRUCache.prototype.set = function (key, value) {
|
|
1251
1186
|
var exists = this.cache.has(key);
|
|
1252
1187
|
|
|
1253
1188
|
if (exists) {
|
|
1254
|
-
// 기존 항목 업데이트: 단순 덮어쓰기 (크기 변화 없음)
|
|
1255
1189
|
this.cache.set(key, value);
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
if (this.cache.size >= this.maxSize) {
|
|
1259
|
-
// 가장 오래된 항목 제거 (Map의 첫 번째 항목)
|
|
1260
|
-
var firstKey = this.cache.keys().next().value;
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1261
1192
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1193
|
+
if (this.cache.size >= this.maxSize) {
|
|
1194
|
+
var firstKey = this.cache.keys().next().value;
|
|
1266
1195
|
|
|
1267
|
-
|
|
1196
|
+
if (firstKey !== undefined) {
|
|
1197
|
+
this.cache.delete(firstKey);
|
|
1198
|
+
}
|
|
1268
1199
|
}
|
|
1269
|
-
|
|
1200
|
+
|
|
1201
|
+
this.cache.set(key, value);
|
|
1202
|
+
}; // 캐시 초기화
|
|
1203
|
+
|
|
1270
1204
|
|
|
1271
1205
|
LRUCache.prototype.clear = function () {
|
|
1272
1206
|
this.cache.clear();
|
|
1273
|
-
};
|
|
1207
|
+
}; // 캐시 크기 반환
|
|
1208
|
+
|
|
1274
1209
|
|
|
1275
1210
|
LRUCache.prototype.size = function () {
|
|
1276
1211
|
return this.cache.size;
|
|
1277
|
-
};
|
|
1212
|
+
}; // 키 존재 여부 확인
|
|
1213
|
+
|
|
1278
1214
|
|
|
1279
1215
|
LRUCache.prototype.has = function (key) {
|
|
1280
1216
|
return this.cache.has(key);
|
|
@@ -1284,16 +1220,10 @@
|
|
|
1284
1220
|
}();
|
|
1285
1221
|
/**
|
|
1286
1222
|
* Spatial Hash Grid (공간 해시 그리드)
|
|
1287
|
-
* 공간 인덱싱을 위한 그리드 기반 자료구조 (개선 버전)
|
|
1288
1223
|
*
|
|
1289
|
-
*
|
|
1290
|
-
* 1. 중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전
|
|
1291
|
-
* 2. 메모리 누수 방지: 기존 항목 자동 제거
|
|
1292
|
-
* 3. 성능 최적화: 불필요한 배열 생성 최소화
|
|
1224
|
+
* 빠른 Hit Test를 위한 그리드 기반 공간 인덱싱 자료구조
|
|
1293
1225
|
*
|
|
1294
|
-
*
|
|
1295
|
-
* - 빠른 Hit Test (마우스 클릭 시 어떤 마커/폴리곤인지 찾기)
|
|
1296
|
-
* - 30,000개 항목 → 클릭 위치 주변 ~10개만 체크 (3,000배 빠름)
|
|
1226
|
+
* @template T 인덱싱할 항목 타입
|
|
1297
1227
|
*/
|
|
1298
1228
|
|
|
1299
1229
|
var SpatialHashGrid =
|
|
@@ -1307,28 +1237,24 @@
|
|
|
1307
1237
|
this.cellSize = cellSize;
|
|
1308
1238
|
this.grid = new Map();
|
|
1309
1239
|
this.itemToCells = new Map();
|
|
1310
|
-
}
|
|
1311
|
-
/**
|
|
1312
|
-
* 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
|
|
1313
|
-
*/
|
|
1240
|
+
} // 셀 키 생성 (x, y 좌표 → 그리드 셀 ID)
|
|
1314
1241
|
|
|
1315
1242
|
|
|
1316
1243
|
SpatialHashGrid.prototype.getCellKey = function (x, y) {
|
|
1244
|
+
// 좌표를 셀 크기로 나눈 몫으로 셀 인덱스 계산
|
|
1317
1245
|
var cellX = Math.floor(x / this.cellSize);
|
|
1318
1246
|
var cellY = Math.floor(y / this.cellSize);
|
|
1319
1247
|
return "".concat(cellX, ",").concat(cellY);
|
|
1320
|
-
};
|
|
1321
|
-
/**
|
|
1322
|
-
* 바운딩 박스가 걸치는 모든 셀 키 배열 반환
|
|
1323
|
-
*/
|
|
1248
|
+
}; // 바운딩 박스가 걸치는 모든 셀 키 배열 반환
|
|
1324
1249
|
|
|
1325
1250
|
|
|
1326
1251
|
SpatialHashGrid.prototype.getCellsForBounds = function (minX, minY, maxX, maxY) {
|
|
1327
|
-
var cells = [];
|
|
1252
|
+
var cells = []; // 바운딩 박스가 걸치는 셀 범위 계산
|
|
1253
|
+
|
|
1328
1254
|
var startCellX = Math.floor(minX / this.cellSize);
|
|
1329
1255
|
var startCellY = Math.floor(minY / this.cellSize);
|
|
1330
1256
|
var endCellX = Math.floor(maxX / this.cellSize);
|
|
1331
|
-
var endCellY = Math.floor(maxY / this.cellSize);
|
|
1257
|
+
var endCellY = Math.floor(maxY / this.cellSize); // 바운딩 박스가 걸치는 모든 셀을 배열에 추가
|
|
1332
1258
|
|
|
1333
1259
|
for (var x = startCellX; x <= endCellX; x++) {
|
|
1334
1260
|
for (var y = startCellY; y <= endCellY; y++) {
|
|
@@ -1337,22 +1263,15 @@
|
|
|
1337
1263
|
}
|
|
1338
1264
|
|
|
1339
1265
|
return cells;
|
|
1340
|
-
};
|
|
1341
|
-
/**
|
|
1342
|
-
* 항목 추가 (바운딩 박스 기반)
|
|
1343
|
-
*
|
|
1344
|
-
* 개선 사항:
|
|
1345
|
-
* - 중복 삽입 방지: 기존 항목이 있으면 먼저 제거 후 재삽입
|
|
1346
|
-
* - 메모리 누수 방지: 이전 셀 참조 완전 제거
|
|
1347
|
-
*/
|
|
1266
|
+
}; // 항목 추가 (바운딩 박스 기반, 중복 삽입 방지)
|
|
1348
1267
|
|
|
1349
1268
|
|
|
1350
1269
|
SpatialHashGrid.prototype.insert = function (item, minX, minY, maxX, maxY) {
|
|
1351
|
-
//
|
|
1352
|
-
this.remove(item); //
|
|
1270
|
+
// 기존 항목 제거 (중복 삽입 방지: 같은 항목을 여러 번 insert 해도 안전)
|
|
1271
|
+
this.remove(item); // 바운딩 박스가 걸치는 모든 셀에 항목 등록
|
|
1353
1272
|
|
|
1354
1273
|
var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
|
|
1355
|
-
this.itemToCells.set(item, cells);
|
|
1274
|
+
this.itemToCells.set(item, cells); // 항목과 셀의 매핑 저장 (제거 시 필요)
|
|
1356
1275
|
|
|
1357
1276
|
for (var _i = 0, cells_1 = cells; _i < cells_1.length; _i++) {
|
|
1358
1277
|
var cell = cells_1[_i];
|
|
@@ -1363,17 +1282,12 @@
|
|
|
1363
1282
|
|
|
1364
1283
|
this.grid.get(cell).push(item);
|
|
1365
1284
|
}
|
|
1366
|
-
};
|
|
1367
|
-
/**
|
|
1368
|
-
* 항목 제거
|
|
1369
|
-
*
|
|
1370
|
-
* 추가된 메서드: 메모리 누수 방지 및 업데이트 지원
|
|
1371
|
-
*/
|
|
1285
|
+
}; // 항목 제거 (모든 셀에서 참조 제거)
|
|
1372
1286
|
|
|
1373
1287
|
|
|
1374
1288
|
SpatialHashGrid.prototype.remove = function (item) {
|
|
1375
1289
|
var prevCells = this.itemToCells.get(item);
|
|
1376
|
-
if (!prevCells) return; //
|
|
1290
|
+
if (!prevCells) return; // 항목이 등록된 모든 셀에서 참조 제거 (메모리 누수 방지)
|
|
1377
1291
|
|
|
1378
1292
|
for (var _i = 0, prevCells_1 = prevCells; _i < prevCells_1.length; _i++) {
|
|
1379
1293
|
var cell = prevCells_1[_i];
|
|
@@ -1384,51 +1298,39 @@
|
|
|
1384
1298
|
|
|
1385
1299
|
if (index !== -1) {
|
|
1386
1300
|
cellItems.splice(index, 1);
|
|
1387
|
-
} // 빈 셀 정리 (메모리
|
|
1301
|
+
} // 빈 셀 정리 (메모리 효율: 사용하지 않는 셀 제거)
|
|
1388
1302
|
|
|
1389
1303
|
|
|
1390
1304
|
if (cellItems.length === 0) {
|
|
1391
1305
|
this.grid.delete(cell);
|
|
1392
1306
|
}
|
|
1393
1307
|
}
|
|
1394
|
-
}
|
|
1308
|
+
} // 항목과 셀의 매핑 제거
|
|
1309
|
+
|
|
1395
1310
|
|
|
1396
1311
|
this.itemToCells.delete(item);
|
|
1397
|
-
};
|
|
1398
|
-
/**
|
|
1399
|
-
* 항목 위치 업데이트
|
|
1400
|
-
*
|
|
1401
|
-
* 추가된 메서드: remove + insert의 편의 함수
|
|
1402
|
-
*/
|
|
1312
|
+
}; // 항목 위치 업데이트 (remove + insert)
|
|
1403
1313
|
|
|
1404
1314
|
|
|
1405
1315
|
SpatialHashGrid.prototype.update = function (item, minX, minY, maxX, maxY) {
|
|
1406
1316
|
this.insert(item, minX, minY, maxX, maxY);
|
|
1407
|
-
};
|
|
1408
|
-
/**
|
|
1409
|
-
* 점 주변의 항목 조회 (1개 셀만)
|
|
1410
|
-
*
|
|
1411
|
-
* 성능: O(해당 셀의 항목 수) - 보통 ~10개
|
|
1412
|
-
*/
|
|
1317
|
+
}; // 점 주변의 항목 조회 (Hit Test용)
|
|
1413
1318
|
|
|
1414
1319
|
|
|
1415
1320
|
SpatialHashGrid.prototype.queryPoint = function (x, y) {
|
|
1321
|
+
// 클릭 위치가 속한 셀의 모든 항목 조회 (O(1) 수준의 빠른 조회)
|
|
1416
1322
|
var cellKey = this.getCellKey(x, y);
|
|
1417
1323
|
var items = this.grid.get(cellKey); // 빈 배열 재사용 (메모리 할당 최소화)
|
|
1418
1324
|
|
|
1419
1325
|
return items || [];
|
|
1420
|
-
};
|
|
1421
|
-
/**
|
|
1422
|
-
* 영역 내 항목 조회
|
|
1423
|
-
*
|
|
1424
|
-
* 성능: O(셀 개수 × 셀당 평균 항목 수)
|
|
1425
|
-
* Set으로 중복 제거 보장
|
|
1426
|
-
*/
|
|
1326
|
+
}; // 영역 내 항목 조회 (Viewport Culling용)
|
|
1427
1327
|
|
|
1428
1328
|
|
|
1429
1329
|
SpatialHashGrid.prototype.queryBounds = function (minX, minY, maxX, maxY) {
|
|
1330
|
+
// 영역이 걸치는 모든 셀 찾기
|
|
1430
1331
|
var cells = this.getCellsForBounds(minX, minY, maxX, maxY);
|
|
1431
|
-
var results = new Set();
|
|
1332
|
+
var results = new Set(); // 중복 제거를 위해 Set 사용
|
|
1333
|
+
// 각 셀의 모든 항목을 결과에 추가
|
|
1432
1334
|
|
|
1433
1335
|
for (var _i = 0, cells_2 = cells; _i < cells_2.length; _i++) {
|
|
1434
1336
|
var cell = cells_2[_i];
|
|
@@ -1437,37 +1339,24 @@
|
|
|
1437
1339
|
if (items) {
|
|
1438
1340
|
for (var _a = 0, items_1 = items; _a < items_1.length; _a++) {
|
|
1439
1341
|
var item = items_1[_a];
|
|
1440
|
-
results.add(item);
|
|
1342
|
+
results.add(item); // Set이므로 중복 자동 제거
|
|
1441
1343
|
}
|
|
1442
1344
|
}
|
|
1443
1345
|
}
|
|
1444
1346
|
|
|
1445
1347
|
return Array.from(results);
|
|
1446
|
-
};
|
|
1447
|
-
/**
|
|
1448
|
-
* 항목 존재 여부 확인
|
|
1449
|
-
*
|
|
1450
|
-
* 추가된 메서드: 빠른 존재 여부 체크
|
|
1451
|
-
*/
|
|
1348
|
+
}; // 항목 존재 여부 확인
|
|
1452
1349
|
|
|
1453
1350
|
|
|
1454
1351
|
SpatialHashGrid.prototype.has = function (item) {
|
|
1455
1352
|
return this.itemToCells.has(item);
|
|
1456
|
-
};
|
|
1457
|
-
/**
|
|
1458
|
-
* 전체 초기화
|
|
1459
|
-
*/
|
|
1353
|
+
}; // 전체 초기화
|
|
1460
1354
|
|
|
1461
1355
|
|
|
1462
1356
|
SpatialHashGrid.prototype.clear = function () {
|
|
1463
1357
|
this.grid.clear();
|
|
1464
1358
|
this.itemToCells.clear();
|
|
1465
|
-
};
|
|
1466
|
-
/**
|
|
1467
|
-
* 통계 정보
|
|
1468
|
-
*
|
|
1469
|
-
* 개선: totalItems는 실제 고유 항목 수를 정확히 반환
|
|
1470
|
-
*/
|
|
1359
|
+
}; // 통계 정보 반환
|
|
1471
1360
|
|
|
1472
1361
|
|
|
1473
1362
|
SpatialHashGrid.prototype.stats = function () {
|
|
@@ -1485,116 +1374,374 @@
|
|
|
1485
1374
|
return SpatialHashGrid;
|
|
1486
1375
|
}();
|
|
1487
1376
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1377
|
+
/**
|
|
1378
|
+
* 현재 뷰포트 영역 계산
|
|
1379
|
+
*
|
|
1380
|
+
* @param stage Konva Stage 인스턴스
|
|
1381
|
+
* @param cullingMargin 컬링 여유 공간 (px)
|
|
1382
|
+
* @param viewportRef 뷰포트 경계를 저장할 ref
|
|
1383
|
+
*/
|
|
1384
|
+
var updateViewport = function (stage, cullingMargin, viewportRef) {
|
|
1385
|
+
if (!stage) return;
|
|
1386
|
+
viewportRef.current = {
|
|
1387
|
+
minX: -cullingMargin,
|
|
1388
|
+
maxX: stage.width() + cullingMargin,
|
|
1389
|
+
minY: -cullingMargin,
|
|
1390
|
+
maxY: stage.height() + cullingMargin
|
|
1391
|
+
};
|
|
1392
|
+
};
|
|
1393
|
+
/**
|
|
1394
|
+
* 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
|
|
1395
|
+
*
|
|
1396
|
+
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1397
|
+
* @param item 확인할 아이템
|
|
1398
|
+
* @param viewportRef 뷰포트 경계 ref
|
|
1399
|
+
* @param boundingBoxCacheRef 바운딩 박스 캐시 ref
|
|
1400
|
+
* @param computeBoundingBox 바운딩 박스 계산 함수
|
|
1401
|
+
* @returns 뷰포트 안에 있으면 true
|
|
1402
|
+
*/
|
|
1491
1403
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
zoomLevel = _a.zoomLevel,
|
|
1496
|
-
center = _a.center,
|
|
1497
|
-
_c = _a.centerMoveWithPanning,
|
|
1498
|
-
centerMoveWithPanning = _c === void 0 ? false : _c,
|
|
1499
|
-
children = _a.children; //controller
|
|
1404
|
+
var isInViewport = function (item, viewportRef, boundingBoxCacheRef, computeBoundingBox) {
|
|
1405
|
+
if (!viewportRef.current) return true;
|
|
1406
|
+
var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
|
|
1500
1407
|
|
|
1501
|
-
var
|
|
1408
|
+
var bbox = boundingBoxCacheRef.current.get(item.id);
|
|
1502
1409
|
|
|
1503
|
-
|
|
1410
|
+
if (!bbox) {
|
|
1411
|
+
// 바운딩 박스 계산 (공통 함수 사용)
|
|
1412
|
+
var computed = computeBoundingBox(item);
|
|
1413
|
+
if (!computed) return false;
|
|
1414
|
+
bbox = computed;
|
|
1415
|
+
boundingBoxCacheRef.current.set(item.id, bbox);
|
|
1416
|
+
} // 바운딩 박스와 viewport 교차 체크
|
|
1504
1417
|
|
|
1505
|
-
var _d = React.useState(false),
|
|
1506
|
-
mapInitialized = _d[0],
|
|
1507
|
-
setMapInitialized = _d[1];
|
|
1508
1418
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
(function () {
|
|
1512
|
-
return tslib.__awaiter(_this, void 0, void 0, function () {
|
|
1513
|
-
var map_1;
|
|
1514
|
-
return tslib.__generator(this, function (_a) {
|
|
1515
|
-
switch (_a.label) {
|
|
1516
|
-
case 0:
|
|
1517
|
-
if (!(elementRef && elementRef.current)) return [3
|
|
1518
|
-
/*break*/
|
|
1519
|
-
, 2];
|
|
1520
|
-
return [4
|
|
1521
|
-
/*yield*/
|
|
1522
|
-
, controller.initializingMap(elementRef.current)];
|
|
1419
|
+
return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
|
|
1420
|
+
};
|
|
1523
1421
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1422
|
+
/**
|
|
1423
|
+
* 지도 이벤트 핸들러 생성 함수
|
|
1424
|
+
*
|
|
1425
|
+
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1426
|
+
* @param deps 이벤트 핸들러 생성에 필요한 의존성
|
|
1427
|
+
* @returns 지도 이벤트 핸들러 객체
|
|
1428
|
+
*/
|
|
1526
1429
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1430
|
+
var createMapEventHandlers = function (deps) {
|
|
1431
|
+
var controller = deps.controller,
|
|
1432
|
+
containerRef = deps.containerRef,
|
|
1433
|
+
markerRef = deps.markerRef,
|
|
1434
|
+
options = deps.options,
|
|
1435
|
+
prevCenterOffsetRef = deps.prevCenterOffsetRef,
|
|
1436
|
+
accumTranslateRef = deps.accumTranslateRef,
|
|
1437
|
+
offsetCacheRef = deps.offsetCacheRef,
|
|
1438
|
+
boundingBoxCacheRef = deps.boundingBoxCacheRef,
|
|
1439
|
+
renderAllImmediate = deps.renderAllImmediate; // 지도 이동/줌 완료 시 처리 (캐시 초기화 및 렌더링)
|
|
1529
1440
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1441
|
+
var handleIdle = function () {
|
|
1442
|
+
prevCenterOffsetRef.current = null;
|
|
1443
|
+
accumTranslateRef.current = {
|
|
1444
|
+
x: 0,
|
|
1445
|
+
y: 0
|
|
1446
|
+
}; // 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
|
|
1536
1447
|
|
|
1537
|
-
|
|
1448
|
+
offsetCacheRef.current.clear();
|
|
1449
|
+
boundingBoxCacheRef.current.clear(); // 마커 위치 업데이트
|
|
1538
1450
|
|
|
1539
|
-
|
|
1540
|
-
return [2
|
|
1541
|
-
/*return*/
|
|
1542
|
-
];
|
|
1543
|
-
}
|
|
1544
|
-
});
|
|
1545
|
-
});
|
|
1546
|
-
})();
|
|
1547
|
-
}, [controller, elementRef]); //줌레벨
|
|
1451
|
+
var bounds = controller.getCurrBounds();
|
|
1548
1452
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1453
|
+
var markerOptions = tslib.__assign({
|
|
1454
|
+
position: bounds.nw
|
|
1455
|
+
}, options);
|
|
1552
1456
|
|
|
1553
|
-
|
|
1554
|
-
controller === null || controller === void 0 ? void 0 : controller.setZoomLevel(zoomLevel);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}, [zoomLevel]); //센터
|
|
1457
|
+
markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // transform 제거 전에 새 데이터로 즉시 렌더링 (transform 제거 시 잠깐 빈 화면이 보이는 것 방지)
|
|
1558
1458
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1459
|
+
if (containerRef.current) {
|
|
1460
|
+
containerRef.current.style.transform = '';
|
|
1461
|
+
containerRef.current.style.visibility = '';
|
|
1462
|
+
} // 새 위치에서 렌더링 (캐시는 이미 초기화됨)
|
|
1562
1463
|
|
|
1563
|
-
if (!Position.equals(prevCenter, center)) {
|
|
1564
|
-
centerMoveWithPanning ? controller === null || controller === void 0 ? void 0 : controller.panningTo(center) : controller === null || controller === void 0 ? void 0 : controller.setCenter(center);
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
}, [center]);
|
|
1568
|
-
return React__default["default"].createElement("div", {
|
|
1569
|
-
className: cn$3('mint-map-root')
|
|
1570
|
-
}, mapInitialized && React__default["default"].createElement(WoongCanvasProvider, null, children), React__default["default"].createElement("div", {
|
|
1571
|
-
className: cn$3('mint-map-container'),
|
|
1572
|
-
style: {
|
|
1573
|
-
visibility: visible ? 'inherit' : 'hidden'
|
|
1574
|
-
},
|
|
1575
|
-
ref: elementRef
|
|
1576
|
-
}));
|
|
1577
|
-
}
|
|
1578
1464
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
function () {
|
|
1582
|
-
function AnimationPlayer(drawFunction, fps) {
|
|
1583
|
-
this.prevtime = 0;
|
|
1584
|
-
this.elapsedTime = 0;
|
|
1585
|
-
this.fps = null;
|
|
1586
|
-
this.baseDrawGapTime = null;
|
|
1587
|
-
this.deltaTime = 0;
|
|
1588
|
-
this.playing = false;
|
|
1589
|
-
this.draw = drawFunction;
|
|
1590
|
-
this.fps = fps || null;
|
|
1465
|
+
renderAllImmediate();
|
|
1466
|
+
}; // 줌 시작 시 처리 (일시적으로 숨김)
|
|
1591
1467
|
|
|
1592
|
-
if (fps !== undefined) {
|
|
1593
|
-
this.baseDrawGapTime = 1000 / fps;
|
|
1594
|
-
}
|
|
1595
1468
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1469
|
+
var handleZoomStart = function () {
|
|
1470
|
+
if (!containerRef.current) return;
|
|
1471
|
+
containerRef.current.style.visibility = 'hidden';
|
|
1472
|
+
}; // 줌 종료 시 처리 (다시 표시)
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
var handleZoomEnd = function () {
|
|
1476
|
+
if (!containerRef.current) return;
|
|
1477
|
+
containerRef.current.style.visibility = '';
|
|
1478
|
+
}; // 지도 중심 변경 시 처리 (transform으로 이동 추적, 캐시 유지)
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
var handleCenterChanged = function () {
|
|
1482
|
+
var center = controller.getCurrBounds().getCenter();
|
|
1483
|
+
var curr = controller.positionToOffset(center);
|
|
1484
|
+
var prev = prevCenterOffsetRef.current; // 첫 번째 호출 시 이전 위치 저장만 하고 종료
|
|
1485
|
+
|
|
1486
|
+
if (!prev) {
|
|
1487
|
+
prevCenterOffsetRef.current = {
|
|
1488
|
+
x: curr.x,
|
|
1489
|
+
y: curr.y
|
|
1490
|
+
};
|
|
1491
|
+
return;
|
|
1492
|
+
} // 이전 위치와 현재 위치의 차이 계산 (이동 거리)
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
var dx = prev.x - curr.x;
|
|
1496
|
+
var dy = prev.y - curr.y; // 누적 이동 거리 저장 (transform으로 화면만 이동, 캐시는 유지하여 성능 최적화)
|
|
1497
|
+
|
|
1498
|
+
accumTranslateRef.current = {
|
|
1499
|
+
x: accumTranslateRef.current.x + dx,
|
|
1500
|
+
y: accumTranslateRef.current.y + dy
|
|
1501
|
+
};
|
|
1502
|
+
prevCenterOffsetRef.current = {
|
|
1503
|
+
x: curr.x,
|
|
1504
|
+
y: curr.y
|
|
1505
|
+
}; // CSS transform으로 컨테이너 이동 (캐시된 좌표는 그대로 유지)
|
|
1506
|
+
|
|
1507
|
+
if (containerRef.current) {
|
|
1508
|
+
containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
var handleDragStart = function () {// 커서는 각 컴포넌트에서 처리
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
var handleDragEnd = function () {// 커서는 각 컴포넌트에서 처리
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
return {
|
|
1519
|
+
handleIdle: handleIdle,
|
|
1520
|
+
handleZoomStart: handleZoomStart,
|
|
1521
|
+
handleZoomEnd: handleZoomEnd,
|
|
1522
|
+
handleCenterChanged: handleCenterChanged,
|
|
1523
|
+
handleDragStart: handleDragStart,
|
|
1524
|
+
handleDragEnd: handleDragEnd
|
|
1525
|
+
};
|
|
1526
|
+
};
|
|
1527
|
+
/**
|
|
1528
|
+
* 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
|
|
1529
|
+
*
|
|
1530
|
+
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1531
|
+
* @param data 공간 인덱스에 삽입할 데이터 배열
|
|
1532
|
+
* @param spatialIndex Spatial Hash Grid 인스턴스
|
|
1533
|
+
* @param computeBoundingBox 바운딩 박스 계산 함수
|
|
1534
|
+
*/
|
|
1535
|
+
|
|
1536
|
+
var buildSpatialIndex = function (data, spatialIndex, computeBoundingBox) {
|
|
1537
|
+
spatialIndex.clear();
|
|
1538
|
+
|
|
1539
|
+
for (var _i = 0, data_1 = data; _i < data_1.length; _i++) {
|
|
1540
|
+
var item = data_1[_i];
|
|
1541
|
+
var bbox = computeBoundingBox(item);
|
|
1542
|
+
|
|
1543
|
+
if (bbox) {
|
|
1544
|
+
spatialIndex.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
/**
|
|
1549
|
+
* 선택 상태 동기화 (화면 밖 데이터도 선택 상태 유지)
|
|
1550
|
+
*
|
|
1551
|
+
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1552
|
+
* @param data 최신 데이터 배열
|
|
1553
|
+
* @param selectedIds 선택된 항목 ID Set
|
|
1554
|
+
* @param selectedItemsMap 현재 선택된 항목 Map
|
|
1555
|
+
* @returns 업데이트된 선택된 항목 Map
|
|
1556
|
+
*/
|
|
1557
|
+
|
|
1558
|
+
var syncSelectedItems = function (data, selectedIds, selectedItemsMap) {
|
|
1559
|
+
var dataMap = new Map(data.map(function (m) {
|
|
1560
|
+
return [m.id, m];
|
|
1561
|
+
}));
|
|
1562
|
+
var newSelectedItemsMap = new Map();
|
|
1563
|
+
selectedIds.forEach(function (id) {
|
|
1564
|
+
// 현재 data에 있으면 최신 데이터 사용
|
|
1565
|
+
var currentItem = dataMap.get(id);
|
|
1566
|
+
|
|
1567
|
+
if (currentItem) {
|
|
1568
|
+
newSelectedItemsMap.set(id, currentItem);
|
|
1569
|
+
} else {
|
|
1570
|
+
// 화면 밖이면 기존 데이터 유지
|
|
1571
|
+
var prevItem = selectedItemsMap.get(id);
|
|
1572
|
+
|
|
1573
|
+
if (prevItem) {
|
|
1574
|
+
newSelectedItemsMap.set(id, prevItem);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
return newSelectedItemsMap;
|
|
1579
|
+
};
|
|
1580
|
+
/**
|
|
1581
|
+
* 외부 selectedItems를 내부 상태로 동기화
|
|
1582
|
+
*
|
|
1583
|
+
* @template T 마커/폴리곤 데이터의 추가 속성 타입
|
|
1584
|
+
* @param externalSelectedItems 외부에서 전달된 선택된 항목 배열
|
|
1585
|
+
* @param selectedIdsRef 선택된 ID Set ref
|
|
1586
|
+
* @param selectedItemsMapRef 선택된 항목 Map ref
|
|
1587
|
+
*/
|
|
1588
|
+
|
|
1589
|
+
var syncExternalSelectedItems = function (externalSelectedItems, selectedIdsRef, selectedItemsMapRef) {
|
|
1590
|
+
if (externalSelectedItems === undefined) return;
|
|
1591
|
+
var newSelectedIds = new Set();
|
|
1592
|
+
var newSelectedItemsMap = new Map();
|
|
1593
|
+
externalSelectedItems.forEach(function (item) {
|
|
1594
|
+
newSelectedIds.add(item.id);
|
|
1595
|
+
newSelectedItemsMap.set(item.id, item);
|
|
1596
|
+
});
|
|
1597
|
+
selectedIdsRef.current = newSelectedIds;
|
|
1598
|
+
selectedItemsMapRef.current = newSelectedItemsMap;
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
1602
|
+
* 이벤트 유효성 검증 및 좌표 변환
|
|
1603
|
+
*
|
|
1604
|
+
* @param event 이벤트 파라미터
|
|
1605
|
+
* @param context WoongCanvasContext 인스턴스
|
|
1606
|
+
* @param controller MintMapController 인스턴스
|
|
1607
|
+
* @returns 유효한 화면 좌표 또는 null
|
|
1608
|
+
*/
|
|
1609
|
+
var validateEvent = function (event, context, controller) {
|
|
1610
|
+
var _a;
|
|
1611
|
+
|
|
1612
|
+
if (context) return null;
|
|
1613
|
+
if (!((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return null;
|
|
1614
|
+
|
|
1615
|
+
try {
|
|
1616
|
+
return controller.positionToOffset(event.param.position);
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
console.error('[WoongCanvas] validateEvent error:', error);
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
/**
|
|
1623
|
+
* Map의 values를 배열로 변환
|
|
1624
|
+
*
|
|
1625
|
+
* @template T Map 값의 타입
|
|
1626
|
+
* @param map 변환할 Map
|
|
1627
|
+
* @returns Map의 값 배열
|
|
1628
|
+
*/
|
|
1629
|
+
|
|
1630
|
+
var mapValuesToArray = function (map) {
|
|
1631
|
+
if (map.size === 0) return [];
|
|
1632
|
+
return Array.from(map.values());
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
var cn$3 = classNames__default["default"].bind(styles$1);
|
|
1636
|
+
function MintMapCore(_a) {
|
|
1637
|
+
var _this = this;
|
|
1638
|
+
|
|
1639
|
+
var onLoad = _a.onLoad,
|
|
1640
|
+
_b = _a.visible,
|
|
1641
|
+
visible = _b === void 0 ? true : _b,
|
|
1642
|
+
zoomLevel = _a.zoomLevel,
|
|
1643
|
+
center = _a.center,
|
|
1644
|
+
_c = _a.centerMoveWithPanning,
|
|
1645
|
+
centerMoveWithPanning = _c === void 0 ? false : _c,
|
|
1646
|
+
children = _a.children; //controller
|
|
1647
|
+
|
|
1648
|
+
var controller = useMintMapController(); //맵 초기화
|
|
1649
|
+
|
|
1650
|
+
var elementRef = React.useRef(null);
|
|
1651
|
+
|
|
1652
|
+
var _d = React.useState(false),
|
|
1653
|
+
mapInitialized = _d[0],
|
|
1654
|
+
setMapInitialized = _d[1];
|
|
1655
|
+
|
|
1656
|
+
var currMapInitialized = React.useRef(false);
|
|
1657
|
+
React.useEffect(function () {
|
|
1658
|
+
(function () {
|
|
1659
|
+
return tslib.__awaiter(_this, void 0, void 0, function () {
|
|
1660
|
+
var map_1;
|
|
1661
|
+
return tslib.__generator(this, function (_a) {
|
|
1662
|
+
switch (_a.label) {
|
|
1663
|
+
case 0:
|
|
1664
|
+
if (!(elementRef && elementRef.current)) return [3
|
|
1665
|
+
/*break*/
|
|
1666
|
+
, 2];
|
|
1667
|
+
return [4
|
|
1668
|
+
/*yield*/
|
|
1669
|
+
, controller.initializingMap(elementRef.current)];
|
|
1670
|
+
|
|
1671
|
+
case 1:
|
|
1672
|
+
map_1 = _a.sent();
|
|
1673
|
+
|
|
1674
|
+
if (!currMapInitialized.current) {
|
|
1675
|
+
currMapInitialized.current = true; //onload callback (setTimeout 으로 맵이 초기화 될 텀을 준다. 특히 google map..)
|
|
1676
|
+
|
|
1677
|
+
setTimeout(function () {
|
|
1678
|
+
// console.log('setMapInitialized true');
|
|
1679
|
+
setMapInitialized(true);
|
|
1680
|
+
onLoad && onLoad(map_1, controller);
|
|
1681
|
+
}, 100);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
_a.label = 2;
|
|
1685
|
+
|
|
1686
|
+
case 2:
|
|
1687
|
+
return [2
|
|
1688
|
+
/*return*/
|
|
1689
|
+
];
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
});
|
|
1693
|
+
})();
|
|
1694
|
+
}, [controller, elementRef]); //줌레벨
|
|
1695
|
+
|
|
1696
|
+
React.useEffect(function () {
|
|
1697
|
+
if (zoomLevel && controller && mapInitialized) {
|
|
1698
|
+
var prevZoomLevel = controller === null || controller === void 0 ? void 0 : controller.getZoomLevel();
|
|
1699
|
+
|
|
1700
|
+
if (prevZoomLevel !== zoomLevel) {
|
|
1701
|
+
controller === null || controller === void 0 ? void 0 : controller.setZoomLevel(zoomLevel);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}, [zoomLevel]); //센터
|
|
1705
|
+
|
|
1706
|
+
React.useEffect(function () {
|
|
1707
|
+
if (center && controller && mapInitialized) {
|
|
1708
|
+
var prevCenter = controller.getCenter();
|
|
1709
|
+
|
|
1710
|
+
if (!Position.equals(prevCenter, center)) {
|
|
1711
|
+
centerMoveWithPanning ? controller === null || controller === void 0 ? void 0 : controller.panningTo(center) : controller === null || controller === void 0 ? void 0 : controller.setCenter(center);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}, [center]);
|
|
1715
|
+
return React__default["default"].createElement("div", {
|
|
1716
|
+
className: cn$3('mint-map-root')
|
|
1717
|
+
}, mapInitialized && React__default["default"].createElement(WoongCanvasProvider, null, children), React__default["default"].createElement("div", {
|
|
1718
|
+
className: cn$3('mint-map-container'),
|
|
1719
|
+
style: {
|
|
1720
|
+
visibility: visible ? 'inherit' : 'hidden'
|
|
1721
|
+
},
|
|
1722
|
+
ref: elementRef
|
|
1723
|
+
}));
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
var AnimationPlayer =
|
|
1727
|
+
/** @class */
|
|
1728
|
+
function () {
|
|
1729
|
+
function AnimationPlayer(drawFunction, fps) {
|
|
1730
|
+
this.prevtime = 0;
|
|
1731
|
+
this.elapsedTime = 0;
|
|
1732
|
+
this.fps = null;
|
|
1733
|
+
this.baseDrawGapTime = null;
|
|
1734
|
+
this.deltaTime = 0;
|
|
1735
|
+
this.playing = false;
|
|
1736
|
+
this.draw = drawFunction;
|
|
1737
|
+
this.fps = fps || null;
|
|
1738
|
+
|
|
1739
|
+
if (fps !== undefined) {
|
|
1740
|
+
this.baseDrawGapTime = 1000 / fps;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
this.init();
|
|
1744
|
+
}
|
|
1598
1745
|
|
|
1599
1746
|
AnimationPlayer.prototype.init = function () {
|
|
1600
1747
|
this.deltaTime = 0;
|
|
@@ -5425,9 +5572,6 @@
|
|
|
5425
5572
|
}))));
|
|
5426
5573
|
}
|
|
5427
5574
|
|
|
5428
|
-
// 메인 컴포넌트
|
|
5429
|
-
// ============================================================================
|
|
5430
|
-
|
|
5431
5575
|
var WoongCanvasMarker = function (props) {
|
|
5432
5576
|
var data = props.data,
|
|
5433
5577
|
onClick = props.onClick,
|
|
@@ -5437,232 +5581,88 @@
|
|
|
5437
5581
|
enableMultiSelect = _a === void 0 ? false : _a,
|
|
5438
5582
|
_b = props.topOnHover,
|
|
5439
5583
|
topOnHover = _b === void 0 ? false : _b,
|
|
5440
|
-
_c = props.
|
|
5441
|
-
|
|
5442
|
-
_d = props.
|
|
5443
|
-
|
|
5444
|
-
_e = props.maxCacheSize,
|
|
5445
|
-
maxCacheSize = _e === void 0 ? DEFAULT_MAX_CACHE_SIZE : _e,
|
|
5584
|
+
_c = props.cullingMargin,
|
|
5585
|
+
cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
|
|
5586
|
+
_d = props.maxCacheSize,
|
|
5587
|
+
maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
|
|
5446
5588
|
externalSelectedItems = props.selectedItems,
|
|
5447
5589
|
externalSelectedItem = props.selectedItem,
|
|
5448
|
-
|
|
5449
|
-
disableInteraction =
|
|
5590
|
+
_e = props.disableInteraction,
|
|
5591
|
+
disableInteraction = _e === void 0 ? false : _e,
|
|
5450
5592
|
renderBase = props.renderBase,
|
|
5451
|
-
renderAnimation = props.renderAnimation,
|
|
5452
5593
|
renderEvent = props.renderEvent,
|
|
5453
|
-
options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "
|
|
5454
|
-
// Hooks & Context
|
|
5455
|
-
// --------------------------------------------------------------------------
|
|
5456
|
-
|
|
5594
|
+
options = tslib.__rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderEvent"]);
|
|
5457
5595
|
|
|
5458
5596
|
var controller = useMintMapController();
|
|
5459
5597
|
var context = useWoongCanvasContext();
|
|
5460
|
-
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; //
|
|
5461
|
-
// DOM Refs
|
|
5462
|
-
// --------------------------------------------------------------------------
|
|
5598
|
+
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
|
|
5463
5599
|
|
|
5464
5600
|
var divRef = React.useRef(document.createElement('div'));
|
|
5465
5601
|
var divElement = divRef.current;
|
|
5466
5602
|
var containerRef = React.useRef(null);
|
|
5467
|
-
var markerRef = React.useRef(); //
|
|
5468
|
-
// Konva Refs
|
|
5469
|
-
// --------------------------------------------------------------------------
|
|
5603
|
+
var markerRef = React.useRef(); // Konva Refs
|
|
5470
5604
|
|
|
5471
5605
|
var stageRef = React.useRef(null);
|
|
5472
5606
|
var baseLayerRef = React.useRef(null);
|
|
5473
|
-
var
|
|
5474
|
-
var eventLayerRef = React.useRef(null); // --------------------------------------------------------------------------
|
|
5475
|
-
// Data Refs - 선택 및 Hover 상태 관리
|
|
5476
|
-
// --------------------------------------------------------------------------
|
|
5477
|
-
|
|
5478
|
-
/** data prop을 ref로 추적 (stale closure 방지, useEffect에서 동기화) */
|
|
5479
|
-
|
|
5480
|
-
var dataRef = React.useRef(data); // --------------------------------------------------------------------------
|
|
5481
|
-
// State Refs - 선택 및 Hover 상태 관리
|
|
5482
|
-
// --------------------------------------------------------------------------
|
|
5483
|
-
|
|
5484
|
-
/** 상호작용 비활성화 상태 (Ref로 관리하여 클로저 문제 해결) */
|
|
5607
|
+
var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
|
|
5485
5608
|
|
|
5609
|
+
var dataRef = React.useRef(data);
|
|
5486
5610
|
var disableInteractionRef = React.useRef(disableInteraction);
|
|
5487
|
-
/** 현재 Hover 중인 항목 */
|
|
5488
|
-
|
|
5489
5611
|
var hoveredItemRef = React.useRef(null);
|
|
5490
|
-
/** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
|
|
5491
|
-
|
|
5492
5612
|
var selectedItemRef = React.useRef(externalSelectedItem);
|
|
5493
|
-
/**
|
|
5494
|
-
* 선택된 항목의 ID Set
|
|
5495
|
-
*
|
|
5496
|
-
* 용도:
|
|
5497
|
-
* 1. onClick 콜백에 전달 - onClick(data, selectedIdsRef.current)
|
|
5498
|
-
* 2. 선택 여부 빠른 체크 - selectedIdsRef.current.has(id)
|
|
5499
|
-
* 3. 메모리 효율 - ID만 저장 (작음)
|
|
5500
|
-
*
|
|
5501
|
-
* selectedItemsMapRef와 차이:
|
|
5502
|
-
* - selectedIdsRef: ID만 저장 { "id1", "id2" }
|
|
5503
|
-
* - selectedItemsMapRef: 전체 객체 저장 { id1: {...}, id2: {...} }
|
|
5504
|
-
*
|
|
5505
|
-
* 둘 다 필요: ID만 필요한 곳은 이것, 전체 데이터 필요한 곳은 Map
|
|
5506
|
-
*/
|
|
5507
|
-
|
|
5508
5613
|
var selectedIdsRef = React.useRef(new Set());
|
|
5509
|
-
|
|
5510
|
-
* 선택된 항목의 실제 데이터 Map (핵심 성능 최적화!)
|
|
5511
|
-
*
|
|
5512
|
-
* 목적: doRenderEvent에서 filter() 순회 제거
|
|
5513
|
-
* - 이전: markersRef.current.filter() → O(전체 마커 수)
|
|
5514
|
-
* - 현재: Map.values() → O(선택된 항목 수)
|
|
5515
|
-
*
|
|
5516
|
-
* 성능 개선: 10,000개 중 1개 선택 시
|
|
5517
|
-
* - 이전: 10,000번 체크
|
|
5518
|
-
* - 현재: 1번 접근 (10,000배 빠름!)
|
|
5519
|
-
*/
|
|
5520
|
-
|
|
5521
|
-
var selectedItemsMapRef = React.useRef(new Map()); // --------------------------------------------------------------------------
|
|
5522
|
-
// Drag Refs
|
|
5523
|
-
// --------------------------------------------------------------------------
|
|
5614
|
+
var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
|
|
5524
5615
|
|
|
5525
5616
|
var draggingRef = React.useRef(false);
|
|
5526
5617
|
var prevCenterOffsetRef = React.useRef(null);
|
|
5527
5618
|
var accumTranslateRef = React.useRef({
|
|
5528
5619
|
x: 0,
|
|
5529
5620
|
y: 0
|
|
5530
|
-
}); //
|
|
5531
|
-
// Performance Refs (캐싱 & 최적화)
|
|
5532
|
-
// --------------------------------------------------------------------------
|
|
5533
|
-
|
|
5534
|
-
/** 좌표 변환 결과 LRU 캐시 */
|
|
5621
|
+
}); // 성능 최적화 Refs
|
|
5535
5622
|
|
|
5536
5623
|
var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
|
|
5537
|
-
/** 공간 인덱스 (빠른 Hit Test) */
|
|
5538
|
-
|
|
5539
5624
|
var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
|
|
5540
|
-
/** 바운딩 박스 캐시 (Viewport Culling 최적화) */
|
|
5541
|
-
|
|
5542
5625
|
var boundingBoxCacheRef = React.useRef(new Map());
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
|
|
5546
|
-
// 유틸리티 함수: 뷰포트 관리
|
|
5547
|
-
// --------------------------------------------------------------------------
|
|
5548
|
-
|
|
5549
|
-
/**
|
|
5550
|
-
* 현재 뷰포트 영역 계산
|
|
5551
|
-
*/
|
|
5552
|
-
|
|
5553
|
-
var updateViewport = function () {
|
|
5554
|
-
if (!stageRef.current) return;
|
|
5555
|
-
var stage = stageRef.current;
|
|
5556
|
-
viewportRef.current = {
|
|
5557
|
-
minX: -cullingMargin,
|
|
5558
|
-
maxX: stage.width() + cullingMargin,
|
|
5559
|
-
minY: -cullingMargin,
|
|
5560
|
-
maxY: stage.height() + cullingMargin
|
|
5561
|
-
};
|
|
5562
|
-
};
|
|
5563
|
-
/**
|
|
5564
|
-
* 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
|
|
5565
|
-
*/
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
var isInViewport = function (item) {
|
|
5569
|
-
if (!enableViewportCulling || !viewportRef.current) return true;
|
|
5570
|
-
var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
|
|
5571
|
-
|
|
5572
|
-
var bbox = boundingBoxCacheRef.current.get(item.id);
|
|
5626
|
+
var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling용)
|
|
5573
5627
|
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
if (!computed) return false;
|
|
5578
|
-
bbox = computed;
|
|
5579
|
-
boundingBoxCacheRef.current.set(item.id, bbox);
|
|
5580
|
-
} // 바운딩 박스와 viewport 교차 체크
|
|
5628
|
+
var updateViewport$1 = function () {
|
|
5629
|
+
updateViewport(stageRef.current, cullingMargin, viewportRef);
|
|
5630
|
+
}; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
|
|
5581
5631
|
|
|
5582
5632
|
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
//
|
|
5586
|
-
// --------------------------------------------------------------------------
|
|
5587
|
-
|
|
5588
|
-
/**
|
|
5589
|
-
* 마커 좌표 변환 결과를 캐시하고 반환
|
|
5590
|
-
*
|
|
5591
|
-
* @param markerData 마커 데이터
|
|
5592
|
-
* @returns 변환된 좌표 또는 null
|
|
5593
|
-
*/
|
|
5633
|
+
var isInViewport$1 = function (item) {
|
|
5634
|
+
return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
|
|
5635
|
+
}; // 마커 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
|
|
5594
5636
|
|
|
5595
5637
|
|
|
5596
5638
|
var getOrComputeMarkerOffset = function (markerData) {
|
|
5597
5639
|
var cached = offsetCacheRef.current.get(markerData.id);
|
|
5598
5640
|
if (cached && !Array.isArray(cached)) return cached;
|
|
5599
5641
|
var result = computeMarkerOffset(markerData, controller);
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
offsetCacheRef.current.set(markerData.id, result);
|
|
5603
|
-
}
|
|
5604
|
-
|
|
5642
|
+
if (!result) return null;
|
|
5643
|
+
offsetCacheRef.current.set(markerData.id, result);
|
|
5605
5644
|
return result;
|
|
5606
|
-
}; //
|
|
5607
|
-
// 유틸리티 함수: 바운딩 박스 계산
|
|
5608
|
-
// --------------------------------------------------------------------------
|
|
5609
|
-
|
|
5610
|
-
/**
|
|
5611
|
-
* 마커의 바운딩 박스 계산
|
|
5612
|
-
*
|
|
5613
|
-
* 🎯 마커의 경우:
|
|
5614
|
-
* - boxHeight: 본체만 (Hit Test 영역)
|
|
5615
|
-
* - tailHeight: 꼬리 높이 (Viewport Culling용, 화면에 보이는 전체 영역)
|
|
5616
|
-
*
|
|
5617
|
-
* @param item 마커 데이터
|
|
5618
|
-
* @returns 바운딩 박스 또는 null
|
|
5619
|
-
*/
|
|
5645
|
+
}; // 마커 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
|
|
5620
5646
|
|
|
5621
5647
|
|
|
5622
5648
|
var computeBoundingBox = function (item) {
|
|
5623
|
-
// 마커: 중심점 기준 박스 크기 계산 (꼬리 포함)
|
|
5624
5649
|
var offset = getOrComputeMarkerOffset(item);
|
|
5625
5650
|
if (!offset) return null;
|
|
5626
5651
|
var boxWidth = item.boxWidth || 50;
|
|
5627
5652
|
var boxHeight = item.boxHeight || 28;
|
|
5628
|
-
var tailHeight = item.tailHeight || 0;
|
|
5629
|
-
|
|
5653
|
+
var tailHeight = item.tailHeight || 0;
|
|
5630
5654
|
return {
|
|
5631
5655
|
minX: offset.x - boxWidth / 2,
|
|
5632
5656
|
minY: offset.y - boxHeight - tailHeight,
|
|
5633
5657
|
maxX: offset.x + boxWidth / 2,
|
|
5634
5658
|
maxY: offset.y
|
|
5635
5659
|
};
|
|
5636
|
-
}; //
|
|
5637
|
-
// 유틸리티 함수: 공간 인덱싱
|
|
5638
|
-
// --------------------------------------------------------------------------
|
|
5660
|
+
}; // 공간 인덱스 빌드 (빠른 Hit Test용)
|
|
5639
5661
|
|
|
5640
|
-
/**
|
|
5641
|
-
* 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
|
|
5642
|
-
*/
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
var buildSpatialIndex = function () {
|
|
5646
|
-
var spatial = spatialIndexRef.current;
|
|
5647
|
-
spatial.clear();
|
|
5648
|
-
var currentData = dataRef.current;
|
|
5649
|
-
|
|
5650
|
-
for (var _i = 0, currentData_1 = currentData; _i < currentData_1.length; _i++) {
|
|
5651
|
-
var item = currentData_1[_i]; // 바운딩 박스 계산 (공통 함수 사용)
|
|
5652
|
-
|
|
5653
|
-
var bbox = computeBoundingBox(item);
|
|
5654
|
-
|
|
5655
|
-
if (bbox) {
|
|
5656
|
-
spatial.insert(item, bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
|
|
5657
|
-
}
|
|
5658
|
-
}
|
|
5659
|
-
}; // --------------------------------------------------------------------------
|
|
5660
|
-
// 렌더링 함수 결정 (dataType에 따라)
|
|
5661
|
-
// --------------------------------------------------------------------------
|
|
5662
5662
|
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5663
|
+
var buildSpatialIndex$1 = function () {
|
|
5664
|
+
buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
|
|
5665
|
+
}; // 렌더링 유틸리티 객체
|
|
5666
5666
|
|
|
5667
5667
|
|
|
5668
5668
|
var renderUtils = {
|
|
@@ -5670,48 +5670,29 @@
|
|
|
5670
5670
|
return null;
|
|
5671
5671
|
},
|
|
5672
5672
|
getOrComputeMarkerOffset: getOrComputeMarkerOffset
|
|
5673
|
-
};
|
|
5674
|
-
/** Base Layer에서 사용할 빈 Set (재사용) */
|
|
5675
|
-
|
|
5676
|
-
React.useRef(new Set());
|
|
5677
|
-
/**
|
|
5678
|
-
* Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
|
|
5679
|
-
*
|
|
5680
|
-
* 🔥 최적화:
|
|
5681
|
-
* 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
|
|
5682
|
-
* 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
|
|
5683
|
-
* 3. 클로저로 최신 데이터 참조
|
|
5684
|
-
*
|
|
5685
|
-
* 🎯 topOnHover 지원:
|
|
5686
|
-
* - renderEvent가 없을 때 Base Layer에서 hover 처리 (fallback)
|
|
5687
|
-
* - renderEvent가 있으면 Event Layer에서 처리 (성능 최적화)
|
|
5688
|
-
*/
|
|
5673
|
+
}; // Base Layer 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
|
|
5689
5674
|
|
|
5690
5675
|
var doRenderBase = function () {
|
|
5691
5676
|
var layer = baseLayerRef.current;
|
|
5692
|
-
if (!layer) return;
|
|
5693
|
-
|
|
5677
|
+
if (!layer) return;
|
|
5694
5678
|
var shape = layer.findOne('.base-render-shape');
|
|
5695
5679
|
|
|
5696
5680
|
if (!shape) {
|
|
5697
|
-
// 최초 생성 (한 번만 실행됨)
|
|
5698
|
-
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
5699
5681
|
shape = new Konva__default["default"].Shape({
|
|
5700
5682
|
name: 'base-render-shape',
|
|
5701
5683
|
sceneFunc: function (context, shape) {
|
|
5702
5684
|
var ctx = context;
|
|
5703
|
-
var hovered = hoveredItemRef.current; //
|
|
5685
|
+
var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
|
|
5704
5686
|
|
|
5705
|
-
var visibleItems =
|
|
5706
|
-
return isInViewport(item);
|
|
5707
|
-
})
|
|
5687
|
+
var visibleItems = dataRef.current.filter(function (item) {
|
|
5688
|
+
return isInViewport$1(item);
|
|
5689
|
+
}); // topOnHover 옵션: hover된 항목을 나중에 그려서 최상위에 표시
|
|
5708
5690
|
|
|
5709
5691
|
if (topOnHover && !renderEvent && hovered) {
|
|
5710
|
-
// hover된 항목 제외하고 렌더링
|
|
5711
5692
|
visibleItems = visibleItems.filter(function (item) {
|
|
5712
5693
|
return item.id !== hovered.id;
|
|
5713
5694
|
});
|
|
5714
|
-
} // 일반
|
|
5695
|
+
} // 일반 항목들 먼저 렌더링
|
|
5715
5696
|
|
|
5716
5697
|
|
|
5717
5698
|
renderBase({
|
|
@@ -5720,12 +5701,10 @@
|
|
|
5720
5701
|
selectedIds: selectedIdsRef.current,
|
|
5721
5702
|
hoveredItem: hovered,
|
|
5722
5703
|
utils: renderUtils
|
|
5723
|
-
}); // hover된 항목을
|
|
5704
|
+
}); // hover된 항목을 마지막에 렌더링하여 최상위에 표시
|
|
5724
5705
|
|
|
5725
5706
|
if (topOnHover && !renderEvent && hovered) {
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
if (isHoveredInViewport) {
|
|
5707
|
+
if (isInViewport$1(hovered)) {
|
|
5729
5708
|
renderBase({
|
|
5730
5709
|
ctx: ctx,
|
|
5731
5710
|
items: [hovered],
|
|
@@ -5741,61 +5720,26 @@
|
|
|
5741
5720
|
hitStrokeWidth: 0
|
|
5742
5721
|
});
|
|
5743
5722
|
layer.add(shape);
|
|
5744
|
-
}
|
|
5745
|
-
|
|
5723
|
+
}
|
|
5746
5724
|
|
|
5747
5725
|
layer.batchDraw();
|
|
5748
|
-
};
|
|
5749
|
-
/**
|
|
5750
|
-
* Animation 레이어 렌더링 (선택된 마커 애니메이션)
|
|
5751
|
-
*
|
|
5752
|
-
* 🔥 최적화: sceneFunc 내부에서 최신 items 참조
|
|
5753
|
-
* - 선택 변경 시에만 재생성
|
|
5754
|
-
* - 지도 이동 시에는 기존 Animation 계속 실행
|
|
5755
|
-
*/
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
var doRenderAnimation = function () {
|
|
5759
|
-
if (!renderAnimation) return;
|
|
5760
|
-
var layer = animationLayerRef.current;
|
|
5761
|
-
if (!layer) return;
|
|
5762
|
-
renderAnimation({
|
|
5763
|
-
layer: layer,
|
|
5764
|
-
selectedIds: selectedIdsRef.current,
|
|
5765
|
-
items: dataRef.current,
|
|
5766
|
-
utils: renderUtils
|
|
5767
|
-
});
|
|
5768
|
-
};
|
|
5769
|
-
/**
|
|
5770
|
-
* Event 레이어 렌더링 (hover + 선택 상태 표시)
|
|
5771
|
-
*
|
|
5772
|
-
* 🔥 최적화:
|
|
5773
|
-
* 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
|
|
5774
|
-
* 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
|
|
5775
|
-
* 3. 클로저로 최신 데이터 참조
|
|
5776
|
-
*/
|
|
5726
|
+
}; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
|
|
5777
5727
|
|
|
5778
5728
|
|
|
5779
5729
|
var doRenderEvent = function () {
|
|
5780
5730
|
var layer = eventLayerRef.current;
|
|
5781
|
-
if (!layer) return;
|
|
5782
|
-
if (!renderEvent) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
|
|
5783
|
-
|
|
5731
|
+
if (!layer || !renderEvent) return;
|
|
5784
5732
|
var shape = layer.findOne('.event-render-shape');
|
|
5785
5733
|
|
|
5786
5734
|
if (!shape) {
|
|
5787
|
-
// 최초 생성 (한 번만 실행됨)
|
|
5788
|
-
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
5789
5735
|
shape = new Konva__default["default"].Shape({
|
|
5790
5736
|
name: 'event-render-shape',
|
|
5791
5737
|
sceneFunc: function (context, shape) {
|
|
5792
|
-
var ctx = context;
|
|
5793
|
-
|
|
5794
|
-
var
|
|
5795
|
-
var hovered = hoveredItemRef.current; // topOnHover가 true이면 hover된 항목을 최상단에 렌더링
|
|
5738
|
+
var ctx = context;
|
|
5739
|
+
var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
|
|
5740
|
+
var hovered = hoveredItemRef.current;
|
|
5796
5741
|
|
|
5797
5742
|
if (topOnHover && hovered) {
|
|
5798
|
-
// 1. 먼저 일반 항목들 렌더링 (hover된 항목 제외)
|
|
5799
5743
|
renderEvent({
|
|
5800
5744
|
ctx: ctx,
|
|
5801
5745
|
hoveredItem: null,
|
|
@@ -5804,13 +5748,9 @@
|
|
|
5804
5748
|
return item.id !== hovered.id;
|
|
5805
5749
|
}),
|
|
5806
5750
|
selectedItem: selectedItemRef.current
|
|
5807
|
-
});
|
|
5808
|
-
|
|
5809
|
-
var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
|
|
5751
|
+
});
|
|
5810
5752
|
|
|
5811
|
-
if (
|
|
5812
|
-
// hover된 항목이 선택되어 있다면 hoverSelectedItems에 포함시켜서
|
|
5813
|
-
// renderEvent에서 hover 스타일만 적용되도록 함
|
|
5753
|
+
if (isInViewport$1(hovered)) {
|
|
5814
5754
|
var hoveredIsSelected = selectedItems.some(function (item) {
|
|
5815
5755
|
return item.id === hovered.id;
|
|
5816
5756
|
});
|
|
@@ -5824,7 +5764,6 @@
|
|
|
5824
5764
|
});
|
|
5825
5765
|
}
|
|
5826
5766
|
} else {
|
|
5827
|
-
// topOnHover가 false이거나 hover된 항목이 없으면 일반 렌더링
|
|
5828
5767
|
renderEvent({
|
|
5829
5768
|
ctx: ctx,
|
|
5830
5769
|
hoveredItem: hovered,
|
|
@@ -5839,137 +5778,63 @@
|
|
|
5839
5778
|
hitStrokeWidth: 0
|
|
5840
5779
|
});
|
|
5841
5780
|
layer.add(shape);
|
|
5842
|
-
}
|
|
5843
|
-
|
|
5781
|
+
}
|
|
5844
5782
|
|
|
5845
5783
|
layer.batchDraw();
|
|
5846
|
-
};
|
|
5847
|
-
/**
|
|
5848
|
-
* 전체 즉시 렌더링 (IDLE 시 호출)
|
|
5849
|
-
*/
|
|
5784
|
+
}; // 전체 즉시 렌더링
|
|
5850
5785
|
|
|
5851
5786
|
|
|
5852
5787
|
var renderAllImmediate = function () {
|
|
5853
|
-
updateViewport();
|
|
5854
|
-
buildSpatialIndex();
|
|
5788
|
+
updateViewport$1();
|
|
5789
|
+
buildSpatialIndex$1();
|
|
5855
5790
|
doRenderBase();
|
|
5856
|
-
doRenderAnimation();
|
|
5857
5791
|
doRenderEvent();
|
|
5858
|
-
}; //
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
var markerOptions = tslib.__assign({
|
|
5880
|
-
position: bounds.nw
|
|
5881
|
-
}, options);
|
|
5882
|
-
|
|
5883
|
-
markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
|
|
5884
|
-
|
|
5885
|
-
if (containerRef.current) {
|
|
5886
|
-
containerRef.current.style.transform = '';
|
|
5887
|
-
containerRef.current.style.visibility = '';
|
|
5888
|
-
} // 5. 새 위치에서 렌더링
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
renderAllImmediate();
|
|
5892
|
-
};
|
|
5893
|
-
/**
|
|
5894
|
-
* 줌 시작 시 처리 (일시적으로 숨김)
|
|
5895
|
-
*/
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
var handleZoomStart = function () {
|
|
5899
|
-
if (containerRef.current) {
|
|
5900
|
-
containerRef.current.style.visibility = 'hidden';
|
|
5901
|
-
}
|
|
5902
|
-
};
|
|
5903
|
-
/**
|
|
5904
|
-
* 줌 종료 시 처리 (다시 표시)
|
|
5905
|
-
*/
|
|
5792
|
+
}; // 지도 이벤트 핸들러 생성
|
|
5793
|
+
|
|
5794
|
+
|
|
5795
|
+
var _f = createMapEventHandlers({
|
|
5796
|
+
controller: controller,
|
|
5797
|
+
containerRef: containerRef,
|
|
5798
|
+
markerRef: markerRef,
|
|
5799
|
+
options: options,
|
|
5800
|
+
prevCenterOffsetRef: prevCenterOffsetRef,
|
|
5801
|
+
accumTranslateRef: accumTranslateRef,
|
|
5802
|
+
offsetCacheRef: offsetCacheRef,
|
|
5803
|
+
boundingBoxCacheRef: boundingBoxCacheRef,
|
|
5804
|
+
renderAllImmediate: renderAllImmediate
|
|
5805
|
+
}),
|
|
5806
|
+
handleIdle = _f.handleIdle,
|
|
5807
|
+
handleZoomStart = _f.handleZoomStart,
|
|
5808
|
+
handleZoomEnd = _f.handleZoomEnd,
|
|
5809
|
+
handleCenterChanged = _f.handleCenterChanged,
|
|
5810
|
+
handleDragStartShared = _f.handleDragStart,
|
|
5811
|
+
handleDragEndShared = _f.handleDragEnd;
|
|
5906
5812
|
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
}
|
|
5813
|
+
var handleDragStart = function () {
|
|
5814
|
+
handleDragStartShared();
|
|
5815
|
+
draggingRef.current = true;
|
|
5816
|
+
controller.setMapCursor('grabbing');
|
|
5912
5817
|
};
|
|
5913
|
-
/**
|
|
5914
|
-
* 지도 중심 변경 시 처리 (transform으로 이동 추적)
|
|
5915
|
-
*/
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
var handleCenterChanged = function () {
|
|
5919
|
-
var center = controller.getCurrBounds().getCenter();
|
|
5920
|
-
var curr = controller.positionToOffset(center);
|
|
5921
|
-
var prev = prevCenterOffsetRef.current;
|
|
5922
|
-
|
|
5923
|
-
if (!prev) {
|
|
5924
|
-
prevCenterOffsetRef.current = {
|
|
5925
|
-
x: curr.x,
|
|
5926
|
-
y: curr.y
|
|
5927
|
-
};
|
|
5928
|
-
return;
|
|
5929
|
-
}
|
|
5930
|
-
|
|
5931
|
-
var dx = prev.x - curr.x;
|
|
5932
|
-
var dy = prev.y - curr.y;
|
|
5933
|
-
accumTranslateRef.current = {
|
|
5934
|
-
x: accumTranslateRef.current.x + dx,
|
|
5935
|
-
y: accumTranslateRef.current.y + dy
|
|
5936
|
-
};
|
|
5937
|
-
prevCenterOffsetRef.current = {
|
|
5938
|
-
x: curr.x,
|
|
5939
|
-
y: curr.y
|
|
5940
|
-
};
|
|
5941
|
-
|
|
5942
|
-
if (containerRef.current) {
|
|
5943
|
-
containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
|
|
5944
|
-
}
|
|
5945
|
-
}; // --------------------------------------------------------------------------
|
|
5946
|
-
// Hit Test & 상태 관리
|
|
5947
|
-
// --------------------------------------------------------------------------
|
|
5948
5818
|
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
* - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
|
|
5955
|
-
*
|
|
5956
|
-
* @param offset 검사할 좌표
|
|
5957
|
-
* @returns 찾은 마커 데이터 또는 null
|
|
5958
|
-
*/
|
|
5819
|
+
var handleDragEnd = function () {
|
|
5820
|
+
handleDragEndShared();
|
|
5821
|
+
draggingRef.current = false;
|
|
5822
|
+
controller.setMapCursor('grab');
|
|
5823
|
+
}; // Hit Test: 특정 좌표의 마커 찾기
|
|
5959
5824
|
|
|
5960
5825
|
|
|
5961
5826
|
var findData = function (offset) {
|
|
5962
|
-
// topOnHover
|
|
5827
|
+
// topOnHover 옵션이 켜져 있으면 hover된 항목을 최우선으로 확인
|
|
5963
5828
|
if (topOnHover && hoveredItemRef.current) {
|
|
5964
5829
|
var hovered = hoveredItemRef.current;
|
|
5965
5830
|
|
|
5966
5831
|
if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
|
|
5967
|
-
return hovered;
|
|
5832
|
+
return hovered;
|
|
5968
5833
|
}
|
|
5969
|
-
} //
|
|
5834
|
+
} // 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
|
|
5970
5835
|
|
|
5971
5836
|
|
|
5972
|
-
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); //
|
|
5837
|
+
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
|
|
5973
5838
|
|
|
5974
5839
|
for (var i = candidates.length - 1; i >= 0; i--) {
|
|
5975
5840
|
var item = candidates[i];
|
|
@@ -5980,18 +5845,7 @@
|
|
|
5980
5845
|
}
|
|
5981
5846
|
|
|
5982
5847
|
return null;
|
|
5983
|
-
};
|
|
5984
|
-
/**
|
|
5985
|
-
* Hover 상태 설정 및 레이어 렌더링
|
|
5986
|
-
*
|
|
5987
|
-
* @param data hover된 마커/폴리곤 데이터 또는 null
|
|
5988
|
-
*
|
|
5989
|
-
* 최적화: RAF 제거하여 즉시 렌더링 (16ms 지연 제거)
|
|
5990
|
-
*
|
|
5991
|
-
* 🎯 topOnHover 지원:
|
|
5992
|
-
* - renderEvent가 있으면: Event Layer에서만 처리 (성능 최적화)
|
|
5993
|
-
* - renderEvent가 없고 topOnHover=true면: Base Layer에서 처리
|
|
5994
|
-
*/
|
|
5848
|
+
}; // Hover 상태 설정 및 렌더링
|
|
5995
5849
|
|
|
5996
5850
|
|
|
5997
5851
|
var setHovered = function (data) {
|
|
@@ -6001,32 +5855,18 @@
|
|
|
6001
5855
|
controller.setMapCursor('grabbing');
|
|
6002
5856
|
} else {
|
|
6003
5857
|
controller.setMapCursor(data ? 'pointer' : 'grab');
|
|
6004
|
-
}
|
|
6005
|
-
|
|
5858
|
+
}
|
|
6006
5859
|
|
|
6007
5860
|
if (renderEvent) {
|
|
6008
|
-
// renderEvent가 있으면 Event Layer에서만 처리 (성능 최적화)
|
|
6009
5861
|
doRenderEvent();
|
|
6010
5862
|
} else if (topOnHover) {
|
|
6011
|
-
// renderEvent가 없고 topOnHover가 true면 Base Layer에서 처리
|
|
6012
5863
|
doRenderBase();
|
|
6013
5864
|
}
|
|
6014
|
-
};
|
|
6015
|
-
/**
|
|
6016
|
-
* 클릭 처리 (단일/다중 선택)
|
|
6017
|
-
*
|
|
6018
|
-
* @param data 클릭된 마커/폴리곤 데이터
|
|
6019
|
-
*
|
|
6020
|
-
* 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
|
|
6021
|
-
* - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
|
|
6022
|
-
* - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
|
|
6023
|
-
*/
|
|
5865
|
+
}; // 클릭 처리: 선택 상태 업데이트
|
|
6024
5866
|
|
|
6025
5867
|
|
|
6026
5868
|
var handleLocalClick = function (data) {
|
|
6027
|
-
// 1. 선택 상태 업데이트
|
|
6028
5869
|
if (enableMultiSelect) {
|
|
6029
|
-
// 다중 선택: Set과 Map 동시 업데이트
|
|
6030
5870
|
var newSelected = new Set(selectedIdsRef.current);
|
|
6031
5871
|
|
|
6032
5872
|
if (newSelected.has(data.id)) {
|
|
@@ -6039,7 +5879,6 @@
|
|
|
6039
5879
|
|
|
6040
5880
|
selectedIdsRef.current = newSelected;
|
|
6041
5881
|
} else {
|
|
6042
|
-
// 단일 선택: 토글
|
|
6043
5882
|
var newSelected = new Set();
|
|
6044
5883
|
|
|
6045
5884
|
if (!selectedIdsRef.current.has(data.id)) {
|
|
@@ -6053,153 +5892,80 @@
|
|
|
6053
5892
|
selectedIdsRef.current = newSelected;
|
|
6054
5893
|
}
|
|
6055
5894
|
|
|
6056
|
-
|
|
6057
|
-
// 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
|
|
6058
|
-
doRenderBase(); // 3. Animation Layer 렌더링 (선택된 마커 애니메이션)
|
|
6059
|
-
|
|
6060
|
-
doRenderAnimation();
|
|
6061
|
-
} // 4. Event Layer 렌더링 (hover 처리)
|
|
6062
|
-
|
|
6063
|
-
|
|
5895
|
+
doRenderBase();
|
|
6064
5896
|
doRenderEvent();
|
|
6065
|
-
}; //
|
|
6066
|
-
// 이벤트 핸들러: UI 이벤트
|
|
6067
|
-
// --------------------------------------------------------------------------
|
|
6068
|
-
|
|
6069
|
-
/**
|
|
6070
|
-
* 클릭 이벤트 처리
|
|
6071
|
-
*/
|
|
5897
|
+
}; // 클릭 이벤트 핸들러
|
|
6072
5898
|
|
|
6073
5899
|
|
|
6074
5900
|
var handleClick = function (event) {
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
if (
|
|
6078
|
-
|
|
6079
|
-
if (
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
var data_1 = findData(clickedOffset);
|
|
6084
|
-
|
|
6085
|
-
if (data_1) {
|
|
6086
|
-
handleLocalClick(data_1);
|
|
6087
|
-
|
|
6088
|
-
if (onClick) {
|
|
6089
|
-
onClick(data_1, selectedIdsRef.current);
|
|
6090
|
-
}
|
|
6091
|
-
}
|
|
6092
|
-
} catch (error) {
|
|
6093
|
-
console.error('[WoongCanvasMarker] handleClick error:', error);
|
|
6094
|
-
}
|
|
6095
|
-
};
|
|
6096
|
-
/**
|
|
6097
|
-
* 마우스 이동 이벤트 처리 (hover 감지)
|
|
6098
|
-
*/
|
|
5901
|
+
if (disableInteractionRef.current) return;
|
|
5902
|
+
var clickedOffset = validateEvent(event, context, controller);
|
|
5903
|
+
if (!clickedOffset) return;
|
|
5904
|
+
var data = findData(clickedOffset);
|
|
5905
|
+
if (!data) return;
|
|
5906
|
+
handleLocalClick(data);
|
|
5907
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
|
|
5908
|
+
}; // 마우스 이동 이벤트 핸들러 (hover 감지)
|
|
6099
5909
|
|
|
6100
5910
|
|
|
6101
5911
|
var handleMouseMove = function (event) {
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
if (
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
if (prevHovered !== hoveredItem) {
|
|
6114
|
-
setHovered(hoveredItem);
|
|
6115
|
-
if (prevHovered && onMouseOut) onMouseOut(prevHovered);
|
|
6116
|
-
if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
|
|
6117
|
-
}
|
|
6118
|
-
} catch (error) {
|
|
6119
|
-
console.error('[WoongCanvasMarker] handleMouseMove error:', error);
|
|
6120
|
-
}
|
|
6121
|
-
};
|
|
6122
|
-
/**
|
|
6123
|
-
* 드래그 시작 처리 (커서를 grabbing으로 변경)
|
|
6124
|
-
*/
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
var handleDragStart = function () {
|
|
6128
|
-
draggingRef.current = true;
|
|
6129
|
-
controller.setMapCursor('grabbing');
|
|
6130
|
-
};
|
|
6131
|
-
/**
|
|
6132
|
-
* 드래그 종료 처리 (커서를 기본으로 복원)
|
|
6133
|
-
*/
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
var handleDragEnd = function () {
|
|
6137
|
-
draggingRef.current = false;
|
|
6138
|
-
controller.setMapCursor('grab');
|
|
6139
|
-
};
|
|
6140
|
-
/**
|
|
6141
|
-
* 마우스가 canvas를 벗어날 때 hover cleanup
|
|
6142
|
-
*/
|
|
5912
|
+
if (disableInteractionRef.current) return;
|
|
5913
|
+
var mouseOffset = validateEvent(event, context, controller);
|
|
5914
|
+
if (!mouseOffset) return;
|
|
5915
|
+
var hoveredItem = findData(mouseOffset);
|
|
5916
|
+
var prevHovered = hoveredItemRef.current;
|
|
5917
|
+
if (prevHovered === hoveredItem) return;
|
|
5918
|
+
setHovered(hoveredItem);
|
|
5919
|
+
if (prevHovered) onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
|
|
5920
|
+
if (hoveredItem) onMouseOver === null || onMouseOver === void 0 ? void 0 : onMouseOver(hoveredItem);
|
|
5921
|
+
}; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
|
|
6143
5922
|
|
|
6144
5923
|
|
|
6145
5924
|
var handleMouseLeave = function () {
|
|
6146
|
-
if (disableInteractionRef.current) return;
|
|
6147
|
-
|
|
5925
|
+
if (disableInteractionRef.current) return;
|
|
6148
5926
|
var prevHovered = hoveredItemRef.current;
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
if (onMouseOut) {
|
|
6156
|
-
onMouseOut(prevHovered);
|
|
6157
|
-
}
|
|
6158
|
-
}
|
|
6159
|
-
}; // --------------------------------------------------------------------------
|
|
6160
|
-
// Lifecycle: DOM 초기화
|
|
6161
|
-
// --------------------------------------------------------------------------
|
|
5927
|
+
if (!prevHovered) return;
|
|
5928
|
+
hoveredItemRef.current = null;
|
|
5929
|
+
controller.setMapCursor('grab');
|
|
5930
|
+
doRenderEvent();
|
|
5931
|
+
onMouseOut === null || onMouseOut === void 0 ? void 0 : onMouseOut(prevHovered);
|
|
5932
|
+
}; // DOM 초기화
|
|
6162
5933
|
|
|
6163
5934
|
|
|
6164
5935
|
React.useEffect(function () {
|
|
6165
5936
|
divElement.style.width = 'fit-content';
|
|
6166
5937
|
return function () {
|
|
6167
|
-
if (markerRef.current)
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
}
|
|
5938
|
+
if (!markerRef.current) return;
|
|
5939
|
+
controller.clearDrawable(markerRef.current);
|
|
5940
|
+
markerRef.current = undefined;
|
|
6171
5941
|
};
|
|
6172
|
-
}, []); //
|
|
6173
|
-
// Lifecycle: 마커 생성/업데이트
|
|
6174
|
-
// --------------------------------------------------------------------------
|
|
5942
|
+
}, []); // 마커 생성/업데이트
|
|
6175
5943
|
|
|
6176
5944
|
React.useEffect(function () {
|
|
6177
|
-
if (options)
|
|
6178
|
-
|
|
5945
|
+
if (!options) return;
|
|
5946
|
+
var bounds = controller.getCurrBounds();
|
|
6179
5947
|
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
5948
|
+
var markerOptions = tslib.__assign({
|
|
5949
|
+
position: bounds.nw
|
|
5950
|
+
}, options);
|
|
6183
5951
|
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
markerRef.current.element = divElement;
|
|
6189
|
-
controller.createMarker(markerRef.current);
|
|
5952
|
+
if (markerRef.current) {
|
|
5953
|
+
controller.updateMarker(markerRef.current, markerOptions);
|
|
5954
|
+
return;
|
|
5955
|
+
}
|
|
6190
5956
|
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
5957
|
+
markerRef.current = new Marker(markerOptions);
|
|
5958
|
+
markerRef.current.element = divElement;
|
|
5959
|
+
controller.createMarker(markerRef.current);
|
|
6194
5960
|
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
}
|
|
6198
|
-
}
|
|
5961
|
+
if (divElement.parentElement) {
|
|
5962
|
+
divElement.parentElement.style.pointerEvents = 'none';
|
|
6199
5963
|
}
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
5964
|
+
|
|
5965
|
+
if (options.zIndex !== undefined) {
|
|
5966
|
+
controller.setMarkerZIndex(markerRef.current, options.zIndex);
|
|
5967
|
+
}
|
|
5968
|
+
}, [options]); // Konva 초기화 및 이벤트 리스너 등록
|
|
6203
5969
|
|
|
6204
5970
|
React.useEffect(function () {
|
|
6205
5971
|
var mapDiv = controller.mapDivElement;
|
|
@@ -6208,34 +5974,21 @@
|
|
|
6208
5974
|
width: mapDiv.offsetWidth,
|
|
6209
5975
|
height: mapDiv.offsetHeight
|
|
6210
5976
|
});
|
|
6211
|
-
stageRef.current = stage;
|
|
6212
|
-
|
|
5977
|
+
stageRef.current = stage;
|
|
6213
5978
|
var baseLayer = new Konva__default["default"].Layer({
|
|
6214
|
-
listening: false // 이벤트 리스닝 비활성화로 성능 향상
|
|
6215
|
-
|
|
6216
|
-
});
|
|
6217
|
-
var animationLayer = new Konva__default["default"].Layer({
|
|
6218
5979
|
listening: false
|
|
6219
5980
|
});
|
|
6220
5981
|
var eventLayer = new Konva__default["default"].Layer({
|
|
6221
5982
|
listening: false
|
|
6222
5983
|
});
|
|
6223
5984
|
baseLayerRef.current = baseLayer;
|
|
6224
|
-
animationLayerRef.current = animationLayer;
|
|
6225
5985
|
eventLayerRef.current = eventLayer;
|
|
6226
5986
|
stage.add(baseLayer);
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
stage.add(animationLayer);
|
|
6230
|
-
}
|
|
6231
|
-
|
|
6232
|
-
stage.add(eventLayer); // 초기 뷰포트 설정
|
|
6233
|
-
|
|
6234
|
-
updateViewport(); // ResizeObserver (맵 크기 변경 감지)
|
|
5987
|
+
stage.add(eventLayer);
|
|
5988
|
+
updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
|
|
6235
5989
|
|
|
6236
5990
|
var resizeRafId = null;
|
|
6237
5991
|
var resizeObserver = new ResizeObserver(function () {
|
|
6238
|
-
// RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
|
|
6239
5992
|
if (resizeRafId !== null) {
|
|
6240
5993
|
cancelAnimationFrame(resizeRafId);
|
|
6241
5994
|
}
|
|
@@ -6245,7 +5998,7 @@
|
|
|
6245
5998
|
stage.height(mapDiv.offsetHeight);
|
|
6246
5999
|
offsetCacheRef.current.clear();
|
|
6247
6000
|
boundingBoxCacheRef.current.clear();
|
|
6248
|
-
updateViewport();
|
|
6001
|
+
updateViewport$1();
|
|
6249
6002
|
renderAllImmediate();
|
|
6250
6003
|
resizeRafId = null;
|
|
6251
6004
|
});
|
|
@@ -6258,10 +6011,9 @@
|
|
|
6258
6011
|
controller.addEventListener('CLICK', handleClick);
|
|
6259
6012
|
controller.addEventListener('MOUSEMOVE', handleMouseMove);
|
|
6260
6013
|
controller.addEventListener('DRAGSTART', handleDragStart);
|
|
6261
|
-
controller.addEventListener('DRAGEND', handleDragEnd);
|
|
6262
|
-
|
|
6014
|
+
controller.addEventListener('DRAGEND', handleDragEnd);
|
|
6263
6015
|
mapDiv.addEventListener('mouseleave', handleMouseLeave);
|
|
6264
|
-
renderAllImmediate(); // Context 사용 시 컴포넌트 등록
|
|
6016
|
+
renderAllImmediate(); // Context 사용 시 컴포넌트 등록
|
|
6265
6017
|
|
|
6266
6018
|
var componentInstance = null;
|
|
6267
6019
|
|
|
@@ -6282,22 +6034,17 @@
|
|
|
6282
6034
|
},
|
|
6283
6035
|
isInteractionDisabled: function () {
|
|
6284
6036
|
return disableInteractionRef.current;
|
|
6285
|
-
}
|
|
6286
|
-
|
|
6037
|
+
}
|
|
6287
6038
|
};
|
|
6288
6039
|
context.registerComponent(componentInstance);
|
|
6289
|
-
}
|
|
6290
|
-
|
|
6040
|
+
}
|
|
6291
6041
|
|
|
6292
6042
|
return function () {
|
|
6293
|
-
// RAF 정리
|
|
6294
6043
|
if (resizeRafId !== null) {
|
|
6295
6044
|
cancelAnimationFrame(resizeRafId);
|
|
6296
|
-
}
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
resizeObserver.disconnect(); // 이벤트 리스너 정리
|
|
6045
|
+
}
|
|
6300
6046
|
|
|
6047
|
+
resizeObserver.disconnect();
|
|
6301
6048
|
controller.removeEventListener('IDLE', handleIdle);
|
|
6302
6049
|
controller.removeEventListener('ZOOMSTART', handleZoomStart);
|
|
6303
6050
|
controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
|
|
@@ -6306,69 +6053,41 @@
|
|
|
6306
6053
|
controller.removeEventListener('MOUSEMOVE', handleMouseMove);
|
|
6307
6054
|
controller.removeEventListener('DRAGSTART', handleDragStart);
|
|
6308
6055
|
controller.removeEventListener('DRAGEND', handleDragEnd);
|
|
6309
|
-
mapDiv.removeEventListener('mouseleave', handleMouseLeave);
|
|
6056
|
+
mapDiv.removeEventListener('mouseleave', handleMouseLeave);
|
|
6310
6057
|
|
|
6311
6058
|
if (context && componentInstance) {
|
|
6312
6059
|
context.unregisterComponent(componentInstance);
|
|
6313
|
-
}
|
|
6314
|
-
|
|
6060
|
+
}
|
|
6315
6061
|
|
|
6316
6062
|
baseLayer.destroyChildren();
|
|
6317
|
-
animationLayer.destroyChildren();
|
|
6318
6063
|
eventLayer.destroyChildren();
|
|
6319
|
-
stage.destroy();
|
|
6320
|
-
|
|
6064
|
+
stage.destroy();
|
|
6321
6065
|
offsetCacheRef.current.clear();
|
|
6322
6066
|
boundingBoxCacheRef.current.clear();
|
|
6323
6067
|
spatialIndexRef.current.clear();
|
|
6324
6068
|
};
|
|
6325
|
-
}, []); //
|
|
6326
|
-
// --------------------------------------------------------------------------
|
|
6327
|
-
// Lifecycle: disableInteraction 동기화
|
|
6328
|
-
// --------------------------------------------------------------------------
|
|
6069
|
+
}, []); // disableInteraction 동기화
|
|
6329
6070
|
|
|
6330
6071
|
React.useEffect(function () {
|
|
6331
6072
|
disableInteractionRef.current = disableInteraction;
|
|
6332
|
-
}, [disableInteraction]); //
|
|
6333
|
-
// Lifecycle: 외부 selectedItems 동기화
|
|
6334
|
-
// --------------------------------------------------------------------------
|
|
6073
|
+
}, [disableInteraction]); // 외부 selectedItems 동기화
|
|
6335
6074
|
|
|
6336
6075
|
React.useEffect(function () {
|
|
6337
|
-
if (!stageRef.current) return;
|
|
6338
|
-
|
|
6339
|
-
if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
|
|
6340
|
-
|
|
6341
|
-
var newSelectedIds = new Set();
|
|
6342
|
-
var newSelectedItemsMap = new Map();
|
|
6343
|
-
externalSelectedItems.forEach(function (item) {
|
|
6344
|
-
newSelectedIds.add(item.id);
|
|
6345
|
-
newSelectedItemsMap.set(item.id, item);
|
|
6346
|
-
});
|
|
6347
|
-
selectedIdsRef.current = newSelectedIds;
|
|
6348
|
-
selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
|
|
6349
|
-
|
|
6076
|
+
if (!stageRef.current) return;
|
|
6077
|
+
syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
|
|
6350
6078
|
doRenderBase();
|
|
6351
|
-
doRenderAnimation();
|
|
6352
6079
|
doRenderEvent();
|
|
6353
|
-
}, [externalSelectedItems]); //
|
|
6354
|
-
// --------------------------------------------------------------------------
|
|
6355
|
-
// Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
6356
|
-
// --------------------------------------------------------------------------
|
|
6080
|
+
}, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
6357
6081
|
|
|
6358
6082
|
React.useEffect(function () {
|
|
6359
|
-
if (!stageRef.current) return;
|
|
6360
|
-
|
|
6361
|
-
selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
|
|
6362
|
-
|
|
6083
|
+
if (!stageRef.current) return;
|
|
6084
|
+
selectedItemRef.current = externalSelectedItem;
|
|
6363
6085
|
doRenderEvent();
|
|
6364
|
-
}, [externalSelectedItem]); //
|
|
6365
|
-
// Lifecycle: 데이터 변경 시 렌더링
|
|
6366
|
-
// --------------------------------------------------------------------------
|
|
6086
|
+
}, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
|
|
6367
6087
|
|
|
6368
6088
|
React.useEffect(function () {
|
|
6369
|
-
if (!stageRef.current) return;
|
|
6370
|
-
|
|
6371
|
-
dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
|
|
6089
|
+
if (!stageRef.current) return;
|
|
6090
|
+
dataRef.current = data;
|
|
6372
6091
|
|
|
6373
6092
|
if (containerRef.current) {
|
|
6374
6093
|
containerRef.current.style.transform = '';
|
|
@@ -6378,46 +6097,10 @@
|
|
|
6378
6097
|
accumTranslateRef.current = {
|
|
6379
6098
|
x: 0,
|
|
6380
6099
|
y: 0
|
|
6381
|
-
};
|
|
6382
|
-
|
|
6100
|
+
};
|
|
6383
6101
|
offsetCacheRef.current.clear();
|
|
6384
6102
|
boundingBoxCacheRef.current.clear();
|
|
6385
|
-
|
|
6386
|
-
* 선택 상태 동기화 (최적화 버전)
|
|
6387
|
-
*
|
|
6388
|
-
* data가 변경되면 selectedItemsMapRef도 업데이트 필요
|
|
6389
|
-
* (참조가 바뀌므로 기존 Map의 데이터는 stale 상태)
|
|
6390
|
-
*
|
|
6391
|
-
* 🔥 중요: 화면 밖 데이터도 선택 상태 유지!
|
|
6392
|
-
* - 현재 data에 있으면 최신 데이터로 업데이트
|
|
6393
|
-
* - 없으면 기존 selectedItemsMapRef의 데이터 유지
|
|
6394
|
-
*
|
|
6395
|
-
* 최적화: data를 Map으로 먼저 변환하여 find() 순회 제거
|
|
6396
|
-
* - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
|
|
6397
|
-
*/
|
|
6398
|
-
|
|
6399
|
-
var dataMap = new Map(data.map(function (m) {
|
|
6400
|
-
return [m.id, m];
|
|
6401
|
-
}));
|
|
6402
|
-
var newSelectedItemsMap = new Map();
|
|
6403
|
-
selectedIdsRef.current.forEach(function (id) {
|
|
6404
|
-
// 현재 data에 있으면 최신 데이터 사용
|
|
6405
|
-
var currentItem = dataMap.get(id);
|
|
6406
|
-
|
|
6407
|
-
if (currentItem) {
|
|
6408
|
-
newSelectedItemsMap.set(id, currentItem);
|
|
6409
|
-
} else {
|
|
6410
|
-
// 화면 밖이면 기존 데이터 유지
|
|
6411
|
-
var prevItem = selectedItemsMapRef.current.get(id);
|
|
6412
|
-
|
|
6413
|
-
if (prevItem) {
|
|
6414
|
-
newSelectedItemsMap.set(id, prevItem);
|
|
6415
|
-
}
|
|
6416
|
-
}
|
|
6417
|
-
}); // selectedIdsRef는 그대로 유지 (화면 밖 항목도 선택 상태 유지)
|
|
6418
|
-
|
|
6419
|
-
selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
|
|
6420
|
-
|
|
6103
|
+
selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
|
|
6421
6104
|
renderAllImmediate();
|
|
6422
6105
|
}, [data]);
|
|
6423
6106
|
return reactDom.createPortal(React__default["default"].createElement("div", {
|
|
@@ -6434,10 +6117,35 @@
|
|
|
6434
6117
|
* 폴리곤 렌더링 유틸리티
|
|
6435
6118
|
*
|
|
6436
6119
|
* 이 파일은 폴리곤 렌더링을 위한 헬퍼 함수와 팩토리 함수를 제공합니다.
|
|
6120
|
+
* GeoJSON MultiPolygon 형식을 지원하며, 도넛 폴리곤(구멍이 있는 폴리곤)도 처리할 수 있습니다.
|
|
6437
6121
|
*/
|
|
6438
6122
|
|
|
6439
6123
|
/**
|
|
6440
6124
|
* 폴리곤 그리기 헬퍼 함수 (도넛 폴리곤 지원)
|
|
6125
|
+
*
|
|
6126
|
+
* Canvas 2D Context를 사용하여 폴리곤을 그립니다.
|
|
6127
|
+
* 도넛 폴리곤의 경우 evenodd fill rule을 사용하여 구멍을 처리합니다.
|
|
6128
|
+
*
|
|
6129
|
+
* @param params 폴리곤 그리기 파라미터
|
|
6130
|
+
*
|
|
6131
|
+
* @remarks
|
|
6132
|
+
* - **도넛 폴리곤 처리**:
|
|
6133
|
+
* - 외부 폴리곤과 내부 구멍들을 같은 path에 추가
|
|
6134
|
+
* - `fill('evenodd')`를 사용하여 구멍 뚫기
|
|
6135
|
+
* - **일반 폴리곤 처리**: 각 폴리곤 그룹을 개별적으로 그리기
|
|
6136
|
+
* - **성능**: O(n), n은 폴리곤의 총 좌표 수
|
|
6137
|
+
*
|
|
6138
|
+
* @example
|
|
6139
|
+
* ```typescript
|
|
6140
|
+
* drawPolygon({
|
|
6141
|
+
* ctx,
|
|
6142
|
+
* polygonOffsets: [[[[100, 200], [200, 200], [200, 100], [100, 100]]]],
|
|
6143
|
+
* isDonutPolygon: false,
|
|
6144
|
+
* fillColor: 'rgba(255, 0, 0, 0.5)',
|
|
6145
|
+
* strokeColor: 'rgba(255, 0, 0, 1)',
|
|
6146
|
+
* lineWidth: 2
|
|
6147
|
+
* });
|
|
6148
|
+
* ```
|
|
6441
6149
|
*/
|
|
6442
6150
|
var drawPolygon = function (_a) {
|
|
6443
6151
|
var ctx = _a.ctx,
|
|
@@ -6451,13 +6159,16 @@
|
|
|
6451
6159
|
var multiPolygon = polygonOffsets_1[_i];
|
|
6452
6160
|
|
|
6453
6161
|
if (isDonutPolygon) {
|
|
6454
|
-
|
|
6162
|
+
// 도넛 폴리곤 처리: 외부 폴리곤 + 내부 구멍들을 같은 path에 추가
|
|
6163
|
+
ctx.beginPath(); // 1. 외부 폴리곤 그리기 (첫 번째 폴리곤)
|
|
6455
6164
|
|
|
6456
|
-
|
|
6457
|
-
|
|
6165
|
+
var outerPolygon = multiPolygon[0];
|
|
6166
|
+
|
|
6167
|
+
if (outerPolygon && outerPolygon.length > 0) {
|
|
6168
|
+
ctx.moveTo(outerPolygon[0][0], outerPolygon[0][1]);
|
|
6458
6169
|
|
|
6459
|
-
for (var i = 1; i <
|
|
6460
|
-
ctx.lineTo(
|
|
6170
|
+
for (var i = 1; i < outerPolygon.length; i++) {
|
|
6171
|
+
ctx.lineTo(outerPolygon[i][0], outerPolygon[i][1]);
|
|
6461
6172
|
}
|
|
6462
6173
|
|
|
6463
6174
|
ctx.closePath();
|
|
@@ -6484,7 +6195,7 @@
|
|
|
6484
6195
|
ctx.lineWidth = lineWidth;
|
|
6485
6196
|
ctx.stroke();
|
|
6486
6197
|
} else {
|
|
6487
|
-
// 일반 폴리곤
|
|
6198
|
+
// 일반 폴리곤 처리: 각 폴리곤 그룹을 개별적으로 그리기
|
|
6488
6199
|
for (var _b = 0, multiPolygon_1 = multiPolygon; _b < multiPolygon_1.length; _b++) {
|
|
6489
6200
|
var polygonGroup = multiPolygon_1[_b];
|
|
6490
6201
|
if (!polygonGroup.length) continue;
|
|
@@ -6497,7 +6208,8 @@
|
|
|
6497
6208
|
ctx.lineTo(point[0], point[1]);
|
|
6498
6209
|
}
|
|
6499
6210
|
|
|
6500
|
-
ctx.closePath();
|
|
6211
|
+
ctx.closePath(); // 스타일 설정 및 렌더링
|
|
6212
|
+
|
|
6501
6213
|
ctx.fillStyle = fillColor;
|
|
6502
6214
|
ctx.strokeStyle = strokeColor;
|
|
6503
6215
|
ctx.lineWidth = lineWidth;
|
|
@@ -6508,19 +6220,30 @@
|
|
|
6508
6220
|
}
|
|
6509
6221
|
};
|
|
6510
6222
|
/**
|
|
6511
|
-
* 폴리곤 Base 렌더링 함수
|
|
6223
|
+
* 폴리곤 Base 렌더링 함수 팩토리
|
|
6224
|
+
*
|
|
6225
|
+
* Base Layer에서 사용할 렌더링 함수를 생성합니다.
|
|
6226
|
+
* 선택되지 않은 폴리곤만 렌더링하며, 선택된 항목은 Event Layer에서 처리됩니다.
|
|
6512
6227
|
*
|
|
6228
|
+
* @template T 폴리곤 데이터의 추가 속성 타입
|
|
6513
6229
|
* @param baseFillColor 기본 폴리곤 채우기 색상
|
|
6514
6230
|
* @param baseStrokeColor 기본 폴리곤 테두리 색상
|
|
6515
6231
|
* @param baseLineWidth 기본 폴리곤 테두리 두께
|
|
6516
6232
|
* @returns Base Layer 렌더링 함수
|
|
6517
6233
|
*
|
|
6234
|
+
* @remarks
|
|
6235
|
+
* - 선택된 항목은 Event Layer에서 그려지므로 Base Layer에서는 스킵
|
|
6236
|
+
* - 성능: O(n), n은 렌더링할 폴리곤 개수
|
|
6237
|
+
* - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
|
|
6238
|
+
*
|
|
6518
6239
|
* @example
|
|
6240
|
+
* ```typescript
|
|
6519
6241
|
* const renderBase = renderPolygonBase(
|
|
6520
6242
|
* 'rgba(255, 100, 100, 0.5)',
|
|
6521
6243
|
* 'rgba(200, 50, 50, 0.8)',
|
|
6522
6244
|
* 2
|
|
6523
6245
|
* );
|
|
6246
|
+
* ```
|
|
6524
6247
|
*/
|
|
6525
6248
|
|
|
6526
6249
|
var renderPolygonBase = function (baseFillColor, baseStrokeColor, baseLineWidth) {
|
|
@@ -6531,12 +6254,15 @@
|
|
|
6531
6254
|
utils = _a.utils;
|
|
6532
6255
|
|
|
6533
6256
|
for (var _i = 0, items_1 = items; _i < items_1.length; _i++) {
|
|
6534
|
-
var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림
|
|
6257
|
+
var item = items_1[_i]; // 선택된 항목은 Event Layer에서 그림 (중복 렌더링 방지)
|
|
6258
|
+
|
|
6259
|
+
if (selectedIds.has(item.id)) continue; // paths가 없으면 스킵
|
|
6260
|
+
|
|
6261
|
+
if (!item.paths) continue; // 좌표 변환 (자동 캐싱)
|
|
6535
6262
|
|
|
6536
|
-
if (selectedIds.has(item.id)) continue;
|
|
6537
|
-
if (!item.paths) continue;
|
|
6538
6263
|
var polygonOffsets = utils.getOrComputePolygonOffsets(item);
|
|
6539
|
-
if (!polygonOffsets) continue;
|
|
6264
|
+
if (!polygonOffsets) continue; // 폴리곤 그리기
|
|
6265
|
+
|
|
6540
6266
|
drawPolygon({
|
|
6541
6267
|
ctx: ctx,
|
|
6542
6268
|
polygonOffsets: polygonOffsets,
|
|
@@ -6549,8 +6275,12 @@
|
|
|
6549
6275
|
};
|
|
6550
6276
|
};
|
|
6551
6277
|
/**
|
|
6552
|
-
* 폴리곤 Event 렌더링 함수
|
|
6278
|
+
* 폴리곤 Event 렌더링 함수 팩토리
|
|
6553
6279
|
*
|
|
6280
|
+
* Event Layer에서 사용할 렌더링 함수를 생성합니다.
|
|
6281
|
+
* 선택된 항목, hover된 항목, 마지막 선택된 항목을 각각 다른 스타일로 렌더링합니다.
|
|
6282
|
+
*
|
|
6283
|
+
* @template T 폴리곤 데이터의 추가 속성 타입
|
|
6554
6284
|
* @param baseFillColor 기본 폴리곤 채우기 색상 (필수, fallback용)
|
|
6555
6285
|
* @param baseStrokeColor 기본 폴리곤 테두리 색상 (필수, fallback용)
|
|
6556
6286
|
* @param baseLineWidth 기본 폴리곤 테두리 두께 (필수, fallback용)
|
|
@@ -6565,7 +6295,14 @@
|
|
|
6565
6295
|
* @param hoveredLineWidth Hover 시 폴리곤 테두리 두께 (선택, 기본값: selectedLineWidth)
|
|
6566
6296
|
* @returns Event Layer 렌더링 함수
|
|
6567
6297
|
*
|
|
6298
|
+
* @remarks
|
|
6299
|
+
* - **렌더링 순서**: 선택된 항목 → 마지막 선택된 항목 → hover된 항목 (최상단)
|
|
6300
|
+
* - **성능**: O(m), m은 선택된 항목 수 + hover된 항목 수
|
|
6301
|
+
* - 좌표 변환은 자동으로 캐싱되어 성능 최적화됨
|
|
6302
|
+
* - hover된 항목이 선택되어 있으면 active 스타일 적용
|
|
6303
|
+
*
|
|
6568
6304
|
* @example
|
|
6305
|
+
* ```typescript
|
|
6569
6306
|
* const renderEvent = renderPolygonEvent(
|
|
6570
6307
|
* 'rgba(255, 100, 100, 0.5)', // baseFillColor
|
|
6571
6308
|
* 'rgba(200, 50, 50, 0.8)', // baseStrokeColor
|
|
@@ -6574,6 +6311,7 @@
|
|
|
6574
6311
|
* 'rgba(255, 152, 0, 1)', // selectedStrokeColor
|
|
6575
6312
|
* 4 // selectedLineWidth
|
|
6576
6313
|
* );
|
|
6314
|
+
* ```
|
|
6577
6315
|
*/
|
|
6578
6316
|
|
|
6579
6317
|
var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth) {
|
|
@@ -6601,13 +6339,19 @@
|
|
|
6601
6339
|
hoveredItem = _a.hoveredItem,
|
|
6602
6340
|
utils = _a.utils,
|
|
6603
6341
|
selectedItems = _a.selectedItems,
|
|
6604
|
-
selectedItem = _a.selectedItem; //
|
|
6342
|
+
selectedItem = _a.selectedItem; // 성능 최적화: selectedItems를 Set으로 변환하여 O(1) 조회 (매번 some() 체크 방지)
|
|
6343
|
+
|
|
6344
|
+
var selectedIdsSet = selectedItems ? new Set(selectedItems.map(function (item) {
|
|
6345
|
+
return item.id;
|
|
6346
|
+
})) : new Set();
|
|
6347
|
+
var hoveredItemId = hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.id;
|
|
6348
|
+
var selectedItemId = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id; // 1. 선택된 항목들 그리기 (마지막 선택 항목과 호버된 항목 제외)
|
|
6605
6349
|
|
|
6606
6350
|
if (selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.length) {
|
|
6607
6351
|
for (var _i = 0, selectedItems_1 = selectedItems; _i < selectedItems_1.length; _i++) {
|
|
6608
6352
|
var item = selectedItems_1[_i]; // 마지막 선택 항목과 호버된 항목은 나중에 따로 그림
|
|
6609
6353
|
|
|
6610
|
-
if (item.id ===
|
|
6354
|
+
if (item.id === selectedItemId || item.id === hoveredItemId) continue;
|
|
6611
6355
|
if (!item.paths) continue;
|
|
6612
6356
|
var polygonOffsets = utils.getOrComputePolygonOffsets(item);
|
|
6613
6357
|
if (!polygonOffsets) continue;
|
|
@@ -6623,7 +6367,7 @@
|
|
|
6623
6367
|
} // 2. 마지막 선택된 항목 그리기 (호버되지 않은 경우)
|
|
6624
6368
|
|
|
6625
6369
|
|
|
6626
|
-
if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) &&
|
|
6370
|
+
if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.paths) && hoveredItemId !== selectedItemId) {
|
|
6627
6371
|
var polygonOffsets = utils.getOrComputePolygonOffsets(selectedItem);
|
|
6628
6372
|
|
|
6629
6373
|
if (polygonOffsets) {
|
|
@@ -6641,10 +6385,10 @@
|
|
|
6641
6385
|
|
|
6642
6386
|
if (hoveredItem === null || hoveredItem === void 0 ? void 0 : hoveredItem.paths) {
|
|
6643
6387
|
var polygonOffsets = utils.getOrComputePolygonOffsets(hoveredItem);
|
|
6644
|
-
if (!polygonOffsets) return;
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6388
|
+
if (!polygonOffsets) return; // 좌표 변환 실패 시 스킵 (return은 렌더링 함수 종료)
|
|
6389
|
+
// 성능 최적화: Set을 사용하여 O(1) 조회 (이전: O(m) some() 체크)
|
|
6390
|
+
|
|
6391
|
+
var isSelected = selectedIdsSet.has(hoveredItem.id);
|
|
6648
6392
|
drawPolygon({
|
|
6649
6393
|
ctx: ctx,
|
|
6650
6394
|
polygonOffsets: polygonOffsets,
|
|
@@ -6657,24 +6401,19 @@
|
|
|
6657
6401
|
};
|
|
6658
6402
|
};
|
|
6659
6403
|
|
|
6660
|
-
// 메인 컴포넌트
|
|
6661
|
-
// ============================================================================
|
|
6662
|
-
|
|
6663
6404
|
var WoongCanvasPolygon = function (props) {
|
|
6664
6405
|
var data = props.data,
|
|
6665
6406
|
onClick = props.onClick,
|
|
6666
6407
|
_a = props.enableMultiSelect,
|
|
6667
6408
|
enableMultiSelect = _a === void 0 ? false : _a,
|
|
6668
|
-
_b = props.
|
|
6669
|
-
|
|
6670
|
-
_c = props.
|
|
6671
|
-
|
|
6672
|
-
_d = props.maxCacheSize,
|
|
6673
|
-
maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
|
|
6409
|
+
_b = props.cullingMargin,
|
|
6410
|
+
cullingMargin = _b === void 0 ? DEFAULT_CULLING_MARGIN : _b,
|
|
6411
|
+
_c = props.maxCacheSize,
|
|
6412
|
+
maxCacheSize = _c === void 0 ? DEFAULT_MAX_CACHE_SIZE : _c,
|
|
6674
6413
|
externalSelectedItems = props.selectedItems,
|
|
6675
6414
|
externalSelectedItem = props.selectedItem,
|
|
6676
|
-
|
|
6677
|
-
disableInteraction =
|
|
6415
|
+
_d = props.disableInteraction,
|
|
6416
|
+
disableInteraction = _d === void 0 ? false : _d,
|
|
6678
6417
|
baseFillColor = props.baseFillColor,
|
|
6679
6418
|
baseStrokeColor = props.baseStrokeColor,
|
|
6680
6419
|
baseLineWidth = props.baseLineWidth,
|
|
@@ -6687,173 +6426,67 @@
|
|
|
6687
6426
|
hoveredFillColor = props.hoveredFillColor,
|
|
6688
6427
|
hoveredStrokeColor = props.hoveredStrokeColor,
|
|
6689
6428
|
hoveredLineWidth = props.hoveredLineWidth,
|
|
6690
|
-
options = tslib.__rest(props, ["data", "onClick", "enableMultiSelect", "
|
|
6429
|
+
options = tslib.__rest(props, ["data", "onClick", "enableMultiSelect", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
|
|
6691
6430
|
// Hooks & Context
|
|
6692
6431
|
// --------------------------------------------------------------------------
|
|
6693
6432
|
|
|
6694
6433
|
|
|
6695
6434
|
var controller = useMintMapController();
|
|
6696
6435
|
var context = useWoongCanvasContext();
|
|
6697
|
-
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; //
|
|
6698
|
-
// DOM Refs
|
|
6699
|
-
// --------------------------------------------------------------------------
|
|
6436
|
+
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // DOM Refs
|
|
6700
6437
|
|
|
6701
6438
|
var divRef = React.useRef(document.createElement('div'));
|
|
6702
6439
|
var divElement = divRef.current;
|
|
6703
6440
|
var containerRef = React.useRef(null);
|
|
6704
|
-
var markerRef = React.useRef(); //
|
|
6705
|
-
// Konva Refs
|
|
6706
|
-
// --------------------------------------------------------------------------
|
|
6441
|
+
var markerRef = React.useRef(); // Konva Refs
|
|
6707
6442
|
|
|
6708
6443
|
var stageRef = React.useRef(null);
|
|
6709
6444
|
var baseLayerRef = React.useRef(null);
|
|
6710
|
-
var eventLayerRef = React.useRef(null); //
|
|
6711
|
-
// Data Refs - 선택 및 Hover 상태 관리
|
|
6712
|
-
// --------------------------------------------------------------------------
|
|
6713
|
-
|
|
6714
|
-
/** data prop을 ref로 추적 (stale closure 방지, useEffect에서 동기화) */
|
|
6715
|
-
|
|
6716
|
-
var dataRef = React.useRef(data); // --------------------------------------------------------------------------
|
|
6717
|
-
// State Refs - 선택 및 Hover 상태 관리
|
|
6718
|
-
// --------------------------------------------------------------------------
|
|
6719
|
-
|
|
6720
|
-
/** 상호작용 비활성화 상태 (Ref로 관리하여 클로저 문제 해결) */
|
|
6445
|
+
var eventLayerRef = React.useRef(null); // 상태 관리 Refs (React 리렌더링 최소화)
|
|
6721
6446
|
|
|
6447
|
+
var dataRef = React.useRef(data);
|
|
6722
6448
|
var disableInteractionRef = React.useRef(disableInteraction);
|
|
6723
|
-
/** 현재 Hover 중인 항목 */
|
|
6724
|
-
|
|
6725
6449
|
var hoveredItemRef = React.useRef(null);
|
|
6726
|
-
/** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
|
|
6727
|
-
|
|
6728
6450
|
var selectedItemRef = React.useRef(externalSelectedItem);
|
|
6729
|
-
/**
|
|
6730
|
-
* 선택된 항목의 ID Set
|
|
6731
|
-
*
|
|
6732
|
-
* 용도:
|
|
6733
|
-
* 1. onClick 콜백에 전달 - onClick(data, selectedIdsRef.current)
|
|
6734
|
-
* 2. 선택 여부 빠른 체크 - selectedIdsRef.current.has(id)
|
|
6735
|
-
* 3. 메모리 효율 - ID만 저장 (작음)
|
|
6736
|
-
*
|
|
6737
|
-
* selectedItemsMapRef와 차이:
|
|
6738
|
-
* - selectedIdsRef: ID만 저장 { "id1", "id2" }
|
|
6739
|
-
* - selectedItemsMapRef: 전체 객체 저장 { id1: {...}, id2: {...} }
|
|
6740
|
-
*
|
|
6741
|
-
* 둘 다 필요: ID만 필요한 곳은 이것, 전체 데이터 필요한 곳은 Map
|
|
6742
|
-
*/
|
|
6743
|
-
|
|
6744
6451
|
var selectedIdsRef = React.useRef(new Set());
|
|
6745
|
-
|
|
6746
|
-
* 선택된 항목의 실제 데이터 Map (핵심 성능 최적화!)
|
|
6747
|
-
*
|
|
6748
|
-
* 목적: doRenderEvent에서 filter() 순회 제거
|
|
6749
|
-
* - 이전: markersRef.current.filter() → O(전체 마커 수)
|
|
6750
|
-
* - 현재: Map.values() → O(선택된 항목 수)
|
|
6751
|
-
*
|
|
6752
|
-
* 성능 개선: 10,000개 중 1개 선택 시
|
|
6753
|
-
* - 이전: 10,000번 체크
|
|
6754
|
-
* - 현재: 1번 접근 (10,000배 빠름!)
|
|
6755
|
-
*/
|
|
6756
|
-
|
|
6757
|
-
var selectedItemsMapRef = React.useRef(new Map()); // --------------------------------------------------------------------------
|
|
6758
|
-
// Drag Refs
|
|
6759
|
-
// --------------------------------------------------------------------------
|
|
6452
|
+
var selectedItemsMapRef = React.useRef(new Map()); // 드래그 상태 Refs
|
|
6760
6453
|
|
|
6761
6454
|
var draggingRef = React.useRef(false);
|
|
6762
6455
|
var prevCenterOffsetRef = React.useRef(null);
|
|
6763
6456
|
var accumTranslateRef = React.useRef({
|
|
6764
6457
|
x: 0,
|
|
6765
6458
|
y: 0
|
|
6766
|
-
}); //
|
|
6767
|
-
// Performance Refs (캐싱 & 최적화)
|
|
6768
|
-
// --------------------------------------------------------------------------
|
|
6769
|
-
|
|
6770
|
-
/** 좌표 변환 결과 LRU 캐시 */
|
|
6459
|
+
}); // 성능 최적화 Refs
|
|
6771
6460
|
|
|
6772
6461
|
var offsetCacheRef = React.useRef(new LRUCache(maxCacheSize));
|
|
6773
|
-
/** 공간 인덱스 (빠른 Hit Test) */
|
|
6774
|
-
|
|
6775
6462
|
var spatialIndexRef = React.useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
|
|
6776
|
-
/** 바운딩 박스 캐시 (Viewport Culling 최적화) */
|
|
6777
|
-
|
|
6778
6463
|
var boundingBoxCacheRef = React.useRef(new Map());
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
var viewportRef = React.useRef(null); // --------------------------------------------------------------------------
|
|
6782
|
-
// 유틸리티 함수: 뷰포트 관리
|
|
6783
|
-
// --------------------------------------------------------------------------
|
|
6784
|
-
|
|
6785
|
-
/**
|
|
6786
|
-
* 현재 뷰포트 영역 계산
|
|
6787
|
-
*/
|
|
6788
|
-
|
|
6789
|
-
var updateViewport = function () {
|
|
6790
|
-
if (!stageRef.current) return;
|
|
6791
|
-
var stage = stageRef.current;
|
|
6792
|
-
viewportRef.current = {
|
|
6793
|
-
minX: -cullingMargin,
|
|
6794
|
-
maxX: stage.width() + cullingMargin,
|
|
6795
|
-
minY: -cullingMargin,
|
|
6796
|
-
maxY: stage.height() + cullingMargin
|
|
6797
|
-
};
|
|
6798
|
-
};
|
|
6799
|
-
/**
|
|
6800
|
-
* 아이템이 현재 뷰포트 안에 있는지 확인 (바운딩 박스 캐싱)
|
|
6801
|
-
*/
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
var isInViewport = function (item) {
|
|
6805
|
-
if (!enableViewportCulling || !viewportRef.current) return true;
|
|
6806
|
-
var viewport = viewportRef.current; // 캐시된 바운딩 박스 확인
|
|
6464
|
+
var viewportRef = React.useRef(null); // 뷰포트 영역 계산 (Viewport Culling용)
|
|
6807
6465
|
|
|
6808
|
-
|
|
6466
|
+
var updateViewport$1 = function () {
|
|
6467
|
+
updateViewport(stageRef.current, cullingMargin, viewportRef);
|
|
6468
|
+
}; // 뷰포트 내부 여부 확인 (바운딩 박스 캐싱)
|
|
6809
6469
|
|
|
6810
|
-
if (!bbox) {
|
|
6811
|
-
// 바운딩 박스 계산 (공통 함수 사용)
|
|
6812
|
-
var computed = computeBoundingBox(item);
|
|
6813
|
-
if (!computed) return false;
|
|
6814
|
-
bbox = computed;
|
|
6815
|
-
boundingBoxCacheRef.current.set(item.id, bbox);
|
|
6816
|
-
} // 바운딩 박스와 viewport 교차 체크
|
|
6817
6470
|
|
|
6818
|
-
|
|
6819
|
-
return
|
|
6820
|
-
}; //
|
|
6821
|
-
// 유틸리티 함수: 좌표 변환 캐싱
|
|
6822
|
-
// --------------------------------------------------------------------------
|
|
6823
|
-
|
|
6824
|
-
/**
|
|
6825
|
-
* 폴리곤 좌표 변환 결과를 캐시하고 반환
|
|
6826
|
-
* @param polygonData 폴리곤 데이터
|
|
6827
|
-
* @returns 변환된 좌표 배열 또는 null
|
|
6828
|
-
*/
|
|
6471
|
+
var isInViewport$1 = function (item) {
|
|
6472
|
+
return isInViewport(item, viewportRef, boundingBoxCacheRef, computeBoundingBox);
|
|
6473
|
+
}; // 폴리곤 좌표 변환 (위경도 → 화면 좌표, LRU 캐시 사용)
|
|
6829
6474
|
|
|
6830
6475
|
|
|
6831
6476
|
var getOrComputePolygonOffsets = function (polygonData) {
|
|
6832
6477
|
var cached = offsetCacheRef.current.get(polygonData.id);
|
|
6833
6478
|
if (cached && Array.isArray(cached)) return cached;
|
|
6834
6479
|
var result = computePolygonOffsets(polygonData, controller);
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
offsetCacheRef.current.set(polygonData.id, result);
|
|
6838
|
-
}
|
|
6839
|
-
|
|
6480
|
+
if (!result) return null;
|
|
6481
|
+
offsetCacheRef.current.set(polygonData.id, result);
|
|
6840
6482
|
return result;
|
|
6841
|
-
}; //
|
|
6842
|
-
// 유틸리티 함수: 바운딩 박스 계산
|
|
6843
|
-
// --------------------------------------------------------------------------
|
|
6844
|
-
|
|
6845
|
-
/**
|
|
6846
|
-
* 폴리곤의 바운딩 박스 계산
|
|
6847
|
-
*
|
|
6848
|
-
* @param item 폴리곤 데이터
|
|
6849
|
-
* @returns 바운딩 박스 또는 null
|
|
6850
|
-
*/
|
|
6483
|
+
}; // 폴리곤 바운딩 박스 계산 (Viewport Culling 및 Hit Test용)
|
|
6851
6484
|
|
|
6852
6485
|
|
|
6853
6486
|
var computeBoundingBox = function (item) {
|
|
6854
|
-
// 폴리곤: 모든 좌표의 최소/최대값 계산
|
|
6855
6487
|
var offsets = getOrComputePolygonOffsets(item);
|
|
6856
|
-
if (!offsets) return null;
|
|
6488
|
+
if (!offsets) return null; // 모든 좌표를 순회하며 최소/최대값 찾기
|
|
6489
|
+
|
|
6857
6490
|
var minX = Infinity,
|
|
6858
6491
|
minY = Infinity,
|
|
6859
6492
|
maxX = -Infinity,
|
|
@@ -6883,86 +6516,39 @@
|
|
|
6883
6516
|
maxX: maxX,
|
|
6884
6517
|
maxY: maxY
|
|
6885
6518
|
};
|
|
6886
|
-
}; //
|
|
6887
|
-
// 유틸리티 함수: 공간 인덱싱
|
|
6888
|
-
// --------------------------------------------------------------------------
|
|
6519
|
+
}; // 공간 인덱스 빌드 (빠른 Hit Test용)
|
|
6889
6520
|
|
|
6890
|
-
/**
|
|
6891
|
-
* 공간 인덱스 빌드 (빠른 Hit Test를 위한 자료구조)
|
|
6892
|
-
*/
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
var buildSpatialIndex = function () {
|
|
6896
|
-
var spatial = spatialIndexRef.current;
|
|
6897
|
-
spatial.clear();
|
|
6898
|
-
var currentData = dataRef.current;
|
|
6899
|
-
|
|
6900
|
-
for (var _i = 0, currentData_1 = currentData; _i < currentData_1.length; _i++) {
|
|
6901
|
-
var item = currentData_1[_i]; // 바운딩 박스 계산 (공통 함수 사용)
|
|
6902
|
-
|
|
6903
|
-
var bbox = computeBoundingBox(item);
|
|
6904
6521
|
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
}
|
|
6909
|
-
}; // --------------------------------------------------------------------------
|
|
6910
|
-
// 렌더링 함수 결정 (dataType에 따라)
|
|
6911
|
-
// --------------------------------------------------------------------------
|
|
6912
|
-
|
|
6913
|
-
/**
|
|
6914
|
-
* 외부 렌더링 함수에 전달할 유틸리티 객체
|
|
6915
|
-
*/
|
|
6522
|
+
var buildSpatialIndex$1 = function () {
|
|
6523
|
+
buildSpatialIndex(dataRef.current, spatialIndexRef.current, computeBoundingBox);
|
|
6524
|
+
}; // 렌더링 유틸리티 객체
|
|
6916
6525
|
|
|
6917
6526
|
|
|
6918
6527
|
var renderUtils = {
|
|
6919
6528
|
getOrComputePolygonOffsets: getOrComputePolygonOffsets,
|
|
6920
6529
|
getOrComputeMarkerOffset: function () {
|
|
6921
6530
|
return null;
|
|
6922
|
-
}
|
|
6923
|
-
|
|
6924
|
-
};
|
|
6925
|
-
/**
|
|
6926
|
-
* 렌더링 함수 생성 (props 기반)
|
|
6927
|
-
*/
|
|
6531
|
+
}
|
|
6532
|
+
}; // 렌더링 함수 생성
|
|
6928
6533
|
|
|
6929
6534
|
var renderBase = renderPolygonBase(baseFillColor, baseStrokeColor, baseLineWidth);
|
|
6930
|
-
var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth);
|
|
6931
|
-
/** Base Layer에서 사용할 빈 Set (재사용) */
|
|
6932
|
-
|
|
6933
|
-
React.useRef(new Set());
|
|
6934
|
-
/**
|
|
6935
|
-
* Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
|
|
6936
|
-
*
|
|
6937
|
-
* 🔥 최적화:
|
|
6938
|
-
* 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
|
|
6939
|
-
* 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
|
|
6940
|
-
* 3. 클로저로 최신 데이터 참조
|
|
6941
|
-
*
|
|
6942
|
-
* 🎯 topOnHover 지원:
|
|
6943
|
-
* - renderEvent가 없을 때 Base Layer에서 hover 처리 (fallback)
|
|
6944
|
-
* - renderEvent가 있으면 Event Layer에서 처리 (성능 최적화)
|
|
6945
|
-
*/
|
|
6535
|
+
var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth); // Base Layer 렌더링 (뷰포트 컬링 적용)
|
|
6946
6536
|
|
|
6947
6537
|
var doRenderBase = function () {
|
|
6948
6538
|
var layer = baseLayerRef.current;
|
|
6949
|
-
if (!layer) return;
|
|
6950
|
-
|
|
6539
|
+
if (!layer) return;
|
|
6951
6540
|
var shape = layer.findOne('.base-render-shape');
|
|
6952
6541
|
|
|
6953
6542
|
if (!shape) {
|
|
6954
|
-
// 최초 생성 (한 번만 실행됨)
|
|
6955
|
-
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
6956
6543
|
shape = new Konva__default["default"].Shape({
|
|
6957
6544
|
name: 'base-render-shape',
|
|
6958
6545
|
sceneFunc: function (context, shape) {
|
|
6959
6546
|
var ctx = context;
|
|
6960
|
-
var hovered = hoveredItemRef.current; //
|
|
6961
|
-
|
|
6962
|
-
var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
|
|
6963
|
-
return isInViewport(item);
|
|
6964
|
-
}) : dataRef.current; // 일반 항목 렌더링
|
|
6547
|
+
var hovered = hoveredItemRef.current; // 뷰포트 컬링: 화면에 보이는 항목만 필터링
|
|
6965
6548
|
|
|
6549
|
+
var visibleItems = dataRef.current.filter(function (item) {
|
|
6550
|
+
return isInViewport$1(item);
|
|
6551
|
+
});
|
|
6966
6552
|
renderBase({
|
|
6967
6553
|
ctx: ctx,
|
|
6968
6554
|
items: visibleItems,
|
|
@@ -6976,38 +6562,24 @@
|
|
|
6976
6562
|
hitStrokeWidth: 0
|
|
6977
6563
|
});
|
|
6978
6564
|
layer.add(shape);
|
|
6979
|
-
}
|
|
6980
|
-
|
|
6565
|
+
}
|
|
6981
6566
|
|
|
6982
6567
|
layer.batchDraw();
|
|
6983
|
-
};
|
|
6984
|
-
/**
|
|
6985
|
-
* Event 레이어 렌더링 (hover + 선택 상태 표시)
|
|
6986
|
-
*
|
|
6987
|
-
* 🔥 최적화:
|
|
6988
|
-
* 1. Shape 재사용으로 객체 생성/파괴 오버헤드 제거
|
|
6989
|
-
* 2. sceneFunc 한 번만 설정 (함수 재생성 제거)
|
|
6990
|
-
* 3. 클로저로 최신 데이터 참조
|
|
6991
|
-
*/
|
|
6568
|
+
}; // Event Layer 렌더링 (hover 효과 및 선택 상태 표시)
|
|
6992
6569
|
|
|
6993
6570
|
|
|
6994
6571
|
var doRenderEvent = function () {
|
|
6995
6572
|
var layer = eventLayerRef.current;
|
|
6996
|
-
if (!layer) return;
|
|
6997
|
-
|
|
6573
|
+
if (!layer) return;
|
|
6998
6574
|
var shape = layer.findOne('.event-render-shape');
|
|
6999
6575
|
|
|
7000
6576
|
if (!shape) {
|
|
7001
|
-
// 최초 생성 (한 번만 실행됨)
|
|
7002
|
-
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
7003
6577
|
shape = new Konva__default["default"].Shape({
|
|
7004
6578
|
name: 'event-render-shape',
|
|
7005
6579
|
sceneFunc: function (context, shape) {
|
|
7006
|
-
var ctx = context;
|
|
7007
|
-
|
|
7008
|
-
var
|
|
7009
|
-
var hovered = hoveredItemRef.current; // 일반 렌더링
|
|
7010
|
-
|
|
6580
|
+
var ctx = context;
|
|
6581
|
+
var selectedItems = mapValuesToArray(selectedItemsMapRef.current);
|
|
6582
|
+
var hovered = hoveredItemRef.current;
|
|
7011
6583
|
renderEvent({
|
|
7012
6584
|
ctx: ctx,
|
|
7013
6585
|
hoveredItem: hovered,
|
|
@@ -7021,126 +6593,57 @@
|
|
|
7021
6593
|
hitStrokeWidth: 0
|
|
7022
6594
|
});
|
|
7023
6595
|
layer.add(shape);
|
|
7024
|
-
}
|
|
7025
|
-
|
|
6596
|
+
}
|
|
7026
6597
|
|
|
7027
6598
|
layer.batchDraw();
|
|
7028
|
-
};
|
|
7029
|
-
/**
|
|
7030
|
-
* 전체 즉시 렌더링 (IDLE 시 호출)
|
|
7031
|
-
*/
|
|
6599
|
+
}; // 전체 즉시 렌더링
|
|
7032
6600
|
|
|
7033
6601
|
|
|
7034
6602
|
var renderAllImmediate = function () {
|
|
7035
|
-
updateViewport();
|
|
7036
|
-
buildSpatialIndex();
|
|
6603
|
+
updateViewport$1();
|
|
6604
|
+
buildSpatialIndex$1();
|
|
7037
6605
|
doRenderBase();
|
|
7038
6606
|
doRenderEvent();
|
|
7039
|
-
}; //
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
var markerOptions = tslib.__assign({
|
|
7061
|
-
position: bounds.nw
|
|
7062
|
-
}, options);
|
|
7063
|
-
|
|
7064
|
-
markerRef.current && controller.updateMarker(markerRef.current, markerOptions); // 4. transform 제거 전에 새 데이터로 즉시 렌더링 (겹침 방지)
|
|
7065
|
-
|
|
7066
|
-
if (containerRef.current) {
|
|
7067
|
-
containerRef.current.style.transform = '';
|
|
7068
|
-
containerRef.current.style.visibility = '';
|
|
7069
|
-
} // 5. 새 위치에서 렌더링
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
renderAllImmediate();
|
|
7073
|
-
};
|
|
7074
|
-
/**
|
|
7075
|
-
* 줌 시작 시 처리 (일시적으로 숨김)
|
|
7076
|
-
*/
|
|
6607
|
+
}; // 지도 이벤트 핸들러 생성
|
|
6608
|
+
|
|
6609
|
+
|
|
6610
|
+
var _e = createMapEventHandlers({
|
|
6611
|
+
controller: controller,
|
|
6612
|
+
containerRef: containerRef,
|
|
6613
|
+
markerRef: markerRef,
|
|
6614
|
+
options: options,
|
|
6615
|
+
prevCenterOffsetRef: prevCenterOffsetRef,
|
|
6616
|
+
accumTranslateRef: accumTranslateRef,
|
|
6617
|
+
offsetCacheRef: offsetCacheRef,
|
|
6618
|
+
boundingBoxCacheRef: boundingBoxCacheRef,
|
|
6619
|
+
renderAllImmediate: renderAllImmediate
|
|
6620
|
+
}),
|
|
6621
|
+
handleIdle = _e.handleIdle,
|
|
6622
|
+
handleZoomStart = _e.handleZoomStart,
|
|
6623
|
+
handleZoomEnd = _e.handleZoomEnd,
|
|
6624
|
+
handleCenterChanged = _e.handleCenterChanged,
|
|
6625
|
+
handleDragStartShared = _e.handleDragStart,
|
|
6626
|
+
handleDragEndShared = _e.handleDragEnd;
|
|
7077
6627
|
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
}
|
|
7083
|
-
};
|
|
7084
|
-
/**
|
|
7085
|
-
* 줌 종료 시 처리 (다시 표시)
|
|
7086
|
-
*/
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
var handleZoomEnd = function () {
|
|
7090
|
-
if (containerRef.current) {
|
|
7091
|
-
containerRef.current.style.visibility = '';
|
|
7092
|
-
}
|
|
6628
|
+
var handleDragStart = function () {
|
|
6629
|
+
handleDragStartShared();
|
|
6630
|
+
draggingRef.current = true;
|
|
6631
|
+
controller.setMapCursor('grabbing');
|
|
7093
6632
|
};
|
|
7094
|
-
/**
|
|
7095
|
-
* 지도 중심 변경 시 처리 (transform으로 이동 추적)
|
|
7096
|
-
*/
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
var handleCenterChanged = function () {
|
|
7100
|
-
var center = controller.getCurrBounds().getCenter();
|
|
7101
|
-
var curr = controller.positionToOffset(center);
|
|
7102
|
-
var prev = prevCenterOffsetRef.current;
|
|
7103
|
-
|
|
7104
|
-
if (!prev) {
|
|
7105
|
-
prevCenterOffsetRef.current = {
|
|
7106
|
-
x: curr.x,
|
|
7107
|
-
y: curr.y
|
|
7108
|
-
};
|
|
7109
|
-
return;
|
|
7110
|
-
}
|
|
7111
|
-
|
|
7112
|
-
var dx = prev.x - curr.x;
|
|
7113
|
-
var dy = prev.y - curr.y;
|
|
7114
|
-
accumTranslateRef.current = {
|
|
7115
|
-
x: accumTranslateRef.current.x + dx,
|
|
7116
|
-
y: accumTranslateRef.current.y + dy
|
|
7117
|
-
};
|
|
7118
|
-
prevCenterOffsetRef.current = {
|
|
7119
|
-
x: curr.x,
|
|
7120
|
-
y: curr.y
|
|
7121
|
-
};
|
|
7122
|
-
|
|
7123
|
-
if (containerRef.current) {
|
|
7124
|
-
containerRef.current.style.transform = "translate(".concat(accumTranslateRef.current.x, "px, ").concat(accumTranslateRef.current.y, "px)");
|
|
7125
|
-
}
|
|
7126
|
-
}; // --------------------------------------------------------------------------
|
|
7127
|
-
// Hit Test & 상태 관리
|
|
7128
|
-
// --------------------------------------------------------------------------
|
|
7129
6633
|
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
*/
|
|
6634
|
+
var handleDragEnd = function () {
|
|
6635
|
+
handleDragEndShared();
|
|
6636
|
+
draggingRef.current = false;
|
|
6637
|
+
controller.setMapCursor('grab');
|
|
6638
|
+
}; // Hit Test: 특정 좌표의 폴리곤 찾기
|
|
7136
6639
|
|
|
7137
6640
|
|
|
7138
6641
|
var findData = function (offset) {
|
|
7139
|
-
//
|
|
7140
|
-
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); //
|
|
6642
|
+
// 공간 인덱스에서 후보 항목 조회 (O(1) 수준의 빠른 조회)
|
|
6643
|
+
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 역순 순회: 나중에 추가된 항목(최상위)이 먼저 선택되도록
|
|
7141
6644
|
|
|
7142
6645
|
for (var i = candidates.length - 1; i >= 0; i--) {
|
|
7143
|
-
var item = candidates[i];
|
|
6646
|
+
var item = candidates[i]; // 정확한 Hit Test: Ray Casting 알고리즘으로 폴리곤 내부 여부 확인
|
|
7144
6647
|
|
|
7145
6648
|
if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
|
|
7146
6649
|
return item;
|
|
@@ -7148,18 +6651,7 @@
|
|
|
7148
6651
|
}
|
|
7149
6652
|
|
|
7150
6653
|
return null;
|
|
7151
|
-
};
|
|
7152
|
-
/**
|
|
7153
|
-
* Hover 상태 설정 및 레이어 렌더링
|
|
7154
|
-
*
|
|
7155
|
-
* @param data hover된 마커/폴리곤 데이터 또는 null
|
|
7156
|
-
*
|
|
7157
|
-
* 최적화: RAF 제거하여 즉시 렌더링 (16ms 지연 제거)
|
|
7158
|
-
*
|
|
7159
|
-
* 🎯 topOnHover 지원:
|
|
7160
|
-
* - renderEvent가 있으면: Event Layer에서만 처리 (성능 최적화)
|
|
7161
|
-
* - renderEvent가 없고 topOnHover=true면: Base Layer에서 처리
|
|
7162
|
-
*/
|
|
6654
|
+
}; // Hover 상태 설정 및 렌더링
|
|
7163
6655
|
|
|
7164
6656
|
|
|
7165
6657
|
var setHovered = function (data) {
|
|
@@ -7169,26 +6661,14 @@
|
|
|
7169
6661
|
controller.setMapCursor('grabbing');
|
|
7170
6662
|
} else {
|
|
7171
6663
|
controller.setMapCursor(data ? 'pointer' : 'grab');
|
|
7172
|
-
}
|
|
7173
|
-
|
|
6664
|
+
}
|
|
7174
6665
|
|
|
7175
6666
|
doRenderEvent();
|
|
7176
|
-
};
|
|
7177
|
-
/**
|
|
7178
|
-
* 클릭 처리 (단일/다중 선택)
|
|
7179
|
-
*
|
|
7180
|
-
* @param data 클릭된 마커/폴리곤 데이터
|
|
7181
|
-
*
|
|
7182
|
-
* 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
|
|
7183
|
-
* - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
|
|
7184
|
-
* - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
|
|
7185
|
-
*/
|
|
6667
|
+
}; // 클릭 처리: 선택 상태 업데이트
|
|
7186
6668
|
|
|
7187
6669
|
|
|
7188
6670
|
var handleLocalClick = function (data) {
|
|
7189
|
-
// 1. 선택 상태 업데이트
|
|
7190
6671
|
if (enableMultiSelect) {
|
|
7191
|
-
// 다중 선택: Set과 Map 동시 업데이트
|
|
7192
6672
|
var newSelected = new Set(selectedIdsRef.current);
|
|
7193
6673
|
|
|
7194
6674
|
if (newSelected.has(data.id)) {
|
|
@@ -7201,7 +6681,6 @@
|
|
|
7201
6681
|
|
|
7202
6682
|
selectedIdsRef.current = newSelected;
|
|
7203
6683
|
} else {
|
|
7204
|
-
// 단일 선택: 토글
|
|
7205
6684
|
var newSelected = new Set();
|
|
7206
6685
|
|
|
7207
6686
|
if (!selectedIdsRef.current.has(data.id)) {
|
|
@@ -7213,144 +6692,79 @@
|
|
|
7213
6692
|
}
|
|
7214
6693
|
|
|
7215
6694
|
selectedIdsRef.current = newSelected;
|
|
7216
|
-
}
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
doRenderBase(); // 3. Event Layer 렌더링 (hover 처리)
|
|
6695
|
+
}
|
|
7220
6696
|
|
|
6697
|
+
doRenderBase();
|
|
7221
6698
|
doRenderEvent();
|
|
7222
|
-
}; //
|
|
7223
|
-
// 이벤트 핸들러: UI 이벤트
|
|
7224
|
-
// --------------------------------------------------------------------------
|
|
7225
|
-
|
|
7226
|
-
/**
|
|
7227
|
-
* 클릭 이벤트 처리
|
|
7228
|
-
*/
|
|
6699
|
+
}; // 클릭 이벤트 핸들러
|
|
7229
6700
|
|
|
7230
6701
|
|
|
7231
6702
|
var handleClick = function (event) {
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
if (
|
|
7235
|
-
|
|
7236
|
-
if (
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
var data_1 = findData(clickedOffset);
|
|
7241
|
-
|
|
7242
|
-
if (data_1) {
|
|
7243
|
-
handleLocalClick(data_1);
|
|
7244
|
-
|
|
7245
|
-
if (onClick) {
|
|
7246
|
-
onClick(data_1, selectedIdsRef.current);
|
|
7247
|
-
}
|
|
7248
|
-
}
|
|
7249
|
-
} catch (error) {
|
|
7250
|
-
console.error('[WoongCanvasPolygon] handleClick error:', error);
|
|
7251
|
-
}
|
|
7252
|
-
};
|
|
7253
|
-
/**
|
|
7254
|
-
* 마우스 이동 이벤트 처리 (hover 감지)
|
|
7255
|
-
*/
|
|
6703
|
+
if (disableInteractionRef.current) return;
|
|
6704
|
+
var clickedOffset = validateEvent(event, context, controller);
|
|
6705
|
+
if (!clickedOffset) return;
|
|
6706
|
+
var data = findData(clickedOffset);
|
|
6707
|
+
if (!data) return;
|
|
6708
|
+
handleLocalClick(data);
|
|
6709
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(data, selectedIdsRef.current);
|
|
6710
|
+
}; // 마우스 이동 이벤트 핸들러 (hover 감지)
|
|
7256
6711
|
|
|
7257
6712
|
|
|
7258
6713
|
var handleMouseMove = function (event) {
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
if (
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
var hoveredItem = findData(mouseOffset);
|
|
7268
|
-
var prevHovered = hoveredItemRef.current;
|
|
7269
|
-
|
|
7270
|
-
if (prevHovered !== hoveredItem) {
|
|
7271
|
-
setHovered(hoveredItem);
|
|
7272
|
-
}
|
|
7273
|
-
} catch (error) {
|
|
7274
|
-
console.error('[WoongCanvasPolygon] handleMouseMove error:', error);
|
|
7275
|
-
}
|
|
7276
|
-
};
|
|
7277
|
-
/**
|
|
7278
|
-
* 드래그 시작 처리 (커서를 grabbing으로 변경)
|
|
7279
|
-
*/
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
var handleDragStart = function () {
|
|
7283
|
-
draggingRef.current = true;
|
|
7284
|
-
controller.setMapCursor('grabbing');
|
|
7285
|
-
};
|
|
7286
|
-
/**
|
|
7287
|
-
* 드래그 종료 처리 (커서를 기본으로 복원)
|
|
7288
|
-
*/
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
var handleDragEnd = function () {
|
|
7292
|
-
draggingRef.current = false;
|
|
7293
|
-
controller.setMapCursor('grab');
|
|
7294
|
-
};
|
|
7295
|
-
/**
|
|
7296
|
-
* 마우스가 canvas를 벗어날 때 hover cleanup
|
|
7297
|
-
*/
|
|
6714
|
+
if (disableInteractionRef.current) return;
|
|
6715
|
+
var mouseOffset = validateEvent(event, context, controller);
|
|
6716
|
+
if (!mouseOffset) return;
|
|
6717
|
+
var hoveredItem = findData(mouseOffset);
|
|
6718
|
+
var prevHovered = hoveredItemRef.current;
|
|
6719
|
+
if (prevHovered === hoveredItem) return;
|
|
6720
|
+
setHovered(hoveredItem);
|
|
6721
|
+
}; // 마우스가 맵 영역을 벗어날 때 hover 상태 초기화
|
|
7298
6722
|
|
|
7299
6723
|
|
|
7300
6724
|
var handleMouseLeave = function () {
|
|
7301
|
-
if (disableInteractionRef.current) return;
|
|
7302
|
-
|
|
6725
|
+
if (disableInteractionRef.current) return;
|
|
7303
6726
|
var prevHovered = hoveredItemRef.current;
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
}
|
|
7310
|
-
}; // --------------------------------------------------------------------------
|
|
7311
|
-
// Lifecycle: DOM 초기화
|
|
7312
|
-
// --------------------------------------------------------------------------
|
|
6727
|
+
if (!prevHovered) return;
|
|
6728
|
+
hoveredItemRef.current = null;
|
|
6729
|
+
controller.setMapCursor('grab');
|
|
6730
|
+
doRenderEvent();
|
|
6731
|
+
}; // DOM 초기화
|
|
7313
6732
|
|
|
7314
6733
|
|
|
7315
6734
|
React.useEffect(function () {
|
|
7316
6735
|
divElement.style.width = 'fit-content';
|
|
7317
6736
|
return function () {
|
|
7318
|
-
if (markerRef.current)
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
}
|
|
6737
|
+
if (!markerRef.current) return;
|
|
6738
|
+
controller.clearDrawable(markerRef.current);
|
|
6739
|
+
markerRef.current = undefined;
|
|
7322
6740
|
};
|
|
7323
|
-
}, []); //
|
|
7324
|
-
// Lifecycle: 마커 생성/업데이트
|
|
7325
|
-
// --------------------------------------------------------------------------
|
|
6741
|
+
}, []); // 마커 생성/업데이트
|
|
7326
6742
|
|
|
7327
6743
|
React.useEffect(function () {
|
|
7328
|
-
if (options)
|
|
7329
|
-
|
|
6744
|
+
if (!options) return;
|
|
6745
|
+
var bounds = controller.getCurrBounds();
|
|
7330
6746
|
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
6747
|
+
var markerOptions = tslib.__assign({
|
|
6748
|
+
position: bounds.nw
|
|
6749
|
+
}, options);
|
|
7334
6750
|
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
markerRef.current.element = divElement;
|
|
7340
|
-
controller.createMarker(markerRef.current);
|
|
6751
|
+
if (markerRef.current) {
|
|
6752
|
+
controller.updateMarker(markerRef.current, markerOptions);
|
|
6753
|
+
return;
|
|
6754
|
+
}
|
|
7341
6755
|
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
6756
|
+
markerRef.current = new Marker(markerOptions);
|
|
6757
|
+
markerRef.current.element = divElement;
|
|
6758
|
+
controller.createMarker(markerRef.current);
|
|
7345
6759
|
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
}
|
|
7349
|
-
}
|
|
6760
|
+
if (divElement.parentElement) {
|
|
6761
|
+
divElement.parentElement.style.pointerEvents = 'none';
|
|
7350
6762
|
}
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
6763
|
+
|
|
6764
|
+
if (options.zIndex !== undefined) {
|
|
6765
|
+
controller.setMarkerZIndex(markerRef.current, options.zIndex);
|
|
6766
|
+
}
|
|
6767
|
+
}, [options]); // Konva 초기화 및 이벤트 리스너 등록
|
|
7354
6768
|
|
|
7355
6769
|
React.useEffect(function () {
|
|
7356
6770
|
var mapDiv = controller.mapDivElement;
|
|
@@ -7359,11 +6773,9 @@
|
|
|
7359
6773
|
width: mapDiv.offsetWidth,
|
|
7360
6774
|
height: mapDiv.offsetHeight
|
|
7361
6775
|
});
|
|
7362
|
-
stageRef.current = stage;
|
|
7363
|
-
|
|
6776
|
+
stageRef.current = stage;
|
|
7364
6777
|
var baseLayer = new Konva__default["default"].Layer({
|
|
7365
|
-
listening: false
|
|
7366
|
-
|
|
6778
|
+
listening: false
|
|
7367
6779
|
});
|
|
7368
6780
|
var eventLayer = new Konva__default["default"].Layer({
|
|
7369
6781
|
listening: false
|
|
@@ -7371,13 +6783,11 @@
|
|
|
7371
6783
|
baseLayerRef.current = baseLayer;
|
|
7372
6784
|
eventLayerRef.current = eventLayer;
|
|
7373
6785
|
stage.add(baseLayer);
|
|
7374
|
-
stage.add(eventLayer);
|
|
7375
|
-
|
|
7376
|
-
updateViewport(); // ResizeObserver (맵 크기 변경 감지)
|
|
6786
|
+
stage.add(eventLayer);
|
|
6787
|
+
updateViewport$1(); // ResizeObserver: 맵 크기 변경 감지 (RAF로 debounce)
|
|
7377
6788
|
|
|
7378
6789
|
var resizeRafId = null;
|
|
7379
6790
|
var resizeObserver = new ResizeObserver(function () {
|
|
7380
|
-
// RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
|
|
7381
6791
|
if (resizeRafId !== null) {
|
|
7382
6792
|
cancelAnimationFrame(resizeRafId);
|
|
7383
6793
|
}
|
|
@@ -7387,7 +6797,7 @@
|
|
|
7387
6797
|
stage.height(mapDiv.offsetHeight);
|
|
7388
6798
|
offsetCacheRef.current.clear();
|
|
7389
6799
|
boundingBoxCacheRef.current.clear();
|
|
7390
|
-
updateViewport();
|
|
6800
|
+
updateViewport$1();
|
|
7391
6801
|
renderAllImmediate();
|
|
7392
6802
|
resizeRafId = null;
|
|
7393
6803
|
});
|
|
@@ -7400,10 +6810,9 @@
|
|
|
7400
6810
|
controller.addEventListener('CLICK', handleClick);
|
|
7401
6811
|
controller.addEventListener('MOUSEMOVE', handleMouseMove);
|
|
7402
6812
|
controller.addEventListener('DRAGSTART', handleDragStart);
|
|
7403
|
-
controller.addEventListener('DRAGEND', handleDragEnd);
|
|
7404
|
-
|
|
6813
|
+
controller.addEventListener('DRAGEND', handleDragEnd);
|
|
7405
6814
|
mapDiv.addEventListener('mouseleave', handleMouseLeave);
|
|
7406
|
-
renderAllImmediate(); // Context 사용 시 컴포넌트 등록
|
|
6815
|
+
renderAllImmediate(); // Context 사용 시 컴포넌트 등록
|
|
7407
6816
|
|
|
7408
6817
|
var componentInstance = null;
|
|
7409
6818
|
|
|
@@ -7422,22 +6831,17 @@
|
|
|
7422
6831
|
},
|
|
7423
6832
|
isInteractionDisabled: function () {
|
|
7424
6833
|
return disableInteractionRef.current;
|
|
7425
|
-
}
|
|
7426
|
-
|
|
6834
|
+
}
|
|
7427
6835
|
};
|
|
7428
6836
|
context.registerComponent(componentInstance);
|
|
7429
|
-
}
|
|
7430
|
-
|
|
6837
|
+
}
|
|
7431
6838
|
|
|
7432
6839
|
return function () {
|
|
7433
|
-
// RAF 정리
|
|
7434
6840
|
if (resizeRafId !== null) {
|
|
7435
6841
|
cancelAnimationFrame(resizeRafId);
|
|
7436
|
-
}
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
resizeObserver.disconnect(); // 이벤트 리스너 정리
|
|
6842
|
+
}
|
|
7440
6843
|
|
|
6844
|
+
resizeObserver.disconnect();
|
|
7441
6845
|
controller.removeEventListener('IDLE', handleIdle);
|
|
7442
6846
|
controller.removeEventListener('ZOOMSTART', handleZoomStart);
|
|
7443
6847
|
controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
|
|
@@ -7446,67 +6850,41 @@
|
|
|
7446
6850
|
controller.removeEventListener('MOUSEMOVE', handleMouseMove);
|
|
7447
6851
|
controller.removeEventListener('DRAGSTART', handleDragStart);
|
|
7448
6852
|
controller.removeEventListener('DRAGEND', handleDragEnd);
|
|
7449
|
-
mapDiv.removeEventListener('mouseleave', handleMouseLeave);
|
|
6853
|
+
mapDiv.removeEventListener('mouseleave', handleMouseLeave);
|
|
7450
6854
|
|
|
7451
6855
|
if (context && componentInstance) {
|
|
7452
6856
|
context.unregisterComponent(componentInstance);
|
|
7453
|
-
}
|
|
7454
|
-
|
|
6857
|
+
}
|
|
7455
6858
|
|
|
7456
6859
|
baseLayer.destroyChildren();
|
|
7457
6860
|
eventLayer.destroyChildren();
|
|
7458
|
-
stage.destroy();
|
|
7459
|
-
|
|
6861
|
+
stage.destroy();
|
|
7460
6862
|
offsetCacheRef.current.clear();
|
|
7461
6863
|
boundingBoxCacheRef.current.clear();
|
|
7462
6864
|
spatialIndexRef.current.clear();
|
|
7463
6865
|
};
|
|
7464
|
-
}, []); //
|
|
7465
|
-
// --------------------------------------------------------------------------
|
|
7466
|
-
// Lifecycle: disableInteraction 동기화
|
|
7467
|
-
// --------------------------------------------------------------------------
|
|
6866
|
+
}, []); // disableInteraction 동기화
|
|
7468
6867
|
|
|
7469
6868
|
React.useEffect(function () {
|
|
7470
6869
|
disableInteractionRef.current = disableInteraction;
|
|
7471
|
-
}, [disableInteraction]); //
|
|
7472
|
-
// Lifecycle: 외부 selectedItems 동기화
|
|
7473
|
-
// --------------------------------------------------------------------------
|
|
6870
|
+
}, [disableInteraction]); // 외부 selectedItems 동기화
|
|
7474
6871
|
|
|
7475
6872
|
React.useEffect(function () {
|
|
7476
|
-
if (!stageRef.current) return;
|
|
7477
|
-
|
|
7478
|
-
if (externalSelectedItems === undefined) return; // 외부에서 전달된 selectedItems로 동기화
|
|
7479
|
-
|
|
7480
|
-
var newSelectedIds = new Set();
|
|
7481
|
-
var newSelectedItemsMap = new Map();
|
|
7482
|
-
externalSelectedItems.forEach(function (item) {
|
|
7483
|
-
newSelectedIds.add(item.id);
|
|
7484
|
-
newSelectedItemsMap.set(item.id, item);
|
|
7485
|
-
});
|
|
7486
|
-
selectedIdsRef.current = newSelectedIds;
|
|
7487
|
-
selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
|
|
7488
|
-
|
|
6873
|
+
if (!stageRef.current) return;
|
|
6874
|
+
syncExternalSelectedItems(externalSelectedItems, selectedIdsRef, selectedItemsMapRef);
|
|
7489
6875
|
doRenderBase();
|
|
7490
6876
|
doRenderEvent();
|
|
7491
|
-
}, [externalSelectedItems]); //
|
|
7492
|
-
// --------------------------------------------------------------------------
|
|
7493
|
-
// Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
7494
|
-
// --------------------------------------------------------------------------
|
|
6877
|
+
}, [externalSelectedItems]); // 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
7495
6878
|
|
|
7496
6879
|
React.useEffect(function () {
|
|
7497
|
-
if (!stageRef.current) return;
|
|
7498
|
-
|
|
7499
|
-
selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
|
|
7500
|
-
|
|
6880
|
+
if (!stageRef.current) return;
|
|
6881
|
+
selectedItemRef.current = externalSelectedItem;
|
|
7501
6882
|
doRenderEvent();
|
|
7502
|
-
}, [externalSelectedItem]); //
|
|
7503
|
-
// Lifecycle: 데이터 변경 시 렌더링
|
|
7504
|
-
// --------------------------------------------------------------------------
|
|
6883
|
+
}, [externalSelectedItem]); // 데이터 변경 시 렌더링 (캐시 정리 및 선택 상태 동기화)
|
|
7505
6884
|
|
|
7506
6885
|
React.useEffect(function () {
|
|
7507
|
-
if (!stageRef.current) return;
|
|
7508
|
-
|
|
7509
|
-
dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
|
|
6886
|
+
if (!stageRef.current) return;
|
|
6887
|
+
dataRef.current = data;
|
|
7510
6888
|
|
|
7511
6889
|
if (containerRef.current) {
|
|
7512
6890
|
containerRef.current.style.transform = '';
|
|
@@ -7516,46 +6894,10 @@
|
|
|
7516
6894
|
accumTranslateRef.current = {
|
|
7517
6895
|
x: 0,
|
|
7518
6896
|
y: 0
|
|
7519
|
-
};
|
|
7520
|
-
|
|
6897
|
+
};
|
|
7521
6898
|
offsetCacheRef.current.clear();
|
|
7522
6899
|
boundingBoxCacheRef.current.clear();
|
|
7523
|
-
|
|
7524
|
-
* 선택 상태 동기화 (최적화 버전)
|
|
7525
|
-
*
|
|
7526
|
-
* data가 변경되면 selectedItemsMapRef도 업데이트 필요
|
|
7527
|
-
* (참조가 바뀌므로 기존 Map의 데이터는 stale 상태)
|
|
7528
|
-
*
|
|
7529
|
-
* 🔥 중요: 화면 밖 데이터도 선택 상태 유지!
|
|
7530
|
-
* - 현재 data에 있으면 최신 데이터로 업데이트
|
|
7531
|
-
* - 없으면 기존 selectedItemsMapRef의 데이터 유지
|
|
7532
|
-
*
|
|
7533
|
-
* 최적화: data를 Map으로 먼저 변환하여 find() 순회 제거
|
|
7534
|
-
* - O(전체 데이터 수 + 선택된 개수) - 매우 효율적
|
|
7535
|
-
*/
|
|
7536
|
-
|
|
7537
|
-
var dataMap = new Map(data.map(function (m) {
|
|
7538
|
-
return [m.id, m];
|
|
7539
|
-
}));
|
|
7540
|
-
var newSelectedItemsMap = new Map();
|
|
7541
|
-
selectedIdsRef.current.forEach(function (id) {
|
|
7542
|
-
// 현재 data에 있으면 최신 데이터 사용
|
|
7543
|
-
var currentItem = dataMap.get(id);
|
|
7544
|
-
|
|
7545
|
-
if (currentItem) {
|
|
7546
|
-
newSelectedItemsMap.set(id, currentItem);
|
|
7547
|
-
} else {
|
|
7548
|
-
// 화면 밖이면 기존 데이터 유지
|
|
7549
|
-
var prevItem = selectedItemsMapRef.current.get(id);
|
|
7550
|
-
|
|
7551
|
-
if (prevItem) {
|
|
7552
|
-
newSelectedItemsMap.set(id, prevItem);
|
|
7553
|
-
}
|
|
7554
|
-
}
|
|
7555
|
-
}); // selectedIdsRef는 그대로 유지 (화면 밖 항목도 선택 상태 유지)
|
|
7556
|
-
|
|
7557
|
-
selectedItemsMapRef.current = newSelectedItemsMap; // 즉시 렌더링
|
|
7558
|
-
|
|
6900
|
+
selectedItemsMapRef.current = syncSelectedItems(data, selectedIdsRef.current, selectedItemsMapRef.current);
|
|
7559
6901
|
renderAllImmediate();
|
|
7560
6902
|
}, [data]);
|
|
7561
6903
|
return reactDom.createPortal(React__default["default"].createElement("div", {
|
|
@@ -10445,19 +9787,27 @@
|
|
|
10445
9787
|
exports.WoongCanvasMarker = WoongCanvasMarker;
|
|
10446
9788
|
exports.WoongCanvasPolygon = WoongCanvasPolygon;
|
|
10447
9789
|
exports.WoongCanvasProvider = WoongCanvasProvider;
|
|
9790
|
+
exports.buildSpatialIndex = buildSpatialIndex;
|
|
10448
9791
|
exports.calculateTextBoxWidth = calculateTextBoxWidth;
|
|
10449
9792
|
exports.computeMarkerOffset = computeMarkerOffset;
|
|
10450
9793
|
exports.computePolygonOffsets = computePolygonOffsets;
|
|
9794
|
+
exports.createMapEventHandlers = createMapEventHandlers;
|
|
10451
9795
|
exports.getClusterInfo = getClusterInfo;
|
|
10452
9796
|
exports.getMapOfType = getMapOfType;
|
|
10453
9797
|
exports.hexToRgba = hexToRgba;
|
|
9798
|
+
exports.isInViewport = isInViewport;
|
|
10454
9799
|
exports.isPointInMarkerData = isPointInMarkerData;
|
|
10455
9800
|
exports.isPointInPolygon = isPointInPolygon;
|
|
10456
9801
|
exports.isPointInPolygonData = isPointInPolygonData;
|
|
10457
9802
|
exports.log = log;
|
|
9803
|
+
exports.mapValuesToArray = mapValuesToArray;
|
|
9804
|
+
exports.syncExternalSelectedItems = syncExternalSelectedItems;
|
|
9805
|
+
exports.syncSelectedItems = syncSelectedItems;
|
|
9806
|
+
exports.updateViewport = updateViewport;
|
|
10458
9807
|
exports.useMarkerMoving = useMarkerMoving;
|
|
10459
9808
|
exports.useMintMapController = useMintMapController;
|
|
10460
9809
|
exports.useWoongCanvasContext = useWoongCanvasContext;
|
|
9810
|
+
exports.validateEvent = validateEvent;
|
|
10461
9811
|
exports.waiting = waiting;
|
|
10462
9812
|
|
|
10463
9813
|
Object.defineProperty(exports, '__esModule', { value: true });
|