diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js
index d2594ba95..45b44c882 100644
--- a/packages/common/business_components/src/ui/map/GisContent.js
+++ b/packages/common/business_components/src/ui/map/GisContent.js
@@ -1,74 +1,61 @@
-import { Search } from 'lucide-react';
-import { useCallback, useEffect, useRef, useState } from 'react';
-import { useGisContext } from './Gis';
-import './lib/css/gis.edit.css';
-import GisApp from './lib/gis.core';
+import { Search } from "lucide-react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { useGisContext } from "./Gis";
+import "./lib/css/gis.edit.css";
+import GisApp from "./lib/gis.core";
-export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) {
- const { gisMap, gisCore } = useGisContext();
- const [visibleComponents, setVisibleComponents] = useState({});
- const addressResultLayer = useRef(null);
- const [playShow, setPlayShow] = useState({ show: false });
+export default function GisContent({
+ edit = false,
+ onFeatureSelect,
+ isSearch,
+ setIsSearchModalOpen,
+}) {
+ const { gisMap, gisCore } = useGisContext();
- //셀렉트 버튼 이벤트
- // const clickFeature = useCallback(feature => {
- // if (feature) {
- // const layer = feature.getLayer();
- // setVisibleComponents(prevState => ({
- // ...prevState,
- // }));
- // }
- // }, []);
+ //셀렉트 버튼 이벤트
+ const selectRslt = useCallback((feature) => {
+ GisApp.Module.layer.highLightFeature(
+ [feature],
+ {
+ id: GisApp.hlLayerId,
+ zIndex: 10,
+ opacity: 1,
+ selectable: true,
+ },
+ { width: 30 },
+ );
+ clickFeature(feature);
+ }, []);
- const selectRslt = useCallback(feature => {
- GisApp.Module.layer.highLightFeature(
- [feature],
- {
- id: GisApp.hlLayerId,
- zIndex: 10,
- opacity: 1,
- selectable: true,
- },
- { width: 30 }
- );
- clickFeature(feature);
- }, []);
+ useEffect(() => {
+ if (gisMap) {
+ setVisibleComponents((prevState) => ({
+ ...prevState,
+ }));
+ }
+ }, [gisMap]);
- useEffect(() => {
- if (gisMap) {
- setVisibleComponents(prevState => ({
- ...prevState,
- }));
- }
- }, [gisMap]);
+ if (!gisMap) return null;
- if (!gisMap) return null;
+ return (
+ <>
+
+ )}
+ >
+ );
}
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/GisTab.js b/packages/common/business_components/src/ui/map/GisTab.js
deleted file mode 100644
index 57d8a48e8..000000000
--- a/packages/common/business_components/src/ui/map/GisTab.js
+++ /dev/null
@@ -1,84 +0,0 @@
-// import React, { useState } from 'react';
-// import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs';
-// import GisTree from './GisTree';
-// import GisSearch from './GisSearch';
-// import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
-// import SearchModal from '@/map/SearchModal';
-
-
-// function GisTab({ visible, setVisible, isSearch, isSearchModalOpen, setIsSearchModalOpen, onFeatureSelect }) {
-// const [activeTab, setActiveTab] = useState('layers');
-
-// return (
-// <>
-//
-//
-//
-// 레이어
-// {/* {isSearch && (
-//
-// //
-// //
-// //
-// )} */}
-//
-
-// {/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */}
-//
-//
-//
-
-// {/* GisSearch도 동일하게 처리 */}
-// {/* {isSearch && (
-//
-//
-//
-// )} */}
-
-
-// {/* 🔍 검색 모달 */}
-// {/* */}
-//
-
-// {/* 접기 버튼 */}
-//
-//
-
-// {/* 열기 버튼 */}
-// {!visible && (
-//
-// )}
-// >
-// );
-// }
-
-// export default React.memo(GisTab);
diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js
index ed6bb00fc..7285caa07 100644
--- a/packages/common/business_components/src/ui/map/GisTree.js
+++ b/packages/common/business_components/src/ui/map/GisTree.js
@@ -1,446 +1,493 @@
import {
MultiBackend,
Tree,
- getBackendOptions
-} 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({
+ 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({
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);
- console.log('defaultChecked : ', 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.data.lyrTyNm,
+ 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 a5268ac44..5a6e705da 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
@@ -16,11 +16,8 @@ import { Point } from 'ol/geom';
import { getTopLeft } from 'ol/extent';
import TileWMTS from 'ol/source/WMTS';
import WMTSTileGrid from 'ol/tilegrid/WMTS';
-// import { get as getProjection } from 'ol/proj';
import proj4 from 'proj4';
import { register } from 'ol/proj/proj4';
-import olSourceImageWMS from 'ol/source/ImageWMS';
-import { GeoJSON } from 'ol/format';
import GisOlEditLayer from './gis.edit';
import GisOlLayer from './gis.layer';
@@ -34,7 +31,6 @@ GisApp.layerInfos = [];
GisApp.DROUGHT_DATES = { startYmd: '', endYmd: '' };
//기본정보
GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows';
-GisApp.SN_GEOSERVER_URL = '/sewerage-service/api/v1/map/geometry';
GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms';
//맵 정보
@@ -44,9 +40,8 @@ GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z
GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`;
-// TODO: NGII 정보
-GisApp.NGII_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키
-GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`;
+// NGII 정보(배경지도)
+GisApp.NGII_WMTS_URL = process.env.NEXT_PUBLIC_NGII_URL;
GisApp.EMAP_URL = '/emap/';
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
@@ -563,18 +558,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.style.js b/packages/common/business_components/src/ui/map/lib/gis.style.js
index 8cff08dca..f38853962 100644
--- a/packages/common/business_components/src/ui/map/lib/gis.style.js
+++ b/packages/common/business_components/src/ui/map/lib/gis.style.js
@@ -88,7 +88,7 @@ GisOlStyle.prototype = {
}
let baseResolution = 2;
- let minScale = 0.4;
+ let minScale = 0.8;
let maxScale = 1.5;
let scale = baseResolution / resolution > maxScale ? maxScale : baseResolution / resolution;
scale = Math.max(minScale, Math.min(maxScale, scale));
@@ -176,7 +176,7 @@ GisOlStyle.prototype = {
const properties = feature.getProperties();
const overLayId = feature?.get('overLayId');
- const overLayMinZoom = layer.get('overLayMinZoom') || '12';
+ const overLayMinZoom = layer.get('overLayMinZoom') || '10';
const overLayOffset = layer?.get('overLayOffset');
let findoOverlay = this.overlays[overLayId];
if (findoOverlay) {
@@ -193,7 +193,8 @@ GisOlStyle.prototype = {
if (typeof overlayHtml === 'string') {
//const sanitizedHtml = DOMPurify.sanitize(overlayHtml);
- overlayContainer.innerHTML = sanitizedHtml;
+ //overlayContainer.innerHTML = sanitizedHtml;
+ overlayContainer.innerHTML = overlayHtml;
}
// React 컴포넌트일 경우 createPortal() 사용
else if (React.isValidElement(overlayHtml)) {
@@ -205,7 +206,7 @@ GisOlStyle.prototype = {
element: overlayContainer,
positioning: 'bottom-center',
stopEvent: false,
- offset: overLayOffset ? overLayOffset : [0, 0], // 오프셋 설정 (위치 조정)
+ offset: overLayOffset ? overLayOffset : [80, 0], // 오프셋 설정 (위치 조정)
});
this.overlays[overLayId] = overlay;
@@ -274,7 +275,8 @@ GisOlStyle.prototype = {
let fontExtnColor = layer.get('fontExtnColor');
let fontExtnWdth = Number(layer.get('fontExtnWdth') || '1');
let fontIntnColor = layer.get('fontIntnColor');
- let minViewSize = layer.get('minViewSize');
+ //let minViewSize = layer.get('minViewSize') || 13;
+ let minViewSize = 14;
if (feature == null) {
return;
}
@@ -282,14 +284,14 @@ GisOlStyle.prototype = {
let label = prop[layer.get('lblAtrbNm')];
let scaleFactor = 1 / resolution; // 해상도가 낮아질수록 글자 크기 증가
const zoom = GisApp.Module.core.map.getView().getZoom();
- if (zoom < minViewSize - 3) {
+ if (zoom <= minViewSize) {
label = '';
}
let text = new Text({
- stroke: new Stroke({
- color: '#FFFFFF', // 흰색 테두리
- width: fontExtnWdth,
- }),
+ // stroke: new Stroke({
+ // color: '#FFFFFF', // 흰색 테두리
+ // width: fontExtnWdth,
+ // }),
fill: new Fill({
color: fontIntnColor,
width: fontExtnWdth,
diff --git a/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx b/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx
new file mode 100644
index 000000000..97ba51fb7
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.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/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/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx
new file mode 100644
index 000000000..edd2853f2
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx
@@ -0,0 +1,344 @@
+"use client";
+import { SelectBox, type SelectDataType } from "../../../ui";
+import { Button } from "@common/components/ui";
+import { useCallback, useEffect, useState } from "react";
+import SearchInputText from "./SearchInpuText";
+import GisApp from "../lib/gis.core";
+
+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 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);
+ }
+
+ onFeatureFocus(foundFeature);
+ };
+
+ 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;
+ handleFeatureFocus({
+ layerNm: type,
+ ...selected,
+ });
+ }}
+ searchList={searchList}
+ onEnter={handleSearch}
+ className="mr-5 w-[150px]"
+ />
+
+
+
+
+
+
+
+ );
+}