From a83ea455fb2ede235c3dc655022ca62ec1beb455 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 16:35:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20GisTree=20=EB=A0=88=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=20Check=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84=20(=EC=B9=A8?= =?UTF-8?q?=EC=88=98=EC=8B=AC=20=EB=B2=94=EB=A1=80=20=EA=B8=B0=EB=8A=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 12 ++ .../business_components/src/ui/map/GisTree.js | 184 +++++++++++++++++- 2 files changed, 190 insertions(+), 6 deletions(-) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 48868313b..046619de1 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -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" && (
setIsOpen(false)} + isPlaying={isPlaying} + setIsPlaying={setIsPlaying} />
)} diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 8a2db3ae2..1880eb225 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -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) => { - if(!checked.includes(nodeId)) alert('시설을 선택해주세요') + 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 ( <>
handleCheck(node.id)} className="cursor-pointer" /> {node.icon && {node.icon}} {node.text} - {imageVisible ? depth > 0 && {handleLayerImgClick(node.text, node.id)}} alt={node.text}> : null} + {imageVisible ? depth > 0 && { handleLayerImgClick(node.text, node.id, node.data.lyrTyNm) }} alt={node.text}> : null}
)} sort={(a, b) => {