diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 3728332f0..200d0fb19 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -6,8 +6,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; import SearchModal from "./searchModal/searchModal"; import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup"; -import { SearchBar } from "./searchBar/searchBar"; import { WaterDepth, WATER_DEPTH_LAYER_TYPES, ORDERED_LAYERS } from "./waterDepth/waterDepth"; +import { ProcSearchBar } from "@common/business_components/ui/map/searchBar"; // 타입 정의 추가 export type LyrStngProps = { @@ -120,6 +120,7 @@ const Map = () => { setIsSearchModalOpen(false); }; + const facilityTypes = [{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" }]; // WaterDepth 표시 여부를 결정하는 함수 const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => { const waterDepthLayers = checkedNodesData.filter(node => @@ -153,7 +154,7 @@ const Map = () => { isOpen={isOpen} isSearchModalOpen={isSearchModalOpen} onCheckedLayersChange={handleCheckedLayersChange} - searchBar={SearchBar} + searchBar={} waterDepth={showWaterDepth ? WaterDepth : undefined} isPlaying={isPlaying} setIsPlaying={setIsPlaying as any} diff --git a/apps/wr/app/map/mainMap/page.tsx b/apps/wr/app/map/mainMap/page.tsx index 7b1d46c88..d7028130b 100644 --- a/apps/wr/app/map/mainMap/page.tsx +++ b/apps/wr/app/map/mainMap/page.tsx @@ -6,6 +6,7 @@ import { mapService } from "../service"; import NewWindowPopup from "../components/newWindowPopup"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; import { SearchBar } from "../searchBar/searchBar"; +import { UndoIcon } from "lucide-react"; const Map = () => { const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 @@ -46,7 +47,8 @@ const Map = () => { }; // SearchModal, GisContent 등에서 호출 - const onFeatureSelect = (data: any) => { + const onFeatureFocus = (data: any) => { + console.log("page:onFeatureFocus"); const map = gisMaps.current; if (!map) { console.warn("지도가 초기화되지 않았습니다."); @@ -64,26 +66,6 @@ const Map = () => { let foundFeature: any = null; let featureId = data.fid; const layerName = data.layerNm; - console.log("선택된 레이어 이름:", data, layerName); - if (layerName === "하수재이용시설") { - // 재이용시설 - featureId = `${layerName}.${data.searchPk}`; - } else if ( - //빗물이용시설, 중수도시설 - layerName === "빗물이용시설" || - layerName == "중수도시설" - ) { - featureId = `${layerName}.${data.bdstInfoSn}`; - } else if (layerName === "하수처리시설") { //하수처리시설 - featureId = `${layerName}.${data.bscFctCd}`; - } else if (layerName === "온배수시설") { //온배수시설 - featureId = `${layerName}.${data.bdstRuseFcltSn}`; - } else if (layerName == "수요처") { //수요처 - featureId = `${layerName}.${data.dmdInfoId.replaceAll("-", "")}`; - } else { - console.warn("해당 레이어에 대한 featureId 매핑 없음", layerName); - featureId = `${layerName}.${data.fid || data.id || data.ftrIdn}`; - } map .getLayers() .getArray() @@ -109,28 +91,15 @@ const Map = () => { nearest: true, }); } - select.selectFeature(foundFeature, true); + // select.selectFeature(foundFeature, true); } else { console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); } const currentId = data?.fid || data?.id || data?.ftrIdn; const lastId = featureData?.fid || featureData?.id || featureData?.ftrIdn; if (featureData && currentId === lastId) return; // 이전과 같으면 무시 - setFeatureData(data); - setIsOpen(true); - setIsSearchModalOpen(false); }; - // MapSearchBar 래퍼 컴포넌트 - // const CustomMapSearchBar = (props: any) => { - // return ( - // - // ); - // }; const renderDetailView = useCallback( (props: any) => ( { isMiniMap={false} searchView={undefined} detailView={renderDetailView} - onFeatureSelect={onFeatureSelect} + onFeatureSelect={onFeatureFocus} setIsOpen={setIsOpen} setFeatureData={setFeatureData} featureData={featureData} @@ -173,8 +142,9 @@ const Map = () => { ]; return ( ); }} diff --git a/apps/wr/app/map/searchBar/SearchInputText.tsx b/apps/wr/app/map/searchBar/SearchInputText.tsx index caf4c9f2a..97ba51fb7 100644 --- a/apps/wr/app/map/searchBar/SearchInputText.tsx +++ b/apps/wr/app/map/searchBar/SearchInputText.tsx @@ -6,8 +6,8 @@ import { useEffect, useRef, useState } from "react"; interface SearchInputTextProps { initValue?: string; setValue: (val: string) => void; - setRtnData: (val: { bscFctNm: string; bscFctCd: string }) => void; - searchList: { bscFctNm: string; bscFctCd: string }[]; + setRtnData: (val: { fnm: string; fid: string }) => void; + searchList: { fnm: string; fid: string }[]; onEnter?: () => void; className?: string; } @@ -59,9 +59,9 @@ export default function SearchInputText({ setHighlightIndex(-1); }; - const handleSelect = (item: { bscFctNm: string; bscFctCd: string }) => { - setText(item.bscFctNm); - setValue(item.bscFctNm); + const handleSelect = (item: { fnm: string; fid: string }) => { + setText(item.fnm); + setValue(item.fnm); setRtnData(item); setShowResults(false); }; @@ -107,7 +107,7 @@ export default function SearchInputText({ }`} onClick={() => handleSelect(item)} > - {item.bscFctNm} + {item.fnm} )) ) : ( diff --git a/apps/wr/app/map/searchBar/searchBar.tsx b/apps/wr/app/map/searchBar/searchBar.tsx index 7760ee0b8..17f164646 100644 --- a/apps/wr/app/map/searchBar/searchBar.tsx +++ b/apps/wr/app/map/searchBar/searchBar.tsx @@ -1,4 +1,3 @@ -// SearchBar.tsx "use client"; import { SelectBox, type SelectDataType } from "@common/business_components/ui"; import { Button } from "@common/components/ui"; @@ -23,15 +22,15 @@ const FACILITY_TYPE_URL_MAP: Record = { }; interface SearchBarProps { - onFeatureSelect: (feature: any) => void; - // gisMaps: React.RefObject; + gisMaps: React.RefObject; facilityTypes: { text: string; val: string }[]; + onFeatureFocus: any; } export function SearchBar({ - onFeatureSelect, - // gisMaps, + gisMaps, facilityTypes, + onFeatureFocus, }: SearchBarProps) { const [sidoValue, setSidoValue] = useState("0"); const [sigunValue, setSigunValue] = useState("0"); @@ -39,9 +38,9 @@ export function SearchBar({ facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0" ); const [fcltyNm, setFcltyNm] = useState(""); - const [searchList, setSearchList] = useState< - { bscFctNm: string; bscFctCd: string }[] - >([]); + const [searchList, setSearchList] = useState<{ fnm: string; fid: string }[]>( + [] + ); const [sidoData, setSidoData] = useState([ { text: "시도", val: "0" }, @@ -163,9 +162,7 @@ export function SearchBar({ body: JSON.stringify(procResult), }); const procJson2 = await procRes2.json(); - const procResult2 = (procJson2.data || []).map( - (item: any) => item.bscFctCd - ); + const procResult2 = (procJson2.data || []).map((item: any) => item.fid); //최종 처리시설코드로 재이용시설조회 const res = await fetch(url, { @@ -220,11 +217,62 @@ export function SearchBar({ const newList = list.map((item: any) => { const code = item.val; const name = item.text; - return { bscFctNm: name, bscFctCd: code }; + 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 (
@@ -269,13 +317,10 @@ export function SearchBar({ setValue={(val: string = "") => { setFcltyNm(val); }} - setRtnData={(selected: { - bscFctNm: string; - bscFctCd: string; - }) => { + setRtnData={(selected: { fnm: string; fid: string }) => { // 선택된 항목이 있을 경우 onFeatureSelect 실행 const type = facilityType; - onFeatureSelect({ + moveFocus({ layerNm: type, ...selected, }); diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 7c92c9f59..6cefc454d 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -154,18 +154,30 @@ function Gis({ setFeatureData(null); }; - // Feature Click Event + // 피처 클릭시 const handleFeatureSelect = useCallback( (data) => { + console.log("handleFeatureSelect"); onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, [mapInstanceRef], ); + //피처로 포커스 이동 + const handleFeatureFocus = useCallback( + (layerName, data) => { + console.log("handleFeatureFocus"); + console.log(layerName); + console.log(data); + // onFeatureFocus?.(data); + }, + [mapInstanceRef], + ); + // 지도 생성 및 초기화 useEffect(() => { if (!mapRef.current) return; - if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 + //if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 // EPSG:5179 등록 proj4.defs([ @@ -176,33 +188,53 @@ function Gis({ ]); register(proj4); - // OpenLayers 지도 객체 생성 - const map = new OlMap({ - layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]], - renderer: "webgl", - target: mapRef.current, - interactions: defaults({ doubleClickZoom: false }), - controls: defaultControls({ - zoom: false, - rotate: false, - attributionOptions: { - collapsible: false, - }, - }).extend([]), - view: new View({ - projection: GisApp.TO_PROJ, - zoom: gisCore.defaults.zoom, - center: olProj.transform( - [127.4875, 37.4916], - GisApp.FROM_PROJ, - GisApp.TO_PROJ, - ), - minZoom: gisCore.defaults.minZoom, - maxZoom: gisCore.defaults.maxZoom, - }), - }); + let map = mapInstanceRef.current; - gisMaps.current = map; + // OpenLayers 지도 객체 생성 + if (!map) { + console.log("지도 객체 생성"); + map = new OlMap({ + layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]], + renderer: "webgl", + target: mapRef.current, + interactions: defaults({ doubleClickZoom: false }), + controls: defaultControls({ + zoom: false, + rotate: false, + attributionOptions: { + collapsible: false, + }, + }).extend([]), + view: new View({ + projection: GisApp.TO_PROJ, + zoom: gisCore.defaults.zoom, + center: olProj.transform( + [127.4875, 37.4916], + GisApp.FROM_PROJ, + GisApp.TO_PROJ, + ), + minZoom: gisCore.defaults.minZoom, + maxZoom: gisCore.defaults.maxZoom, + }), + }); + } else { + // 📌 재마운트 시 target 없으면 다시 지정 + if (!map.getTarget()) { + console.log("재마운트"); + map.setTarget(mapRef.current); + } + } + + const select = GisApp.Module.select; + if (select) { + console.log("select 바인딩"); + if (select.selectInteraction) { + map.removeInteraction(select.selectInteraction); + } + select.map = map; // map 바인딩 + select.setInteraction(); + select.pointerMoveEventHandler(); + } // 미니맵 추가 if (isMiniMap) { @@ -219,12 +251,21 @@ function Gis({ map.addControl(overviewMap); } + gisMaps.current = map; + // 스케일 컨트롤 및 베이스 레이어 설정 gisCore.__proto__.map = map; gisCore.scaleControl(); gisCore.changeBaseLayer("base"); setMapObject({ map: map }); mapInstanceRef.current = map; // 생성 완료 플래그 설정 + + // 📌 언마운트 시 완전 제거 + return () => { + console.log("지도 제거"); + map.setTarget(null); // DOM 연결 해제 + mapInstanceRef.current = null; // 참조 초기화 + }; }, []); // treeData 변경 시 레이어 코드 생성 @@ -238,11 +279,6 @@ function Gis({ // 트리 데이터를 레이어 코드 객체로 변환 const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, "lyrSn"); GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 - - //setTreeData(data); - // 편집 가능한 레이어 목록 생성 - //const layerItems = getLayerItems(layerCode); - //setEditLayerItems(prev => [...layerItems]) }, [treeData]); const getLayerItems = (layerCode) => { @@ -274,9 +310,9 @@ function Gis({ const handleCheckedLayersChange = useCallback( (checkedIds, checkedNodesData) => { setTimeout(() => { - setCheckedLayers(checkedIds); - setCheckedLayersData(checkedNodesData); - onCheckedLayersChange?.(checkedIds, checkedNodesData); + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + onCheckedLayersChange?.(checkedIds, checkedNodesData); }, 1000); }, [onCheckedLayersChange], @@ -357,10 +393,8 @@ function Gis({ )} {/* 시도/시군구 조회 영역 */} {SearchBar && ( -
- +
+ {SearchBar}
)} 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 ? {item.lyrNm} : '', - }; - }); - }; - - 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 ? {item.lyrNm} : "", + }; + }); + }; + + + 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]" + /> +
+
+ +
+
+
+
+ ); +}