- 하수재이용시설, 빗물이용시설, 중수도시설, 온배수시설 화면의
현황(지도), 목록, 상세정보 UI 구현 (지도점검 결과 미구현)
This commit is contained in:
swcho
2025-06-24 12:14:54 +09:00
parent a59d814bc7
commit 04d43408f7
14 changed files with 2921 additions and 172 deletions
+621
View File
@@ -0,0 +1,621 @@
"use client";
import {
Form,
FormField,
FormUi,
SelectBox,
} from "@common/business_components/ui";
import { Button, Input } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useWrStore } from "@/app/store";
import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg";
import {
hdtflZod,
type customHtdflBdst,
} from "@/app/store/ruseFcltMng/fcltReg/htdflReg";
import { useEffect } from "react";
import BdstReg from "@/components/ui/form/fcltReg/bdstReg/bdstReg";
import {
formatCommaNumber,
handleCommaNumberChange,
} from "@/components/lib/func/commom";
import { formH1Class } from "@/components/lib/class/comClass";
export default function HtdflPrfmncSttnPopup({
htdflForm,
isModal,
}: {
htdflForm: customHtdflBdst;
isModal: boolean;
}) {
const { code, htdflBdstUpdate, htdflBdstReg } = useWrStore((state) => state);
const defaultValues: z.infer<typeof hdtflZod> = hdtflZod.parse({});
const form = useForm<z.infer<typeof hdtflZod>>({
resolver: zodResolver(hdtflZod),
defaultValues: defaultValues,
});
const onSubmit = (values: z.infer<typeof hdtflZod>) => {
if (isModal) {
htdflBdstReg(values, '41');
} else {
htdflBdstUpdate(values, '41');
}
};
const cleanHtdflData = (raw: any): z.infer<typeof hdtflZod> => {
return {
...raw,
frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null),
archChgPrmsnDt: raw?.archChgPrmsnDt
? new Date(raw?.archChgPrmsnDt)
: null,
chgBuilding: (raw?.chgBuilding || []).map((item: any) => ({
...item,
archChgPrmsnDt:
item?.archChgPrmsnDt === null
? new Date()
: new Date(item?.archChgPrmsnDt),
bdstChgNm: item?.bdstChgNm ?? "", // null → ""
})),
reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({
...item,
instlCmptnDt: new Date(item?.instlCmptnDt),
htdflObj: {
...item?.htdflObj,
},
})),
};
};
useEffect(() => {
console.log("form errors:", form.formState.errors);
}, [form.formState.errors]);
useEffect(() => {
if (hdtflZod) form.reset(cleanHtdflData(htdflForm));
}, [htdflForm]);
return (
<div className="!mx-[1.6rem] !p-0r">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* <div className="flex justify-between items-center mx-3"> */}
<div>
<div className="grid grid-cols-12 flex items-center card-header">
<h1 className="text-2xl font-medium text-label col-span-10">온배수 시설 정보</h1>
</div>
</div>
{isModal && htdflForm != undefined ? <BdstReg form={form} /> : <></>}
{isModal ? (
<h1 className="text-xl col-span-10 mt-4">
온배수 시설 정보{" "}
</h1>
) : (
<></>
)}
<div className="grid grid-cols-6 gap-2 mt-4">
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.etgtType"
render={({ field }) => (
<FormUi label="발전유형(코드정비후 수정)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.fclCpct" // 이름 이상함.
render={({ field }) => (
<FormUi label="설비용량">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.plwtYn"
render={({ field }) => (
<FormUi label="담수화 여부">
<SelectBox
className={""}
label={""}
selectClass={selectClass}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className=" my-4">
<h1 className={formH1Class}>
온배수 시설 운영현황
</h1>
</div>
<div className="grid grid-cols-10 gap-1 mt-4">
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.orgwtWtritSpt"
render={({ field }) => (
<FormUi label="원수취수 지점(코드정비후 수정)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
value={field.value ?? 0}
onChange={(e) => {
field.onChange(Number(e.target.value));
}}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.orgwtOcrnProcs"
render={({ field }) => (
<FormUi label="원수발생 공정">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.wtritEntrcTp"
render={({ field }) => (
<FormUi label="취수구 온도">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.amtgnr"
render={({ field }) => (
<FormUi label="발생량(m³/년)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.prcsProcsYn"
render={({ field }) => (
<FormUi label="처리공정">
<SelectBox
className={""}
label={""}
selectClass={selectClass}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.prcsFcltCpct"
render={({ field }) => (
<FormUi label="처리시설 용량(m³/년)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.fmYn"
render={({ field }) => (
<FormUi label="유량계유무">
<SelectBox
className={""}
selectClass={selectClass}
label={""}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.utztnCpct"
render={({ field }) => (
<FormUi label="이용량(m³/년)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.utztnUsg"
render={({ field }) => (
<FormUi label="이용용도">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className=" my-2">
<h1 className={formH1Class}>
요구수질
</h1>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.grssClfg"
render={({ field }) => (
<FormUi label="총대장균군">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.tn"
render={({ field }) => (
<FormUi label="총질소">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.combResdch"
render={({ field }) => (
<FormUi label="결합잔류염소">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6 ">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.tph"
render={({ field }) => (
<FormUi label="총인">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.trbt"
render={({ field }) => (
<FormUi label="탁도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.ph"
render={({ field }) => (
<FormUi label="수소이온농도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.bod"
render={({ field }) => (
<FormUi label="생물학적 산소요구량">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.clIonQnt"
render={({ field }) => (
<FormUi label="염화물">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.smll"
render={({ field }) => (
<FormUi label="냄새">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.cdtnv"
render={({ field }) => (
<FormUi label="전기전도도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.crmty"
render={({ field }) => (
<FormUi label="색도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-6">
<FormField
control={form.control}
name="reuseBuilding.0.htdflObj.emsnExitTp"
render={({ field }) => (
<FormUi label="배출구온도">
<Input
className={'w-full !text-xl'}
placeholder="10"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
</form>
</Form>
</div>
);
}
+83 -59
View File
@@ -1,18 +1,23 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, useCallback } from "react";
import FeatureClickEventPopup from "../featureClickEventPopup";
import { mapService } from "../service";
import { MapSearchBar } from "../mapSearchBar";
import NewWindowPopup from "../newWindowPopup";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
const Map = () => {
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태
const gisMaps = useRef(null);
const gisMaps = useRef<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const [checkedLayers, setCheckedLayers] = useState<string[]>([]);
const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]);
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
@@ -22,80 +27,92 @@ const Map = () => {
}
};
// 팝업 닫기 핸들러를 useCallback으로 메모이제이션
const handlePopupClose = useCallback(() => {
setIsOpen(false);
setFeatureData(null);
}, []);
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
// 체크된 레이어 정보를 받는 핸들러
const handleCheckedLayersChange = (checkedIds: string[], checkedNodesData: any[]) => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
};
// SearchModal, GisContent 등에서 호출
// const onFeatureSelect = (data: any) => {
// let map = gisMaps.current;
// if (!map) {
// console.warn("지도가 초기화되지 않았습니다.");
// return;
// }
const onFeatureSelect = (data: any) => {
let map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// // 항상 같은 select 객체 사용
// const select = GisApp.Module.select as any;
// if (!select) return;
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
// // 1. 기존 선택 초기화
// select.resetSelectedFeature();
// 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;
// }
// });
// }
// 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();
// 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);
// }
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);
// };
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
};
// MapSearchBar 래퍼 컴포넌트
const CustomMapSearchBar = (props: any) => {
return (
<MapSearchBar
{...props}
searchType="우리동네가뭄지도보기"
searchType="공급처 후보지 조회"
showApplyButton={false}
/>
);
@@ -109,13 +126,20 @@ const Map = () => {
gisMaps={gisMaps}
isMiniMap={true}
searchView={CustomMapSearchBar}
detailView={FeatureClickEventPopup}
detailView={(props: any) => (
<NewWindowPopup
onClose={handlePopupClose} // 메모이제이션된 함수 사용
{...props}
/>
)}
onFeatureSelect={undefined}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
/>
</div>
);
+138 -54
View File
@@ -2,15 +2,35 @@
import { Button, Input, Label } from "@common/components/ui";
import React, { useCallback, useEffect, useRef, useState, useMemo } from "react";
import { SearchFormLeft, type EnvPartSelectDataType } from "@common/business_components/ui/search_form_left";
import { SearchFormRight } from "@common/business_components/ui/search_form_right";
import type { SelectDataType } from "@common/business_components/ui/search_input_select";
import type { ReturnType } from "@common/business_components/ui/search_base_form";
import { useForm } from "react-hook-form";
import { MapSearchFormLeft } from "./mapSearchForm";
import { useWrStore } from "@/app/store";
import { btnClass } from "@/app/store/common/code";
import { SelectBox } from "@common/business_components/ui/select";
import { SearchBaseForm } from "@common/business_components";
import { FormUi, SelectBox, SearchFormLeft, SearchFormRight, Form, FormField } from "@common/business_components/ui";
import type { ReturnType, SelectDataType, EnvPartSelectDataType } from "@common/business_components/ui";
import SearchInputText from "@/components/ui/searchInput/searchInput";
import { zodResolver } from "@hookform/resolvers/zod";
// 건물 검색 타입 정의
interface BuildingInfo {
bdstInfoPk: number;
ctpvCd: number;
sggCd: number;
daddr: string;
bdstNm: string;
frstArchPrmsnDt: string;
archArea: number;
ctpvNm: string | null;
sggNm: string | null;
lat: number | null;
lot: number | null;
regDt: string;
rgtr: string | null;
mdfcnDt: string;
mdfr: string | null;
}
type SearchBaseType = {
searchType: string;
@@ -20,8 +40,6 @@ type SearchBaseType = {
selectUnitWaterShedData?: SelectDataType[] | [];
selectMiddleRegionData?: SelectDataType[] | [];
selectEnvPartData?: EnvPartSelectDataType[] | [];
queryClick: (returnType: ReturnType) => void;
initClick: () => void;
onFeatureSelect: () => void;
btnClass?: string;
ref?: React.RefObject<ReturnType>;
@@ -58,8 +76,6 @@ export function MapSearchBar({
selectMiddleRegionData,
selectEnvPartData,
btnClass,
queryClick,
initClick,
onFeatureSelect,
isAdmin = false,
leftClassName,
@@ -77,6 +93,10 @@ export function MapSearchBar({
const [isChHead, setIsChHead] = useState(false);
const ref = useRef<ReturnType>({} as ReturnType);
const [selectedBuilding, setSelectedBuilding] = useState<BuildingInfo | null>(null);
const [buildingSearchText, setBuildingSearchText] = useState("");
const [buildingList, setBuildingList] = useState<BuildingInfo[]>([]);
// 전체 목록
const [partValue, setPartValue] = useState<string>("A");
const [sidoValue, setSidoValue] = useState<string>("0");
@@ -97,15 +117,6 @@ export function MapSearchBar({
// 우리동네가뭄지도 필터 목록
const [selectedDong, setSelectedDong] = useState<SelectedDataTypeWithSggNm>({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 // 행정동
const [allSido, setAllSido] = useState<SelectDataType[]>([
{ text: "전체", val: '0' },
...(code?.cityCd || [])
]);
const [allRegion, setAllRegion] = useState<SelectDataType[]>([
{ text: "전체", val: '0' },
...(code?.rvbsnCd || [])
]);
const filteredSigunData = useMemo(() => {
if (!selectedSido || selectedSido.val === '0' || !selectedSido.val) return [{ text: "전체", val: '0' }];
return [
@@ -130,26 +141,82 @@ export function MapSearchBar({
];
}, [selectedSigun]);
// 건물 검색 API 호출 함수
const searchBuildings = async (searchText: string) => {
if (!searchText.trim()) {
setBuildingList([]);
return;
}
try {
const flg = "41"; // 적절한 플래그 값으로 설정
const sigunCode = selectedSigun.val !== '0' ? selectedSigun.val : '';
const apiEndpoint = `${API_URL}/v1/reuseFc/building/${flg}/${sigunCode}/${encodeURIComponent(searchText)}`;
const response = await fetch(apiEndpoint, {
method: "GET",
headers: {
accept: "application/json",
"Content-Type": "application/json"
}
});
if (!response.ok) {
throw new Error(`API 호출 실패: ${response.status}`);
}
const result = await response.json();
if (result.status === 200 && Array.isArray(result.data)) {
// 건물 검색 결과를 demandCompList 형태로 변환
const convertedList = result.data.map((building: BuildingInfo) => ({
dmdInfoPk: String(building.bdstInfoPk),
dmdCoNm: building.bdstNm,
daddr: building.daddr,
useUsgCd: "",
splyCpct: 0,
splyFmYn: "Y",
// 추가 필드들...
}));
setBuildingList(result.data);
// 글로벌 상태에도 업데이트 (SearchInputText가 사용할 수 있도록)
// getDemandCompList를 직접 호출하는 대신 상태를 직접 설정
const { setBuildingSearchResults } = useWrStore.getState();
if (setBuildingSearchResults) {
setBuildingSearchResults(convertedList);
}
} else {
setBuildingList([]);
}
} catch (error) {
console.error("건물 검색 중 오류 발생:", error);
setBuildingList([]);
}
};
// 조회 함수
const queryVal = () => {
console.log("조회버튼 클릭");
if (searchType == '공급처 후보지 조회') {
// queryClick({
// partValue: partValue,
// sidoValue: sidoValue,
// sigunValue: sigunValue,
// regionValue: regionValue,
// unitWaterShedValue: untitWaterShedValue,
// middleRegionValue: middleRegionValue,
// envPartValue: envPartValue,
// sidoText: selectedSido.text,
// sigunText: selectedSigun.text,
// });
} else if (searchType == '우리동네가뭄지도보기') {
} else if (searchType == '지도보기') {
console.log("조회버튼 클릭");
queryClick({
partValue: partValue,
sidoValue: sidoValue,
sigunValue: sigunValue,
regionValue: regionValue,
unitWaterShedValue: untitWaterShedValue,
middleRegionValue: middleRegionValue,
envPartValue: envPartValue,
sidoText: selectedSido.text,
sigunText: selectedSigun.text,
});
}
}
const initVal = useCallback(() => {
// 초기화 함수
const initVal = () => {
setPartValue("A");
setSidoValue("0");
setSigunValue("0");
@@ -164,13 +231,12 @@ export function MapSearchBar({
setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' });
setSelectedDong({ text: "전체", val: '0', sggNm: '전체' });
setResetTrigger(true);
initClick(); // 부모로부터 받은 초기화 함수 호출
}, [initClick]);
};
const applyVal = useCallback(() => {
onFeatureSelect
}, [onFeatureSelect]);
// 적용 버튼
const applyVal = () => {
onFeatureSelect();
};
useEffect(() => {
if (resetTrigger) {
@@ -251,10 +317,11 @@ export function MapSearchBar({
<SelectBox
className="flex items-center"
label={'시도'}
selectArray={allSido}
selectArray={code.cityCd}
onSelectValue={(val) => {
const selected = code.cityCd.find(item => (item.val === val));
setSelectedSido(selected || { text: "전체", val: '0' });
setSelectedSigun({ text: "전체", val: '0' });
}}
value={selectedSido.val}
selectClass={'w-32'}
@@ -266,24 +333,39 @@ export function MapSearchBar({
onSelectValue={(val) => {
const selected = filteredSigunData.find(item => (item.val === val));
setSelectedSigun(selected || { text: "전체", val: '0' });
setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' });
}}
value={selectedSigun.val}
selectClass={'w-32'}
/>
<SelectBox
className="flex items-center"
label={'시설명'}
selectArray={filteredFcltData}
onSelectValue={(val) => {
const selected = allFclt.find(item => (item.val === val));
setSelectedFclt(selected || { text: "전체", val: '0', sggNm: '전체' });
}}
value={selectedFclt.val}
selectClass={'w-32'}
/>
<Label className="w-auto text-xl text-gray-600 whitespace-nowrap">건물명</Label>
{/* <Label className="w-auto text-xl text-gray-600 whitespace-nowrap">시설명</Label>
<div className="w-44">
<Input className="mr-4" />
</div> */}
<div className="flex items-center">
<Label className="w-20 text-xl text-gray-600">시설명</Label>
<div className="w-64">
<SearchInputText
label="시설명"
initValue={buildingSearchText}
setValue={(searchText) => {
setBuildingSearchText(searchText || "");
if (searchText) {
searchBuildings(searchText);
}
}}
setRtnData={(buildingData) => {
if (buildingData && buildingData.length > 0 && buildingData[0]) {
// 선택된 건물 정보를 찾아서 설정
const selectedBuildingInfo = buildingList.find(building =>
building.bdstNm === buildingData[0].dmdCoNm
);
setSelectedBuilding(selectedBuildingInfo || null);
console.log("선택된 시설:", selectedBuildingInfo);
}
}}
/>
</div>
</div>
</>
)}
@@ -292,13 +374,14 @@ export function MapSearchBar({
<SelectBox
className="flex items-center"
label={'시도'}
selectArray={allSido}
selectArray={code.cityCd}
onSelectValue={(val) => {
const selected = code.cityCd.find(item => (item.val === val));
setSelectedSido(selected || { text: "전체", val: '0' });
setSelectedSigun({ text: "전체", val: '0' });
}}
value={selectedSido.val}
selectClass={'w-32'}
selectClass={'w-44'}
/>
<SelectBox
className="flex items-center"
@@ -307,6 +390,7 @@ export function MapSearchBar({
onSelectValue={(val) => {
const selected = filteredSigunData.find(item => (item.val === val));
setSelectedSigun(selected || { text: "전체", val: '0' });
setSelectedDong({ text: "전체", val: '0', sggNm: '전체' });
}}
value={selectedSigun.val}
selectClass={'w-32'}
@@ -329,9 +413,9 @@ export function MapSearchBar({
<div className="flex min-w-0"> {/* SearchFormLeft 래퍼 */}
<MapSearchFormLeft
isShowPart={true}
selectSidoData={allSido}
selectSidoData={code.cityCd}
selectSigunData={filteredSigunData}
selectRegionData={allRegion}
selectRegionData={code.rvbsnCd}
selectUnitWaterShedData={selectUnitWaterShedData}
selectMiddleRegionData={selectMiddleRegionData}
selectEnvPartData={selectEnvPartData}
+2 -2
View File
@@ -324,8 +324,8 @@ function MapSearchFormLeft({ isShowPart = false, children, selectSidoData = [],
</>
) : (
<>
<SelectBox className="w-auto whitespace-nowrap" label={'시도'} selectArray={selectSidoData} onSelectValue={(val) => onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={'w-40'} />
<SelectBox className="w-auto whitespace-nowrap" label={'시군구'} selectArray={filteredSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={'w-40'} />
<SelectBox className="w-auto whitespace-nowrap" label={'시도'} selectArray={selectSidoData} onSelectValue={(val) => onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={'w-44'} />
<SelectBox className="w-auto whitespace-nowrap" label={'시군구'} selectArray={filteredSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={'w-44'} />
</>
)}
</div>
+380
View File
@@ -0,0 +1,380 @@
import { useWrStore } from "@/app/store";
import { btnClass } from "@/app/store/common/code";
import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type";
import { excelDownload } from "@/components/lib/func/commom";
import { Title, UiTable, UITab, ContentsDiv } from '@common/business_components';
import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan";
import { Button } from "@common/components/ui";
import React, { useCallback, useState, useEffect } from 'react';
import NewWindowPortal from '@/app/map/newWindowPortal';
import PbnstPrfmncSttnPopup from "@/app/map/pbnstPrfmncSttnPopup";
import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal";
import type { WaterReuseFacility } from "@/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type";
import type { chgBuilding, customRainBdst } from "@/app/store/ruseFcltMng/fcltReg/rainReg";
import RainPrfmncSttnPopup from "@/app/map/rainPrfmncSttnPopup";
import RswtrPrfmncsSttnPopup from "@/app/map/rswtrPrfmncSttnPopup";
import type { customRswtBdst } from "@/app/store/ruseFcltMng/fcltReg/rswtReg";
import HtdflPrfmncSttnPopup from "@/app/map/htdflPrfmncSttnPopup";
import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg";
interface NewWindowPopupProps {
featureData: any;
checkedLayers?: string[];
checkedLayersData?: any[];
onClose: () => void;
}
// 탭
const TABS = [
{ val: "0", text: "재이용시설1" },
{ val: "1", text: "재이용시설2" },
{ val: "2", text: "재이용시설n" },
];
// 데이터의 속성(key)을 사용자에게 보여줄 한글 이름으로 변환하기 위한 맵
// 실제 데이터에 맞게 이 부분을 확장해야 합니다.
const propertyNameMap: { [key: string]: string } = {
fid: '고유 ID',
fcltNm: '시설명',
opertDe: '운영일자',
addr: '주소',
// ... 필요한 만큼 속성 매핑 추가
};
const NewWindowPopup: React.FC<NewWindowPopupProps> = ({
featureData,
checkedLayers = [],
checkedLayersData = [],
onClose
}) => {
const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "0");
const [showDetailView, setShowDetailView] = useState(false); // 상세정보 화면 표시 상태 추가
// onClose 함수를 useCallback으로 메모이제이션
const memoizedOnClose = useCallback(() => {
onClose();
}, [onClose]);
// 하수재이용시설 변수
const {
code,
pbnstSttn,
pbnstSttnSearch,
pbnSttnInit,
getPbnStnDetail,
pbnNmSearch,
} = useWrStore((state) => state);
const [bdstTable, setBdstTable] = useState<WaterReuseFacility[]>();
const [pageNation, setPageNation] = useState(1);
const [pbnDetaileData, setPbnDetaileData] = useState<{
isModal: boolean;
data: customPbnst;
}>({ isModal: false, data: {} as customPbnst });
// 빗물재이용시설 변수
const { rainSttn, rainSttnSearch, rainSttnInit } = useWrStore(
(state) => state
);
const { rainSttnRes } = rainSttn;
const [rainTable, setRainTable] = useState<customRainBdst[]>();
const [rainBdstChgProps, setRainBdstChgProps] = useState<{
isOpen: boolean;
chBuilding: chgBuilding[];
}>({ isOpen: false, chBuilding: [] });
const [rainDetailModal, setRainDetailModal] = useState<{ isOpen: boolean, data: customRainBdst | undefined }>({ data: undefined, isOpen: false });
// 중수도시설 변수
const { rswtSttn, rswtSttnSearch, rswtSttnInit } = useWrStore(
(state) => state
);
const { rswtSttnRes } = rswtSttn;
const [rswtrTable, setRswtrTable] = useState<customRswtBdst[]>();
const [rswtrDetailModal, setRswtrDetailModal] = useState<{ isOpen: boolean, data: customRswtBdst | undefined }>({ data: undefined, isOpen: false });
const [rswtrBdstChgProps, setRswtrBdstChgProps] = useState<{
isOpen: boolean;
chBuilding: chgBuilding[];
}>({ isOpen: false, chBuilding: [] });
// 온배수시설 변수
const { htdflSttn, htdflSttnSearch, initHtdfl } = useWrStore((state) => state);
const { htdflSttnRes } = htdflSttn;
const [htdflTable, setHtdflTable] = useState<customHtdflBdst[]>();
const [htdflDetailModal, setHtdflDetailModal] = useState<{ isOpen: boolean, data: customHtdflBdst | undefined }>({ data: undefined, isOpen: false });
const [bdstChgProps, setBdstChgProps] = useState<{ isOpen: boolean, chBuilding: chgBuilding[] }>({ isOpen: false, chBuilding: [] });
if (!featureData) {
return null;
}
// 2. RuseMngTable에 필요한 tableData를 생성합니다. (단일 행 데이터)
const tableData = [featureData];
console.log("tableData", tableData);
// 물재이용시설 cellClick 이벤트 (상세정보)
const pbnstCellClick = (index: number | undefined) => {
if (
index !== undefined &&
pbnstSttn.pbnstSttnRes &&
pbnstSttn.pbnstSttnRes[index]
) {
const pbnstRuseFcltPk = pbnstSttn.pbnstSttnRes[index].pbnstRuseFcltPk;
getPbnStnDetail(pbnstRuseFcltPk);
}
};
useEffect(() => {
setBdstTable(pbnstSttn.pbnstSttnRes);
}, [pbnstSttn.pbnstSttnRes]);
useEffect(() => {
if (pbnstSttn.pbnstDetail.pbnstRuseFcltPk) setShowDetailView(true);
}, [pbnstSttn.pbnstDetail]);
// 빗물재이용시설 cellClick 이벤트 (상세정보)
const rainCellClick = (index: number | undefined, headId: string) => {
if (
index !== undefined &&
rainTable !== undefined &&
headId !== 'chBuildingYn') {
setRainDetailModal({ isOpen: true, data: rainTable[index] })
} else if (headId === 'chBuildingYn' && index !== undefined && rainTable !== undefined) {
if (rainTable[index] && rainTable[index]?.chgBuilding[0]?.bdstChgNm !== null)
setRainBdstChgProps({ isOpen: true, chBuilding: rainTable[index].chgBuilding });
}
}
// 중수도시설 cellClick 이벤트 (상세정보)
const rswtCellClick = (index: number | undefined, headId: string) => {
if (headId !== 'chBuildingYn' && index !== undefined && rswtrTable !== undefined) {
setRswtrDetailModal({ isOpen: true, data: rswtrTable[index] })
} else if (headId === 'chBuildingYn' && index !== undefined && rswtrTable !== undefined) {
if (rswtrTable[index] && rswtrTable[index]?.chBuildingYn === 'Y') {
setRswtrBdstChgProps({ isOpen: true, chBuilding: rswtrTable[index].chgBuilding });
}
}
}
// 온배수시설 cellClick 이벤트 (상세정보)
const htdflCellClick = (index: number | undefined, headId: string) => {
if (
headId !== "chBuildingYn" &&
index !== undefined &&
htdflTable !== undefined
) {
setHtdflDetailModal({ isOpen: true, data: htdflTable[index] });
}
};
const excelClick = useCallback(() => {
// if (codeData) pbnstSttnSearch(1, "10000", codeData);
excelDownload(
[[]],
"하수재이용시설 현황",
"sample_pbnstSttn.xlsx",
"A1:U2"
);
}, [pbnstSttnSearch]);
// 체크된 레이어에 따른 조건부 컴포넌트 렌더링 함수
const renderConditionalContent = () => {
// 특정 레이어가 체크되었는지 확인
const hasWaterTreatmentLayer = checkedLayersData.some(layer =>
layer.data?.lyrNm?.includes('하수처리') || layer.data?.lyrNm?.includes('재이용시설')
);
// 재이용시설
const isPbnst = false;
// 빗물이용시설
const isRain = false;
// 중수도시설
const isRswt = false;
// 온배수시설
const isHtdfl = true;
return (
<div className="mt-4">
{/* 체크된 레이어가 없는 경우 (재이용시설 목록) */}
{isPbnst && (
<>
{showDetailView ? (
// 재이용시설 상세
<>
<div className="flex justify-between items-center mb-4">
<Title title='하수재이용시설 상세정보' />
</div>
<div className="flex justify-between items-center mb-8">
<UITab
defaultValue={TABS[0]?.val}
tabList={TABS.map((tab) => ({
value: tab.val,
label: tab.text,
onClick: () => setActiveTab(tab.val),
}))}
children={undefined}
/>
</div>
<PbnstSttnModal
data={pbnDetaileData.data || ({} as customPbnst)}
titleVisible={false}
/>
<PbnstPrfmncSttnPopup isVisible={false} />
</>
) : (
// 재이용시설 목록
<>
<div className="flex justify-between items-center">
<Title title='하수재이용시설 현황' />
<div className="flex gap-2 mt-8">
<Button className={btnClass} color="primary" onClick={excelClick}>엑셀다운로드</Button>
</div>
</div>
<UiTable
headName={[]}
tableData={bdstTable || []}
children={
<HeadMakeColSpan upHeadList={pbnstSttn.pbnstSttNodeList} />
}
headlist={pbnstSttn.pbnstSttHeadList}
pageSize={20}
total={100000}
cellClick={(index: number | undefined, headId: string) => {
pbnstCellClick(index);
}}
onPageChange={setPageNation}
/>
</>
)}
</>
)}
{/* 빗물이용시설 */}
{isRain && (
<>
{showDetailView ? (
// 빗물이용시설 상세
<>
<div className="flex justify-between items-center mb-4">
<Title title='빗물이용시설 상세정보' />
</div>
<RainPrfmncSttnPopup rainForm={rainDetailModal.data as customRainBdst} isModal={false} />
{/* TODO: 지도 점검 결과 현황 추가 할 것 */}
</>
) : (
// 빗물이용시설 목록
<>
<div className="flex justify-between items-center">
<Title title='빗물이용시설 현황' />
<div className="flex gap-2 mt-8">
<Button className={btnClass} color="primary" onClick={excelClick}>엑셀다운로드</Button>
</div>
</div>
<UiTable
headName={[]}
tableData={rainTable || []}
children={
<HeadMakeColSpan upHeadList={rainSttn.rainSttNodeList} />
}
headlist={rainSttn.rainSttHeadList}
pageSize={20}
total={100000}
cellClick={(index: number | undefined, headId: string) => {
rainCellClick(index ?? undefined, headId)
}}
onPageChange={setPageNation}
/>
</>
)}
</>
)}
{/* 중수도시설 */}
{isRswt && (
<>
{showDetailView ? (
// 재이용시설 상세
<>
<div className="flex justify-between items-center mb-4">
<Title title='중수도시설 상세정보' />
</div>
<RswtrPrfmncsSttnPopup rswtForm={rswtrDetailModal.data as customRswtBdst} isModal={false} />
{/* TODO: 지도 점검 결과 현황 추가 할 것 */}
</>
) : (
// 재이용시설 목록
<>
<div className="flex justify-between items-center">
<Title title='중수도시설 현황' />
<div className="flex gap-2 mt-8">
<Button className={btnClass} color="primary" onClick={excelClick}>엑셀다운로드</Button>
</div>
</div>
<UiTable
headName={[]}
tableData={rswtrTable || []}
children={
<HeadMakeColSpan upHeadList={rswtSttn.rswtSttNodeList} />
}
headlist={rswtSttn.rswtSttHeadList}
pageSize={20}
total={100000}
cellClick={(index: number | undefined, headId: string) => {
rswtCellClick(index, headId);
}}
onPageChange={setPageNation}
/>
</>
)}
</>
)}
{/* 온배수시설 */}
{isHtdfl && (
<>
{showDetailView ? (
// 재이용시설 상세
<>
<div className="flex justify-between items-center mb-4">
<Title title='온배수시설 상세정보' />
</div>
{/* <HtdflPrfmncSttnPopup htdflForm={htdflDetailModal.data as customHtdflBdst} isModal={false} /> */}
<HtdflPrfmncSttnPopup htdflForm={htdflDetailModal.data as customHtdflBdst} isModal={false} />
</>
) : (
// 재이용시설 목록
<>
<div className="flex justify-between items-center">
<Title title='온배수시설 현황' />
<div className="flex gap-2 mt-8">
<Button className={btnClass} color="primary" onClick={excelClick}>엑셀다운로드</Button>
</div>
</div>
<UiTable
headName={[]}
tableData={bdstTable || []}
children={
<HeadMakeColSpan upHeadList={htdflSttn.htdflSttNodeList} />
}
headlist={htdflSttn.htdflSttHeadList}
pageSize={20}
total={100000}
cellClick={(index: number | undefined, headId: string) => {
htdflCellClick(index ?? undefined, headId);
}}
onPageChange={setPageNation}
/>
</>
)}
</>
)}
</div>
);
};
return (
<NewWindowPortal onClose={memoizedOnClose} title="시설 상세 정보">
{/* 체크된 레이어에 따른 조건부 렌더링 */}
{renderConditionalContent()}
</NewWindowPortal>
);
};
export default NewWindowPopup;
+90
View File
@@ -0,0 +1,90 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수
const copyStyles = (sourceDoc: Document, targetDoc: Document) => {
Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
try {
if (styleSheet.cssRules) { // <style> 태그로 주입된 CSS
const newStyleEl = sourceDoc.createElement('style');
Array.from(styleSheet.cssRules).forEach(cssRule => {
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
});
targetDoc.head.appendChild(newStyleEl);
} else if (styleSheet.href) { // <link> 태그로 연결된 CSS 파일
const newLinkEl = sourceDoc.createElement('link');
newLinkEl.rel = 'stylesheet';
newLinkEl.href = styleSheet.href;
targetDoc.head.appendChild(newLinkEl);
}
} catch (e) {
console.warn('스타일시트를 복사할 수 없습니다:', e);
}
});
};
interface NewWindowPortalProps {
children: React.ReactNode;
onClose: () => void;
title?: string;
}
const NewWindowPortal: React.FC<NewWindowPortalProps> = ({ children, onClose, title = "Detail" }) => {
const [container, setContainer] = useState<HTMLElement | null>(null);
const newWindowRef = useRef<Window | null>(null);
const onCloseRef = useRef(onClose);
// onClose 함수를 ref로 저장하여 의존성 문제 해결
useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
const handleUnload = useCallback(() => {
onCloseRef.current();
}, []);
useEffect(() => {
// 이미 창이 열려있으면 새로 열지 않음
if (newWindowRef.current && !newWindowRef.current.closed) return;
// 1. 새 창을 열고 참조를 저장합니다.
newWindowRef.current = window.open('', title, 'width=1400,height=1024,resizable=yes,scrollbars=yes');
if (newWindowRef.current) {
newWindowRef.current.document.title = title;
// 2. React 컴포넌트를 마운트할 컨테이너 div를 생성하여 새 창의 body에 추가합니다.
const div = newWindowRef.current.document.createElement('div');
newWindowRef.current.document.body.appendChild(div);
setContainer(div);
// 3. 메인 창의 스타일을 새 창으로 복사합니다. (매우 중요)
copyStyles(document, newWindowRef.current.document);
// 4. 사용자가 창을 닫을 때 부모에게 알리기 위한 이벤트 리스너를 설정합니다.
// const handleUnload = () => {
// onClose();
// };
newWindowRef.current.addEventListener('unload', handleUnload);
// 5. 컴포넌트가 사라질 때 창을 닫는 클린업 함수를 반환합니다.
return () => {
if (newWindowRef.current) {
newWindowRef.current.removeEventListener('unload', handleUnload);
if (!newWindowRef.current.closed) {
newWindowRef.current.close();
}
}
};
} else {
alert('팝업이 차단되었습니다. 팝업 차단을 해제하고 다시 시도해주세요.');
onCloseRef.current();
// onClose();
}
}, [onClose, title]);
// `createPortal`을 사용해 children을 새 창의 container div에 렌더링합니다.
return container ? createPortal(children, container) : null;
};
export default NewWindowPortal;
+596
View File
@@ -0,0 +1,596 @@
"use client";
import { useWrStore } from "@/app/store";
import { initValInUsage, initValOutUsage, subTotalObj, totalObj, type FcPrfType, type PrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type";
import { CHART_FORM, ContentsDiv, MainChart, ScrollArea, SelectBox, UiTable, type ChartConfig, type ReturnType } from "@common/business_components";
import ListDiv from "@common/business_components/ui/list_div";
import { useEffect, useRef, useState } from "react";
import { Toaster } from "react-hot-toast";
interface PbnstPrfmncSttnPopupProps {
isVisible?: boolean;
}
export default function PbnstPrfmncSttnPopup({ isVisible = true }: PbnstPrfmncSttnPopupProps) {
const { code, tableHeadMaker, pbnstPrfmncSttn, pbnstPrfmncInit, pbnstPrfmncSearch, prfmnc } = useWrStore((state) => state);
const { monthHeadList } = prfmnc
const { pbnstPrfHeadList, pbnstPrfmncRes, prfmncRes, rawData, selectYearList } = pbnstPrfmncSttn;
const [isChHead, setIsChHead] = useState(false);
const [fromDate, setFromDate] = useState<string>(String(new Date().getFullYear()));
const [toDate, setToDate] = useState<string>(String(new Date().getFullYear()));
const [year, setYear] = useState<string>(String(new Date().getFullYear()));
const [bdstTable, setbdstTable] = useState<FcPrfType[]>();
const ref = useRef<ReturnType>({} as ReturnType);
const [swtrmtFcltNm, setSwtrmtFcltNm] = useState<string>();
const [chartConfig, setChartConfig] = useState<ChartConfig>({});
const [chartData, setChartData] = useState<dnChartForm[]>([]);
const [dnInUsage, setDnInUsage] = useState<dnChartForm[]>([]);
const [dnInChartConfig, setDnInChartConfig] = useState<ChartConfig>({});
const [dnOutUsage, setDnOutUsage] = useState<dnChartForm[]>([]);
const [dnOutChartConfig, setDnOutChartConfig] = useState<ChartConfig>({});
const [barData, setBarData] = useState<barChartForm[]>([]);
const [barChartConfig, setBarChartConfig] = useState<ChartConfig>({});
const [prfData, setPrfData] = useState<any[]>();
const [lnInChartConfig, setLnInChartConfig] = useState<ChartConfig>({});
const [lnOutChartConfig, setLnOutChartConfig] = useState<ChartConfig>({});
const [inLineData, setInLineData] = useState<{
chartData1: lineChartDataProps[];
chartData2: lineChartDataProps[];
chartData3: lineChartDataProps[];
chartData4: lineChartDataProps[];
chartData5: lineChartDataProps[];
chartData6: lineChartDataProps[];
chartData7: lineChartDataProps[];
chartData8: lineChartDataProps[];
chartData9: lineChartDataProps[];
}>();
// const [lnInChartConfig, setLnInChartConfig] = useState<ChartConfig>({});
const [outLineData, setOutLineData] = useState<{
chartData1: lineChartDataProps[];
chartData2: lineChartDataProps[];
chartData3: lineChartDataProps[];
chartData4: lineChartDataProps[];
chartData5: lineChartDataProps[];
chartData6: lineChartDataProps[];
}>();
type dnChartForm = { label: string, val: number, fill?: string };
type barChartForm = { label: string, "장내용수": number, "장외용수": number };
type lineChartDataProps = { label: string, val: number, color: string };
const prfTableData = (prfList: PrfType[], year: string) => {
const inUsageData = initValInUsage;
const outUsageData = initValOutUsage;
const inSubtotal = subTotalObj;
const outSubtotal = subTotalObj;
console.log(' prfList ', prfList)
const prfFilter = prfList?.filter((e) => e.ym.slice(0, 4) === year)
if (prfList && prfFilter.length > 0) {
prfList?.forEach((prfItem) => {
const month = Number(prfItem?.ym?.slice(4)); // "MM"
const monthKey = `${month}월`;
inUsageData.forEach((inData: any) => {
const usageKey = inData.usage;
const value = (prfItem as any)[usageKey] ?? 0;
inData[monthKey] = value;
inData.total += value;
if (inSubtotal && usageKey !== "subtotal") {
(inSubtotal as any)[monthKey] =
((inSubtotal as any)[monthKey] ?? 0) + value;
inSubtotal.total += value;
}
});
outUsageData.forEach((outData: any) => {
const usageKey = outData.usage;
const value = (prfItem as any)[usageKey] ?? 0;
outData[monthKey] = value;
outData.total += value;
if (usageKey !== "subtotal") {
(outSubtotal as any)[monthKey] =
((outSubtotal as any)[monthKey] ?? 0) + value;
outSubtotal.total += value;
}
});
});
inUsageData.push(inSubtotal);
outUsageData.push(outSubtotal);
const usageTotalObj: { [key: string]: any } = totalObj;
for (const key in usageTotalObj) {
if (["usage", "gubun", "edit"].includes(key)) continue;
const val =
(Number((inSubtotal as any)[key]) || 0) +
(Number((outSubtotal as any)[key]) || 0);
(usageTotalObj as any)[key] = val;
}
const tableData = [...inUsageData, ...outUsageData, usageTotalObj];
setPrfData(tableData);
} else {
setPrfData([]);
}
}
useEffect(() => {
let wshwtr = 0;
let coolUswtr = 0;
let cleanUswtr = 0;
let drwtUswtr = 0;
let dltnUswtr = 0;
let ithlRswtr = 0;
let ithlEtcUswtr = 0;
let cleanToilUswtr = 0;
let resn = 0;
let lndwtr = 0;
let hydUswtr = 0;
let ugwtChr = 0;
let farmUswtr = 0;
let rvrKeepUswtr = 0;
let indstUswtr = 0;
if (rawData) {
rawData.forEach((item) => {
prfTableData(item.prfList, year);
item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => {
wshwtr += element.wshwtr; //세척수
coolUswtr += element.coolUswtr; //냉각수
cleanUswtr += element.cleanUswtr; //청소수
drwtUswtr += element.drwtUswtr; //식수대
dltnUswtr += element.dltnUswtr //희석용수
ithlRswtr += element.ithlRswtr //중수도
ithlEtcUswtr += element.ithlEtcUswtr //기타
cleanToilUswtr += element.cleanToilUswtr //청소화장실용수
resn += element.resn //세척살수용수
lndwtr += element.lndwtr; //조경용수
hydUswtr += element.hydUswtr; //친수용수
ugwtChr += element.ugwtChr; //지하수충전
farmUswtr += element.farmUswtr //농업용수
rvrKeepUswtr += element.rvrKeepUswtr //하천등유지용수
indstUswtr += element.indstUswtr //공업용수
});
})
const data: dnChartForm[] = []
data?.push({ label: "세척수", val: wshwtr, fill: "red" })
data?.push({ label: "냉각수", val: coolUswtr, fill: "orange" })
data?.push({ label: "청소수", val: cleanUswtr, fill: "yellow" })
data?.push({ label: "식수대", val: drwtUswtr, fill: "green" })
data?.push({ label: "희석용수", val: dltnUswtr, fill: "skyblue" })
data?.push({ label: "중수도", val: ithlRswtr, fill: "blue" })
data?.push({ label: "기타", val: ithlEtcUswtr, fill: "purple" })
data?.push({ label: "청소화장실용수", val: cleanToilUswtr, fill: "red" })
data?.push({ label: "세척살수용수", val: resn, fill: "orange" })
data?.push({ label: "조경용수", val: lndwtr, fill: "yellow" })
data?.push({ label: "친수용수", val: hydUswtr, fill: "green" })
data?.push({ label: "지하수충전", val: ugwtChr, fill: "skyblue" })
data?.push({ label: "농업용수", val: farmUswtr, fill: "blue" })
data?.push({ label: "하천등유지용수", val: rvrKeepUswtr, fill: "purple" })
data?.push({ label: "공업용수", val: indstUswtr, fill: "black" })
setChartData(data);
}
}, [rawData, year])
useEffect(() => {
const newChartConfig = chartData.reduce((config, element) => {
config[element.label] = {
label: element.label,
fill: element.fill
};
return config;
}, {} as Record<string, { label: string; fill?: string }>) satisfies ChartConfig;
setChartConfig(newChartConfig);
}, [chartData]);
// 장내용수 도넛차트
useEffect(() => {
let wshwtr = 0;
let coolUswtr = 0;
let cleanUswtr = 0;
let drwtUswtr = 0;
let dltnUswtr = 0;
let ithlRswtr = 0;
let ithlEtcUswtr = 0;
let cleanToilUswtr = 0;
let resn = 0;
if (rawData) {
rawData.forEach((item) => {
item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => {
wshwtr += element.wshwtr; //세척수
coolUswtr += element.coolUswtr; //냉각수
cleanUswtr += element.cleanUswtr; //청소수
drwtUswtr += element.drwtUswtr; //식수대
dltnUswtr += element.dltnUswtr //희석용수
ithlRswtr += element.ithlRswtr //중수도
ithlEtcUswtr += element.ithlEtcUswtr //기타
cleanToilUswtr += element.cleanToilUswtr //청소화장실용수
resn += element.resn //세척살수용수
});
})
const data: dnChartForm[] = []
data?.push({ label: "세척수", val: wshwtr, fill: "yellow" })
data?.push({ label: "냉각수", val: coolUswtr, fill: "green" })
data?.push({ label: "청소수", val: cleanUswtr, fill: "yellow" })
data?.push({ label: "식수대", val: drwtUswtr, fill: "green" })
data?.push({ label: "희석용수", val: dltnUswtr, fill: "yellow" })
data?.push({ label: "중수도", val: ithlRswtr, fill: "green" })
data?.push({ label: "기타", val: ithlEtcUswtr, fill: "yellow" })
data?.push({ label: "청소화장실용수", val: cleanToilUswtr, fill: "green" })
data?.push({ label: "세척살수용수", val: resn, fill: "yellow" })
setDnInUsage(data);
}
}, [rawData, year])
useEffect(() => {
const newChartConfig = dnInUsage.reduce((config, element) => {
config[element.label] = {
label: element.label,
fill: element.fill
};
return config;
}, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig;
setDnInChartConfig(newChartConfig);
}, [dnInUsage]);
// 장외용수 도넛차트
useEffect(() => {
let lndwtr = 0;
let hydUswtr = 0;
let ugwtChr = 0;
let farmUswtr = 0;
let rvrKeepUswtr = 0;
let indstUswtr = 0;
if (rawData) {
rawData.forEach((item) => {
item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => {
lndwtr += element.lndwtr; //조경용수
hydUswtr += element.hydUswtr; //친수용수
ugwtChr += element.ugwtChr; //지하수충전
farmUswtr += element.farmUswtr //농업용수
rvrKeepUswtr += element.rvrKeepUswtr //하천등유지용수
indstUswtr += element.indstUswtr //공업용수
});
})
const data: dnChartForm[] = []
data?.push({ label: "조경용수", val: lndwtr, fill: "yellow" })
data?.push({ label: "친수용수", val: hydUswtr, fill: "black" })
data?.push({ label: "지하수충전", val: ugwtChr, fill: "yellow" })
data?.push({ label: "농업용수", val: farmUswtr, fill: "black" })
data?.push({ label: "하천등유지용수", val: rvrKeepUswtr, fill: "yellow" })
data?.push({ label: "공업용수", val: indstUswtr, fill: "black" })
setDnOutUsage(data);
}
}, [rawData, year])
useEffect(() => {
const newChartConfig = dnOutUsage.reduce((config, element) => {
config[element.label] = {
label: element.label,
fill: element.fill
};
return config;
}, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig;
setDnOutChartConfig(newChartConfig);
}, [dnOutUsage]);
//월별 장내외 용수 총 실적 막대차트
useEffect(() => {
let wshwtr = 0;
let coolUswtr = 0;
let cleanUswtr = 0;
let drwtUswtr = 0;
let dltnUswtr = 0;
let ithlRswtr = 0;
let ithlEtcUswtr = 0;
let cleanToilUswtr = 0;
let resn = 0;
let lndwtr = 0;
let hydUswtr = 0;
let ugwtChr = 0;
let farmUswtr = 0;
let rvrKeepUswtr = 0;
let indstUswtr = 0;
let inUsageSum = 0;
let outUsageSum = 0;
let inUsageArr = [];
let outUsageArr = [];
if (rawData) {
const data: barChartForm[] = []
rawData.forEach((item) => {
item.prfList
.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => {
wshwtr = element.wshwtr; //세척수
coolUswtr = element.coolUswtr; //냉각수
cleanUswtr = element.cleanUswtr; //청소수
drwtUswtr = element.drwtUswtr; //식수대
dltnUswtr = element.dltnUswtr //희석용수
ithlRswtr = element.ithlRswtr //중수도
ithlEtcUswtr = element.ithlEtcUswtr //기타
cleanToilUswtr = element.cleanToilUswtr //청소화장실용수
resn = element.resn //세척살수용수
lndwtr = element.lndwtr; //조경용수
hydUswtr = element.hydUswtr; //친수용수
ugwtChr = element.ugwtChr; //지하수충전
farmUswtr = element.farmUswtr //농업용수
rvrKeepUswtr = element.rvrKeepUswtr //하천등유지용수
indstUswtr = element.indstUswtr //공업용수
inUsageSum = wshwtr + coolUswtr + cleanUswtr + drwtUswtr + dltnUswtr + ithlRswtr + ithlEtcUswtr + cleanToilUswtr + resn;
inUsageArr.push(inUsageSum)
outUsageSum = lndwtr + hydUswtr + ugwtChr + farmUswtr + rvrKeepUswtr + indstUswtr;
outUsageArr.push(outUsageSum)
const month = Number(element.ym.slice(4, 6))
data.push({ label: `${month}월`, "장내용수": inUsageSum, "장외용수": outUsageSum })
},
);
})
setBarData(data);
}
}, [rawData, year])
useEffect(() => {
const newChartConfig = barData.reduce((config, element) => {
config[element.label] = {
label: element.label,
};
return config;
}, {} as Record<string, { label: string }>) satisfies ChartConfig;
setBarChartConfig(newChartConfig);
}, [barData]);
//월별 장내 용수 실적 라인차트
useEffect(() => {
if (rawData) {
const wshwtrData: lineChartDataProps[] = []
const coolUswtrData: lineChartDataProps[] = []
const cleanUswtrData: lineChartDataProps[] = []
const drwtUswtrData: lineChartDataProps[] = []
const dltnUswtrData: lineChartDataProps[] = []
const ithlRswtrData: lineChartDataProps[] = []
const ithlEtcUswtrData: lineChartDataProps[] = []
const cleanToilUswtrData: lineChartDataProps[] = []
const resnData: lineChartDataProps[] = []
rawData.forEach((item) => {
item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => {
const month = Number(element.ym.slice(4, 6))
wshwtrData.push({ label: `${month}월`, val: element.wshwtr, color: "red" })
coolUswtrData.push({ label: `${month}월`, val: element.coolUswtr, color: "red" })
cleanUswtrData.push({ label: `${month}월`, val: element.cleanUswtr, color: "red" })
drwtUswtrData.push({ label: `${month}월`, val: element.drwtUswtr, color: "red" })
dltnUswtrData.push({ label: `${month}월`, val: element.dltnUswtr, color: "red" })
ithlRswtrData.push({ label: `${month}월`, val: element.ithlRswtr, color: "red" })
ithlEtcUswtrData.push({ label: `${month}월`, val: element.ithlEtcUswtr, color: "red" })
cleanToilUswtrData.push({ label: `${month}월`, val: element.cleanToilUswtr, color: "red" })
resnData.push({ label: `${month}월`, val: element.resn, color: "red" })
},
);
})
const rtndata = {
chartData1: wshwtrData,
chartData2: coolUswtrData,
chartData3: cleanUswtrData,
chartData4: drwtUswtrData,
chartData5: dltnUswtrData,
chartData6: ithlRswtrData,
chartData7: ithlEtcUswtrData,
chartData8: cleanToilUswtrData,
chartData9: resnData
}
setInLineData(rtndata)
}
}, [rawData, year])
useEffect(() => {
if (inLineData?.chartData1) {
const newChartConfig = {
chartData1: {
label: '세척수',
color: "red",
},
chartData2: {
label: '냉각수',
color: "orange",
},
chartData3: {
label: '청소수',
color: "yellow",
},
chartData4: {
label: '식수대',
color: "pink",
},
chartData5: {
label: '희석용수',
color: "purple",
},
chartData6: {
label: '중수도',
color: "green",
},
chartData7: {
label: '기타',
color: "blue",
},
chartData8: {
label: '청소화장실용수',
color: "magenta",
},
chartData9: {
label: '세척살수용수',
color: "skyblue",
},
} satisfies ChartConfig;
setLnInChartConfig(newChartConfig);
}
}, [inLineData]);
//월별 장외 용수 실적 라인차트
useEffect(() => {
if (rawData) {
const lndwtrData: lineChartDataProps[] = []
const hydUswtrData: lineChartDataProps[] = []
const ugwtChrData: lineChartDataProps[] = []
const farmUswtrData: lineChartDataProps[] = []
const rvrKeepUswtrData: lineChartDataProps[] = []
const indstUswtrData: lineChartDataProps[] = []
rawData.forEach((item) => {
item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => {
const month = Number(element.ym.slice(4, 6))
lndwtrData.push({ label: `${month}월`, val: element.lndwtr, color: "red" })
hydUswtrData.push({ label: `${month}월`, val: element.hydUswtr, color: "red" })
ugwtChrData.push({ label: `${month}월`, val: element.ugwtChr, color: "red" })
farmUswtrData.push({ label: `${month}월`, val: element.farmUswtr, color: "red" })
rvrKeepUswtrData.push({ label: `${month}월`, val: element.rvrKeepUswtr, color: "red" })
indstUswtrData.push({ label: `${month}월`, val: element.indstUswtr, color: "red" })
},
);
})
setOutLineData({
chartData1: lndwtrData,
chartData2: hydUswtrData,
chartData3: ugwtChrData,
chartData4: farmUswtrData,
chartData5: rvrKeepUswtrData,
chartData6: indstUswtrData,
})
}
}, [rawData, year])
useEffect(() => {
if (outLineData?.chartData1) {
const newChartConfig = {
chartData1: {
label: '조경용수',
color: "red",
},
chartData2: {
label: '친수용수',
color: "orange",
},
chartData3: {
label: '지하수충전',
color: "yellow",
},
chartData4: {
label: '농업용수',
color: "green",
},
chartData5: {
label: '하천등유지용수',
color: "blue",
},
chartData6: {
label: '공업용수',
color: "purple",
},
} satisfies ChartConfig;
setLnOutChartConfig(newChartConfig);
}
}, [outLineData]);
useEffect(() => {
if (!pbnstPrfmncSttn.isInit) {
pbnstPrfmncInit("1", "11");
}
}, [pbnstPrfmncSttn.isInit, pbnstPrfmncInit]);
useEffect(() => {
setbdstTable(prfmncRes);
}, [prfmncRes]);
return (
<>
<ContentsDiv>
<ListDiv>
<hr />
<h3 className="mt-4 ml-2 text-2xl font-medium text-label">하수재이용실적 상세현황</h3>
<SelectBox
className="w-1/12 text-2xl mt-4 ml-2"
label="연도"
selectClass={""}
selectArray={selectYearList}
value={year}
onSelectValue={(val: string | number) => setYear(String(val))} />
<h1 className="mt-4 ml-2">장내·외 용도별 하수재이용실적 현황(계)</h1>
<div className="flex">
<div className="w-1/3">
<MainChart chartConfig={chartConfig} chartData={chartData} chartTitle={"장내·외 용수 총 실적"} chartForm={CHART_FORM.DN} />
</div>
<div className="w-1/3">
<MainChart chartConfig={dnInChartConfig} chartData={dnInUsage} chartTitle={"장내용수 실적"} chartForm={CHART_FORM.DN} />
</div>
<div className="w-1/3">
<MainChart chartConfig={dnOutChartConfig} chartData={dnOutUsage} chartTitle={"장외용수 실적"} chartForm={CHART_FORM.DN} />
</div>
</div>
<h1 className="">장내·외 월별 하수재이용실적 현황</h1>
<div className="flex">
<div className="w-1/3">
<MainChart chartConfig={barChartConfig} chartData={barData} chartTitle={"월별 장내·외 용수 총 실적"} chartForm={CHART_FORM.DOUBLEBAR} />
</div>
<div className="w-1/3">
<MainChart chartConfig={lnInChartConfig} chartData={inLineData} chartTitle={"월별 장내 용수 실적"} chartForm={CHART_FORM.LINE} />
</div>
<div className="w-1/3">
<MainChart chartConfig={lnOutChartConfig} chartData={outLineData} chartTitle={"월별 장외 용수 총 실적"} chartForm={CHART_FORM.LINE} />
</div>
</div>
{/* 하수재이용실적 현황 - 조건부 렌더링 */}
{isVisible && (
<>
<h1 className="">하수재이용실적 현황</h1>
<ListDiv className="col-span-8 pr-6 ">
<ScrollArea className="h-[60vh]">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">
{"하수처리수재이용 실적(m3) 등록결과"}
</h3>
</div>
<UiTable
headName={undefined}
tableData={prfData || []}
headlist={monthHeadList}
pageSize={0}
total={0}
cellClick={undefined}
onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}>
</UiTable>
</ScrollArea>
</ListDiv>
</>
)}
</ListDiv>
</ContentsDiv>
<Toaster position="top-center" />
</>
)
}
+421
View File
@@ -0,0 +1,421 @@
"use client";
import { useWrStore } from "@/app/store";
import {
rainZod,
selectClass,
TEST_UUID,
type customRainBdst,
} from "@/app/store/ruseFcltMng/fcltReg/rainReg";
import { DatePicker, FormUi } from "@common/business_components";
import { Form, FormField, SelectBox } from "@common/business_components/ui";
import { Button, Input } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm, useWatch, type UseFormReturn } from "react-hook-form";
import { z } from "zod";
import BdstReg from "@/components/ui/form/fcltReg/bdstReg/bdstReg";
import {
formatCommaNumber,
handleCommaNumberChange,
} from "@/components/lib/func/commom";
import { formH1Class, formH2Class } from "@/components/lib/class/comClass";
import { Label } from "@common/business_components/ui/form/lib/label";
export default function RainPrfmncSttnPopup({
rainForm,
isModal,
}: {
rainForm: customRainBdst;
isModal: boolean;
}) {
const { code, rainBdstReg, rainBdstUpdate, rain } = useWrStore(
(state) => state
);
const { rainFcltCd } = code;
const defaultValues: z.infer<typeof rainZod> = rainZod.parse({});
const form = useForm<z.infer<typeof rainZod>>({
resolver: zodResolver(rainZod),
defaultValues: defaultValues,
});
const [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctYn] = form.watch([
"reuseBuilding.0.grntchBdstCertYn",
"reuseBuilding.0.instlCstSprtYn",
"reuseBuilding.0.rainyObj.filtrTrtmfctYn",
]);
const cleanRainData = (raw: any): z.infer<typeof rainZod> => {
return {
...raw,
frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null),
archChgPrmsnDt: raw?.archChgPrmsnDt
? new Date(raw?.archChgPrmsnDt)
: null,
reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({
...item,
instlCmptnDt: new Date(item?.instlCmptnDt),
operMngCd: item?.operMngCd ?? "", // null → ""
grntchBdstCertYr:
typeof item?.grntchBdstCertYr === "string"
? item?.grntchBdstCertYr
: "", // null → ""
rainObj: {
...item?.rainObj,
},
})),
};
};
useEffect(() => {
if (grntchBdstCertYn === "N") {
form.setValue("reuseBuilding.0.grntchBdstCertYr", "");
form.setValue("reuseBuilding.0.grntchBdstCertNo", "");
form.setValue("reuseBuilding.0.grntchBdstCertGrd", "");
} else if (instlCstSprtYn === "N") {
form.setValue("reuseBuilding.0.instlCstSprtAmt", 0);
} else if (filtrTrtmfctYn === "N") {
form.setValue("reuseBuilding.0.rainyObj.filtrTrtmfctCpct", 0);
form.setValue("reuseBuilding.0.rainyObj.filtrTrtmfctPrs", "");
}
}, [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctYn]);
// form 값이 변경될 때마다 로그 출력
useEffect(() => {
const subscription = form.watch((value) => {
});
return () => subscription.unsubscribe();
}, [form]);
useEffect(() => {
console.log("form errors:", form.formState.errors);
}, [form.formState.errors]);
const onSubmit = (values: z.infer<typeof rainZod>) => {
if (isModal) {
rainBdstReg(values, "41");
} else {
rainBdstUpdate(values, "41");
}
};
useEffect(() => {
if (rainZod) form.reset(cleanRainData(rainForm));
}, [rainForm]);
return (
<div className="!mx-[1.6rem] !p-0r">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<div className="grid grid-cols-12 flex items-center card-header">
<h1 className="text-2xl font-medium text-label col-span-10">
{isModal ? "건축물 운영정보" : "빗물이용시설 정보"}
</h1>
</div>
</div>
{isModal && rainForm != undefined ? <BdstReg form={form} /> : <></>}
{isModal ? (
<h1 className="pl-3 text-xl col-span-10 mt-4">
빗물이용시설 정보{" "}
</h1>
) : (
<></>
)}
<h1 className={formH1Class}>
기본정보
</h1>
<div className="grid grid-cols-4 gap-2 mt-4">
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.instlCmptnDt"
render={({ field }) => (
<FormUi label="설치완료일">
<DatePicker
mode="single"
field={{
value:
field.value && !isNaN(new Date(field.value).getTime())
? new Date(field.value)
: new Date(), // Provide a default Date value
onChange: (date) => field.onChange(date),
}}
selectClass={"w-full"}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.instlCst"
render={({ field }) => (
<FormUi label="설치비">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="">
<h1 className={formH1Class}>
빗물이용시설 정보
</h1>
</div>
<div>
<h2 className={formH2Class}>집수면</h2>
</div>
<div className="grid grid-cols-10 gap-4 mt-4 ">
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.rainyObj.pntfColKnd"
render={({ field }) => (
<FormUi label="종류">
<SelectBox
className={""}
selectClass={selectClass}
label={""}
selectArray={rainFcltCd}
onSelectValue={field.onChange}
value={field.value ?? rainFcltCd[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.rainyObj.pntfColArea"
render={({ field }) => (
<FormUi label="면적(m³)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
/>
</FormUi>
)}
/>
</div>
</div>
<div>
<h2 className={formH2Class}>여과 등 처리시설</h2>
</div>
<div className="grid grid-cols-12 gap-2 mt-4">
<div className="col-span-4">
<FormField
control={form.control}
name="reuseBuilding.0.rainyObj.filtrTrtmfctYn"
render={({ field }) => (
<FormUi label="유무">
<SelectBox
className={""}
selectClass={selectClass}
label={""}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-4">
<FormField
control={form.control}
name="reuseBuilding.0.rainyObj.filtrTrtmfctCpct"
render={({ field }) => (
<FormUi label="용량(m³/일)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={filtrTrtmfctYn !== "Y"}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-4">
<FormField
control={form.control}
name="reuseBuilding.0.rainyObj.filtrTrtmfctPrs"
render={({ field }) => (
<FormUi label="처리공정">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={filtrTrtmfctYn !== "Y"}
/>
</FormUi>
)}
/>
</div>
</div>
<div className=" my-2">
<h1 className={formH1Class}>
인센티브 정보
</h1>
</div>
<div>
<h2 className={formH2Class}>설치비 지원</h2>
</div>
<div className="grid grid-cols-10 gap-2 mt-4">
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.instlCstSprtYn"
render={({ field }) => (
<FormUi label="지원 여부">
<SelectBox
className={""}
selectClass={selectClass}
label={""}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-5">
<FormField
control={form.control}
name="reuseBuilding.0.instlCstSprtAmt"
render={({ field }) => (
<FormUi label="지원금액(원)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={instlCstSprtYn !== "Y"}
/>
</FormUi>
)}
/>
</div>
</div>
<div>
<h2 className={formH2Class}>녹색건축물 인증</h2>
</div>
<div className="grid grid-cols-6 gap-2 mt-4">
<div className="col-span-3">
<FormField
control={form.control}
name="reuseBuilding.0.grntchBdstCertYn"
render={({ field }) => (
<FormUi label="인증여부">
<SelectBox
className={""}
selectClass={selectClass}
label={""}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-3">
<FormField
control={form.control}
name="reuseBuilding.0.grntchBdstCertYr"
render={({ field }) => (
<FormUi label="인증년도">
<Input
className={'w-full !text-xl'}
placeholder="2024"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={grntchBdstCertYn !== "Y"}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-3 ">
<FormField
control={form.control}
name="reuseBuilding.0.grntchBdstCertNo"
render={({ field }) => (
<FormUi label="인증번호">
<Input
className={'w-full !text-xl'}
placeholder="ab1234678"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={grntchBdstCertYn !== "Y"}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-3">
<FormField
control={form.control}
name="reuseBuilding.0.grntchBdstCertGrd"
render={({ field }) => (
<FormUi label="인증등급">
<Input
className={'w-full !text-xl'}
placeholder="A"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={grntchBdstCertYn !== "Y"}
/>
</FormUi>
)}
/>
</div>
</div>
</form>
</Form>
</div>
);
}
+404
View File
@@ -0,0 +1,404 @@
"use client";
import {
DatePicker,
Form,
FormField,
FormUi,
SelectBox,
} from "@common/business_components/ui";
import { Button, Input } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useWrStore } from "@/app/store";
import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg";
import {
rswtZod,
type customRswtBdst,
} from "@/app/store/ruseFcltMng/fcltReg/rswtReg";
import { useEffect } from "react";
import BdstReg from "@/components/ui/form/fcltReg/bdstReg/bdstReg";
import {
formatCommaNumber,
handleCommaNumberChange,
} from "@/components/lib/func/commom";
import { formH1Class, formH2Class } from "@/components/lib/class/comClass";
import { Label } from "@common/business_components/ui/form/lib/label";
export default function RswtrPrfmncsSttnPopup({
rswtForm,
isModal,
}: {
rswtForm: customRswtBdst;
isModal: boolean;
}) {
const { rswt, code, rswtBdstUpdate, rswtBdstReg } = useWrStore(
(state) => state
);
const defaultValues: z.infer<typeof rswtZod> = rswtZod.parse({});
const form = useForm<z.infer<typeof rswtZod>>({
resolver: zodResolver(rswtZod),
defaultValues: defaultValues,
});
const [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctProcs] = form.watch([
"reuseBuilding.0.grntchBdstCertYn",
"reuseBuilding.0.instlCstSprtYn",
"reuseBuilding.0.rswtrObj.filtrTrtmfctProcs",
]);
const onSubmit = (values: z.infer<typeof rswtZod>) => {
if (isModal) {
rswtBdstReg(values, "41");
} else {
rswtBdstUpdate(values, "41");
}
};
const cleanRswtData = (raw: any): z.infer<typeof rswtZod> => {
return {
...raw,
frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null),
archChgPrmsnDt: raw?.archChgPrmsnDt
? new Date(raw?.archChgPrmsnDt)
: null,
reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({
...item,
instlCmptnDt: new Date(item?.instlCmptnDt),
operMngCd: item?.operMngCd ?? "", // null → ""
grntchBdstCertYr:
typeof item?.grntchBdstCertYr === "string"
? item?.grntchBdstCertYr
: "", // null → ""
rswtrObj: {
...item?.rswtrObj,
},
})),
};
};
useEffect(() => {
console.log("form errors:", form.formState.errors);
}, [form.formState.errors]);
useEffect(() => {
console.log("filtrTrtmfctProcs ", filtrTrtmfctProcs);
if (grntchBdstCertYn === "N") {
form.setValue("reuseBuilding.0.grntchBdstCertYr", "");
form.setValue("reuseBuilding.0.grntchBdstCertNo", "");
form.setValue("reuseBuilding.0.grntchBdstCertGrd", "");
} else if (instlCstSprtYn === "N") {
form.setValue("reuseBuilding.0.instlCstSprtAmt", 0);
}
}, [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctProcs]);
useEffect(() => {
if (rswtZod) form.reset(cleanRswtData(rswtForm));
}, [rswtForm]);
return (
<div className="!mx-[1.6rem] !p-0r">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* <div className="flex justify-between items-center mx-3"> */}
<div>
<div className="grid grid-cols-12 flex items-center card-header">
<h1 className="text-2xl font-medium text-label col-span-10">
건축물 운영정보
</h1>
</div>
</div>
{isModal && rswtForm != undefined ? <BdstReg form={form} /> : <></>}
{isModal ? (
<h1 className="pl-3 text-xl col-span-10 mt-4 ">
중수도 시설 정보{" "}
</h1>
) : (
<></>
)}
<h1 className={formH1Class}>기본정보</h1>
<div className="grid grid-cols-4 gap-2 mt-4">
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.instlCmptnDt"
render={({ field }) => (
<FormUi label="설치완료일">
<DatePicker
mode="single"
field={{
value:
field.value && !isNaN(new Date(field.value).getTime())
? new Date(field.value)
: new Date(), // Provide a default Date value
onChange: (date) => field.onChange(date),
}}
selectClass={"w-full"}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.instlCst"
render={({ field }) => (
<FormUi label="설치비">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className="grid grid-cols-4 gap-2 mt-4">
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.rswtrObj.wtrUsqty"
render={({ field }) => (
<FormUi label="물사용량(m³)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-2">
<FormField
control={form.control}
name="reuseBuilding.0.rswtrObj.oblgTrgtWtrUsqty"
render={({ field }) => (
<FormUi label="의무대상 물사용량(m³)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className=" my-4">
<h1 className={formH1Class}>중수도 시설 정보</h1>
</div>
<div className="grid grid-cols-12 gap-1 mt-4">
<div className="col-span-4">
<FormField
control={form.control}
name="reuseBuilding.0.rswtrObj.filtrTrtmfctCpct"
render={({ field }) => (
<FormUi label="처리시설 용량">
<Input
className={'w-full !text-xl'}
placeholder="0"
type="number"
value={field.value ?? ""}
onChange={(e)=>{field.onChange(Number(e.target.value))} }
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-4">
<FormField
control={form.control}
name="reuseBuilding.0.rswtrObj.orgwtKndCd"
render={({ field }) => (
<FormUi label="원수 종류">
<SelectBox
className={""}
label={""}
selectClass={selectClass}
selectArray={code.orgwtKndCd}
onSelectValue={field.onChange}
value={field.value ?? code.orgwtKndCd[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-4">
<FormField
control={form.control}
name="reuseBuilding.0.rswtrObj.filtrTrtmfctProcs"
render={({ field }) => (
<FormUi label="주요처리공정">
<Input
className={'w-full !text-xl'}
placeholder="주요처리공정"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
<div className=" my-2">
<h1 className={formH1Class}>인센티브 정보</h1>
</div>
<div>
<h2 className={formH2Class}>설치비 지원</h2>
</div>
<div className="grid grid-cols-2 gap-2 mt-4">
<div className="col-span-1">
<FormField
control={form.control}
name="reuseBuilding.0.instlCstSprtYn"
render={({ field }) => (
<FormUi label="설치비 지원여부">
<SelectBox
className={""}
selectClass={selectClass}
label={""}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-1">
<FormField
control={form.control}
name="reuseBuilding.0.instlCstSprtAmt"
render={({ field }) => (
<FormUi label="지원금액(원)">
<Input
className={'w-full !text-xl'}
placeholder="1,000,000"
type="text"
onChange={(e) => {
handleCommaNumberChange(e, field.onChange);
}}
value={formatCommaNumber(field.value)}
readOnly={instlCstSprtYn !== "Y"}
/>
</FormUi>
)}
/>
</div>
</div>
<div>
<h2 className={formH2Class}>녹색건축물 인증</h2>
</div>
<div className="grid grid-cols-6 gap-2 mt-4">
<div className="col-span-3">
<FormField
control={form.control}
name="reuseBuilding.0.grntchBdstCertYn"
render={({ field }) => (
<FormUi label="인증여부">
<SelectBox
className={""}
selectClass={selectClass}
label={""}
selectArray={code.yn}
onSelectValue={field.onChange}
value={field.value ?? code.yn[0]?.val}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-3">
<FormField
control={form.control}
name="reuseBuilding.0.grntchBdstCertYr"
render={({ field }) => (
<FormUi label="인증년도">
<Input
className={'w-full !text-xl'}
placeholder="2024"
type="text"
{...field}
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-3">
<FormField
control={form.control}
name="reuseBuilding.0.grntchBdstCertNo"
render={({ field }) => (
<FormUi label="인증번호">
<Input
className={'w-full !text-xl'}
placeholder="ab1234678"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
<div className="col-span-3">
<FormField
control={form.control}
name="reuseBuilding.0.grntchBdstCertGrd"
render={({ field }) => (
<FormUi label="인증등급">
<Input
className={'w-full !text-xl'}
placeholder="A"
type=""
value={field.value ?? ""}
onChange={field.onChange}
readOnly={true}
/>
</FormUi>
)}
/>
</div>
</div>
</form>
</Form>
</div>
);
}
+1 -1
View File
@@ -160,7 +160,7 @@ export default function Home() {
<div className="grid grid-cols-12 gap-4">
<div className="col-span-2">
<Button className="w-full">
<Link href="map/htdflSttn">온배수시설 현황(지도)</Link>
<Link href="map/htdflSttn">시설 현황(지도)</Link>
</Button>
</div>
</div>
@@ -37,10 +37,7 @@ export default function RainSttn() {
const [pageNation, setPageNation] = useState(1);
const [isChHead, setIsChHead] = useState(false);
const [bdstTable, setBdstTable] = useState<customRainBdst[]>();
const [detailModal, setDetailModal] = useState<{
isOpen: boolean;
data: customRainBdst | undefined;
}>({ data: undefined, isOpen: false });
const [detailModal, setDetailModal] = useState<{ isOpen: boolean, data: customRainBdst | undefined }>({ data: undefined, isOpen: false })
const [bdstChgProps, setBdstChgProps] = useState<{
isOpen: boolean;
chBuilding: chgBuilding[];
@@ -49,25 +46,12 @@ export default function RainSttn() {
const codeData = getCityOrSgg(ref);
const cellClick = (index: number | undefined, headId: string) => {
if (
headId !== "chBuildingYn" &&
index !== undefined &&
bdstTable !== undefined
) {
setDetailModal({ isOpen: true, data: bdstTable[index] });
} else if (
headId === "chBuildingYn" &&
index !== undefined &&
bdstTable !== undefined
) {
if (
bdstTable[index] &&
bdstTable[index]?.chgBuilding[0]?.bdstChgNm !== null
) {
setBdstChgProps({
isOpen: true,
chBuilding: bdstTable[index].chgBuilding,
});
if (headId !== 'chBuildingYn' && index !== undefined && bdstTable !== undefined) {
setDetailModal({ isOpen: true, data: bdstTable[index] })
} else if (headId === 'chBuildingYn' && index !== undefined && bdstTable !== undefined) {
if (bdstTable[index] && bdstTable[index]?.chgBuilding[0]?.bdstChgNm !== null) {
setBdstChgProps({ isOpen: true, chBuilding: bdstTable[index].chgBuilding });
}
}
};
@@ -168,9 +152,7 @@ export default function RainSttn() {
headlist={rainSttn.rainSttHeadList}
pageSize={20}
total={100000}
cellClick={(index: number | undefined, headId: string) => {
cellClick(index ?? undefined, headId);
}}
cellClick={(index: number | undefined, headId: string) => { cellClick(index ?? undefined, headId) }}
onPageChange={setPageNation}
></UiTable>
</ListDiv>
@@ -28,7 +28,7 @@ import { useEffect, useState } from "react";
import { set, useForm } from "react-hook-form";
import { z } from "zod";
export function PbnstSttnModal({ data }: { data: customPbnst }) {
export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titleVisible?: boolean }) {
const { code, pbnstSttn, prfmnc } = useWrStore((state) => state);
const [fcltTab, setFcltTab] = useState<string>("fclt");
const [ruseTab, setRuseTab] = useState<string>("inoutTotal");
@@ -233,20 +233,24 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) {
}
}, [pbnstSttn.pbnstSttnRes, data]);
useEffect(() => {}, [ruseBizData?.head]);
useEffect(() => { }, [ruseBizData?.head]);
useEffect(()=>{
useEffect(() => {
if(data){
form.reset({items:[data]})
if (data) {
form.reset({ items: [data] })
}
},[data, form])
}, [data, form])
return (
<>
<div>
<Title title={"하수재이용 시설 상세정보"} />
{titleVisible && (
<>
<Title title={"하수재이용 시설 상세정보"} />
</>
)}
<ScrollArea className={"h-[70vh]"}>
<PbnstMngForm
form={form}
@@ -1,5 +1,6 @@
import { Search } from 'lucide-react';
import OlMap from 'ol/Map';
import Overlay from 'ol/Overlay';
import View from 'ol/View';
import { defaults as defaultControls, OverviewMap } from 'ol/control';
import { defaults } from 'ol/interaction';
@@ -7,31 +8,35 @@ import * as olProj from 'ol/proj';
import { register } from 'ol/proj/proj4';
import proj4 from 'proj4';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import GisLonLat from './GisLonLat';
import GisMapToggle from './GisMapToggle';
import GisSelect from './GisSelect';
import GisToolBar from './GisToolBar';
import GisTree from './GisTree';
import FacilityInfoPopup from './facilityInfoPopup';
import './lib/css/gis.common.css';
import GisApp from './lib/gis.core';
export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null });
export const useGisContext = () => useContext(GisContext);
function Gis({
isOpen ,
gisMaps,
isSearchModalOpen,
treeData = [],
isSearch = false,
isMiniMap = false,
setIsOpen,
setFeatureData,
featureData,
searchView : SearchView,
detailView: DetailView,
onFeatureSelect = null,
setIsSearchModalOpen }) {
function Gis({
isOpen,
gisMaps,
isSearchModalOpen,
treeData = [],
isSearch = false,
isMiniMap = false,
setIsOpen,
setFeatureData,
featureData,
searchView: SearchView,
detailView: DetailView,
onFeatureSelect,
setIsSearchModalOpen,
onCheckedLayersChange
}) {
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
@@ -41,13 +46,21 @@ function Gis({
const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
const mapInstanceRef = useRef(null);
const checkedLayerIds = useRef([]);
// 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가
const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]);
const facilityOverlaysRef = useRef([]);
// 체크된 레이어 상태 관리
const [checkedLayers, setCheckedLayers] = useState([]);
const [checkedLayersData, setCheckedLayersData] = useState([]);
let gisMap = null;
const closePopup = () => {
setIsOpen(false); // 팝업 닫기
setFeatureData(null)
};
// SearchModal, GisContent 등에서 호출
// Feature Click Event
const handleFeatureSelect = useCallback((data) => {
gisMap = mapInstanceRef.current;
@@ -148,7 +161,7 @@ function Gis({
gisMaps.current = map
// 미니맵 추가
if(isMiniMap){
if (isMiniMap) {
const overviewMap = new OverviewMap({
layers: [GisApp.Module.core.baseMiniMapLayers['base']],
view: new View({
@@ -213,15 +226,140 @@ function Gis({
}
}
// 데이터 로드 및 팝업 생성 로직
useEffect(() => {
if (!mapInstanceRef.current) return; // 맵이 생성되지 않았으면 실행 안 함
// 1. 팝업 데이터 가져오기 (feature.js의 getData와 유사)
const fetchPopupData = async () => {
// 실제 API 호출 로직으로 대체해야 합니다.
// 예시: const response = await yourApiService.getFcltyWtCnt();
// setFacilityInfoPopupData(response.data);
// 임시 목업 데이터
const mockData = [
{ ftrIdn: 1, ftrCde: 'SA206', bplcNm: '처리장A', fcltyUseYn: 'Y', x: 127.005, y: 37.282, aCnt: 10, cCnt: 5, eCnt: 3, mCnt: 8, iCnt: 2 },
{ ftrIdn: 2, ftrCde: 'SA112', bplcNm: '처리장B', fcltyUseYn: 'N', x: 127.010, y: 37.285, aCnt: 8, cCnt: 4, eCnt: 2, mCnt: 6, iCnt: 1 }
];
setFacilityInfoPopupData(mockData);
};
fetchPopupData();
}, [mapInstanceRef.current]); // 맵 인스턴스가 생성된 후 한 번만 실행
// 데이터가 변경되면 오버레이를 생성/업데이트하는 로직
useEffect(() => {
const map = mapInstanceRef.current;
if (!map || facilityInfoPopupData.length === 0) return;
// 기존 오버레이 정리
facilityOverlaysRef.current.forEach(overlay => map.removeOverlay(overlay));
facilityOverlaysRef.current = [];
facilityInfoPopupData.forEach(data => {
const popupId = `${data.ftrIdn}_${data.ftrCde}`;
// 팝업을 담을 div 엘리먼트 생성
const popupElement = document.createElement('div');
popupElement.id = popupId;
// OpenLayers Overlay 생성
const position = olProj.transform([Number(data.x), Number(data.y)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
const overlay = new Overlay({
element: popupElement,
position: position,
offset: [-75, -210], // 팝업 너비의 절반, 높이만큼 오프셋 조정
positioning: 'bottom-center',
className: 'facility-popup-overlay' // 필요시 공통 클래스 추가
});
map.addOverlay(overlay);
facilityOverlaysRef.current.push(overlay);
// React 컴포넌트를 popupElement에 렌더링
const root = createRoot(popupElement);
root.render(<FacilityInfoPopup data={data} />);
});
// 초기 가시성 업데이트
updatePopupVisibility();
}, [facilityInfoPopupData]); // 팝업 데이터가 변경될 때마다 실행
// 레이어 가시성에 따라 팝업 가시성을 제어하는 함수
const updatePopupVisibility = useCallback(() => {
const map = mapInstanceRef.current;
if (!map) return;
const visibleLayers = new Set();
map.getLayers().forEach(layer => {
if (layer.getVisible()) {
// feature.js의 로직처럼 typeName이나 ftrCde를 사용해 필터링
const typeName = layer.get('typeName');
if (typeName === 'VW_WTL_FCLTY_AS') { // 예시 레이어 이름
visibleLayers.add(layer.get('ftrCde'));
}
}
});
facilityOverlaysRef.current.forEach(overlay => {
const element = overlay.getElement();
const ftrCde = element.querySelector('table')?.classList[0]; // FacilityPopup의 클래스에서 ftrCde 가져오기
if (visibleLayers.has(ftrCde)) {
element.style.display = '';
} else {
element.style.display = 'none';
}
});
}, []);
// GisTree에서 체크 상태가 변경될 때 이 함수를 호출하도록 연결해야 합니다.
// 예시: GisTree 컴포넌트에 onCheck 콜백을 추가하고, 그 콜백에서 이 함수를 호출
useEffect(() => {
// 레이어 가시성이 변경될 때마다 팝업 가시성 업데이트
const map = mapInstanceRef.current;
if (!map) return;
const handleLayerVisibilityChange = () => {
updatePopupVisibility();
};
map.getLayers().on('change:length', handleLayerVisibilityChange);
map.getLayers().forEach(layer => {
layer.on('change:visible', handleLayerVisibilityChange);
});
return () => {
// 클린업
map.getLayers().un('change:length', handleLayerVisibilityChange);
map.getLayers().forEach(layer => {
layer.un('change:visible', handleLayerVisibilityChange);
});
};
}, [updatePopupVisibility]);
// 체크된 레이어 정보를 처리하는 함수
const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
onCheckedLayersChange?.(checkedIds, checkedNodesData);
}, [onCheckedLayersChange]);
return (
<>
<GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}>
<div className="h-[calc(100vh-10rem)] flex relative">
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
<GisTree visible={tabVisible} setVisible={setTabVisible} />
<GisTree
visible={tabVisible}
setVisible={setTabVisible}
onCheckedChange={handleCheckedLayersChange}
/>
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
{/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
{isOpen && featureData && !isSearchModalOpen && (
<DetailView
@@ -234,9 +372,9 @@ function Gis({
onOpenChange={setIsSearchModalOpen}
onFeatureSelect={handleFeatureSelect}
/>
<GisLonLat/>
<GisToolBar/>
<GisMapToggle/>
<GisLonLat />
<GisToolBar />
<GisMapToggle />
<GisSelect onFeatureSelect={handleFeatureSelect} />
</div>
@@ -9,7 +9,7 @@ import { DndProvider } from "react-dnd";
import { useGisContext } from './Gis';
import GisApp from './lib/gis.core';
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible }) {
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange }) {
const { gisCore, treeData } = useGisContext();
const [checked, setChecked] = useState([]);
const [expanded, setExpanded] = useState([]);
@@ -102,6 +102,11 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
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;
});
};