diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js
index e9deb72f7..d7d5b5b72 100644
--- a/packages/common/business_components/src/ui/map/GisSelect.js
+++ b/packages/common/business_components/src/ui/map/GisSelect.js
@@ -1,46 +1,42 @@
-import React, { useContext, useEffect } from 'react';
-import { GisContext } from './Gis';
-import GisApp from './lib/gis.core';
-import GisOlSelect from './lib/gis.select';
+import React, { useContext, useEffect } from "react";
+import { GisContext } from "./Gis";
+import GisApp from "./lib/gis.core";
+import GisOlSelect from "./lib/gis.select";
export default React.memo(function GisSelect({ onFeatureSelect }) {
+ const { gisMap } = useContext(GisContext);
- const { gisMap } = useContext(GisContext);
+ useEffect(() => {
+ if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함
+ // 이미 select가 있으면 재생성하지 않음
+ const select = new GisOlSelect(gisMap);
+ GisApp.Module.select = select;
- useEffect(() => {
- if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함
+ // 피처 선택 시 콜백 등록
+ select.getSelectedInfo(function (features) {
+ if (features?.[0]) {
+ console.log("GisSelect features", features);
+ onFeatureSelect(features[0]);
+ }
+ });
+ }, [gisMap, onFeatureSelect]);
- // 이미 select가 있으면 재생성하지 않음
- if (!GisApp.Module.select) {
- const select = new GisOlSelect(gisMap);
- GisApp.Module.select = select;
+ // const select = new GisOlSelect(gisMap);
+ // GisApp.Module.select = select;
- // 피처 선택 시 콜백 등록
- select.getSelectedInfo(function (features) {
- if (features?.[0]) {
- console.log('GisSelect features', features);
- onFeatureSelect(features[0]);
- }
- });
- }
- }, [gisMap, onFeatureSelect]);
+ // select.getSelectedInfo(function (features) {
+ // //clickFeature(features[0]);
+ // if(features.length > 0) {
+ // const layer = features[0];
+ // // console.log('GisSelect layer', layer);
+ // onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달
+ // }
+ // });
- // const select = new GisOlSelect(gisMap);
- // GisApp.Module.select = select;
-
- // select.getSelectedInfo(function (features) {
- // //clickFeature(features[0]);
- // if(features.length > 0) {
- // const layer = features[0];
- // // console.log('GisSelect layer', layer);
- // onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달
- // }
- // });
-
- return (
- <>
- {/*
+ return (
+ <>
+ {/*
선,면,다각형을 이용한 시설물 선택 영역
-
@@ -55,6 +51,6 @@ export default React.memo(function GisSelect({ onFeatureSelect }) {
*/}
- >
- );
+ >
+ );
});
diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js
index 3f46848dc..033a244c8 100644
--- a/packages/common/business_components/src/ui/map/GisTree.js
+++ b/packages/common/business_components/src/ui/map/GisTree.js
@@ -2,445 +2,492 @@ import {
MultiBackend,
Tree,
getBackendOptions,
- DndProvider
-} from "@minoru/react-dnd-treeview";
-import { ChevronLeft, ChevronRight } from 'lucide-react';
-import React, { useEffect, useState, useRef, useCallback } from 'react';
-
-import { useGisContext } from './Gis';
-import GisApp from './lib/gis.core';
-
-export default React.memo(function GisTree({
+ DndProvider,
+ } from "@minoru/react-dnd-treeview";
+ import { ChevronLeft, ChevronRight } from "lucide-react";
+ 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 = () => { },
+ setCheckedNodes = () => {},
visible,
setVisible,
onCheckedChange,
onLayerIconClick,
imageVisible = false,
isPlaying = false,
- setIsPlaying = () => { },
+ setIsPlaying = () => {},
targetLayerTypes = [],
orderedLayers = [],
checkInterval = 3000,
- onProgressChange = () => { },
-}) {
+ onProgressChange = () => {},
+ }) {
const { gisCore, treeData } = useGisContext();
const [checked, setChecked] = useState([]);
const [expanded, setExpanded] = useState([]);
const [nodes, setNodes] = useState([]);
- const [activeTab, setActiveTab] = useState('layers');
-
+ 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);
- setNodes(nodeData);
- gisCore.setAllLayersOnMap(false);
- }
- }, [treeData]);
-
- // 트리 노드 체크 시 레이어 표시/숨김
- useEffect(() => {
- if (nodes.length > 0) {
- // 기본으로 체크된 노드들 찾기
- const defaultChecked = getDefaultCheckedNodes(nodes);
- setChecked(defaultChecked);
-
- // 기본으로 확장된 노드들 설정
- const defaultExpanded = Array.from(new Set([
- ...defaultChecked,
- ...defaultChecked.flatMap((id) => getAncestors(id, nodes))
- ]));
- setExpanded(defaultExpanded);
-
- // 기본 체크된 레이어들을 지도에 표시
- gisCore.setLayerOnMap(defaultChecked, true);
-
- // 초기 체크된 레이어 정보를 상위로 전달
- const defaultCheckedNodesData = nodes.filter(node => defaultChecked.includes(node.id));
- onCheckedChange?.(defaultChecked, defaultCheckedNodesData);
- }
- }, [nodes]); // onCheckedChange를 의존성에 추가
-
- // 초기 표시 여부가 'Y'인 레이어들 찾기
- const getDefaultCheckedNodes = (nodes, values = []) => {
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i];
- if (node.data.initEprssYn === 'Y') {
- values.push(node.data.lyrSn);
- }
- }
- return values;
- };
-
+ const targetNodesRef = useRef([]);
+ const isPausedRef = useRef(false);
+ const timeoutIdRef = useRef(null);
+
+ // ===== 유틸 =====
const getDescendants = (id, all) => {
- const children = all.filter((n) => n.parent === id);
- return children.reduce(
- (acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)),
- []
- );
+ const children = all.filter((n) => n.parent === id);
+ return children.reduce(
+ (acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)),
+ []
+ );
};
-
+
const getAncestors = (id, all) => {
- const node = all.find((n) => n.id === id);
- if (!node || node.parent == null) return [];
- return [node.parent, ...getAncestors(node.parent, all)];
+ const node = all.find((n) => n.id === id);
+ if (!node || node.parent == null) return [];
+ return [node.parent, ...getAncestors(node.parent, all)];
};
-
+
+ const getChildren = (parentId, all) =>
+ all.filter((n) => n.parent === parentId);
+
+ const areAllChildrenChecked = (parentId, all, checkedIds) => {
+ const children = getChildren(parentId, all);
+ return children.length > 0 && children.every((c) => checkedIds.includes(c.id));
+ };
+
+ const updateParentCheckState = (nodeId, all, checkedIds) => {
+ const parent = all.find((n) => n.id === nodeId)?.parent;
+ if (parent == null) return checkedIds;
+
+ if (areAllChildrenChecked(parent, all, checkedIds)) {
+ if (!checkedIds.includes(parent)) {
+ checkedIds.push(parent);
+ }
+ } else {
+ checkedIds = checkedIds.filter((id) => id !== parent);
+ }
+ return updateParentCheckState(parent, all, checkedIds);
+ };
+
const isEveryChildChecked = (parentId, all, checked) => {
- const children = all.filter((n) => n.parent === parentId);
- return children.every((c) => checked.includes(c.id));
+ const children = getChildren(parentId, all);
+ return children.length > 0 && children.every((c) => checked.includes(c.id));
};
-
- const handleCheck = (id) => {
- setChecked((prevChecked) => {
- const isChecked = prevChecked.includes(id);
- let newChecked = [...prevChecked];
- if (isChecked) {
- const descendants = getDescendants(id, nodes);
- newChecked = newChecked.filter((cid) => cid !== id && !descendants.includes(cid));
- const ancestors = getAncestors(id, nodes);
- ancestors.forEach((p) => {
- if (newChecked.includes(p)) {
- newChecked = newChecked.filter((cid) => cid !== p);
- }
- });
- } else {
- const descendants = getDescendants(id, nodes);
- newChecked = Array.from(new Set([...newChecked, id, ...descendants]));
- const ancestors = getAncestors(id, nodes);
- ancestors.forEach((p) => {
- if (isEveryChildChecked(p, nodes, newChecked)) {
- newChecked.push(p);
- }
- });
- // setExpanded((prev) => Array.from(new Set([...prev, ...ancestors])));
- }
- 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;
- });
+
+ // ===== 기본 체크 노드 =====
+ const getDefaultCheckedNodes = (nodes) => {
+ let checkedIds = nodes
+ .filter((n) => n.data.initEprssYn === "Y")
+ .map((n) => n.id);
+
+ nodes.forEach((node) => {
+ checkedIds = updateParentCheckState(node.id, nodes, checkedIds);
+ });
+
+ return Array.from(new Set(checkedIds));
};
-
- const setNodeValues = (result) => {
- return result.map((item) => {
- const src = makeLegendSrc(item);
- let label = item.lyrNm;
- return {
- id: item.lyrSn,
- text: label,
- parent: item.upperLyrSn,
- droppable: !!item.children,
- data: item,
- icon: src ?

: '',
- };
- });
- };
-
- const makeLegendSrc = layerinfo => {
- if (!layerinfo.gobjTy) return null;
-
- let src;
- if (layerinfo.iconNm) {
- src = GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm;
- } else {
- const ss = makeLegendImage(layerinfo);
- src = ss.src;
- }
- return src;
- };
-
- const makeLegendImage = layerinfo => {
- let image = document.createElement('img');
- let canvas = document.createElement('canvas');
- canvas.width = 16;
- canvas.height = 16;
- let ctx = canvas.getContext('2d');
- ctx.beginPath();
-
- if (layerinfo.gobjTy === 'lineLabelPoligon') {
- ctx.moveTo(1, 1);
- ctx.lineTo(15, 1);
- ctx.lineTo(15, 15);
- ctx.lineTo(1, 15);
- ctx.lineTo(1, 1);
- } else if (layerinfo.gobjTy === 'line') {
- ctx.moveTo(1, 15);
- ctx.lineTo(5, 1);
- ctx.lineTo(9, 15);
- ctx.lineTo(15, 1);
- } else {
- ctx.moveTo(1, 1);
- ctx.lineTo(15, 1);
- ctx.lineTo(15, 15);
- ctx.lineTo(1, 15);
- ctx.lineTo(1, 1);
- }
-
- if (layerinfo.gobjTy === 'lineLabelPoligon') {
- ctx.fillStyle = layerinfo.polgnColor;
- ctx.fill();
- ctx.lineWidth = 1;
- ctx.strokeStyle = layerinfo.lnColor;
- ctx.stroke();
- }
-
- if (layerinfo.gobjTy === 'line') {
- ctx.lineWidth = 1;
- ctx.strokeStyle = layerinfo.lnColor;
- ctx.stroke();
- }
-
- image.src = canvas.toDataURL('image/png');
- return image;
- };
- 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;
- });
+
+ // ===== 초기 로딩 =====
+ useEffect(() => {
+ if (treeData && treeData.length > 0) {
+ const nodeData = setNodeValues(treeData);
+ setNodes(nodeData);
+ gisCore.setAllLayersOnMap(false);
+ }
+ }, [treeData]);
+
+ useEffect(() => {
+ if (nodes.length > 0) {
+ const defaultChecked = getDefaultCheckedNodes(nodes);
+ setChecked(defaultChecked);
+
+ const defaultExpanded = Array.from(
+ new Set([
+ ...defaultChecked,
+ ...defaultChecked.flatMap((id) => getAncestors(id, nodes)),
+ ])
+ );
+ setExpanded(defaultExpanded);
+
+ gisCore.setLayerOnMap(defaultChecked, true);
+
+ const defaultCheckedNodesData = nodes.filter((node) =>
+ defaultChecked.includes(node.id)
+ );
+ onCheckedChange?.(defaultChecked, defaultCheckedNodesData);
+ }
}, [nodes]);
-
- // 침수위험지도 실행 함수
- const handleAutoCheckAnimation = useCallback((isEnabled, layerTypes = [], interval = 3000) => {
- // 기존 타임아웃 정리
- if (timeoutIdRef.current) {
- clearTimeout(timeoutIdRef.current);
- timeoutIdRef.current = null;
+
+ // ===== 체크 이벤트 =====
+ const handleCheck = (id) => {
+ setChecked((prevChecked) => {
+ let newChecked = [...prevChecked];
+ const isChecked = prevChecked.includes(id);
+
+ if (isChecked) {
+ const descendants = getDescendants(id, nodes);
+ newChecked = newChecked.filter(
+ (cid) => cid !== id && !descendants.includes(cid)
+ );
+ } else {
+ const descendants = getDescendants(id, nodes);
+ newChecked = Array.from(new Set([...newChecked, id, ...descendants]));
}
-
+
+ newChecked = updateParentCheckState(id, nodes, newChecked);
+
+ 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;
+ });
+ };
+
+ // ===== 특정 노드 체크/해제 =====
+ 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;
+ onProgressChange(0, 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) return;
+
+ if (targetNodesRef.current.length === 0) {
+ targetNodesRef.current = targetNodes;
+ currentAnimationIndexRef.current = 0;
+ onProgressChange(targetNodes.length, 0);
+ 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++;
+ onProgressChange(
+ currentTargetNodes.length,
+ currentAnimationIndexRef.current
+ );
+ timeoutIdRef.current = setTimeout(processNextLayer, interval);
+ } else {
isPausedRef.current = false;
targetNodesRef.current = [];
currentAnimationIndexRef.current = 0;
- onProgressChange(0, 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 })));
-
- // 진행률 초기화 (전체 단계 수 설정)
- onProgressChange(targetNodes.length, 0);
-
- // 모든 대상 레이어를 먼저 언체크
- 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++;
-
- // 진행률 업데이트
- onProgressChange(currentTargetNodes.length, currentAnimationIndexRef.current);
-
- // 다음 레이어 처리를 위한 타이머 설정
- timeoutIdRef.current = setTimeout(processNextLayer, interval);
- } else {
- // 애니메이션 완료 - 상태 초기화 및 완료 신호 전송
- console.log('애니메이션 완료');
- isPausedRef.current = false;
- targetNodesRef.current = [];
- currentAnimationIndexRef.current = 0;
- timeoutIdRef.current = null;
-
- // 진행률 100% 완료
- onProgressChange(currentTargetNodes.length, currentTargetNodes.length);
-
- // 잠시 후 진행률 초기화
- setTimeout(() => {
- onProgressChange(0, 0);
- }, 1000);
-
- setIsPlaying?.(false);
- }
- };
-
- processNextLayer();
-
- }, [nodes, checkSpecificNode, setIsPlaying, isPlaying, onProgressChange]);
-
- // 애니메이션 일시정지 함수
- 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);
- }
+ onProgressChange(
+ currentTargetNodes.length,
+ currentTargetNodes.length
+ );
+ setTimeout(() => {
+ onProgressChange(0, 0);
+ }, 1000);
+ setIsPlaying?.(false);
+ }
};
- }, []);
-
-
- if (!treeData || treeData.length === 0) return null;
-
-
- return (
- <>
-
-
-
레이어
-
-
- (
-
- {node.droppable && (
-
-
- )}
-
handleCheck(node.id)} className="cursor-pointer" />
- {node.icon &&
{node.icon}}
-
{node.text}
- {imageVisible ? depth > 0 &&
{ handleLayerImgClick(node.text, node.id, node.data.lyrTyNm) }} alt={node.text}> : null}
-
- )}
- sort={(a, b) => {
- const aOrder = parseInt(a.data?.sortOrdr ?? '0');
- const bOrder = parseInt(b.data?.sortOrdr ?? '0');
- return aOrder - bOrder;
- }}
- />
-
-
- {/* 접기 버튼 */}
-
-
- {/* 열기 버튼 */}
- {!visible && (
-
- )}
- >
+
+ processNextLayer();
+ },
+ [nodes, checkSpecificNode, setIsPlaying, isPlaying, onProgressChange]
);
-});
+
+ const pauseAnimation = useCallback(() => {
+ if (timeoutIdRef.current) {
+ clearTimeout(timeoutIdRef.current);
+ timeoutIdRef.current = null;
+ }
+ isPausedRef.current = true;
+ }, []);
+
+ useEffect(() => {
+ if (isPlaying)
+ handleAutoCheckAnimation(true, targetLayerTypes, checkInterval);
+ else pauseAnimation();
+ }, [
+ isPlaying,
+ handleAutoCheckAnimation,
+ targetLayerTypes,
+ checkInterval,
+ pauseAnimation,
+ ]);
+
+ useEffect(() => {
+ return () => {
+ if (timeoutIdRef.current) {
+ clearTimeout(timeoutIdRef.current);
+ }
+ };
+ }, []);
+
+ // ===== 노드 세팅 =====
+ const setNodeValues = (result) => {
+ return result.map((item) => {
+ const src = makeLegendSrc(item);
+ return {
+ id: item.lyrSn, // 반드시 고유값
+ text: item.lyrNm,
+ parent: item.upperLyrSn || null, // 루트는 null
+ droppable: true, // children 여부 모를 땐 true로 둬야 트리 확장 가능
+ data: item,
+ icon: src ?

: "",
+ };
+ });
+ };
+
+
+ const makeLegendSrc = (layerinfo) => {
+ if (!layerinfo.gobjTy) return null;
+ if (layerinfo.iconNm) {
+ return GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm;
+ } else {
+ return makeLegendImage(layerinfo).src;
+ }
+ };
+
+ const makeLegendImage = (layerinfo) => {
+ let image = document.createElement("img");
+ let canvas = document.createElement("canvas");
+ canvas.width = 16;
+ canvas.height = 16;
+ let ctx = canvas.getContext("2d");
+ ctx.beginPath();
+
+ if (layerinfo.gobjTy === "lineLabelPoligon") {
+ ctx.moveTo(1, 1);
+ ctx.lineTo(15, 1);
+ ctx.lineTo(15, 15);
+ ctx.lineTo(1, 15);
+ ctx.lineTo(1, 1);
+ } else if (layerinfo.gobjTy === "line") {
+ ctx.moveTo(1, 15);
+ ctx.lineTo(5, 1);
+ ctx.lineTo(9, 15);
+ ctx.lineTo(15, 1);
+ } else {
+ ctx.moveTo(1, 1);
+ ctx.lineTo(15, 1);
+ ctx.lineTo(15, 15);
+ ctx.lineTo(1, 15);
+ ctx.lineTo(1, 1);
+ }
+
+ if (layerinfo.gobjTy === "lineLabelPoligon") {
+ ctx.fillStyle = layerinfo.polgnColor;
+ ctx.fill();
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = layerinfo.lnColor;
+ ctx.stroke();
+ }
+ if (layerinfo.gobjTy === "line") {
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = layerinfo.lnColor;
+ ctx.stroke();
+ }
+
+ image.src = canvas.toDataURL("image/png");
+ return image;
+ };
+
+ const handleLayerImgClick = (nodeNm, nodeId) => {
+ if (!checked.includes(nodeId)) alert("시설을 선택해주세요");
+ else onLayerIconClick(nodeNm);
+ };
+
+ if (!treeData || treeData.length === 0) return null;
+
+ return (
+ <>
+
+
+
레이어
+
+
+ {
+ const isVisible = node.data.exprsYn === "Y";
+ const hasVisibleChildren = nodes.some(
+ (child) =>
+ child.parent === node.id && child.data.exprsYn === "Y"
+ );
+
+ return (
+
+ {hasVisibleChildren ? (
+
+ ) : (
+
+ )}
+
handleCheck(node.id)}
+ className="cursor-pointer"
+ />
+ {node.icon &&
{node.icon}}
+
+ {node.text}
+
+ {imageVisible && depth > 0 && (
+
+
+ handleLayerImgClick(
+ node.text,
+ node.id
+ )
+ }
+ alt={node.text}
+ />
+
+ )}
+
+ );
+ }}
+ sort={(a, b) => {
+ const aOrder = parseInt(a.data?.sortOrdr ?? "0");
+ const bOrder = parseInt(b.data?.sortOrdr ?? "0");
+ return aOrder - bOrder;
+ }}
+ />
+
+
+
+
+ {!visible && (
+
+ )}
+ >
+ );
+ });
+
\ No newline at end of file
diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js
index 5fcc396f3..818b7e8a8 100644
--- a/packages/common/business_components/src/ui/map/lib/gis.core.js
+++ b/packages/common/business_components/src/ui/map/lib/gis.core.js
@@ -566,18 +566,24 @@ GisApp.core.prototype = {
*/
setLayerOnMap: function (layerIds, visible, addQurryFlter) {
for (let i in layerIds) {
-
+
let id = layerIds[i];
let layer = this.getLayerById(id);
if (layer) {
layer.setVisible(visible);
} else if (visible) {
const layerCode = GisApp.layerCode[id];
+ const actlLyrYn = layerCode.actlLyrYn;
+ if(actlLyrYn == 'N') continue;
if(!layerCode.lyrTyNm) continue;
const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || '');
layerCode['flterSql'] = queryFlter;
- //layer = this.addWmsLayer(layerCode);
- layer = this.addWfsLayer(layerCode);
+
+ if(layerCode.srvcNm == 'wms'){
+ layer = this.addWmsLayer(layerCode);
+ }else {
+ layer = this.addWfsLayer(layerCode);
+ }
}
}
if (GisApp.Module.select) {
diff --git a/packages/common/business_components/src/ui/map/lib/gis.layer.js b/packages/common/business_components/src/ui/map/lib/gis.layer.js
index f7bb7b3ac..637406522 100644
--- a/packages/common/business_components/src/ui/map/lib/gis.layer.js
+++ b/packages/common/business_components/src/ui/map/lib/gis.layer.js
@@ -112,6 +112,7 @@ GisOlLayer.prototype = {
let toProj = GisApp.TO_PROJ;
let lyrTyNm = options.lyrTyNm;
let typename = this.storageNm + lyrTyNm;
+ let filter = options?.flterSql && options.flterSql && options.flterSql.trim() !== ''
let params = {};
params['TYPENAME'] = typename;
@@ -120,6 +121,7 @@ GisOlLayer.prototype = {
params['REQUEST'] = 'GetFeature';
params['SRS'] = toProj;
params['OUTPUTFORMAT'] = 'application/json';
+ params['CQL_FILTER'] = filter;
let strParams = new URLSearchParams(params).toString();
let source = new VectorSource({
@@ -141,17 +143,16 @@ GisOlLayer.prototype = {
//레이어 소스 생성
let source;
+ let cmncNm = option.cmncNm;
- let lyrTyNm = option.lyrTyNm;
- if(lyrTyNm == 'tb_ms_flood_dngr_map' //침수위험지도
- || lyrTyNm == 'tb_ms_sbmrg_dngr_map' //도시침수지도
- || lyrTyNm == 'tb_ms_swage_mhl' //맨홀
- || lyrTyNm == 'tb_ms_swage_ppl' //관로
- || lyrTyNm == 'tb_ms_swage_pmstn' //펌프장
- ) {
- source = this.getVectorSourceBbox(option);
- }else {
- source = this.getVectorTileSource(option);
+ try {
+ if(cmncNm == 'bbox') {
+ source = this.getVectorSourceBbox(option);
+ }else {
+ source = this.getVectorTileSource(option);
+ }
+ }catch(error){
+ console.warn(option);
}
returnLayer.setSource(source);
@@ -564,6 +565,15 @@ GisOlLayer.prototype = {
cesiumScene.camera.moveEnd.addEventListener(cesiumScene._moveEndListener);
}
},
+ getFeatureById: function(featureId){
+ this.layers
+ .getArray()
+ .forEach(layer => {
+ const source = layer.getSource();
+ const feature = source.getFeatureById(String(featureId).trim());
+ if (feature) return feature;
+ });
+ }
};
function getOpenLayersZoom(cesiumScene) {
diff --git a/packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx b/packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx
new file mode 100644
index 000000000..97ba51fb7
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx
@@ -0,0 +1,122 @@
+// SearchInputText.tsx
+"use client";
+import { Input } from "@common/components/ui";
+import { useEffect, useRef, useState } from "react";
+
+interface SearchInputTextProps {
+ initValue?: string;
+ setValue: (val: string) => void;
+ setRtnData: (val: { fnm: string; fid: string }) => void;
+ searchList: { fnm: string; fid: string }[];
+ onEnter?: () => void;
+ className?: string;
+}
+
+export default function SearchInputText({
+ initValue,
+ setValue,
+ setRtnData,
+ searchList,
+ onEnter,
+ className,
+}: SearchInputTextProps) {
+ const [showResults, setShowResults] = useState(false);
+ const [text, setText] = useState(initValue ?? "");
+ const [highlightIndex, setHighlightIndex] = useState(-1);
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ setText(initValue ?? "");
+ }, [initValue]);
+
+ useEffect(() => {
+ if (searchList.length > 0) {
+ setShowResults(true);
+ } else {
+ setShowResults(false);
+ }
+ setHighlightIndex(-1);
+ }, [searchList]);
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ wrapperRef.current &&
+ !(wrapperRef.current as any).contains(event.target)
+ ) {
+ setShowResults(false);
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [wrapperRef]);
+
+ const handleChange = (e: React.ChangeEvent
) => {
+ setText(e.target.value);
+ setValue(e.target.value);
+ setHighlightIndex(-1);
+ };
+
+ const handleSelect = (item: { fnm: string; fid: string }) => {
+ setText(item.fnm);
+ setValue(item.fnm);
+ setRtnData(item);
+ setShowResults(false);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ if (highlightIndex >= 0 && highlightIndex < searchList.length) {
+ const selected = searchList[highlightIndex];
+ if (selected) handleSelect(selected);
+ } else if (onEnter) {
+ onEnter();
+ }
+ } else if (e.key === "ArrowDown") {
+ e.preventDefault();
+ setHighlightIndex((prev) =>
+ prev < searchList.length - 1 ? prev + 1 : prev
+ );
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault();
+ setHighlightIndex((prev) => (prev > 0 ? prev - 1 : -1));
+ }
+ };
+
+ return (
+
+
+ {showResults && (
+
+ {searchList.length > 0 ? (
+ searchList.map((item, index) => (
+
handleSelect(item)}
+ >
+ {item.fnm}
+
+ ))
+ ) : (
+
+ 검색 결과가 없습니다.
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/packages/common/business_components/src/ui/map/searchBar/comboSearchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/comboSearchBar.tsx
new file mode 100644
index 000000000..999b46569
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/searchBar/comboSearchBar.tsx
@@ -0,0 +1,306 @@
+"use client";
+import { SelectBox, type SelectDataType } from "../../../ui";
+import { Button } from "@common/components/ui";
+import { useCallback, useEffect, useState } from "react";
+
+const geoserverUrl =
+ "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json";
+
+const FACILITY_TYPE_URL_MAP: Record = {
+ tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설
+ tb_wr_bdst_ruse_fclt:
+ "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000", //재이용시설 post
+ tb_wr_rnwtr_utztn_fclt: "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}", //빗물이용시설 get
+ tb_wr_rswtr_fclt: "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}", //중수도시설 get
+ tb_wr_htdfl_fclt: "/reuse-service/drain/facilityList/1/1000/{sggCd}", //온배수시설 get
+ tb_wr_ruse_fclt_dmd_co: "/reuse-service/reuseFc/request/1/1000/{sggCd}", //수요처 get
+ tb_wr_tmpr_dmd_co:
+ "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}", //잠재수요처 get
+};
+
+interface SearchBarProps {
+ onFeatureSelect: (feature: any) => void;
+ gisMaps: React.RefObject;
+ facilityTypes: { text: string; val: string }[];
+}
+
+export const ComboSearchBar = ({
+ onFeatureSelect,
+ gisMaps,
+ facilityTypes,
+}: SearchBarProps) => {
+ const [sidoValue, setSidoValue] = useState("0");
+ const [sigunValue, setSigunValue] = useState("0");
+ const [sidoData, setSidoData] = useState([
+ { text: "선택", val: "0" },
+ ]);
+ const [sigunData, setSigunData] = useState([
+ { text: "선택", val: "0" },
+ ]);
+ const [filteredSigunData, setFilteredSigunData] = useState([
+ { text: "선택", val: "0" },
+ ]);
+
+ const [facilityData, setFacilityData] = useState([
+ { text: "선택", val: "0" },
+ ]);
+ const [facilityValue, setFacilityValue] = useState("0");
+
+ const [facilityType, setFacilityType] = useState(
+ facilityTypes?.length === 1 && facilityTypes[0]
+ ? facilityTypes[0].val
+ : "0",
+ );
+
+ //시도, 시군구 조회
+ const fetchSido = async () => {
+ const url = `/cms-service/api/v1/code/cache/ADSTDSTRCD/Y`;
+ const res = await fetch(url);
+ const json = await res.json();
+ const sidoList = json
+ .filter(
+ (item: any) =>
+ typeof item.lwpoCd === "string" && item.lwpoCd.length === 2,
+ )
+ .map((feature: any) => ({
+ val: feature.lwpoCd,
+ text: feature.lwpoCdNm,
+ }));
+ setSidoData([{ text: "선택", val: "0" }, ...sidoList]);
+
+ const sigunList = json
+ .filter(
+ (item: any) =>
+ typeof item.lwpoCd === "string" && item.lwpoCd.length !== 2,
+ )
+ .map((feature: any) => ({
+ val: feature.lwpoCd,
+ text: feature.lwpoCdNm,
+ }));
+ setSigunData(sigunList);
+ };
+
+ //시설명 콤보박스 조회
+ const fetchFacilitiesBySggNm = async (
+ sggCd: string,
+ sggNm: string,
+ sidoNm: string,
+ currentFacilityType?: string,
+ ) => {
+ const type = currentFacilityType ?? facilityType;
+ if (!type || type === "0") return;
+
+ const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type];
+ if (!baseUrlTemplate) return;
+
+ const year = new Date().getFullYear().toString();
+ let url = baseUrlTemplate
+ .replace("{year}", year)
+ .replace("{sggCd}", sggCd)
+ .replace("{sidoNm}", encodeURIComponent(sidoNm))
+ .replace("{sggNm}", encodeURIComponent(sggNm));
+
+ let list: SelectDataType[] = [];
+
+ if (
+ type === "tb_il_swage_prcs_fclt" //하수처리시설
+ ) {
+ const cqlFilter = `ctpv_nm='${encodeURIComponent(sidoNm)}' AND sgg_nm='${encodeURIComponent(sggNm)}'`;
+ url = `${url}&typeName=hasudo:${type}&cql_filter=${cqlFilter}`;
+ const res = await fetch(url);
+ const json = await res.json();
+ list = json.features.map((f: any) => ({
+ text: f.properties.swtrmt_fclt_nm ?? "(이름없음)",
+ val: f.id,
+ }));
+ } else if (
+ type === "tb_wr_bdst_ruse_fclt" // 재이용시설
+ ) {
+ //재용에서 참조하는 하수처리시설 목록
+ const paramsRes = await fetch("/reuse-service/reuseFc/pubFc");
+ const paramsJson = await paramsRes.json();
+ const params = paramsJson.data;
+
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(params),
+ });
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.ruseUtztnFcltNm ?? "(이름없음)",
+ val: item.pbnstRuseFcltId,
+ }));
+ } else if (
+ type === "tb_wr_ruse_fclt_dmd_co" //수요처
+ ) {
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.dmdCoNm ?? "(이름없음)",
+ val: item.dmdInfoId,
+ }));
+ } else if (
+ type === "tb_wr_tmpr_dmd_co" //잠재수요처
+ ) {
+ // GET 방식
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.dmdCoNm ?? "(이름없음)",
+ val: item.fctryMngSn,
+ }));
+ } else {
+ // GET 방식
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.bdstNm ?? "(이름없음)",
+ val: item.bdstInfoSn,
+ }));
+ }
+
+ setFacilityData([{ text: "선택", val: "0" }, ...list]);
+ setFacilityValue("0");
+ };
+
+ //시도, 시군구 초기 데이터 조회
+ useEffect(() => {
+ fetchSido();
+ }, []);
+
+ //시군구 데이터 초기화
+ const initializeSigunData = useCallback(
+ (sidoCd: string) => {
+ if (sidoCd === "0") {
+ setFilteredSigunData([{ text: "선택", val: "0" }]);
+ setSigunValue("0");
+ return;
+ }
+ const prefix = sidoCd.substring(0, 2);
+ const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
+ setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]);
+ setSigunValue("0");
+ },
+ [sigunData],
+ );
+
+ //콤보박스 선택시 연관 콤보박스 처리
+ const onSelectValue = useCallback(
+ (val: string, type: string) => {
+ if (type === "ctpvCd") {
+ setSidoValue(val);
+ initializeSigunData(val);
+ setFacilityData([{ text: "선택", val: "0" }]);
+ setFacilityValue("0");
+ } else if (type === "sggCd") {
+ setSigunValue(val);
+ const sidoNm =
+ sidoData.find((item) => item.val === sidoValue)?.text || "";
+ const sggNm = sigunData.find((item) => item.val === val)?.text || "";
+ const sggCd = sigunData.find((item) => item.val === val)?.val || "";
+ if (sggNm && sidoNm)
+ fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType);
+ } else if (type === "facilityType") {
+ setFacilityType(val);
+ setFacilityValue("0");
+ setFacilityData([{ text: "선택", val: "0" }]);
+ const sidoNm =
+ sidoData.find((item) => item.val === sidoValue)?.text || "";
+ const sggNm =
+ sigunData.find((item) => item.val === sigunValue)?.text || "";
+ const sggCd =
+ sigunData.find((item) => item.val === sigunValue)?.val || "";
+ if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val);
+ } else if (type === "fcltCd") {
+ setFacilityValue(val);
+ }
+ },
+ [initializeSigunData, sigunData, sigunValue, sidoData, sidoValue],
+ );
+
+ //조회 버튼 클릭
+ const handleSearch = () => {
+ if (!gisMaps.current || facilityValue === "0") return;
+ const layers = gisMaps.current.getLayers().getArray();
+ for (const layer of layers) {
+ const source = layer.getSource?.();
+ const feature = source?.getFeatureById?.(facilityValue);
+ if (feature) {
+ onFeatureSelect(feature);
+ return;
+ }
+ }
+ alert("해당 처리시설을 지도에서 찾을 수 없습니다.");
+ };
+
+ return (
+
+
+
+
+ {facilityTypes.length > 1 && (
+
+ onSelectValue(val.toString(), "facilityType")
+ }
+ value={facilityType}
+ />
+ )}
+
+ onSelectValue(val.toString(), "ctpvCd")
+ }
+ value={sidoValue}
+ />
+
+ onSelectValue(val.toString(), "sggCd")
+ }
+ value={sigunValue}
+ />
+
+ onSelectValue(val.toString(), "fcltCd")
+ }
+ value={facilityValue}
+ />
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/common/business_components/src/ui/map/searchBar/index.tsx b/packages/common/business_components/src/ui/map/searchBar/index.tsx
new file mode 100644
index 000000000..4ba24de3a
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/searchBar/index.tsx
@@ -0,0 +1,3 @@
+export * from "./comboSearchBar";
+export * from "./procSearchBar";
+export * from './searchBar'
diff --git a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx
new file mode 100644
index 000000000..b5f00d07b
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx
@@ -0,0 +1,307 @@
+"use client";
+import { SelectBox, type SelectDataType } from "../../../ui";
+import { useCallback, useEffect, useState } from "react";
+import GisApp from "../lib/gis.core";
+
+const geoserverUrl =
+ "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json";
+
+const FACILITY_TYPE_URL_MAP: Record = {
+ tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설
+};
+
+interface SearchBarProps {
+ gisMaps: React.RefObject;
+ facilityTypes: { text: string; val: string }[];
+}
+
+export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
+ const [sidoValue, setSidoValue] = useState("0");
+ const [sigunValue, setSigunValue] = useState("0");
+ const [sidoData, setSidoData] = useState([
+ { text: "시도", val: "0" },
+ ]);
+ const [sigunData, setSigunData] = useState([
+ { text: "시군구", val: "0" },
+ ]);
+ const [filteredSigunData, setFilteredSigunData] = useState([
+ { text: "시군구", val: "0" },
+ ]);
+
+ const [facilityData, setFacilityData] = useState([
+ { text: "하수처리시설", val: "0" },
+ ]);
+ const [facilityValue, setFacilityValue] = useState("0");
+
+ const [facilityType, setFacilityType] = useState(
+ facilityTypes?.length === 1 && facilityTypes[0]
+ ? facilityTypes[0].val
+ : "0",
+ );
+
+ //시도, 시군구 조회
+ const fetchSido = async () => {
+ const url = `/cms-service/api/v1/code/cache/ADSTDSTRCD/Y`;
+ const res = await fetch(url);
+ const json = await res.json();
+ const sidoList = json
+ .filter(
+ (item: any) =>
+ typeof item.lwpoCd === "string" && item.lwpoCd.length === 2,
+ )
+ .map((feature: any) => ({
+ val: feature.lwpoCd,
+ text: feature.lwpoCdNm,
+ }));
+ setSidoData([{ text: "시도", val: "0" }, ...sidoList]);
+
+ const sigunList = json
+ .filter(
+ (item: any) =>
+ typeof item.lwpoCd === "string" && item.lwpoCd.length !== 2,
+ )
+ .map((feature: any) => ({
+ val: feature.lwpoCd,
+ text: feature.lwpoCdNm,
+ }));
+ setSigunData(sigunList);
+ };
+
+ //시설명 콤보박스 조회
+ const fetchFacilitiesBySggNm = async (
+ sggCd: string,
+ sggNm: string,
+ sidoNm: string,
+ currentFacilityType?: string,
+ ) => {
+ const type = currentFacilityType ?? facilityType;
+ if (!type || type === "0") return;
+
+ const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type];
+ if (!baseUrlTemplate) return;
+
+ const year = new Date().getFullYear().toString();
+ let url = baseUrlTemplate
+ .replace("{year}", year)
+ .replace("{sggCd}", sggCd)
+ .replace("{sidoNm}", encodeURIComponent(sidoNm))
+ .replace("{sggNm}", encodeURIComponent(sggNm));
+
+ let list: SelectDataType[] = [];
+
+ if (
+ type === "tb_il_swage_prcs_fclt" //하수처리시설
+ ) {
+ const cqlFilter = `ctpv_nm='${encodeURIComponent(sidoNm)}' AND sgg_nm='${encodeURIComponent(sggNm)}'`;
+ url = `${url}&typeName=hasudo:${type}&cql_filter=${cqlFilter}`;
+ const res = await fetch(url);
+ const json = await res.json();
+ list = json.features.map((f: any) => ({
+ text: f.properties.swtrmt_fclt_nm ?? "(이름없음)",
+ val: f.id,
+ }));
+ } else if (
+ type === "tb_wr_bdst_ruse_fclt" // 재이용시설
+ ) {
+ //재용에서 참조하는 하수처리시설 목록
+ const paramsRes = await fetch("/reuse-service/reuseFc/pubFc");
+ const paramsJson = await paramsRes.json();
+ const params = paramsJson.data;
+
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(params),
+ });
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.ruseUtztnFcltNm ?? "(이름없음)",
+ val: item.pbnstRuseFcltId,
+ }));
+ } else if (
+ type === "tb_wr_ruse_fclt_dmd_co" //수요처
+ ) {
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.dmdCoNm ?? "(이름없음)",
+ val: item.dmdInfoId,
+ }));
+ } else if (
+ type === "tb_wr_tmpr_dmd_co" //잠재수요처
+ ) {
+ // GET 방식
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.dmdCoNm ?? "(이름없음)",
+ val: item.fctryMngSn,
+ }));
+ } else {
+ // GET 방식
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.bdstNm ?? "(이름없음)",
+ val: item.bdstInfoSn,
+ }));
+ }
+
+ setFacilityData([{ text: "시도", val: "0" }, ...list]);
+ setFacilityValue("0");
+ };
+
+ //시도, 시군구 초기 데이터 조회
+ useEffect(() => {
+ fetchSido();
+ }, []);
+
+ //시군구 데이터 초기화
+ const initializeSigunData = useCallback(
+ (sidoCd: string) => {
+ if (sidoCd === "0") {
+ setFilteredSigunData([{ text: "시군구", val: "0" }]);
+ setSigunValue("0");
+ return;
+ }
+ const prefix = sidoCd.substring(0, 2);
+ const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
+ setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]);
+ setSigunValue("0");
+ },
+ [sigunData],
+ );
+
+ //콤보박스 선택시 연관 콤보박스 처리
+ const onSelectValue = useCallback(
+ (val: string, type: string) => {
+ if (type === "ctpvCd") {
+ setSidoValue(val);
+ initializeSigunData(val);
+ setFacilityData([{ text: "하수처리시설", val: "0" }]);
+ setFacilityValue("0");
+ } else if (type === "sggCd") {
+ setSigunValue(val);
+ const sidoNm =
+ sidoData.find((item) => item.val === sidoValue)?.text || "";
+ const sggNm = sigunData.find((item) => item.val === val)?.text || "";
+ const sggCd = sigunData.find((item) => item.val === val)?.val || "";
+ if (sggNm && sidoNm)
+ fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType);
+ } else if (type === "facilityType") {
+ setFacilityType(val);
+ setFacilityValue("0");
+ setFacilityData([{ text: "하수처리시설", val: "0" }]);
+ const sidoNm =
+ sidoData.find((item) => item.val === sidoValue)?.text || "";
+ const sggNm =
+ sigunData.find((item) => item.val === sigunValue)?.text || "";
+ const sggCd =
+ sigunData.find((item) => item.val === sigunValue)?.val || "";
+ if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val);
+ } else if (type === "fcltCd") {
+ setFacilityValue(val);
+
+ // 하수처리시설일 때 바로 handleFeatureFocus 호출
+ if (val !== "0") {
+ handleFeatureFocus({
+ fid: val,
+ layerNm: "tb_il_swage_prcs_fclt", // 레이어 이름
+ });
+ }
+ }
+ },
+ [initializeSigunData, sigunData, sigunValue, sidoData, sidoValue],
+ );
+
+ const handleFeatureFocus = (data: any) => {
+ const map = gisMaps.current;
+ if (!map) {
+ console.warn("지도가 초기화되지 않았습니다.");
+ return;
+ }
+ // 항상 같은 select 객체 사용
+ const select = GisApp.Module.select as any;
+ if (!select) return;
+ // 1. 기존 선택 초기화
+ //select.resetSelectedFeature();
+ // 2. data가 OpenLayers Feature 인스턴스인지 확인
+ let foundFeature: any = null;
+ let featureId = data.fid;
+ map
+ .getLayers()
+ .getArray()
+ .forEach((layer: any) => {
+ const source = layer.getSource?.();
+ if (source?.getFeatureById) {
+ const feature = source.getFeatureById(String(featureId).trim());
+ if (feature) foundFeature = feature;
+ }
+ });
+ // 3. 피처 선택
+ if (foundFeature) {
+ const geometry = foundFeature.getGeometry();
+ if (geometry) {
+ const extent = geometry.getExtent();
+ map.getView().fit(extent, {
+ duration: 300,
+ padding: [50, 50, 50, 50],
+ maxZoom: 16,
+ constrainResolution: true,
+ nearest: true,
+ });
+ } else {
+ console.warn("geometry를 찾을 수 없습니다:", data);
+ }
+ } else {
+ console.warn("Feature를 찾을 수 없습니다:", data);
+ }
+ };
+
+ return (
+
+
+
+
+
+ onSelectValue(val.toString(), "ctpvCd")
+ }
+ value={sidoValue}
+ />
+
+ onSelectValue(val.toString(), "sggCd")
+ }
+ value={sigunValue}
+ />
+
+ onSelectValue(val.toString(), "fcltCd")
+ }
+ value={facilityValue}
+ />
+
+
+
+
+ );
+};
diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx
new file mode 100644
index 000000000..68377f5e3
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx
@@ -0,0 +1,347 @@
+"use client";
+import { SelectBox, type SelectDataType } from "../../../ui";
+import { Button } from "@common/components/ui";
+import { useCallback, useEffect, useState } from "react";
+import SearchInputText from "./SearchInputText";
+
+const FACILITY_TYPE_URL_MAP: Record = {
+ tb_il_swage_prcs_fclt:
+ "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_il_swage_prcs_fclt&outputFormat=application/json&propertyName=swtrmt_fclt_cd,swtrmt_fclt_nm&cql_filter=",
+ tb_wr_bdst_ruse_fclt:
+ "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000/",
+ tb_wr_rnwtr_utztn_fclt:
+ "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}/{fcltyNm}",
+ tb_wr_rswtr_fclt:
+ "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}/{fcltyNm}",
+ tb_wr_htdfl_fclt:
+ "/reuse-service/drain/facilityList/1/1000/{sggCd}/{fcltyNm}",
+ tb_wr_ruse_fclt_dmd_co:
+ "/reuse-service/reuseFc/request/1/1000/{sggCd}/{fcltyNm}",
+ tb_wr_tmpr_dmd_co:
+ "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}/{fcltyNm}",
+};
+
+interface SearchBarProps {
+ gisMaps: React.RefObject;
+ facilityTypes: { text: string; val: string }[];
+ onFeatureFocus: any;
+}
+
+export const SearchBar = ({
+ gisMaps,
+ facilityTypes,
+ onFeatureFocus,
+}: SearchBarProps) => {
+ const [sidoValue, setSidoValue] = useState("0");
+ const [sigunValue, setSigunValue] = useState("0");
+ const [facilityType, setFacilityType] = useState(
+ facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0"
+ );
+ const [fcltyNm, setFcltyNm] = useState("");
+ const [searchList, setSearchList] = useState<{ fnm: string; fid: string }[]>(
+ []
+ );
+
+ const [sidoData, setSidoData] = useState([
+ { text: "시도", val: "0" },
+ ]);
+ const [sigunData, setSigunData] = useState([
+ { text: "시군구", val: "0" },
+ ]);
+ const [filteredSigunData, setFilteredSigunData] = useState([
+ { text: "시군구", val: "0" },
+ ]);
+
+ const fetchSido = async () => {
+ const res = await fetch("/cms-service/api/v1/code/cache/ADSTDSTRCD/Y");
+ const json = await res.json();
+ const sidoList = json
+ .filter((item: any) => item.lwpoCd?.length === 2)
+ .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm }));
+ const sigunList = json
+ .filter((item: any) => item.lwpoCd?.length > 2)
+ .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm }));
+ setSidoData([{ text: "시도", val: "0" }, ...sidoList]);
+ setSigunData(sigunList);
+ };
+
+ const initializeSigunData = useCallback(
+ (sidoCd: string) => {
+ if (sidoCd === "0") {
+ setFilteredSigunData([{ text: "시도", val: "0" }]);
+ setSigunValue("0");
+ return;
+ }
+ const prefix = sidoCd.substring(0, 2);
+ const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
+ setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]);
+ setSigunValue("0");
+ },
+ [sigunData]
+ );
+
+ useEffect(() => {
+ fetchSido();
+ }, []);
+
+ const onSelectValue = useCallback(
+ (val: string, type: string) => {
+ if (type === "ctpvCd") {
+ setSidoValue(val);
+ initializeSigunData(val);
+ setSearchList([]);
+ setFcltyNm("");
+ } else if (type === "sggCd") {
+ setSigunValue(val);
+ setSearchList([]);
+ setFcltyNm("");
+ } else if (type === "facilityType") {
+ setFacilityType(val);
+ setSearchList([]);
+ setFcltyNm("");
+ }
+ },
+ [initializeSigunData]
+ );
+
+ const handleSearch = async () => {
+ if (!fcltyNm) return alert("시설명을 입력하세요.");
+
+ const sidoNm = sidoData.find((item) => item.val === sidoValue)?.text || "";
+ const sggNm = sigunData.find((item) => item.val === sigunValue)?.text || "";
+
+ const type = facilityType;
+ const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type];
+ if (!baseUrlTemplate) return;
+
+ let url = baseUrlTemplate;
+ const year = new Date().getFullYear().toString();
+ url = url
+ .replace("{year}", year)
+ .replace("{sggCd}", sigunValue)
+ .replace("{sidoNm}", encodeURIComponent(sidoNm))
+ .replace("{sggNm}", encodeURIComponent(sggNm))
+ .replace("{fcltyNm}", encodeURIComponent(fcltyNm));
+
+ let list: SelectDataType[] = [];
+
+ if (type === "tb_il_swage_prcs_fclt") {
+ const cqlFilter = `ctpv_nm='${sidoNm}' and sgg_nm='${sggNm}' and swtrmt_fclt_nm like '%${fcltyNm}%'`;
+ url += encodeURIComponent(cqlFilter);
+ const res = await fetch(url);
+ const json = await res.json();
+ list = json.features.map((f: any) => ({
+ text: f.properties.swtrmt_fclt_nm ?? "(이름없음)",
+ val: f.id,
+ }));
+ } else if (
+ type === "tb_wr_bdst_ruse_fclt" // 재이용시설
+ ) {
+ //재이용에서 참조하는 하수처리시설 목록
+ const procRes = await fetch("/reuse-service/reuseFc/pubFc");
+ const procJson = await procRes.json();
+ const procData = procJson.data;
+ const procResult = procData
+ .filter((code: string) => code.slice(0, 5) === sigunValue)
+ .map((code: string) => {
+ return code;
+ });
+
+ //처리시설명으로 처리시설코드 조회
+ let procUrl2 =
+ "/operation-service/prcsFclt/ruseInfo/1/1000/A/{sggCd}/0/1/SW/{fcltyNm}";
+ procUrl2 = procUrl2
+ .replace("{sggCd}", sigunValue)
+ .replace("{fcltyNm}", encodeURIComponent(fcltyNm));
+ const procRes2 = await fetch(procUrl2, {
+ method: "POST",
+ headers: {
+ accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(procResult),
+ });
+ const procJson2 = await procRes2.json();
+ const procResult2 = (procJson2.data || []).map((item: any) => item.fid);
+
+ //최종 처리시설코드로 재이용시설조회
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(procResult2),
+ });
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.ruseUtztnFcltNm ?? "(이름없음)",
+ val: item.pbnstRuseFcltId,
+ }));
+ } else if (
+ type === "tb_wr_ruse_fclt_dmd_co" //수요처
+ ) {
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.dmdCoNm ?? "(이름없음)",
+ val: item.dmdInfoId,
+ }));
+ } else if (
+ type === "tb_wr_tmpr_dmd_co" //잠재수요처
+ ) {
+ // GET 방식
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.dmdCoNm ?? "(이름없음)",
+ val: item.fctryMngSn,
+ }));
+ } else {
+ // GET 방식
+ const res = await fetch(url);
+ const json = await res.json();
+ list = (json.data || []).map((item: any) => ({
+ text: item.bdstNm ?? "(이름없음)",
+ val: item.bdstInfoSn,
+ }));
+ }
+
+ if (list.length === 0) {
+ alert("검색 결과가 없습니다.");
+ setSearchList([]);
+ setFcltyNm("");
+ return;
+ }
+
+ const newList = list.map((item: any) => {
+ const code = item.val;
+ const name = item.text;
+ return { fnm: name, fid: code };
+ });
+ setSearchList(newList);
+ };
+
+ const moveFocus = (feature: any) => {
+
+ // console.log('searchbar:moveFocus')
+ // const map = gisMaps.current;
+ // if (!map) {
+ // console.warn("지도가 초기화되지 않았습니다.");
+ // return;
+ // }
+
+ // // 항상 같은 select 객체 사용
+ // const select = GisApp.Module.select as any;
+ // if (!select) return;
+
+ // // 1. 기존 선택 초기화
+ // select.resetSelectedFeature();
+
+ // // 2. data가 OpenLayers Feature 인스턴스인지 확인
+ // let foundFeature: any = null;
+ // let featureId = data.fid;
+ // const layerName = data.layerNm;
+ // map
+ // .getLayers()
+ // .getArray()
+ // .forEach((layer: any) => {
+ // const source = layer.getSource?.();
+ // if (source?.getFeatureById) {
+ // const feature = source.getFeatureById(String(featureId).trim());
+ // if (feature) foundFeature = feature;
+ // }
+ // });
+
+ // // 3. 피처 선택
+ // if (foundFeature) {
+ // const geometry = foundFeature.getGeometry();
+ // if (geometry) {
+ // const extent = geometry.getExtent();
+
+ // map.getView().fit(extent, {
+ // duration: 300,
+ // padding: [50, 50, 50, 50],
+ // maxZoom: 16,
+ // constrainResolution: true,
+ // nearest: true,
+ // });
+ // }
+ // // select.selectFeature(foundFeature, true);
+ // } else {
+ // console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
+ // }
+ };
+
+ return (
+
+
+
+
+ {facilityTypes.length > 1 && (
+
+ onSelectValue(val.toString(), "facilityType")
+ }
+ value={facilityType}
+ />
+ )}
+ onSelectValue(val.toString(), "ctpvCd")}
+ value={sidoValue}
+ />
+ onSelectValue(val.toString(), "sggCd")}
+ value={sigunValue}
+ />
+ {
+ setFcltyNm(val);
+ }}
+ setRtnData={(selected: { fnm: string; fid: string }) => {
+ // 선택된 항목이 있을 경우 onFeatureSelect 실행
+ const type = facilityType;
+ moveFocus({
+ layerNm: type,
+ ...selected,
+ });
+ }}
+ searchList={searchList}
+ onEnter={handleSearch}
+ className="mr-5 w-[150px]"
+ />
+
+
+
+
+
+
+
+ );
+}