feat: GisTree 레이어 Check 함수 구현 (침수심 범례 기능)

This commit is contained in:
swcho
2025-08-05 16:35:03 +09:00
parent ab63cbe992
commit a83ea455fb
2 changed files with 190 additions and 6 deletions
@@ -117,6 +117,11 @@ function Gis({
onCheckedLayersChange,
searchBar: SearchBar,
waterDepth: WaterDepth,
isPlaying = false,
setIsPlaying = () => {},
targetLayerTypes = [],
orderedLayers = [],
checkInterval = 3000,
height: height = "h-[calc(100vh-10rem)]",
}) {
const mapRef = useRef();
@@ -305,12 +310,19 @@ function Gis({
onFeatureSelect(dummyFeature);
}}
imageVisible={imgVisble}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
targetLayerTypes={targetLayerTypes}
orderedLayers={orderedLayers}
checkInterval={checkInterval}
/>
{typeof WaterDepth === "function" && (
<div className={`absolute bottom-4 z-40 ${tabVisible ? 'left-[260px]' : 'left-4'}`}>
<WaterDepth
onClose={() => setIsOpen(false)}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
/>
</div>
)}
@@ -4,19 +4,36 @@ import {
getBackendOptions
} from "@minoru/react-dnd-treeview";
import { ChevronLeft, ChevronRight } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { DndProvider } from "react-dnd";
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useGisContext } from './Gis';
import GisApp from './lib/gis.core';
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange, onLayerIconClick,imageVisible=false }) {
export default React.memo(function GisTree({
checkedNodes = [],
setCheckedNodes = () => { },
visible,
setVisible,
onCheckedChange,
onLayerIconClick,
imageVisible = false,
isPlaying = false,
setIsPlaying = () => {},
targetLayerTypes = [],
orderedLayers = [],
checkInterval = 3000,
}) {
const { gisCore, treeData } = useGisContext();
const [checked, setChecked] = useState([]);
const [expanded, setExpanded] = useState([]);
const [nodes, setNodes] = useState([]);
const [activeTab, setActiveTab] = useState('layers');
const currentAnimationIndexRef = useRef(0);
const targetNodesRef = useRef([]); // 대상 노드들을 저장할 ref
const isPausedRef = useRef(false); // 일시정지 상태를 저장할 ref
const timeoutIdRef = useRef(null); // setTimeout ID를 저장할 ref
useEffect(() => {
if (treeData && treeData.length > 0) {
const nodeData = setNodeValues(treeData);
@@ -188,12 +205,167 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
image.src = canvas.toDataURL('image/png');
return image;
};
const handleLayerImgClick = (nodeNm, nodeId) => {
const handleLayerImgClick = (nodeNm, nodeId, layerNm) => {
if (!checked.includes(nodeId)) alert('시설을 선택해주세요')
else onLayerIconClick(nodeNm)
}
// 특정 노드를 체크/언체크하는 함수
const checkSpecificNode = useCallback((nodeId, shouldCheck = true) => {
setChecked((prevChecked) => {
let newChecked = [...prevChecked];
const isCurrentlyChecked = prevChecked.includes(nodeId);
if (shouldCheck && !isCurrentlyChecked) {
// 체크하기
const descendants = getDescendants(nodeId, nodes);
newChecked = Array.from(new Set([...newChecked, nodeId, ...descendants]));
const ancestors = getAncestors(nodeId, nodes);
ancestors.forEach((p) => {
if (isEveryChildChecked(p, nodes, newChecked)) {
newChecked.push(p);
}
});
} else if (!shouldCheck && isCurrentlyChecked) {
// 언체크하기
const descendants = getDescendants(nodeId, nodes);
newChecked = newChecked.filter((cid) => cid !== nodeId && !descendants.includes(cid));
const ancestors = getAncestors(nodeId, nodes);
ancestors.forEach((p) => {
if (newChecked.includes(p)) {
newChecked = newChecked.filter((cid) => cid !== p);
}
});
}
// 지도에 레이어 표시/숨김 적용
const allIds = nodes.map((n) => n.id);
const unchecked = allIds.filter((nid) => !newChecked.includes(nid));
gisCore.setLayerOnMap(newChecked, true);
gisCore.setLayerOnMap(unchecked, false);
// 상위로 변경 사항 전달
const checkedNodesData = nodes.filter(node => newChecked.includes(node.id));
onCheckedChange?.(newChecked, checkedNodesData);
return newChecked;
});
}, [nodes]);
// 침수위험지도 실행 함수
const handleAutoCheckAnimation = useCallback((isEnabled, layerTypes = [], interval = 3000) => {
// 기존 타임아웃 정리
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
if (!isEnabled || layerTypes.length === 0) {
// 애니메이션 비활성화 시 상태 초기화
isPausedRef.current = false;
targetNodesRef.current = [];
currentAnimationIndexRef.current = 0;
return;
}
// 대상 노드들을 찾아서 특정 순서로 정렬 후 배열로 반환
const getTargetNodesInOrder = () => {
const orderedNodes = [];
orderedLayers.forEach(lyrSn => {
const node = nodes.find(n => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm));
if (node) {
orderedNodes.push(node);
}
});
return orderedNodes;
};
const targetNodes = getTargetNodesInOrder();
if (targetNodes.length === 0) {
console.warn('대상 레이어를 찾을 수 없습니다:', layerTypes);
return;
}
// 처음 시작하는 경우에만 초기화
if (targetNodesRef.current.length === 0) {
targetNodesRef.current = targetNodes;
currentAnimationIndexRef.current = 0;
console.log('자동 체크 대상 노드들 (순서대로):', targetNodes.map(n => ({ id: n.id, name: n.text, type: n.data?.lyrTyNm })));
// 모든 대상 레이어를 먼저 언체크
targetNodes.forEach(node => {
checkSpecificNode(node.id, false);
});
}
// 애니메이션 재개/시작
isPausedRef.current = false;
const processNextLayer = () => {
// 일시정지 상태이거나 애니메이션이 비활성화되면 중단
if (isPausedRef.current || !isPlaying) {
return;
}
const currentTargetNodes = targetNodesRef.current;
const currentIndex = currentAnimationIndexRef.current;
if (currentIndex < currentTargetNodes.length) {
checkSpecificNode(currentTargetNodes[currentIndex].id, true);
if (currentIndex > 0) {
checkSpecificNode(currentTargetNodes[currentIndex - 1].id, false);
}
currentAnimationIndexRef.current++;
// 다음 레이어 처리를 위한 타이머 설정
timeoutIdRef.current = setTimeout(processNextLayer, interval);
} else {
// 애니메이션 완료 - 상태 초기화 및 완료 신호 전송
console.log('애니메이션 완료');
isPausedRef.current = false;
targetNodesRef.current = [];
currentAnimationIndexRef.current = 0;
timeoutIdRef.current = null;
setIsPlaying?.(false);
}
};
processNextLayer();
}, [nodes, checkSpecificNode, setIsPlaying, isPlaying]);
// 애니메이션 일시정지 함수
const pauseAnimation = useCallback(() => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
isPausedRef.current = true;
}, []);
// 외부에서 전달받은 props로 자동 체크 제어
useEffect(() => {
if (isPlaying) handleAutoCheckAnimation(true, targetLayerTypes, checkInterval); // 재생 시작/재개
else pauseAnimation(); // 일시정지
}, [isPlaying, handleAutoCheckAnimation, targetLayerTypes, checkInterval, pauseAnimation]);
// 컴포넌트 언마운트 시 정리
useEffect(() => {
return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
if (!treeData || treeData.length === 0) return null;
return (
<>
<div
@@ -224,7 +396,7 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
<input type="checkbox" checked={checked.includes(node.id)} onChange={() => handleCheck(node.id)} className="cursor-pointer" />
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">{node.text}</span>
{imageVisible ? depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={()=>{handleLayerImgClick(node.text, node.id)}} alt={node.text}></img></span> : null}
{imageVisible ? depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={() => { handleLayerImgClick(node.text, node.id, node.data.lyrTyNm) }} alt={node.text}></img></span> : null}
</div>
)}
sort={(a, b) => {