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) => {