@mint-ui/map 1.2.0-test.31 → 1.2.0-test.33
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/MintMapCore.js +5 -5
- package/dist/components/mint-map/core/advanced/index.d.ts +2 -1
- package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/context.d.ts +5 -5
- package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/context.js +8 -8
- package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/performance.d.ts +1 -1
- package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/performance.js +1 -1
- package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/types.d.ts +9 -12
- package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/utils.d.ts +2 -2
- package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/utils.js +3 -3
- package/dist/components/mint-map/core/advanced/woongCanvasMarker/WoongCanvasMarker.d.ts +97 -0
- package/dist/components/mint-map/core/advanced/{woongCanvas/WoongCanvasLayer.js → woongCanvasMarker/WoongCanvasMarker.js} +42 -123
- package/dist/components/mint-map/core/advanced/woongCanvasMarker/index.d.ts +3 -0
- package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.d.ts +113 -0
- package/dist/components/mint-map/core/advanced/woongCanvasPolygon/WoongCanvasPolygon.js +948 -0
- package/dist/components/mint-map/core/advanced/woongCanvasPolygon/index.d.ts +3 -0
- package/dist/components/mint-map/core/advanced/{woongCanvas/shared → woongCanvasPolygon}/renderer.d.ts +1 -1
- package/dist/components/mint-map/google/GoogleMintMapController.js +4 -4
- package/dist/components/mint-map/kakao/KakaoMintMapController.js +4 -4
- package/dist/components/mint-map/naver/NaverMintMapController.js +4 -4
- package/dist/index.es.js +1101 -260
- package/dist/index.js +10 -8
- package/dist/index.umd.js +1104 -262
- package/package.json +1 -1
- package/dist/components/mint-map/core/advanced/woongCanvas/WoongCanvasLayer.d.ts +0 -162
- package/dist/components/mint-map/core/advanced/woongCanvas/index.d.ts +0 -3
- /package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/index.d.ts +0 -0
- /package/dist/components/mint-map/core/advanced/{woongCanvas/shared → shared}/types.js +0 -0
- /package/dist/components/mint-map/core/advanced/{woongCanvas/shared → woongCanvasPolygon}/renderer.js +0 -0
package/dist/index.es.js
CHANGED
|
@@ -992,8 +992,8 @@ var calculateTextBoxWidth = function (_a) {
|
|
|
992
992
|
return Math.max(minWidth, textWidth + padding);
|
|
993
993
|
};
|
|
994
994
|
|
|
995
|
-
var
|
|
996
|
-
var
|
|
995
|
+
var WoongCanvasContext = createContext(null);
|
|
996
|
+
var WoongCanvasProvider = function (_a) {
|
|
997
997
|
var children = _a.children;
|
|
998
998
|
var controller = useMintMapController(); // Refs
|
|
999
999
|
|
|
@@ -1138,12 +1138,12 @@ var KonvaMarkerProvider = function (_a) {
|
|
|
1138
1138
|
unregisterComponent: unregisterComponent
|
|
1139
1139
|
};
|
|
1140
1140
|
}, [registerComponent, unregisterComponent]);
|
|
1141
|
-
return React.createElement(
|
|
1141
|
+
return React.createElement(WoongCanvasContext.Provider, {
|
|
1142
1142
|
value: contextValue
|
|
1143
1143
|
}, children);
|
|
1144
1144
|
};
|
|
1145
|
-
var
|
|
1146
|
-
var context = useContext(
|
|
1145
|
+
var useWoongCanvasContext = function () {
|
|
1146
|
+
var context = useContext(WoongCanvasContext);
|
|
1147
1147
|
return context;
|
|
1148
1148
|
};
|
|
1149
1149
|
|
|
@@ -1225,7 +1225,7 @@ function () {
|
|
|
1225
1225
|
* - 장점: 읽기 성능 대폭 향상 (10,000번 get → 이전보다 2배 빠름)
|
|
1226
1226
|
* - 단점: 접근 빈도가 아닌 삽입 순서 기반 eviction (FIFO)
|
|
1227
1227
|
*
|
|
1228
|
-
*
|
|
1228
|
+
* WoongCanvasMarker 사용 사례에 최적:
|
|
1229
1229
|
* - 좌표 변환 결과는 zoom/pan 시 어차피 전체 초기화
|
|
1230
1230
|
* - 접근 빈도 추적보다 빠른 조회가 더 중요
|
|
1231
1231
|
*/
|
|
@@ -1563,7 +1563,7 @@ function MintMapCore(_a) {
|
|
|
1563
1563
|
}, [center]);
|
|
1564
1564
|
return React.createElement("div", {
|
|
1565
1565
|
className: cn$3('mint-map-root')
|
|
1566
|
-
}, mapInitialized && React.createElement(
|
|
1566
|
+
}, mapInitialized && React.createElement(WoongCanvasProvider, null, children), React.createElement("div", {
|
|
1567
1567
|
className: cn$3('mint-map-container'),
|
|
1568
1568
|
style: {
|
|
1569
1569
|
visibility: visible ? 'inherit' : 'hidden'
|
|
@@ -5421,6 +5421,1011 @@ function LoadingImage(_a) {
|
|
|
5421
5421
|
}))));
|
|
5422
5422
|
}
|
|
5423
5423
|
|
|
5424
|
+
// 메인 컴포넌트
|
|
5425
|
+
// ============================================================================
|
|
5426
|
+
|
|
5427
|
+
var WoongCanvasMarker = function (props) {
|
|
5428
|
+
var data = props.data,
|
|
5429
|
+
onClick = props.onClick,
|
|
5430
|
+
onMouseOver = props.onMouseOver,
|
|
5431
|
+
onMouseOut = props.onMouseOut,
|
|
5432
|
+
_a = props.enableMultiSelect,
|
|
5433
|
+
enableMultiSelect = _a === void 0 ? false : _a,
|
|
5434
|
+
_b = props.topOnHover,
|
|
5435
|
+
topOnHover = _b === void 0 ? false : _b,
|
|
5436
|
+
_c = props.enableViewportCulling,
|
|
5437
|
+
enableViewportCulling = _c === void 0 ? true : _c,
|
|
5438
|
+
_d = props.cullingMargin,
|
|
5439
|
+
cullingMargin = _d === void 0 ? DEFAULT_CULLING_MARGIN : _d,
|
|
5440
|
+
_e = props.maxCacheSize,
|
|
5441
|
+
maxCacheSize = _e === void 0 ? DEFAULT_MAX_CACHE_SIZE : _e,
|
|
5442
|
+
externalSelectedItems = props.selectedItems,
|
|
5443
|
+
externalSelectedItem = props.selectedItem,
|
|
5444
|
+
_f = props.disableInteraction,
|
|
5445
|
+
disableInteraction = _f === void 0 ? false : _f,
|
|
5446
|
+
renderBase = props.renderBase,
|
|
5447
|
+
renderAnimation = props.renderAnimation,
|
|
5448
|
+
renderEvent = props.renderEvent,
|
|
5449
|
+
options = __rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "topOnHover", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "renderBase", "renderAnimation", "renderEvent"]); // --------------------------------------------------------------------------
|
|
5450
|
+
// Hooks & Context
|
|
5451
|
+
// --------------------------------------------------------------------------
|
|
5452
|
+
|
|
5453
|
+
|
|
5454
|
+
var controller = useMintMapController();
|
|
5455
|
+
var context = useWoongCanvasContext();
|
|
5456
|
+
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
|
|
5457
|
+
// DOM Refs
|
|
5458
|
+
// --------------------------------------------------------------------------
|
|
5459
|
+
|
|
5460
|
+
var divRef = useRef(document.createElement('div'));
|
|
5461
|
+
var divElement = divRef.current;
|
|
5462
|
+
var containerRef = useRef(null);
|
|
5463
|
+
var markerRef = useRef(); // --------------------------------------------------------------------------
|
|
5464
|
+
// Konva Refs
|
|
5465
|
+
// --------------------------------------------------------------------------
|
|
5466
|
+
|
|
5467
|
+
var stageRef = useRef(null);
|
|
5468
|
+
var baseLayerRef = useRef(null);
|
|
5469
|
+
var animationLayerRef = useRef(null);
|
|
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로 관리하여 클로저 문제 해결) */
|
|
5481
|
+
|
|
5482
|
+
var disableInteractionRef = useRef(disableInteraction);
|
|
5483
|
+
/** 현재 Hover 중인 항목 */
|
|
5484
|
+
|
|
5485
|
+
var hoveredItemRef = useRef(null);
|
|
5486
|
+
/** 외부에서 전달된 선택 항목 (Ref로 관리하여 클로저 문제 해결) */
|
|
5487
|
+
|
|
5488
|
+
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
|
+
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
|
+
// --------------------------------------------------------------------------
|
|
5520
|
+
|
|
5521
|
+
var draggingRef = useRef(false);
|
|
5522
|
+
var prevCenterOffsetRef = useRef(null);
|
|
5523
|
+
var accumTranslateRef = useRef({
|
|
5524
|
+
x: 0,
|
|
5525
|
+
y: 0
|
|
5526
|
+
}); // --------------------------------------------------------------------------
|
|
5527
|
+
// Performance Refs (캐싱 & 최적화)
|
|
5528
|
+
// --------------------------------------------------------------------------
|
|
5529
|
+
|
|
5530
|
+
/** 좌표 변환 결과 LRU 캐시 */
|
|
5531
|
+
|
|
5532
|
+
var offsetCacheRef = useRef(new LRUCache(maxCacheSize));
|
|
5533
|
+
/** 공간 인덱스 (빠른 Hit Test) */
|
|
5534
|
+
|
|
5535
|
+
var spatialIndexRef = useRef(new SpatialHashGrid(SPATIAL_GRID_CELL_SIZE));
|
|
5536
|
+
/** 바운딩 박스 캐시 (Viewport Culling 최적화) */
|
|
5537
|
+
|
|
5538
|
+
var boundingBoxCacheRef = useRef(new Map());
|
|
5539
|
+
/** 뷰포트 경계 캐시 (Viewport Culling) */
|
|
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);
|
|
5569
|
+
|
|
5570
|
+
if (!bbox) {
|
|
5571
|
+
// 바운딩 박스 계산 (공통 함수 사용)
|
|
5572
|
+
var computed = computeBoundingBox(item);
|
|
5573
|
+
if (!computed) return false;
|
|
5574
|
+
bbox = computed;
|
|
5575
|
+
boundingBoxCacheRef.current.set(item.id, bbox);
|
|
5576
|
+
} // 바운딩 박스와 viewport 교차 체크
|
|
5577
|
+
|
|
5578
|
+
|
|
5579
|
+
return !(bbox.maxX < viewport.minX || bbox.minX > viewport.maxX || bbox.maxY < viewport.minY || bbox.minY > viewport.maxY);
|
|
5580
|
+
}; // --------------------------------------------------------------------------
|
|
5581
|
+
// 유틸리티 함수: 좌표 변환 캐싱
|
|
5582
|
+
// --------------------------------------------------------------------------
|
|
5583
|
+
|
|
5584
|
+
/**
|
|
5585
|
+
* 마커 좌표 변환 결과를 캐시하고 반환
|
|
5586
|
+
*
|
|
5587
|
+
* @param markerData 마커 데이터
|
|
5588
|
+
* @returns 변환된 좌표 또는 null
|
|
5589
|
+
*/
|
|
5590
|
+
|
|
5591
|
+
|
|
5592
|
+
var getOrComputeMarkerOffset = function (markerData) {
|
|
5593
|
+
var cached = offsetCacheRef.current.get(markerData.id);
|
|
5594
|
+
if (cached && !Array.isArray(cached)) return cached;
|
|
5595
|
+
var result = computeMarkerOffset(markerData, controller);
|
|
5596
|
+
|
|
5597
|
+
if (result) {
|
|
5598
|
+
offsetCacheRef.current.set(markerData.id, result);
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
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
|
+
*/
|
|
5616
|
+
|
|
5617
|
+
|
|
5618
|
+
var computeBoundingBox = function (item) {
|
|
5619
|
+
// 마커: 중심점 기준 박스 크기 계산 (꼬리 포함)
|
|
5620
|
+
var offset = getOrComputeMarkerOffset(item);
|
|
5621
|
+
if (!offset) return null;
|
|
5622
|
+
var boxWidth = item.boxWidth || 50;
|
|
5623
|
+
var boxHeight = item.boxHeight || 28;
|
|
5624
|
+
var tailHeight = item.tailHeight || 0; // 🎯 tailHeight 사용 (Viewport Culling용)
|
|
5625
|
+
|
|
5626
|
+
return {
|
|
5627
|
+
minX: offset.x - boxWidth / 2,
|
|
5628
|
+
minY: offset.y - boxHeight - tailHeight,
|
|
5629
|
+
maxX: offset.x + boxWidth / 2,
|
|
5630
|
+
maxY: offset.y
|
|
5631
|
+
};
|
|
5632
|
+
}; // --------------------------------------------------------------------------
|
|
5633
|
+
// 유틸리티 함수: 공간 인덱싱
|
|
5634
|
+
// --------------------------------------------------------------------------
|
|
5635
|
+
|
|
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
|
+
|
|
5659
|
+
/**
|
|
5660
|
+
* 외부 렌더링 함수에 전달할 유틸리티 객체
|
|
5661
|
+
*/
|
|
5662
|
+
|
|
5663
|
+
|
|
5664
|
+
var renderUtils = {
|
|
5665
|
+
getOrComputePolygonOffsets: function () {
|
|
5666
|
+
return null;
|
|
5667
|
+
},
|
|
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
|
+
*/
|
|
5685
|
+
|
|
5686
|
+
var doRenderBase = function () {
|
|
5687
|
+
var layer = baseLayerRef.current;
|
|
5688
|
+
if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
|
|
5689
|
+
|
|
5690
|
+
var shape = layer.findOne('.base-render-shape');
|
|
5691
|
+
|
|
5692
|
+
if (!shape) {
|
|
5693
|
+
// 최초 생성 (한 번만 실행됨)
|
|
5694
|
+
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
5695
|
+
shape = new Konva.Shape({
|
|
5696
|
+
name: 'base-render-shape',
|
|
5697
|
+
sceneFunc: function (context, shape) {
|
|
5698
|
+
var ctx = context;
|
|
5699
|
+
var hovered = hoveredItemRef.current; // 클로저로 최신 ref 값 참조
|
|
5700
|
+
|
|
5701
|
+
var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
|
|
5702
|
+
return isInViewport(item);
|
|
5703
|
+
}) : dataRef.current; // topOnHover가 true이고 renderEvent가 없으면 Base Layer에서 hover 처리
|
|
5704
|
+
|
|
5705
|
+
if (topOnHover && !renderEvent && hovered) {
|
|
5706
|
+
// hover된 항목 제외하고 렌더링
|
|
5707
|
+
visibleItems = visibleItems.filter(function (item) {
|
|
5708
|
+
return item.id !== hovered.id;
|
|
5709
|
+
});
|
|
5710
|
+
} // 일반 항목 렌더링
|
|
5711
|
+
|
|
5712
|
+
|
|
5713
|
+
renderBase({
|
|
5714
|
+
ctx: ctx,
|
|
5715
|
+
items: visibleItems,
|
|
5716
|
+
selectedIds: selectedIdsRef.current,
|
|
5717
|
+
hoveredItem: hovered,
|
|
5718
|
+
utils: renderUtils
|
|
5719
|
+
}); // hover된 항목을 최상단에 렌더링 (renderEvent가 없을 때만)
|
|
5720
|
+
|
|
5721
|
+
if (topOnHover && !renderEvent && hovered) {
|
|
5722
|
+
var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
|
|
5723
|
+
|
|
5724
|
+
if (isHoveredInViewport) {
|
|
5725
|
+
renderBase({
|
|
5726
|
+
ctx: ctx,
|
|
5727
|
+
items: [hovered],
|
|
5728
|
+
selectedIds: selectedIdsRef.current,
|
|
5729
|
+
hoveredItem: hovered,
|
|
5730
|
+
utils: renderUtils
|
|
5731
|
+
});
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
},
|
|
5735
|
+
perfectDrawEnabled: false,
|
|
5736
|
+
listening: false,
|
|
5737
|
+
hitStrokeWidth: 0
|
|
5738
|
+
});
|
|
5739
|
+
layer.add(shape);
|
|
5740
|
+
} // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
|
|
5741
|
+
|
|
5742
|
+
|
|
5743
|
+
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
|
+
*/
|
|
5773
|
+
|
|
5774
|
+
|
|
5775
|
+
var doRenderEvent = function () {
|
|
5776
|
+
var layer = eventLayerRef.current;
|
|
5777
|
+
if (!layer) return;
|
|
5778
|
+
if (!renderEvent) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
|
|
5779
|
+
|
|
5780
|
+
var shape = layer.findOne('.event-render-shape');
|
|
5781
|
+
|
|
5782
|
+
if (!shape) {
|
|
5783
|
+
// 최초 생성 (한 번만 실행됨)
|
|
5784
|
+
// sceneFunc도 여기서 한 번만 설정 (클로저로 최신 데이터 참조)
|
|
5785
|
+
shape = new Konva.Shape({
|
|
5786
|
+
name: 'event-render-shape',
|
|
5787
|
+
sceneFunc: function (context, shape) {
|
|
5788
|
+
var ctx = context; // 클로저로 최신 ref 값 참조
|
|
5789
|
+
|
|
5790
|
+
var selectedItems = Array.from(selectedItemsMapRef.current.values());
|
|
5791
|
+
var hovered = hoveredItemRef.current; // topOnHover가 true이면 hover된 항목을 최상단에 렌더링
|
|
5792
|
+
|
|
5793
|
+
if (topOnHover && hovered) {
|
|
5794
|
+
// 1. 먼저 일반 항목들 렌더링 (hover된 항목 제외)
|
|
5795
|
+
renderEvent({
|
|
5796
|
+
ctx: ctx,
|
|
5797
|
+
hoveredItem: null,
|
|
5798
|
+
utils: renderUtils,
|
|
5799
|
+
selectedItems: selectedItems.filter(function (item) {
|
|
5800
|
+
return item.id !== hovered.id;
|
|
5801
|
+
}),
|
|
5802
|
+
selectedItem: selectedItemRef.current
|
|
5803
|
+
}); // 2. hover된 항목을 최상단에 렌더링
|
|
5804
|
+
|
|
5805
|
+
var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
|
|
5806
|
+
|
|
5807
|
+
if (isHoveredInViewport) {
|
|
5808
|
+
// hover된 항목이 선택되어 있다면 hoverSelectedItems에 포함시켜서
|
|
5809
|
+
// renderEvent에서 hover 스타일만 적용되도록 함
|
|
5810
|
+
var hoveredIsSelected = selectedItems.some(function (item) {
|
|
5811
|
+
return item.id === hovered.id;
|
|
5812
|
+
});
|
|
5813
|
+
var hoverSelectedItems = hoveredIsSelected ? [hovered] : [];
|
|
5814
|
+
renderEvent({
|
|
5815
|
+
ctx: ctx,
|
|
5816
|
+
hoveredItem: hovered,
|
|
5817
|
+
utils: renderUtils,
|
|
5818
|
+
selectedItems: hoverSelectedItems,
|
|
5819
|
+
selectedItem: selectedItemRef.current
|
|
5820
|
+
});
|
|
5821
|
+
}
|
|
5822
|
+
} else {
|
|
5823
|
+
// topOnHover가 false이거나 hover된 항목이 없으면 일반 렌더링
|
|
5824
|
+
renderEvent({
|
|
5825
|
+
ctx: ctx,
|
|
5826
|
+
hoveredItem: hovered,
|
|
5827
|
+
utils: renderUtils,
|
|
5828
|
+
selectedItems: selectedItems,
|
|
5829
|
+
selectedItem: selectedItemRef.current
|
|
5830
|
+
});
|
|
5831
|
+
}
|
|
5832
|
+
},
|
|
5833
|
+
perfectDrawEnabled: false,
|
|
5834
|
+
listening: false,
|
|
5835
|
+
hitStrokeWidth: 0
|
|
5836
|
+
});
|
|
5837
|
+
layer.add(shape);
|
|
5838
|
+
} // sceneFunc는 이미 설정되어 있으므로 다시 그리기만
|
|
5839
|
+
|
|
5840
|
+
|
|
5841
|
+
layer.batchDraw();
|
|
5842
|
+
};
|
|
5843
|
+
/**
|
|
5844
|
+
* 전체 즉시 렌더링 (IDLE 시 호출)
|
|
5845
|
+
*/
|
|
5846
|
+
|
|
5847
|
+
|
|
5848
|
+
var renderAllImmediate = function () {
|
|
5849
|
+
updateViewport();
|
|
5850
|
+
buildSpatialIndex();
|
|
5851
|
+
doRenderBase();
|
|
5852
|
+
doRenderAnimation();
|
|
5853
|
+
doRenderEvent();
|
|
5854
|
+
}; // --------------------------------------------------------------------------
|
|
5855
|
+
// 이벤트 핸들러: 지도 이벤트
|
|
5856
|
+
// --------------------------------------------------------------------------
|
|
5857
|
+
|
|
5858
|
+
/**
|
|
5859
|
+
* 지도 이동/줌 완료 시 처리
|
|
5860
|
+
*/
|
|
5861
|
+
|
|
5862
|
+
|
|
5863
|
+
var handleIdle = function () {
|
|
5864
|
+
prevCenterOffsetRef.current = null;
|
|
5865
|
+
accumTranslateRef.current = {
|
|
5866
|
+
x: 0,
|
|
5867
|
+
y: 0
|
|
5868
|
+
}; // 2. 캐시 정리 (지도 이동/줌으로 좌표 변환 결과가 바뀜)
|
|
5869
|
+
|
|
5870
|
+
offsetCacheRef.current.clear();
|
|
5871
|
+
boundingBoxCacheRef.current.clear(); // 3. 마커 위치 업데이트
|
|
5872
|
+
|
|
5873
|
+
var bounds = controller.getCurrBounds();
|
|
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
|
+
*/
|
|
5902
|
+
|
|
5903
|
+
|
|
5904
|
+
var handleZoomEnd = function () {
|
|
5905
|
+
if (containerRef.current) {
|
|
5906
|
+
containerRef.current.style.visibility = '';
|
|
5907
|
+
}
|
|
5908
|
+
};
|
|
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
|
+
|
|
5945
|
+
/**
|
|
5946
|
+
* 특정 좌표의 마커 데이터 찾기 (Spatial Index 사용)
|
|
5947
|
+
*
|
|
5948
|
+
* topOnHover가 true일 때:
|
|
5949
|
+
* - 현재 hover된 항목을 최우선으로 체크
|
|
5950
|
+
* - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
|
|
5951
|
+
*
|
|
5952
|
+
* @param offset 검사할 좌표
|
|
5953
|
+
* @returns 찾은 마커 데이터 또는 null
|
|
5954
|
+
*/
|
|
5955
|
+
|
|
5956
|
+
|
|
5957
|
+
var findData = function (offset) {
|
|
5958
|
+
// topOnHover가 true이고 현재 hover된 항목이 있으면, 그것을 먼저 체크
|
|
5959
|
+
if (topOnHover && hoveredItemRef.current) {
|
|
5960
|
+
var hovered = hoveredItemRef.current;
|
|
5961
|
+
|
|
5962
|
+
if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
|
|
5963
|
+
return hovered; // 여전히 hover된 항목 위에 있음
|
|
5964
|
+
}
|
|
5965
|
+
} // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 → ~10개)
|
|
5966
|
+
|
|
5967
|
+
|
|
5968
|
+
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 마커 체크
|
|
5969
|
+
|
|
5970
|
+
for (var i = candidates.length - 1; i >= 0; i--) {
|
|
5971
|
+
var item = candidates[i];
|
|
5972
|
+
|
|
5973
|
+
if (isPointInMarkerData(offset, item, getOrComputeMarkerOffset)) {
|
|
5974
|
+
return item;
|
|
5975
|
+
}
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
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
|
+
*/
|
|
5991
|
+
|
|
5992
|
+
|
|
5993
|
+
var setHovered = function (data) {
|
|
5994
|
+
hoveredItemRef.current = data;
|
|
5995
|
+
|
|
5996
|
+
if (draggingRef.current) {
|
|
5997
|
+
controller.setMapCursor('grabbing');
|
|
5998
|
+
} else {
|
|
5999
|
+
controller.setMapCursor(data ? 'pointer' : 'grab');
|
|
6000
|
+
} // 즉시 렌더링 (RAF 없이)
|
|
6001
|
+
|
|
6002
|
+
|
|
6003
|
+
if (renderEvent) {
|
|
6004
|
+
// renderEvent가 있으면 Event Layer에서만 처리 (성능 최적화)
|
|
6005
|
+
doRenderEvent();
|
|
6006
|
+
} else if (topOnHover) {
|
|
6007
|
+
// renderEvent가 없고 topOnHover가 true면 Base Layer에서 처리
|
|
6008
|
+
doRenderBase();
|
|
6009
|
+
}
|
|
6010
|
+
};
|
|
6011
|
+
/**
|
|
6012
|
+
* 클릭 처리 (단일/다중 선택)
|
|
6013
|
+
*
|
|
6014
|
+
* @param data 클릭된 마커/폴리곤 데이터
|
|
6015
|
+
*
|
|
6016
|
+
* 🔥 최적화: 단일 Shape 렌더링으로 Base Layer 재렌더링 속도 향상
|
|
6017
|
+
* - sceneFunc에서 selectedIds를 체크하여 선택된 마커만 스킵
|
|
6018
|
+
* - 객체 생성 오버헤드 제거로 1000개 이상도 부드럽게 처리
|
|
6019
|
+
*/
|
|
6020
|
+
|
|
6021
|
+
|
|
6022
|
+
var handleLocalClick = function (data) {
|
|
6023
|
+
// 1. 선택 상태 업데이트
|
|
6024
|
+
if (enableMultiSelect) {
|
|
6025
|
+
// 다중 선택: Set과 Map 동시 업데이트
|
|
6026
|
+
var newSelected = new Set(selectedIdsRef.current);
|
|
6027
|
+
|
|
6028
|
+
if (newSelected.has(data.id)) {
|
|
6029
|
+
newSelected.delete(data.id);
|
|
6030
|
+
selectedItemsMapRef.current.delete(data.id);
|
|
6031
|
+
} else {
|
|
6032
|
+
newSelected.add(data.id);
|
|
6033
|
+
selectedItemsMapRef.current.set(data.id, data);
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
selectedIdsRef.current = newSelected;
|
|
6037
|
+
} else {
|
|
6038
|
+
// 단일 선택: 토글
|
|
6039
|
+
var newSelected = new Set();
|
|
6040
|
+
|
|
6041
|
+
if (!selectedIdsRef.current.has(data.id)) {
|
|
6042
|
+
newSelected.add(data.id);
|
|
6043
|
+
selectedItemsMapRef.current.clear();
|
|
6044
|
+
selectedItemsMapRef.current.set(data.id, data);
|
|
6045
|
+
} else {
|
|
6046
|
+
selectedItemsMapRef.current.clear();
|
|
6047
|
+
}
|
|
6048
|
+
|
|
6049
|
+
selectedIdsRef.current = newSelected;
|
|
6050
|
+
}
|
|
6051
|
+
|
|
6052
|
+
if (!!renderAnimation) {
|
|
6053
|
+
// 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
|
|
6054
|
+
doRenderBase(); // 3. Animation Layer 렌더링 (선택된 마커 애니메이션)
|
|
6055
|
+
|
|
6056
|
+
doRenderAnimation();
|
|
6057
|
+
} // 4. Event Layer 렌더링 (hover 처리)
|
|
6058
|
+
|
|
6059
|
+
|
|
6060
|
+
doRenderEvent();
|
|
6061
|
+
}; // --------------------------------------------------------------------------
|
|
6062
|
+
// 이벤트 핸들러: UI 이벤트
|
|
6063
|
+
// --------------------------------------------------------------------------
|
|
6064
|
+
|
|
6065
|
+
/**
|
|
6066
|
+
* 클릭 이벤트 처리
|
|
6067
|
+
*/
|
|
6068
|
+
|
|
6069
|
+
|
|
6070
|
+
var handleClick = function (event) {
|
|
6071
|
+
var _a;
|
|
6072
|
+
|
|
6073
|
+
if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
|
|
6074
|
+
|
|
6075
|
+
if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
|
|
6076
|
+
|
|
6077
|
+
try {
|
|
6078
|
+
var clickedOffset = controller.positionToOffset(event.param.position);
|
|
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
|
+
*/
|
|
6095
|
+
|
|
6096
|
+
|
|
6097
|
+
var handleMouseMove = function (event) {
|
|
6098
|
+
var _a;
|
|
6099
|
+
|
|
6100
|
+
if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
|
|
6101
|
+
|
|
6102
|
+
if (context || !((_a = event === null || event === void 0 ? void 0 : event.param) === null || _a === void 0 ? void 0 : _a.position)) return;
|
|
6103
|
+
|
|
6104
|
+
try {
|
|
6105
|
+
var mouseOffset = controller.positionToOffset(event.param.position);
|
|
6106
|
+
var hoveredItem = findData(mouseOffset);
|
|
6107
|
+
var prevHovered = hoveredItemRef.current;
|
|
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
|
+
*/
|
|
6139
|
+
|
|
6140
|
+
|
|
6141
|
+
var handleMouseLeave = function () {
|
|
6142
|
+
if (disableInteractionRef.current) return; // 🚫 상호작용 비활성화 시 즉시 반환
|
|
6143
|
+
|
|
6144
|
+
var prevHovered = hoveredItemRef.current;
|
|
6145
|
+
|
|
6146
|
+
if (prevHovered) {
|
|
6147
|
+
hoveredItemRef.current = null;
|
|
6148
|
+
controller.setMapCursor('grab');
|
|
6149
|
+
doRenderEvent();
|
|
6150
|
+
|
|
6151
|
+
if (onMouseOut) {
|
|
6152
|
+
onMouseOut(prevHovered);
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
}; // --------------------------------------------------------------------------
|
|
6156
|
+
// Lifecycle: DOM 초기화
|
|
6157
|
+
// --------------------------------------------------------------------------
|
|
6158
|
+
|
|
6159
|
+
|
|
6160
|
+
useEffect(function () {
|
|
6161
|
+
divElement.style.width = 'fit-content';
|
|
6162
|
+
return function () {
|
|
6163
|
+
if (markerRef.current) {
|
|
6164
|
+
controller.clearDrawable(markerRef.current);
|
|
6165
|
+
markerRef.current = undefined;
|
|
6166
|
+
}
|
|
6167
|
+
};
|
|
6168
|
+
}, []); // --------------------------------------------------------------------------
|
|
6169
|
+
// Lifecycle: 마커 생성/업데이트
|
|
6170
|
+
// --------------------------------------------------------------------------
|
|
6171
|
+
|
|
6172
|
+
useEffect(function () {
|
|
6173
|
+
if (options) {
|
|
6174
|
+
var bounds = controller.getCurrBounds();
|
|
6175
|
+
|
|
6176
|
+
var markerOptions = __assign({
|
|
6177
|
+
position: bounds.nw
|
|
6178
|
+
}, options);
|
|
6179
|
+
|
|
6180
|
+
if (markerRef.current) {
|
|
6181
|
+
controller.updateMarker(markerRef.current, markerOptions);
|
|
6182
|
+
} else {
|
|
6183
|
+
markerRef.current = new Marker(markerOptions);
|
|
6184
|
+
markerRef.current.element = divElement;
|
|
6185
|
+
controller.createMarker(markerRef.current);
|
|
6186
|
+
|
|
6187
|
+
if (divElement.parentElement) {
|
|
6188
|
+
divElement.parentElement.style.pointerEvents = 'none';
|
|
6189
|
+
}
|
|
6190
|
+
|
|
6191
|
+
if (options.zIndex !== undefined) {
|
|
6192
|
+
controller.setMarkerZIndex(markerRef.current, options.zIndex);
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
}
|
|
6196
|
+
}, [options]); // --------------------------------------------------------------------------
|
|
6197
|
+
// Lifecycle: Konva 초기화 및 이벤트 리스너 등록
|
|
6198
|
+
// --------------------------------------------------------------------------
|
|
6199
|
+
|
|
6200
|
+
useEffect(function () {
|
|
6201
|
+
var mapDiv = controller.mapDivElement;
|
|
6202
|
+
var stage = new Konva.Stage({
|
|
6203
|
+
container: containerRef.current,
|
|
6204
|
+
width: mapDiv.offsetWidth,
|
|
6205
|
+
height: mapDiv.offsetHeight
|
|
6206
|
+
});
|
|
6207
|
+
stageRef.current = stage; // 레이어 최적화 설정
|
|
6208
|
+
|
|
6209
|
+
var baseLayer = new Konva.Layer({
|
|
6210
|
+
listening: false // 이벤트 리스닝 비활성화로 성능 향상
|
|
6211
|
+
|
|
6212
|
+
});
|
|
6213
|
+
var animationLayer = new Konva.Layer({
|
|
6214
|
+
listening: false
|
|
6215
|
+
});
|
|
6216
|
+
var eventLayer = new Konva.Layer({
|
|
6217
|
+
listening: false
|
|
6218
|
+
});
|
|
6219
|
+
baseLayerRef.current = baseLayer;
|
|
6220
|
+
animationLayerRef.current = animationLayer;
|
|
6221
|
+
eventLayerRef.current = eventLayer;
|
|
6222
|
+
stage.add(baseLayer);
|
|
6223
|
+
|
|
6224
|
+
if (renderAnimation) {
|
|
6225
|
+
stage.add(animationLayer);
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
stage.add(eventLayer); // 초기 뷰포트 설정
|
|
6229
|
+
|
|
6230
|
+
updateViewport(); // ResizeObserver (맵 크기 변경 감지)
|
|
6231
|
+
|
|
6232
|
+
var resizeRafId = null;
|
|
6233
|
+
var resizeObserver = new ResizeObserver(function () {
|
|
6234
|
+
// RAF로 다음 프레임에 한 번만 실행 (debounce 효과)
|
|
6235
|
+
if (resizeRafId !== null) {
|
|
6236
|
+
cancelAnimationFrame(resizeRafId);
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
resizeRafId = requestAnimationFrame(function () {
|
|
6240
|
+
stage.width(mapDiv.offsetWidth);
|
|
6241
|
+
stage.height(mapDiv.offsetHeight);
|
|
6242
|
+
offsetCacheRef.current.clear();
|
|
6243
|
+
boundingBoxCacheRef.current.clear();
|
|
6244
|
+
updateViewport();
|
|
6245
|
+
renderAllImmediate();
|
|
6246
|
+
resizeRafId = null;
|
|
6247
|
+
});
|
|
6248
|
+
});
|
|
6249
|
+
resizeObserver.observe(mapDiv);
|
|
6250
|
+
controller.addEventListener('IDLE', handleIdle);
|
|
6251
|
+
controller.addEventListener('ZOOMSTART', handleZoomStart);
|
|
6252
|
+
controller.addEventListener('ZOOM_CHANGED', handleZoomEnd);
|
|
6253
|
+
controller.addEventListener('CENTER_CHANGED', handleCenterChanged);
|
|
6254
|
+
controller.addEventListener('CLICK', handleClick);
|
|
6255
|
+
controller.addEventListener('MOUSEMOVE', handleMouseMove);
|
|
6256
|
+
controller.addEventListener('DRAGSTART', handleDragStart);
|
|
6257
|
+
controller.addEventListener('DRAGEND', handleDragEnd); // 맵 컨테이너에 mouseleave 이벤트 추가
|
|
6258
|
+
|
|
6259
|
+
mapDiv.addEventListener('mouseleave', handleMouseLeave);
|
|
6260
|
+
renderAllImmediate(); // Context 사용 시 컴포넌트 등록 (다중 인스턴스 관리)
|
|
6261
|
+
|
|
6262
|
+
var componentInstance = null;
|
|
6263
|
+
|
|
6264
|
+
if (context) {
|
|
6265
|
+
componentInstance = {
|
|
6266
|
+
zIndex: currentZIndex,
|
|
6267
|
+
hitTest: function (offset) {
|
|
6268
|
+
return findData(offset) !== null;
|
|
6269
|
+
},
|
|
6270
|
+
onClick: onClick,
|
|
6271
|
+
onMouseOver: onMouseOver,
|
|
6272
|
+
onMouseOut: onMouseOut,
|
|
6273
|
+
findData: findData,
|
|
6274
|
+
setHovered: setHovered,
|
|
6275
|
+
handleLocalClick: handleLocalClick,
|
|
6276
|
+
getSelectedIds: function () {
|
|
6277
|
+
return selectedIdsRef.current;
|
|
6278
|
+
},
|
|
6279
|
+
isInteractionDisabled: function () {
|
|
6280
|
+
return disableInteractionRef.current;
|
|
6281
|
+
} // 🚫 상호작용 비활성화 여부 반환
|
|
6282
|
+
|
|
6283
|
+
};
|
|
6284
|
+
context.registerComponent(componentInstance);
|
|
6285
|
+
} // Cleanup 함수
|
|
6286
|
+
|
|
6287
|
+
|
|
6288
|
+
return function () {
|
|
6289
|
+
// RAF 정리
|
|
6290
|
+
if (resizeRafId !== null) {
|
|
6291
|
+
cancelAnimationFrame(resizeRafId);
|
|
6292
|
+
} // 옵저버 정리
|
|
6293
|
+
|
|
6294
|
+
|
|
6295
|
+
resizeObserver.disconnect(); // 이벤트 리스너 정리
|
|
6296
|
+
|
|
6297
|
+
controller.removeEventListener('IDLE', handleIdle);
|
|
6298
|
+
controller.removeEventListener('ZOOMSTART', handleZoomStart);
|
|
6299
|
+
controller.removeEventListener('ZOOM_CHANGED', handleZoomEnd);
|
|
6300
|
+
controller.removeEventListener('CENTER_CHANGED', handleCenterChanged);
|
|
6301
|
+
controller.removeEventListener('CLICK', handleClick);
|
|
6302
|
+
controller.removeEventListener('MOUSEMOVE', handleMouseMove);
|
|
6303
|
+
controller.removeEventListener('DRAGSTART', handleDragStart);
|
|
6304
|
+
controller.removeEventListener('DRAGEND', handleDragEnd);
|
|
6305
|
+
mapDiv.removeEventListener('mouseleave', handleMouseLeave); // Context 정리
|
|
6306
|
+
|
|
6307
|
+
if (context && componentInstance) {
|
|
6308
|
+
context.unregisterComponent(componentInstance);
|
|
6309
|
+
} // Konva 리소스 정리
|
|
6310
|
+
|
|
6311
|
+
|
|
6312
|
+
baseLayer.destroyChildren();
|
|
6313
|
+
animationLayer.destroyChildren();
|
|
6314
|
+
eventLayer.destroyChildren();
|
|
6315
|
+
stage.destroy(); // 캐시 정리
|
|
6316
|
+
|
|
6317
|
+
offsetCacheRef.current.clear();
|
|
6318
|
+
boundingBoxCacheRef.current.clear();
|
|
6319
|
+
spatialIndexRef.current.clear();
|
|
6320
|
+
};
|
|
6321
|
+
}, []); // 초기화는 한 번만
|
|
6322
|
+
// --------------------------------------------------------------------------
|
|
6323
|
+
// Lifecycle: disableInteraction 동기화
|
|
6324
|
+
// --------------------------------------------------------------------------
|
|
6325
|
+
|
|
6326
|
+
useEffect(function () {
|
|
6327
|
+
disableInteractionRef.current = disableInteraction;
|
|
6328
|
+
}, [disableInteraction]); // --------------------------------------------------------------------------
|
|
6329
|
+
// Lifecycle: 외부 selectedItems 동기화
|
|
6330
|
+
// --------------------------------------------------------------------------
|
|
6331
|
+
|
|
6332
|
+
useEffect(function () {
|
|
6333
|
+
if (!stageRef.current) return; // externalSelectedItems가 undefined면 외부 제어 안 함
|
|
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
|
+
|
|
6346
|
+
doRenderBase();
|
|
6347
|
+
doRenderAnimation();
|
|
6348
|
+
doRenderEvent();
|
|
6349
|
+
}, [externalSelectedItems]); // 배열 자체를 dependency로 사용
|
|
6350
|
+
// --------------------------------------------------------------------------
|
|
6351
|
+
// Lifecycle: 외부 selectedItem 변경 시 Event Layer 리렌더링
|
|
6352
|
+
// --------------------------------------------------------------------------
|
|
6353
|
+
|
|
6354
|
+
useEffect(function () {
|
|
6355
|
+
if (!stageRef.current) return; // Ref 동기화
|
|
6356
|
+
|
|
6357
|
+
selectedItemRef.current = externalSelectedItem; // selectedItem이 변경되면 Event Layer만 다시 그림
|
|
6358
|
+
|
|
6359
|
+
doRenderEvent();
|
|
6360
|
+
}, [externalSelectedItem]); // --------------------------------------------------------------------------
|
|
6361
|
+
// Lifecycle: 데이터 변경 시 렌더링
|
|
6362
|
+
// --------------------------------------------------------------------------
|
|
6363
|
+
|
|
6364
|
+
useEffect(function () {
|
|
6365
|
+
if (!stageRef.current) return; // dataRef 동기화
|
|
6366
|
+
|
|
6367
|
+
dataRef.current = data; // 데이터 변경 시 즉시 transform 제거 및 캐시 정리 (겹침 방지)
|
|
6368
|
+
|
|
6369
|
+
if (containerRef.current) {
|
|
6370
|
+
containerRef.current.style.transform = '';
|
|
6371
|
+
}
|
|
6372
|
+
|
|
6373
|
+
prevCenterOffsetRef.current = null;
|
|
6374
|
+
accumTranslateRef.current = {
|
|
6375
|
+
x: 0,
|
|
6376
|
+
y: 0
|
|
6377
|
+
}; // 캐시 정리 (새 데이터이므로 기존 캐시는 무효)
|
|
6378
|
+
|
|
6379
|
+
offsetCacheRef.current.clear();
|
|
6380
|
+
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
|
+
|
|
6417
|
+
renderAllImmediate();
|
|
6418
|
+
}, [data]);
|
|
6419
|
+
return createPortal(React.createElement("div", {
|
|
6420
|
+
ref: containerRef,
|
|
6421
|
+
style: {
|
|
6422
|
+
position: 'absolute',
|
|
6423
|
+
width: '100%',
|
|
6424
|
+
height: '100%'
|
|
6425
|
+
}
|
|
6426
|
+
}), divElement);
|
|
6427
|
+
};
|
|
6428
|
+
|
|
5424
6429
|
/**
|
|
5425
6430
|
* 폴리곤 렌더링 유틸리티
|
|
5426
6431
|
*
|
|
@@ -5651,33 +6656,42 @@ var renderPolygonEvent = function (baseFillColor, baseStrokeColor, baseLineWidth
|
|
|
5651
6656
|
// 메인 컴포넌트
|
|
5652
6657
|
// ============================================================================
|
|
5653
6658
|
|
|
5654
|
-
var
|
|
6659
|
+
var WoongCanvasPolygon = function (props) {
|
|
5655
6660
|
var data = props.data,
|
|
5656
|
-
dataType = props.dataType,
|
|
5657
6661
|
onClick = props.onClick,
|
|
5658
6662
|
onMouseOver = props.onMouseOver,
|
|
5659
6663
|
onMouseOut = props.onMouseOut,
|
|
5660
6664
|
_a = props.enableMultiSelect,
|
|
5661
6665
|
enableMultiSelect = _a === void 0 ? false : _a,
|
|
5662
|
-
_b = props.
|
|
5663
|
-
|
|
5664
|
-
_c = props.
|
|
5665
|
-
|
|
5666
|
-
_d = props.
|
|
5667
|
-
|
|
5668
|
-
_e = props.maxCacheSize,
|
|
5669
|
-
maxCacheSize = _e === void 0 ? DEFAULT_MAX_CACHE_SIZE : _e,
|
|
6666
|
+
_b = props.enableViewportCulling,
|
|
6667
|
+
enableViewportCulling = _b === void 0 ? true : _b,
|
|
6668
|
+
_c = props.cullingMargin,
|
|
6669
|
+
cullingMargin = _c === void 0 ? DEFAULT_CULLING_MARGIN : _c,
|
|
6670
|
+
_d = props.maxCacheSize,
|
|
6671
|
+
maxCacheSize = _d === void 0 ? DEFAULT_MAX_CACHE_SIZE : _d,
|
|
5670
6672
|
externalSelectedItems = props.selectedItems,
|
|
5671
6673
|
externalSelectedItem = props.selectedItem,
|
|
5672
|
-
|
|
5673
|
-
disableInteraction =
|
|
5674
|
-
|
|
6674
|
+
_e = props.disableInteraction,
|
|
6675
|
+
disableInteraction = _e === void 0 ? false : _e,
|
|
6676
|
+
baseFillColor = props.baseFillColor,
|
|
6677
|
+
baseStrokeColor = props.baseStrokeColor,
|
|
6678
|
+
baseLineWidth = props.baseLineWidth,
|
|
6679
|
+
selectedFillColor = props.selectedFillColor,
|
|
6680
|
+
selectedStrokeColor = props.selectedStrokeColor,
|
|
6681
|
+
selectedLineWidth = props.selectedLineWidth,
|
|
6682
|
+
activeFillColor = props.activeFillColor,
|
|
6683
|
+
activeStrokeColor = props.activeStrokeColor,
|
|
6684
|
+
activeLineWidth = props.activeLineWidth,
|
|
6685
|
+
hoveredFillColor = props.hoveredFillColor,
|
|
6686
|
+
hoveredStrokeColor = props.hoveredStrokeColor,
|
|
6687
|
+
hoveredLineWidth = props.hoveredLineWidth,
|
|
6688
|
+
options = __rest(props, ["data", "onClick", "onMouseOver", "onMouseOut", "enableMultiSelect", "enableViewportCulling", "cullingMargin", "maxCacheSize", "selectedItems", "selectedItem", "disableInteraction", "baseFillColor", "baseStrokeColor", "baseLineWidth", "selectedFillColor", "selectedStrokeColor", "selectedLineWidth", "activeFillColor", "activeStrokeColor", "activeLineWidth", "hoveredFillColor", "hoveredStrokeColor", "hoveredLineWidth"]); // --------------------------------------------------------------------------
|
|
5675
6689
|
// Hooks & Context
|
|
5676
6690
|
// --------------------------------------------------------------------------
|
|
5677
6691
|
|
|
5678
6692
|
|
|
5679
6693
|
var controller = useMintMapController();
|
|
5680
|
-
var context =
|
|
6694
|
+
var context = useWoongCanvasContext();
|
|
5681
6695
|
var currentZIndex = options.zIndex !== undefined ? options.zIndex : 0; // --------------------------------------------------------------------------
|
|
5682
6696
|
// DOM Refs
|
|
5683
6697
|
// --------------------------------------------------------------------------
|
|
@@ -5691,7 +6705,6 @@ var WoongCanvasLayer = function (props) {
|
|
|
5691
6705
|
|
|
5692
6706
|
var stageRef = useRef(null);
|
|
5693
6707
|
var baseLayerRef = useRef(null);
|
|
5694
|
-
var animationLayerRef = useRef(null);
|
|
5695
6708
|
var eventLayerRef = useRef(null); // --------------------------------------------------------------------------
|
|
5696
6709
|
// Data Refs - 선택 및 Hover 상태 관리
|
|
5697
6710
|
// --------------------------------------------------------------------------
|
|
@@ -5822,91 +6835,52 @@ var WoongCanvasLayer = function (props) {
|
|
|
5822
6835
|
offsetCacheRef.current.set(polygonData.id, result);
|
|
5823
6836
|
}
|
|
5824
6837
|
|
|
5825
|
-
return result;
|
|
5826
|
-
};
|
|
5827
|
-
/**
|
|
5828
|
-
* 마커 좌표 변환 결과를 캐시하고 반환
|
|
5829
|
-
*
|
|
5830
|
-
* @param markerData 마커 데이터
|
|
5831
|
-
* @returns 변환된 좌표 또는 null
|
|
5832
|
-
*/
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
var getOrComputeMarkerOffset = function (markerData) {
|
|
5836
|
-
var cached = offsetCacheRef.current.get(markerData.id);
|
|
5837
|
-
if (cached && !Array.isArray(cached)) return cached;
|
|
5838
|
-
var result = computeMarkerOffset(markerData, controller);
|
|
5839
|
-
|
|
5840
|
-
if (result) {
|
|
5841
|
-
offsetCacheRef.current.set(markerData.id, result);
|
|
5842
|
-
}
|
|
5843
|
-
|
|
5844
6838
|
return result;
|
|
5845
6839
|
}; // --------------------------------------------------------------------------
|
|
5846
6840
|
// 유틸리티 함수: 바운딩 박스 계산
|
|
5847
6841
|
// --------------------------------------------------------------------------
|
|
5848
6842
|
|
|
5849
6843
|
/**
|
|
5850
|
-
*
|
|
5851
|
-
*
|
|
5852
|
-
* 🎯 마커의 경우:
|
|
5853
|
-
* - boxHeight: 본체만 (Hit Test 영역)
|
|
5854
|
-
* - tailHeight: 꼬리 높이 (Viewport Culling용, 화면에 보이는 전체 영역)
|
|
6844
|
+
* 폴리곤의 바운딩 박스 계산
|
|
5855
6845
|
*
|
|
5856
|
-
* @param item
|
|
6846
|
+
* @param item 폴리곤 데이터
|
|
5857
6847
|
* @returns 바운딩 박스 또는 null
|
|
5858
6848
|
*/
|
|
5859
6849
|
|
|
5860
6850
|
|
|
5861
6851
|
var computeBoundingBox = function (item) {
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
if (y > maxY) maxY = y;
|
|
5885
|
-
}
|
|
6852
|
+
// 폴리곤: 모든 좌표의 최소/최대값 계산
|
|
6853
|
+
var offsets = getOrComputePolygonOffsets(item);
|
|
6854
|
+
if (!offsets) return null;
|
|
6855
|
+
var minX = Infinity,
|
|
6856
|
+
minY = Infinity,
|
|
6857
|
+
maxX = -Infinity,
|
|
6858
|
+
maxY = -Infinity;
|
|
6859
|
+
|
|
6860
|
+
for (var _i = 0, offsets_1 = offsets; _i < offsets_1.length; _i++) {
|
|
6861
|
+
var multiPolygon = offsets_1[_i];
|
|
6862
|
+
|
|
6863
|
+
for (var _a = 0, multiPolygon_1 = multiPolygon; _a < multiPolygon_1.length; _a++) {
|
|
6864
|
+
var polygonGroup = multiPolygon_1[_a];
|
|
6865
|
+
|
|
6866
|
+
for (var _b = 0, polygonGroup_1 = polygonGroup; _b < polygonGroup_1.length; _b++) {
|
|
6867
|
+
var _c = polygonGroup_1[_b],
|
|
6868
|
+
x = _c[0],
|
|
6869
|
+
y = _c[1];
|
|
6870
|
+
if (x < minX) minX = x;
|
|
6871
|
+
if (y < minY) minY = y;
|
|
6872
|
+
if (x > maxX) maxX = x;
|
|
6873
|
+
if (y > maxY) maxY = y;
|
|
5886
6874
|
}
|
|
5887
6875
|
}
|
|
5888
|
-
|
|
5889
|
-
return {
|
|
5890
|
-
minX: minX,
|
|
5891
|
-
minY: minY,
|
|
5892
|
-
maxX: maxX,
|
|
5893
|
-
maxY: maxY
|
|
5894
|
-
};
|
|
5895
|
-
} else {
|
|
5896
|
-
// 마커: 중심점 기준 박스 크기 계산 (꼬리 포함)
|
|
5897
|
-
var offset = getOrComputeMarkerOffset(item);
|
|
5898
|
-
if (!offset) return null;
|
|
5899
|
-
var boxWidth = item.boxWidth || 50;
|
|
5900
|
-
var boxHeight = item.boxHeight || 28;
|
|
5901
|
-
var tailHeight = item.tailHeight || 0; // 🎯 tailHeight 사용 (Viewport Culling용)
|
|
5902
|
-
|
|
5903
|
-
return {
|
|
5904
|
-
minX: offset.x - boxWidth / 2,
|
|
5905
|
-
minY: offset.y - boxHeight - tailHeight,
|
|
5906
|
-
maxX: offset.x + boxWidth / 2,
|
|
5907
|
-
maxY: offset.y
|
|
5908
|
-
};
|
|
5909
6876
|
}
|
|
6877
|
+
|
|
6878
|
+
return {
|
|
6879
|
+
minX: minX,
|
|
6880
|
+
minY: minY,
|
|
6881
|
+
maxX: maxX,
|
|
6882
|
+
maxY: maxY
|
|
6883
|
+
};
|
|
5910
6884
|
}; // --------------------------------------------------------------------------
|
|
5911
6885
|
// 유틸리티 함수: 공간 인덱싱
|
|
5912
6886
|
// --------------------------------------------------------------------------
|
|
@@ -5941,23 +6915,20 @@ var WoongCanvasLayer = function (props) {
|
|
|
5941
6915
|
|
|
5942
6916
|
var renderUtils = {
|
|
5943
6917
|
getOrComputePolygonOffsets: getOrComputePolygonOffsets,
|
|
5944
|
-
getOrComputeMarkerOffset:
|
|
5945
|
-
|
|
5946
|
-
|
|
6918
|
+
getOrComputeMarkerOffset: function () {
|
|
6919
|
+
return null;
|
|
6920
|
+
} // 폴리곤에서는 사용하지 않음
|
|
5947
6921
|
|
|
5948
|
-
|
|
6922
|
+
};
|
|
5949
6923
|
/**
|
|
5950
|
-
*
|
|
5951
|
-
* - MARKER: 외부에서 전달받은 renderBase 사용 (필수)
|
|
5952
|
-
* - POLYGON: renderer.ts의 팩토리 함수로 생성 (props 기반)
|
|
6924
|
+
* 렌더링 함수 생성 (props 기반)
|
|
5953
6925
|
*/
|
|
5954
6926
|
|
|
5955
|
-
var renderBase =
|
|
5956
|
-
var
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
}();
|
|
6927
|
+
var renderBase = renderPolygonBase(baseFillColor, baseStrokeColor, baseLineWidth);
|
|
6928
|
+
var renderEvent = renderPolygonEvent(baseFillColor, baseStrokeColor, baseLineWidth, selectedFillColor, selectedStrokeColor, selectedLineWidth, activeFillColor, activeStrokeColor, activeLineWidth, hoveredFillColor, hoveredStrokeColor, hoveredLineWidth);
|
|
6929
|
+
/** Base Layer에서 사용할 빈 Set (재사용) */
|
|
6930
|
+
|
|
6931
|
+
useRef(new Set());
|
|
5961
6932
|
/**
|
|
5962
6933
|
* Base 레이어 렌더링 (뷰포트 컬링 적용, 선택된 마커 제외)
|
|
5963
6934
|
*
|
|
@@ -5988,15 +6959,7 @@ var WoongCanvasLayer = function (props) {
|
|
|
5988
6959
|
|
|
5989
6960
|
var visibleItems = enableViewportCulling ? dataRef.current.filter(function (item) {
|
|
5990
6961
|
return isInViewport(item);
|
|
5991
|
-
}) : dataRef.current; //
|
|
5992
|
-
|
|
5993
|
-
if (topOnHover && !renderEvent && hovered) {
|
|
5994
|
-
// hover된 항목 제외하고 렌더링
|
|
5995
|
-
visibleItems = visibleItems.filter(function (item) {
|
|
5996
|
-
return item.id !== hovered.id;
|
|
5997
|
-
});
|
|
5998
|
-
} // 일반 항목 렌더링
|
|
5999
|
-
|
|
6962
|
+
}) : dataRef.current; // 일반 항목 렌더링
|
|
6000
6963
|
|
|
6001
6964
|
renderBase({
|
|
6002
6965
|
ctx: ctx,
|
|
@@ -6004,21 +6967,7 @@ var WoongCanvasLayer = function (props) {
|
|
|
6004
6967
|
selectedIds: selectedIdsRef.current,
|
|
6005
6968
|
hoveredItem: hovered,
|
|
6006
6969
|
utils: renderUtils
|
|
6007
|
-
});
|
|
6008
|
-
|
|
6009
|
-
if (topOnHover && !renderEvent && hovered) {
|
|
6010
|
-
var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
|
|
6011
|
-
|
|
6012
|
-
if (isHoveredInViewport) {
|
|
6013
|
-
renderBase({
|
|
6014
|
-
ctx: ctx,
|
|
6015
|
-
items: [hovered],
|
|
6016
|
-
selectedIds: selectedIdsRef.current,
|
|
6017
|
-
hoveredItem: hovered,
|
|
6018
|
-
utils: renderUtils
|
|
6019
|
-
});
|
|
6020
|
-
}
|
|
6021
|
-
}
|
|
6970
|
+
});
|
|
6022
6971
|
},
|
|
6023
6972
|
perfectDrawEnabled: false,
|
|
6024
6973
|
listening: false,
|
|
@@ -6030,26 +6979,6 @@ var WoongCanvasLayer = function (props) {
|
|
|
6030
6979
|
|
|
6031
6980
|
layer.batchDraw();
|
|
6032
6981
|
};
|
|
6033
|
-
/**
|
|
6034
|
-
* Animation 레이어 렌더링 (선택된 마커 애니메이션)
|
|
6035
|
-
*
|
|
6036
|
-
* 🔥 최적화: sceneFunc 내부에서 최신 items 참조
|
|
6037
|
-
* - 선택 변경 시에만 재생성
|
|
6038
|
-
* - 지도 이동 시에는 기존 Animation 계속 실행
|
|
6039
|
-
*/
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
var doRenderAnimation = function () {
|
|
6043
|
-
if (!renderAnimation) return;
|
|
6044
|
-
var layer = animationLayerRef.current;
|
|
6045
|
-
if (!layer) return;
|
|
6046
|
-
renderAnimation({
|
|
6047
|
-
layer: layer,
|
|
6048
|
-
selectedIds: selectedIdsRef.current,
|
|
6049
|
-
items: dataRef.current,
|
|
6050
|
-
utils: renderUtils
|
|
6051
|
-
});
|
|
6052
|
-
};
|
|
6053
6982
|
/**
|
|
6054
6983
|
* Event 레이어 렌더링 (hover + 선택 상태 표시)
|
|
6055
6984
|
*
|
|
@@ -6062,8 +6991,7 @@ var WoongCanvasLayer = function (props) {
|
|
|
6062
6991
|
|
|
6063
6992
|
var doRenderEvent = function () {
|
|
6064
6993
|
var layer = eventLayerRef.current;
|
|
6065
|
-
if (!layer) return;
|
|
6066
|
-
if (!renderEvent) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
|
|
6994
|
+
if (!layer) return; // 🔥 Shape 재사용: 이미 존재하면 재사용, 없으면 생성
|
|
6067
6995
|
|
|
6068
6996
|
var shape = layer.findOne('.event-render-shape');
|
|
6069
6997
|
|
|
@@ -6076,47 +7004,15 @@ var WoongCanvasLayer = function (props) {
|
|
|
6076
7004
|
var ctx = context; // 클로저로 최신 ref 값 참조
|
|
6077
7005
|
|
|
6078
7006
|
var selectedItems = Array.from(selectedItemsMapRef.current.values());
|
|
6079
|
-
var hovered = hoveredItemRef.current; //
|
|
6080
|
-
|
|
6081
|
-
if (topOnHover && hovered) {
|
|
6082
|
-
// 1. 먼저 일반 항목들 렌더링 (hover된 항목 제외)
|
|
6083
|
-
renderEvent({
|
|
6084
|
-
ctx: ctx,
|
|
6085
|
-
hoveredItem: null,
|
|
6086
|
-
utils: renderUtils,
|
|
6087
|
-
selectedItems: selectedItems.filter(function (item) {
|
|
6088
|
-
return item.id !== hovered.id;
|
|
6089
|
-
}),
|
|
6090
|
-
selectedItem: selectedItemRef.current
|
|
6091
|
-
}); // 2. hover된 항목을 최상단에 렌더링
|
|
6092
|
-
|
|
6093
|
-
var isHoveredInViewport = enableViewportCulling ? isInViewport(hovered) : true;
|
|
7007
|
+
var hovered = hoveredItemRef.current; // 일반 렌더링
|
|
6094
7008
|
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
renderEvent({
|
|
6103
|
-
ctx: ctx,
|
|
6104
|
-
hoveredItem: hovered,
|
|
6105
|
-
utils: renderUtils,
|
|
6106
|
-
selectedItems: hoverSelectedItems,
|
|
6107
|
-
selectedItem: selectedItemRef.current
|
|
6108
|
-
});
|
|
6109
|
-
}
|
|
6110
|
-
} else {
|
|
6111
|
-
// topOnHover가 false이거나 hover된 항목이 없으면 일반 렌더링
|
|
6112
|
-
renderEvent({
|
|
6113
|
-
ctx: ctx,
|
|
6114
|
-
hoveredItem: hovered,
|
|
6115
|
-
utils: renderUtils,
|
|
6116
|
-
selectedItems: selectedItems,
|
|
6117
|
-
selectedItem: selectedItemRef.current
|
|
6118
|
-
});
|
|
6119
|
-
}
|
|
7009
|
+
renderEvent({
|
|
7010
|
+
ctx: ctx,
|
|
7011
|
+
hoveredItem: hovered,
|
|
7012
|
+
utils: renderUtils,
|
|
7013
|
+
selectedItems: selectedItems,
|
|
7014
|
+
selectedItem: selectedItemRef.current
|
|
7015
|
+
});
|
|
6120
7016
|
},
|
|
6121
7017
|
perfectDrawEnabled: false,
|
|
6122
7018
|
listening: false,
|
|
@@ -6137,7 +7033,6 @@ var WoongCanvasLayer = function (props) {
|
|
|
6137
7033
|
updateViewport();
|
|
6138
7034
|
buildSpatialIndex();
|
|
6139
7035
|
doRenderBase();
|
|
6140
|
-
doRenderAnimation();
|
|
6141
7036
|
doRenderEvent();
|
|
6142
7037
|
}; // --------------------------------------------------------------------------
|
|
6143
7038
|
// 이벤트 핸들러: 지도 이벤트
|
|
@@ -6231,54 +7126,22 @@ var WoongCanvasLayer = function (props) {
|
|
|
6231
7126
|
// --------------------------------------------------------------------------
|
|
6232
7127
|
|
|
6233
7128
|
/**
|
|
6234
|
-
* 특정 좌표의
|
|
6235
|
-
*
|
|
6236
|
-
* topOnHover가 true일 때:
|
|
6237
|
-
* - 현재 hover된 항목을 최우선으로 체크
|
|
6238
|
-
* - 시각적으로 최상단에 있는 항목이 hit test에서도 우선됨
|
|
7129
|
+
* 특정 좌표의 폴리곤 데이터 찾기 (Spatial Index 사용)
|
|
6239
7130
|
*
|
|
6240
7131
|
* @param offset 검사할 좌표
|
|
6241
|
-
* @returns 찾은
|
|
7132
|
+
* @returns 찾은 폴리곤 데이터 또는 null
|
|
6242
7133
|
*/
|
|
6243
7134
|
|
|
6244
7135
|
|
|
6245
7136
|
var findData = function (offset) {
|
|
6246
|
-
//
|
|
6247
|
-
|
|
6248
|
-
var hovered = hoveredItemRef.current; // 폴리곤인 경우
|
|
6249
|
-
|
|
6250
|
-
if (dataType === CanvasDataType.POLYGON) {
|
|
6251
|
-
if (isPointInPolygonData(offset, hovered, getOrComputePolygonOffsets)) {
|
|
6252
|
-
return hovered; // 여전히 hover된 항목 위에 있음
|
|
6253
|
-
}
|
|
6254
|
-
} // 마커인 경우
|
|
6255
|
-
else {
|
|
6256
|
-
if (isPointInMarkerData(offset, hovered, getOrComputeMarkerOffset)) {
|
|
6257
|
-
return hovered; // 여전히 hover된 항목 위에 있음
|
|
6258
|
-
}
|
|
6259
|
-
}
|
|
6260
|
-
} // Spatial Index로 후보 항목만 빠르게 추출 (30,000개 → ~10개)
|
|
6261
|
-
|
|
7137
|
+
// Spatial Index로 후보 항목만 빠르게 추출 (30,000개 → ~10개)
|
|
7138
|
+
var candidates = spatialIndexRef.current.queryPoint(offset.x, offset.y); // 폴리곤 체크
|
|
6262
7139
|
|
|
6263
|
-
var
|
|
7140
|
+
for (var i = candidates.length - 1; i >= 0; i--) {
|
|
7141
|
+
var item = candidates[i];
|
|
6264
7142
|
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
for (var i = candidates.length - 1; i >= 0; i--) {
|
|
6268
|
-
var item = candidates[i];
|
|
6269
|
-
|
|
6270
|
-
if (isPointInMarkerData(offset, item, getOrComputeMarkerOffset)) {
|
|
6271
|
-
return item;
|
|
6272
|
-
}
|
|
6273
|
-
}
|
|
6274
|
-
} else {
|
|
6275
|
-
// 폴리곤 체크
|
|
6276
|
-
for (var i = candidates.length - 1; i >= 0; i--) {
|
|
6277
|
-
var item = candidates[i];
|
|
6278
|
-
|
|
6279
|
-
if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
|
|
6280
|
-
return item;
|
|
6281
|
-
}
|
|
7143
|
+
if (isPointInPolygonData(offset, item, getOrComputePolygonOffsets)) {
|
|
7144
|
+
return item;
|
|
6282
7145
|
}
|
|
6283
7146
|
}
|
|
6284
7147
|
|
|
@@ -6307,13 +7170,7 @@ var WoongCanvasLayer = function (props) {
|
|
|
6307
7170
|
} // 즉시 렌더링 (RAF 없이)
|
|
6308
7171
|
|
|
6309
7172
|
|
|
6310
|
-
|
|
6311
|
-
// renderEvent가 있으면 Event Layer에서만 처리 (성능 최적화)
|
|
6312
|
-
doRenderEvent();
|
|
6313
|
-
} else if (topOnHover) {
|
|
6314
|
-
// renderEvent가 없고 topOnHover가 true면 Base Layer에서 처리
|
|
6315
|
-
doRenderBase();
|
|
6316
|
-
}
|
|
7173
|
+
doRenderEvent();
|
|
6317
7174
|
};
|
|
6318
7175
|
/**
|
|
6319
7176
|
* 클릭 처리 (단일/다중 선택)
|
|
@@ -6354,15 +7211,10 @@ var WoongCanvasLayer = function (props) {
|
|
|
6354
7211
|
}
|
|
6355
7212
|
|
|
6356
7213
|
selectedIdsRef.current = newSelected;
|
|
6357
|
-
}
|
|
6358
|
-
|
|
6359
|
-
if (!!renderAnimation) {
|
|
6360
|
-
// 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
|
|
6361
|
-
doRenderBase(); // 3. Animation Layer 렌더링 (선택된 마커 애니메이션)
|
|
7214
|
+
} // 2. Base Layer 재렌더링 (단일 Shape로 최적화되어 빠름)
|
|
6362
7215
|
|
|
6363
|
-
doRenderAnimation();
|
|
6364
|
-
} // 4. Event Layer 렌더링 (hover 처리)
|
|
6365
7216
|
|
|
7217
|
+
doRenderBase(); // 3. Event Layer 렌더링 (hover 처리)
|
|
6366
7218
|
|
|
6367
7219
|
doRenderEvent();
|
|
6368
7220
|
}; // --------------------------------------------------------------------------
|
|
@@ -6393,7 +7245,7 @@ var WoongCanvasLayer = function (props) {
|
|
|
6393
7245
|
}
|
|
6394
7246
|
}
|
|
6395
7247
|
} catch (error) {
|
|
6396
|
-
console.error('[
|
|
7248
|
+
console.error('[WoongCanvasPolygon] handleClick error:', error);
|
|
6397
7249
|
}
|
|
6398
7250
|
};
|
|
6399
7251
|
/**
|
|
@@ -6419,7 +7271,7 @@ var WoongCanvasLayer = function (props) {
|
|
|
6419
7271
|
if (hoveredItem && onMouseOver) onMouseOver(hoveredItem);
|
|
6420
7272
|
}
|
|
6421
7273
|
} catch (error) {
|
|
6422
|
-
console.error('[
|
|
7274
|
+
console.error('[WoongCanvasPolygon] handleMouseMove error:', error);
|
|
6423
7275
|
}
|
|
6424
7276
|
};
|
|
6425
7277
|
/**
|
|
@@ -6517,21 +7369,12 @@ var WoongCanvasLayer = function (props) {
|
|
|
6517
7369
|
listening: false // 이벤트 리스닝 비활성화로 성능 향상
|
|
6518
7370
|
|
|
6519
7371
|
});
|
|
6520
|
-
var animationLayer = new Konva.Layer({
|
|
6521
|
-
listening: false
|
|
6522
|
-
});
|
|
6523
7372
|
var eventLayer = new Konva.Layer({
|
|
6524
7373
|
listening: false
|
|
6525
7374
|
});
|
|
6526
7375
|
baseLayerRef.current = baseLayer;
|
|
6527
|
-
animationLayerRef.current = animationLayer;
|
|
6528
7376
|
eventLayerRef.current = eventLayer;
|
|
6529
7377
|
stage.add(baseLayer);
|
|
6530
|
-
|
|
6531
|
-
if (renderAnimation) {
|
|
6532
|
-
stage.add(animationLayer);
|
|
6533
|
-
}
|
|
6534
|
-
|
|
6535
7378
|
stage.add(eventLayer); // 초기 뷰포트 설정
|
|
6536
7379
|
|
|
6537
7380
|
updateViewport(); // ResizeObserver (맵 크기 변경 감지)
|
|
@@ -6617,7 +7460,6 @@ var WoongCanvasLayer = function (props) {
|
|
|
6617
7460
|
|
|
6618
7461
|
|
|
6619
7462
|
baseLayer.destroyChildren();
|
|
6620
|
-
animationLayer.destroyChildren();
|
|
6621
7463
|
eventLayer.destroyChildren();
|
|
6622
7464
|
stage.destroy(); // 캐시 정리
|
|
6623
7465
|
|
|
@@ -6651,7 +7493,6 @@ var WoongCanvasLayer = function (props) {
|
|
|
6651
7493
|
selectedItemsMapRef.current = newSelectedItemsMap; // 렌더링
|
|
6652
7494
|
|
|
6653
7495
|
doRenderBase();
|
|
6654
|
-
doRenderAnimation();
|
|
6655
7496
|
doRenderEvent();
|
|
6656
7497
|
}, [externalSelectedItems]); // 배열 자체를 dependency로 사용
|
|
6657
7498
|
// --------------------------------------------------------------------------
|
|
@@ -9564,4 +10405,4 @@ function MintMap(_a) {
|
|
|
9564
10405
|
}), loading));
|
|
9565
10406
|
}
|
|
9566
10407
|
|
|
9567
|
-
export { AnimationPlayer, Bounds, CanvasDataType, CanvasMarker, CanvasMarkerClaude, CanvasMarkerHanquf, CircleMarker, DEFAULT_CULLING_MARGIN, DEFAULT_MAX_CACHE_SIZE, Drawable, GeoCalulator, GoogleMintMapController,
|
|
10408
|
+
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 };
|