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