Merge commit '23d02a63c38cc646e651391b0079d6a64522422a' into iipbs/wr/form_fuction

This commit is contained in:
jyha00
2025-06-09 19:27:15 +09:00
9 changed files with 298 additions and 80 deletions
+12
View File
@@ -0,0 +1,12 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
+90 -12
View File
@@ -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 (
<div className="bg-container">
<Gis treeData={treeData} isSearch={true} />
</div>
<Gis
treeData={treeData}
isSearch={true}
gisMaps={gisMaps}
isMiniMap={true}
searchView={SearchModal}
detailView={FeatureClickEventPopup}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
/>
</div>
);
};
@@ -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 (
<>
<GisContext.Provider value={{gisCore, gisMap, treeData, checkedLayerIds}}>
<GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}>
<div className="h-[calc(100vh-10rem)] flex relative">
<GisTab visible={tabVisible} setVisible={setTabVisible} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} />
<GisContent onFeatureSelect={handleFeatureSelect}/>
{isOpen && featureData && (
<FeatureClickEventPopup
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
<GisTree visible={tabVisible} setVisible={setTabVisible} />
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
{/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
{isOpen && featureData && !isSearchModalOpen && (
<DetailView
featureData={featureData}
onClose={closePopup} // 닫기 콜백 전달
/>
)}
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
{/* <GisLonLat/>
<SearchView
open={isSearchModalOpen}
onOpenChange={setIsSearchModalOpen}
onFeatureSelect={onFeatureSelect}
/>
<GisLonLat/>
<GisToolBar/>
<MapToggleBtn/> */}
<GisMapToggle/>
<GisSelect onFeatureSelect={onFeatureSelect} />
</div>
{isSearch && (
<div className="absolute right-8 top-[305px] z-20">
<button
type="button"
onClick={() => setIsSearchModalOpen(true)}
className="overflow-hidden w-[40px] h-[40px] bg-white
border border-[#9aa7b8]
rounded
shadow-[0_0_5px_#999]
flex items-center justify-center
p-0
"
>
<Search size={24} className="text-gray-700" />
</button>
</div>
)}
</GisContext.Provider>
</>
);
@@ -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 (
<>
{/*<GisTree />*/}
<MapToggleBtn />
{/* <GisMapToggle />
<GisLonLat />
<GisToolBar />
<GisSelect onFeatureSelect={onFeatureSelect}/>
<GisToolBar /> */}
{/* <GisSelect onFeatureSelect={onFeatureSelect} /> */}
{/* 검색 모달 */}
{isSearch && (
<div className="absolute right-8 top-[254px] z-20">
<button
type="button"
onClick={() => setIsSearchModalOpen(true)}
className="overflow-hidden w-[40px] h-[40px] bg-white
border border-[#9aa7b8]
rounded
shadow-[0_0_5px_#999]
flex items-center justify-center
p-0
"
>
<Search size={24} className="text-gray-700" />
</button>
</div>
)}
</>
);
}
@@ -126,8 +126,6 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun
const bznNoChange = useCallback((e: React.ChangeEvent<HTMLInputElement>, 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) {
@@ -20,6 +20,7 @@ type SearchBaseType = {
initClick:() => void;
btnClass:string;
ref:React.RefObject<ReturnType>;
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<string>("A");
const [sidoValue, setSidoValue] = useState<string>("0");
const [sigunValue, setSigunValue] = useState<string>("0");
const [sidoText, setSidoText] = useState<string>("");
const [sigunText, setSigunText] = useState<string>("");
const [regionValue, setRegionValue] = useState<string>("0");
const [untitWaterShedValue, setUntitWaterShedValue] = useState<string>("0");
const [middleRegionValue, setMiddleRegionValue] = useState<string>("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 (
<div className="flex card card-border !mt-6">
<SearchFormLeft isShowPart={true}
@@ -118,7 +129,10 @@ function SearchBaseForm ({
setRtnEnvPartValue={setEnvPartValue}
setRtnEnvPartSidoValue={setSidoValue}
setRtnEnvPartSigunValue={setSigunValue}
setRtnSidoText={setSidoText}
setRtnSigunText={setSigunText}
resetTrigger={resetTrigger}
isAdmin={isAdmin}
>
{leftChildren}
</SearchFormLeft>
@@ -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<string>('A');
//행정별 시도 시군구
const [sidoValue, setSidoValue] = useState<string>('0');
const [sigunValue, setSigunValue] = useState<string>('0');
const [sidoText, setSidoText] = useState<string>('');
const [sigunText, setSigunText] = useState<string>('');
//유역별
const [regionValue, setRegionValue] = useState<string>('0');
const [unitWaterShedValue, setUnitWaterShedValue] = useState<string>('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 ? (
<div className="grid grid-cols-12 gap-5 w-11/12 m-4">
<SelectBox className="col-span-3" label={'구분'} selectArray={selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/>
<SelectBox className="col-span-3" label={'구분'} selectArray={isAdmin ? selectPartData.slice(0, 1) : selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/>
{partValue === 'R' ? (
<>
<SelectBox className="col-span-3" label={'유역'} selectArray={selectRegionData} onSelectValue={(val) => onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined}/>
@@ -44,11 +44,11 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
<TableRow key={rowIndex+ " headerRow"} className="bg-gray-50">
{upHeadList
.filter((item) => item.visible != false)
.map((head: TableUpperProps) => {
.map((head: TableUpperProps, idx) => {
if (head.row === rowIndex) {
return (
<TableHead
key={head.id}
key={head.id?head.id:rowIndex+"_"+idx}
colSpan={head.colSpan || 1}
rowSpan={head.rowSpan || 1}
style={{ whiteSpace: 'pre-line' }}
@@ -29,7 +29,6 @@ type UiHeadProps = {
export function UiTable({ tableData, headlist, pageSize, total, headName, children, cellClick, onPageChange, className = '', resetPageNo = false, boldArray = [] }: UiTableProps) {
const [cur, setCur] = useState<number>(1);
const [idx, setIdx] = useState<number>(-1);
useEffect(() => {
if (resetPageNo) {
setCur(1);