diff --git a/apps/sn/app/layout.js b/apps/sn/app/layout.js new file mode 100644 index 000000000..8525f5f8c --- /dev/null +++ b/apps/sn/app/layout.js @@ -0,0 +1,12 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx index b547fb7e4..7185c9660 100644 --- a/apps/sn/app/map/page.tsx +++ b/apps/sn/app/map/page.tsx @@ -1,18 +1,20 @@ -'use client' +"use client"; -import Gis from '@common/business_components/ui/map/Gis'; -import { SearchDiv, SelectBox, UiTable, MainDiv, Title, ContentsDiv, SearchFormLeft, SearchFormRight } from '@common/business_components'; -import ListDiv from '@common/business_components/ui/list_div'; -import { Button, Input } from '@common/components'; -import { useEffect, useState } from "react"; +import Gis, { useGisContext } from "@common/business_components/ui/map/Gis"; import { mapService } from "@service"; +import { useCallback, useEffect, useRef, useState } from "react"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import SearchModal from "./SearchModal"; +import FeatureClickEventPopup from "./featureClickEventPopup"; const Map = () => { - const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 - const [selectedFeature,setSelectedFeature] = useState(null); // 선택된 피처 상태 - - const fetchData = async () => { + const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const fetchData = async () => { try { const response = await mapService.getLyrStngs(); setTreeData(response.data); // API 응답 저장 @@ -26,10 +28,86 @@ const Map = () => { fetchData(); }, []); + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + let 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 = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + 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) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let 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); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; return (
- -
+ + ); }; diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 5cfff011d..f68d0798a 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -1,6 +1,5 @@ import './lib/css/gis.common.css'; import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; -import { Tabs, TabsList, TabsTrigger, TabsContent } from '@common/components/ui/tabs'; import OlMap from 'ol/Map'; import View from 'ol/View'; import { defaults as defaultControls, OverviewMap } from 'ol/control'; @@ -10,43 +9,88 @@ import proj4 from 'proj4'; import { register } from 'ol/proj/proj4'; import GisApp from './lib/gis.core'; import GisContent from './GisContent'; +import GisTree from './GisTree'; import GisLonLat from './GisLonLat'; +import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; -import GisTab from './GisTab'; -import MapToggleBtn from './MapToggleBtn'; -import FeatureClickEventPopup from '@/map/featureClickEventPopup'; +import GisMapToggle from './GisMapToggle'; +import { Search } from 'lucide-react'; -export const GisContext = createContext({gisCore: null, gisMap: null, treeData:null, checkedLayerIds:null}); +export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); -function Gis({ treeData = [], isSearch = false }) { +function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView : SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) { const mapRef = useRef(); const [mapObject, setMapObject] = useState({}); const [tabVisible, setTabVisible] = useState(true); - const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 - const [featureData,setFeatureData] = useState(null) // 선택된 피쳐 데이터 - const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + // const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + // const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터 + // const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); - const mapInstanceRef = useRef(null); // 맵 생성 여부를 추적할 ref + const mapInstanceRef = useRef(null); const checkedLayerIds = useRef([]); let gisMap = null; const closePopup = () => { setIsOpen(false); // 팝업 닫기 setFeatureData(null) }; - const handleFeatureSelect = (data) => { - console.log('check',data) - setFeatureData(data); // 선택된 피처 데이터 저장 - setIsOpen(true); // 팝업 열기 - }; - // SearchModal이 열릴 때 featureClickEventPopup 닫기 - useEffect(() => { - if (isSearchModalOpen && isOpen) { - setIsOpen(false); - setFeatureData(null); + // SearchModal, GisContent 등에서 호출 + const handleFeatureSelect = useCallback((data) => { + gisMap = mapInstanceRef.current; + + if (!gisMap) { + console.warn('지도가 초기화되지 않았습니다.'); + return; } - }, [isSearchModalOpen]); + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature = null; + if (data && typeof data.getGeometry === 'function' && typeof data.setStyle === 'function') { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + gisMap.getLayers().getArray().forEach(layer => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); + + gisMap.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); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }, [mapInstanceRef]); useEffect(() => { if (!mapRef.current) return; @@ -78,7 +122,7 @@ function Gis({ treeData = [], isSearch = false }) { projection: GisApp.TO_PROJ, zoom: gisCore.defaults.zoom, center: olProj.transform( - [126.85306, 35.571634], + [126.6757, 37.5585], GisApp.FROM_PROJ, GisApp.TO_PROJ ), @@ -87,21 +131,24 @@ function Gis({ treeData = [], isSearch = false }) { }), }); + gisMaps.current = map + //미니맵 - const overviewMap = new OverviewMap({ - layers: [GisApp.Module.core.baseMiniMapLayers['base']], - view: new View({ - projection: GisApp.TO_PROJ, - minZoom: 9, - }), - collapsed: false, - collapseLabel: '\u00BB', - label: '\u00AB', - }); + if(isMiniMap){ + const overviewMap = new OverviewMap({ + layers: [GisApp.Module.core.baseMiniMapLayers['base']], + view: new View({ + projection: GisApp.TO_PROJ, + minZoom: 9, + }), + collapsed: false, + collapseLabel: '\u00BB', + label: '\u00AB', + }); + map.addControl(overviewMap); + } - map.addControl(overviewMap); gisCore.__proto__.map = map; - gisCore.scaleControl(); gisCore.changeBaseLayer('base'); setMapObject({ map: map }); @@ -122,7 +169,7 @@ function Gis({ treeData = [], isSearch = false }) { //setEditLayerItems(prev => [...layerItems]) }, [treeData]); - const getLayerItems= (layerCode) => { + const getLayerItems = (layerCode) => { const layerItems = Object.entries(layerCode) .filter(([key, obj], idx) => obj?.updtPsblYn === 'Y') .map(([key, obj]) => ({ @@ -149,21 +196,48 @@ function Gis({ treeData = [], isSearch = false }) { return ( <> - +
- - - {isOpen && featureData && ( - + + + {/* */} + + {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {isOpen && featureData && !isSearchModalOpen && ( + )} -
- {/* + + - */} + +
+ + {isSearch && ( +
+ +
+ )} ); diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js index 93edcd12c..a8ac408e8 100644 --- a/packages/common/business_components/src/ui/map/GisContent.js +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -1,14 +1,14 @@ -import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import GisApp from './lib/gis.core'; import { useGisContext } from './Gis'; import GisLonLat from './GisLonLat'; import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; import './lib/css/gis.edit.css'; -import MapToggleBtn from './MapToggleBtn'; -import GisEdit from './GisEdit'; +import GisMapToggle from './GisMapToggle'; +import { Search } from 'lucide-react'; -export default function GisContent({ edit = false, onFeatureSelect }) { +export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) { const { gisMap, gisCore } = useGisContext(); const [visibleComponents, setVisibleComponents] = useState({}); const addressResultLayer = useRef(null); @@ -16,12 +16,12 @@ export default function GisContent({ edit = false, onFeatureSelect }) { //셀렉트 버튼 이벤트 // const clickFeature = useCallback(feature => { - // if (feature) { - // const layer = feature.getLayer(); - // setVisibleComponents(prevState => ({ - // ...prevState, - // })); - // } + // if (feature) { + // const layer = feature.getLayer(); + // setVisibleComponents(prevState => ({ + // ...prevState, + // })); + // } // }, []); const selectRslt = useCallback(feature => { @@ -45,16 +45,35 @@ export default function GisContent({ edit = false, onFeatureSelect }) { })); } }, [gisMap]); - + if (!gisMap) return null; - + return ( <> {/**/} - + {/* - - + */} + {/* */} + + {/* 검색 모달 */} + {isSearch && ( +
+ +
+ )} ); } diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index d59108c5b..c5c0ee0db 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -126,8 +126,6 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const bznNoChange = useCallback((e: React.ChangeEvent, noType: string) => { const value = e.target.value; - console.log('form value : '+form.getValues('dmdInfoPk')); - console.log('org value : '+orgBznmNo); if(noType === 'dmdInfoPk') { if(orgBznmNo.substring(0,10) === value) { diff --git a/packages/common/business_components/src/ui/search_base_form/index.tsx b/packages/common/business_components/src/ui/search_base_form/index.tsx index b8d8355ae..2649c67db 100644 --- a/packages/common/business_components/src/ui/search_base_form/index.tsx +++ b/packages/common/business_components/src/ui/search_base_form/index.tsx @@ -20,6 +20,7 @@ type SearchBaseType = { initClick:() => void; btnClass:string; ref:React.RefObject; + isAdmin:boolean; } export type ReturnType = { partValue: string; @@ -29,9 +30,10 @@ export type ReturnType = { unitWaterShedValue: string; middleRegionValue: string; envPartValue: string; + sidoText: string; + sigunText: string; } - function SearchBaseForm ({ leftChildren, rightChildren, @@ -46,11 +48,14 @@ function SearchBaseForm ({ queryClick, excelClick, initClick, - ref + ref, + isAdmin=false }: SearchBaseType) { const [partValue, setPartValue] = useState("A"); const [sidoValue, setSidoValue] = useState("0"); const [sigunValue, setSigunValue] = useState("0"); + const [sidoText, setSidoText] = useState(""); + const [sigunText, setSigunText] = useState(""); const [regionValue, setRegionValue] = useState("0"); const [untitWaterShedValue, setUntitWaterShedValue] = useState("0"); const [middleRegionValue, setMiddleRegionValue] = useState("0"); @@ -66,6 +71,8 @@ function SearchBaseForm ({ unitWaterShedValue: untitWaterShedValue, middleRegionValue: middleRegionValue, envPartValue: envPartValue, + sidoText: sidoText, + sigunText: sigunText, }); } const initVal = useCallback(() => { @@ -86,6 +93,8 @@ function SearchBaseForm ({ unitWaterShedValue: untitWaterShedValue, middleRegionValue: middleRegionValue, envPartValue: envPartValue, + sidoText: sidoText, + sigunText: sigunText, }) }; useEffect(() => { @@ -98,8 +107,10 @@ function SearchBaseForm ({ customRef.unitWaterShedValue = untitWaterShedValue; customRef.middleRegionValue = middleRegionValue; customRef.envPartValue = envPartValue; + customRef.sidoText = sidoText; + customRef.sigunText = sigunText; } - }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]); + }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, sidoText, sigunText, ref]); return (
{leftChildren} diff --git a/packages/common/business_components/src/ui/search_form_left/index.tsx b/packages/common/business_components/src/ui/search_form_left/index.tsx index 02112f084..e7858df2a 100644 --- a/packages/common/business_components/src/ui/search_form_left/index.tsx +++ b/packages/common/business_components/src/ui/search_form_left/index.tsx @@ -40,7 +40,10 @@ type SearchFormLeftProps = { setRtnEnvPartValue?: (value: string) => void; setRtnEnvPartSidoValue?: (value: string) => void; setRtnEnvPartSigunValue?: (value: string) => void; + setRtnSidoText?: (value: string) => void; + setRtnSigunText?: (value: string) => void; resetTrigger?: boolean; + isAdmin?: boolean; } export type EnvPartSelectDataType = SelectDataType & { @@ -53,12 +56,14 @@ const selectPartData:SelectDataType[] = [ { text: "환경청별", val: 'G' }, ]; -function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSigunValue, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false }: SearchFormLeftProps) { +function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false }: SearchFormLeftProps) { /******************선택된 데이터******************/ const [partValue, setPartValue] = useState('A'); //행정별 시도 시군구 const [sidoValue, setSidoValue] = useState('0'); const [sigunValue, setSigunValue] = useState('0'); + const [sidoText, setSidoText] = useState(''); + const [sigunText, setSigunText] = useState(''); //유역별 const [regionValue, setRegionValue] = useState('0'); const [unitWaterShedValue, setUnitWaterShedValue] = useState('0'); @@ -152,6 +157,11 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel } const onSelectValue = (val: string, type: string) => { + let selectedSido; + let selectedSigun; + let selectedEnvPartSido; + let selectedEnvPartSigun; + switch(type) { case 'part': setPartValue(val); @@ -180,9 +190,14 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel break; case 'sido': setSidoValue(val); + selectedSido = selectSidoData.find(item => item.val === val); + setSidoText(selectedSido?.text || ''); if (setRtnSidoValue) { setRtnSidoValue(val); } + if (setRtnSidoText) { + setRtnSidoText(selectedSido?.text || ''); + } initializeSigunData(val); onSelectValue(selectSigunData[0]?.val || '0', 'sigun'); break; @@ -191,6 +206,11 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel setRtnSigunValue(val); } setSigunValue(val); + selectedSigun = filteredSigunData.find(item => item.val === val); + if (setRtnSigunText) { + setRtnSigunText(selectedSigun?.text || ''); + } + setSigunText(selectedSigun?.text || ''); break; // 유역 세팅 @@ -228,6 +248,8 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel break; case 'envPartSido' : setSidoValue(val); + selectedEnvPartSido = filteredEnvPartSidoData.find(item => item.val === val); + setSidoText(selectedEnvPartSido?.text || ''); if (setRtnEnvPartSidoValue) { setRtnEnvPartSidoValue(val); } @@ -236,6 +258,8 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel break; case 'envPartSigun' : setSigunValue(val); + selectedEnvPartSigun = filteredEnvPartSigunData.find(item => item.val === val); + setSigunText(selectedEnvPartSigun?.text || ''); if (setRtnEnvPartSigunValue) { setRtnEnvPartSigunValue(val); } @@ -285,7 +309,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel return ( isShowPart ? (
- onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/> + onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/> {partValue === 'R' ? ( <> onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined}/> diff --git a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx index ebd611c1d..c0ec33233 100644 --- a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx +++ b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx @@ -44,11 +44,11 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => { {upHeadList .filter((item) => item.visible != false) - .map((head: TableUpperProps) => { + .map((head: TableUpperProps, idx) => { if (head.row === rowIndex) { return ( (1); const [idx, setIdx] = useState(-1); - useEffect(() => { if (resetPageNo) { setCur(1);