Merge branch 'iipbs/dm/dev' into iipbs/sn/dev

This commit is contained in:
2026-03-18 15:25:38 +09:00
2 changed files with 540 additions and 472 deletions
@@ -1,268 +1,279 @@
'use client'
import { CHART_FORM, type ChartConfig, MainChart, UiModal } from "@common/business_components";
import React, { useEffect, useMemo, useState } from 'react';
import moment from 'moment';
import {
CHART_FORM,
type ChartConfig,
MainChart,
UiModal,
} from '@common/business_components';
import { SelectBox } from '@common/business_components/ui';
import { Button, Input, Label } from '@common/components';
import React, { useEffect, useState } from 'react';
import moment from "moment";
import type { TInftwtAnls } from './aplcnYms';
import * as fn from './aplcnYms';
import { useInfilChartModalStore } from '../chartModalStore'; // 모달 상태 관리 스토어
import { useInfilChartModalStore } from '../chartModalStore';
type ModalOpenProp = {
pplMngNo?: string, // 관로관리번호
}
type DataType = { label: string; val: number };
pplMngNo?: string; // 관로관리번호
};
const InfilChart = ({ pplMngNo }: ModalOpenProp) => {
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore(); // 모달 상태 관리 스토어
const [popupOpen, setPopupOpen] = React.useState(false); // 조회조건팝업
const buttonRef = React.useRef<HTMLButtonElement>(null); // 버튼 참조
const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치
const [selectedAplcnYm, setSelectedAplcnYm] = useState("");
type DataType = {
label: string;
val: number;
};
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore();
const [selectedAplcnYm, setSelectedAplcnYm] = useState('');
const [inftwtAnlss, setInftwtAnlss] = useState<TInftwtAnls[]>([]);
const [inftwtAnlsInfos, setInftwtAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>([{ val: 0, text: '선택' }]); // 침입수분석정보 options
const [inftwtAnls, setInftwtAnls] = useState<TInftwtAnls | null>(null); // 침입수분석정보
const [selectedInftwtAnls, setSelectedInftwtAnls] = useState<number>(0); // 침입수분석 정보 선택시 사용
const [inftwtAnlsInfos, setInftwtAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>(
INITIAL_ANALYSIS_OPTIONS,
);
const [inftwtAnls, setInftwtAnls] = useState<TInftwtAnls | null>(null);
const [selectedInftwtAnls, setSelectedInftwtAnls] = useState<number>(0);
const [chartData, setChartData] = useState<{ chartData1: DataType[]; chartData2: DataType[] }>({
chartData1: [], chartData2: [],
chartData1: [],
chartData2: [],
});
const [inftwtQty, setInftwtQty] = useState<string>("0"); // 침입수량
const aplcnYms = fn.useAplcnYms('inftwtAnlsRslt'); // 상수사용월 데이터 가져오기
const [inftwtQty, setInftwtQty] = useState<string>('0');
pplMngNo = "L" + pplMngNo; // inp파일에 하수관로와 맨홀의 번호가 같으면 분석 오류가 발행하여
const openPopup = () => {
if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect(); // 버튼 위치 계산
setPopupPosition({
top: rect.bottom + window.scrollY, // 버튼 아래쪽
left: rect.left + window.scrollX, // 버튼 왼쪽
});
}
setPopupOpen(true); // 팝업 열기
};
const aplcnYms = fn.useAplcnYms('inftwtAnlsRslt');
const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : '';
const chartForm = CHART_FORM.LINE;
const chartConfig = {
chartData1: { label: '순오수량', color: "red", },
chartData2: { label: '실측하수량', color: "orange", }
chartData1: { label: '순오수량', color: 'red' },
chartData2: { label: '실측하수량', color: 'orange' },
} as ChartConfig;
const hasChartData = useMemo(() => {
return chartData.chartData1.length > 0 || chartData.chartData2.length > 0;
}, [chartData]);
const resetState = () => {
setSelectedAplcnYm('');
setInftwtAnlss([]);
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
setInftwtAnls(null);
setSelectedInftwtAnls(0);
setChartData({ chartData1: [], chartData2: [] });
setInftwtQty('0');
};
useEffect(() => {
if (isModalOpen === false) {
setPopupOpen(true); // 모달이 닫힐 때 조회조건팝업은 열어두기
setSelectedAplcnYm(""); // 상수사용월 초기화
setInftwtAnlss([]); // 침입수분석 초기화
setInftwtAnlsInfos([{ val: 0, text: '선택' }]); // 침입수분석정보 초기화
setInftwtAnls(null); // 침입수분석정보 초기화
setSelectedInftwtAnls(0); // 침입수분석 일련번호 초기화
setChartData({ chartData1: [], chartData2: [], }); // 차트 데이터 초기화
setInftwtQty("0"); // 침입수량 초기화
if (!isModalOpen) {
resetState(); // 모달이 닫힐 때 리셋
}
}, [isModalOpen]);
// 상수사용월 변경 핸들러
const handleAplcnYmChange = async (val: string | number) => {
const selectedValue = String(val);
setSelectedAplcnYm(selectedValue);
// 즉시 관련 상태 초기화
setInftwtAnlss([]);
setInftwtAnls(null);
setSelectedInftwtAnls(0);
setInftwtQty('0');
setChartData({ chartData1: [], chartData2: [] });
// 빈 값이면 초기화만 하고 종료
if (!selectedValue || selectedValue === "0" || selectedValue === "") {
setInftwtAnlsInfos([{ val: 0, text: '선택' }]);
if (!selectedValue || selectedValue === '0') {
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
return;
}
// API 호출
try {
const data = await fn.selectInftwtAnls(selectedValue);
setInftwtAnlss(data ?? []);
setInftwtAnlsInfos(fn.getOptionsInftwtAnlsInfo(data));
const list = data ?? [];
setInftwtAnlss(list);
setInftwtAnlsInfos(fn.getOptionsInftwtAnlsInfo(list));
} catch (error) {
console.error("selectInftwtAnls 에러:", error);
console.error('selectInftwtAnls 에러:', error);
setInftwtAnlss([]);
setInftwtAnlsInfos([{ val: 0, text: '선택' }]);
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
}
setSelectedInftwtAnls(0);
};
// 분석정보 변경 핸들러
const handleInftwtAnlsSnChange = (val: string | number) => {
const selectedValue = Number(val);
setSelectedInftwtAnls(selectedValue);
// 선택된 침입수분석 정보에 대한 처리 로직
setSelectedInftwtAnls(selectedValue);
setChartData({ chartData1: [], chartData2: [] });
setInftwtQty('0');
if (selectedValue === 0) {
setInftwtAnls(null);
return;
}
const selectedAnls = inftwtAnlss.find(anls => anls.inftwtAnlsSn === selectedValue);
if (selectedAnls) {
setInftwtAnls(selectedAnls);
const selectedAnls = inftwtAnlss.find((anls) => anls.inftwtAnlsSn === selectedValue) ?? null;
setInftwtAnls(selectedAnls);
};
const infilRsltChart = async () => {
if (!selectedAplcnYm || selectedInftwtAnls === 0 || !normalizedPplMngNo) {
setChartData({ chartData1: [], chartData2: [] });
setInftwtQty('0');
return;
}
try {
const [inftwtAnlsRslt, oilq, qty] = await Promise.all([
fn.selectInftwtAnlsRslt(selectedInftwtAnls, normalizedPplMngNo),
fn.selectOilq(selectedInftwtAnls, normalizedPplMngNo).catch((err) => {
console.error('selectOilq 에러:', err);
return [];
}),
fn.selectPplInftwtQty(selectedInftwtAnls, normalizedPplMngNo).catch((err) => {
console.error('selectPplInftwtQty 에러:', err);
return 0;
}),
]);
setChartData({
chartData1: fn.getChartDataInftwtAnlsRslt(inftwtAnlsRslt ?? []),
chartData2: fn.getChartDataOilq(oilq ?? []),
});
const normalizedQty = Number(qty ?? 0);
setInftwtQty(normalizedQty.toLocaleString('ko-KR'));
} catch (error) {
console.error('infilRsltChart 에러:', error);
setChartData({ chartData1: [], chartData2: [] });
setInftwtQty('0');
}
};
const closePopup = () => { // 팝업 닫기
setPopupOpen(false);
}
const infilRsltChart = () => { // 그래프 & 팝업 닫기
const renderInfoInput = (label: string, value?: string | number | null) => (
<div className="grid grid-cols-[125px_minmax(0,1fr)] items-center gap-3">
<Label className="w-auto text-[14px] font-semibold text-[#5c6470] whitespace-nowrap">{label}</Label>
<Input
className="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white px-3 text-xl md:text-xl text-[#2b3440]"
placeholder=""
value={value ?? ''}
readOnly
/>
</div>
);
fn.selectInftwtAnlsRslt(selectedInftwtAnls, pplMngNo).then((data) => {// 침입수분석 결과 조회
const rsltData = fn.getChartDataInftwtAnlsRslt(data ?? []);
setChartData(prev => ({
...prev,
chartData1: rsltData // 침입수분석결과
}));
});
fn.selectOilq(selectedInftwtAnls, pplMngNo).then((data) => {// 유량 조회
const rsltData = fn.getChartDataOilq(data ?? []);
setChartData(prev => ({
...prev,
chartData2: rsltData // 유량
}));
}).catch((err) => {
console.error("selectOilq 에러:", err);
setChartData(prev => ({
...prev,
chartData2: [] // 유량 데이터 초기화
}));
});
fn.selectPplInftwtQty(selectedInftwtAnls, pplMngNo).then((data) => {// 침입수량 조회
if (data > 0) {
setInftwtQty(data.toLocaleString('ko-KR'));
}
});
setPopupOpen(false);
};
const handleReset = () => {
resetState();
}
const renderBody = () => (
<>
<div className="text-black text-3xl font-bold mb-5">침입수분석결과</div>
<div className='grid grid-cols-[1fr_3fr] gap-4'>
<div className="h-[90%] grid grid-rows-4 gap-4 mb-4">
<Button className="bg-white text-white w-[130px] font-bold px-4 mt-2 py-2" onClick={openPopup}>
분석 입력현황 조회
</Button>
{/* 상수사용량 적용연월 */}
<div className="bg-white px-4 py-2 ">
<span className="text-2xl text-red-600 font-bold">● 상수사용량 적용연월</span>
<div className="flex items-center mt-2">
<span className="ml-5 text-black text-xl font-bold">
{(selectedAplcnYm) ? moment(selectedAplcnYm, "YYYYMM").format("YYYY-MM") : ""}
</span>
</div>
</div>
{/* 침입수량 분석결과 */}
<div className="bg-white px-4 py-2 ">
<span className="text-2xl text-red-600 font-bold">● 침입수량 산정결과</span>
<div className="flex items-center mt-2">
<span className="ml-5 text-black text-xl">침입수량 : </span>
<span className="w-[45px] inline-block text-right text-black text-xl font-bold">{inftwtQty}</span>
<span className="ml-2 text-black text-xl">m³/d</span>
</div>
</div>
</div>
{/* 차트 */}
<div className="h-[90%] bg-white text-3xl">
<MainChart chartConfig={chartConfig} chartForm={chartForm} chartData={chartData} chartTitle={'침입수량 그래프'}
yFieldText2="" yFieldText="유량(m³/h)"/>
</div>
<div className="text-black">
<div className="mb-4 flex items-start justify-between">
<div className="text-[18px] font-semibold text-[#111827]" />
</div>
{popupOpen && (
<div
className="absolute bg-gray-100 border border-gray-300 w-[440px] shadow-lg p-6 rounded text-2xl"
style={{
top: popupPosition.top,
left: popupPosition.left,
zIndex: 1000,
}}
>
<div className="font-bold mb-6 text-3xl">침입수분석 입력현황</div>
<div className='flex flex-col'>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">상수사용월:</Label>
<SelectBox
className="w-[220px] mr-8 z-[51] mb-2"
selectClass="rounded-lg bg-white"
label=''
selectArray={aplcnYms}
value={selectedAplcnYm}
onSelectValue={handleAplcnYmChange}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">분석정보:</Label>
<SelectBox
className="w-[220px] mr-8 z-[51] mb-2"
selectClass="rounded-lg bg-white"
label=''
selectArray={inftwtAnlsInfos}
value={selectedInftwtAnls}
onSelectValue={handleInftwtAnlsSnChange}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">오수전환율:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={inftwtAnls?.drtwSwtcRt ?? ""}
readOnly={true}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">시간패턴 파일명:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={inftwtAnls?.atflHrPttnNm ?? ""}
readOnly={true}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">수용가-맨홀 매칭:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={inftwtAnls?.atflMpngNm ?? ""}
readOnly={true}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">관망도 파일명:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={inftwtAnls?.atflInpNm ?? ""}
readOnly={true}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">유량 파일명:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={inftwtAnls?.atflOilqNm ?? ""}
readOnly={true}
/>
<div className="grid min-h-[68vh] grid-cols-[440px_minmax(0,1fr)] gap-5">
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
침입수 분석 선택 조건
</h3>
<div className="rounded-[6px] border border-[#d9dde3] bg-[#eff1f4] px-4 py-6">
<div className="space-y-3">
<div className="grid grid-cols-[125px_minmax(0,1fr)] items-center gap-3">
<Label className="w-auto text-[14px] font-semibold text-[#5c6470] whitespace-nowrap">
상수사용월:
</Label>
<SelectBox
className="w-full"
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
label=""
selectArray={aplcnYms}
value={selectedAplcnYm}
onSelectValue={handleAplcnYmChange}
/>
</div>
<div className="grid grid-cols-[125px_minmax(0,1fr)] items-center gap-3">
<Label className="w-auto text-[14px] font-semibold text-[#5c6470] whitespace-nowrap">
분석정보:
</Label>
<SelectBox
className="w-full"
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
label=""
selectArray={inftwtAnlsInfos}
value={selectedInftwtAnls}
onSelectValue={handleInftwtAnlsSnChange}
/>
</div>
{renderInfoInput('오수전환율:', inftwtAnls?.drtwSwtcRt ?? '')}
{renderInfoInput('시간패턴 파일명:', inftwtAnls?.atflHrPttnNm ?? '')}
{renderInfoInput('수용가-맨홀 매칭:', inftwtAnls?.atflMpngNm ?? '')}
{renderInfoInput('관망도 파일명:', inftwtAnls?.atflInpNm ?? '')}
{renderInfoInput('유량 파일명:', inftwtAnls?.atflOilqNm ?? '')}
</div>
</div>
<div className="flex justify-end mt-4 space-x-2">
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closePopup}>
닫기
</button>
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={infilRsltChart}>
선택조건 적용
</button>
<div className="absolute inset-x-0 bottom-0 mb-[40px] flex justify-center">
<Button size="lg" onClick={infilRsltChart} >
조회
</Button>&nbsp;&nbsp;
<Button size="lg" color="gray" onClick={handleReset} >
초기화
</Button>
</div>
</div>)}
{/* </Modal > */}
</>
)
</section>
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">침입수 분석 결과</h3>
<div className="mb-6 grid grid-cols-2 gap-5">
<div className="flex min-h-[98px] items-center justify-between rounded-[14px] border border-[#4f88c6] bg-white px-6 py-4">
<div className="flex items-center gap-2 text-[16px] font-extrabold text-[#ef4444]">
{/* <span className="text-[14px] leading-none">●</span> */}
<span>상수사용량 적용연월</span>
</div>
<div className="text-[20px] font-extrabold text-[#222]">
{selectedAplcnYm ? moment(selectedAplcnYm, 'YYYYMM').format('YYYY-MM') : '-'}
</div>
</div>
<div className="flex min-h-[98px] items-center justify-between rounded-[14px] border border-[#4f88c6] bg-white px-6 py-4">
<div className="flex items-center gap-2 text-[16px] font-extrabold text-[#ef4444]">
{/* <span className="text-[14px] leading-none">●</span> */}
<span>침입수량 산정결과</span>
</div>
<div className="grid grid-cols-[1fr_auto] items-center gap-7 text-[#222]">
<span className="text-[16px] text-[#666]">침입수량</span>
<div className='w-[66px] flex items-end justify-start gap-2'>
<span className="text-[20px] font-extrabold">{inftwtQty}</span>
<span className="pb-[2px] text-[16px]">m³/d</span>
</div>
</div>
</div>
</div>
<div className="rounded-[10px] bg-white px-2 pb-2 pt-3">
{hasChartData ? (
<MainChart
chartConfig={chartConfig}
chartForm={chartForm}
chartData={chartData}
chartTitle={'침입수량 그래프'}
yFieldText="유량(m³/h)"
yFieldText2=""
/>
) : (
<div className="flex h-[464px] items-center justify-center text-[18px] text-[#2c2f33]">
데이터가 없습니다.
</div>
)}
</div>
</section>
</div>
</div>
);
return (
<UiModal
@@ -270,17 +281,17 @@ const InfilChart = ({ pplMngNo }: ModalOpenProp) => {
onOpenChange={setIsModalOpen}
headerTitle=""
bodyClass={`
min-h-[50vh]
w-[55vw]
max-w-none
max-h-none
overflow-visible
z-[50]
`}
w-[78vw]
max-w-none
min-h-[74vh]
max-h-none
overflow-visible
z-[50]
`}
body={renderBody()}
noFooterYn='Y'
noFooterYn="Y"
/>
)
}
);
};
export default React.memo(InfilChart);
export default React.memo(InfilChart);
@@ -1,282 +1,339 @@
'use client'
import { CHART_FORM, type ChartConfig, MainChart, UiModal } from "@common/business_components";
import React, { useEffect, useMemo, useState } from 'react';
import moment from 'moment';
import {
CHART_FORM,
type ChartConfig,
MainChart,
UiModal,
} from '@common/business_components';
import { SelectBox } from '@common/business_components/ui';
import { Button, Input, Label } from '@common/components';
import React, { useEffect, useState } from 'react';
import moment from "moment";
import type { TRdiiAnls } from './aplcnYms';
import * as fn from './aplcnYms';
import { useRdiiChartModalStore } from '../chartModalStore'; // 모달 상태 관리 스토어
import { useRdiiChartModalStore } from '../chartModalStore';
type ModalOpenProp = {
pplMngNo: string, // 관로관리번호
}
// 차트
type lineBarChartData = {
label : string;
lineVal0 : number;
barVal0 : number;
}
pplMngNo?: string; // 관로관리번호
};
type LineBarChartData = {
label: string;
lineVal0: number;
barVal0: number;
};
const RdiiChart = ({ pplMngNo }: ModalOpenProp) => {
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore(); // 모달 상태 관리 스토어
const [popupOpen, setPopupOpen] = React.useState(false); // 조회조건팝업
const buttonRef = React.useRef<HTMLButtonElement>(null); // 버튼 참조
const [popupPosition, setPopupPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); // 팝업 위치
const [aplcnYm, setAplcnYm] = useState("");
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore();
const [aplcnYm, setAplcnYm] = useState('');
const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]);
const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>([{ val: 0, text: '선택' }]); // 강우유입수분석정보 options
const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>(
INITIAL_ANALYSIS_OPTIONS,
);
const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null); // 강우유입수분석정보
const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState<number>(0); // 강우유입수분석 정보 선택시 사용
const [chartData, setChartData] = useState<lineBarChartData[]>([]);
const [rdiiQty, setRdiiQty] = useState<string>("0"); // 강우유입수량
const [rainTotal, setRainTotal] = useState<string>("0"); // 적용강우량
const [totalVolume, setTotalVolume] = useState<string>("0"); // 총체적 m³/mm
const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState<number>(0);// 강우유입수분석 정보 선택시 사용
const [chartData, setChartData] = useState<LineBarChartData[]>([]);
const [rdiiQty, setRdiiQty] = useState<string>('0'); // 강우유입수량
const [rainTotal, setRainTotal] = useState<string>('0'); // 적용강우량
const [totalVolume, setTotalVolume] = useState<string>('0');// 총체적 m³/mm
const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기
pplMngNo = "L"+pplMngNo; // "L13101003";
const openPopup = () => {
if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect(); // 버튼 위치 계산
setPopupPosition({
top: rect.bottom + window.scrollY, // 버튼 아래쪽
left: rect.left + window.scrollX, // 버튼 왼쪽
});
}
setPopupOpen(true); // 팝업 열기
};
const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : '';
const chartForm = CHART_FORM.LINEBAR2;
const chartConfig = {
barVal0: {
barVal0: {
label: '강수량',
color: "#33AAFF",
color: '#33AAFF',
},
lineVal0: {
label: 'RDII',
color: "#FF0000",
color: '#FF0000',
},
} as ChartConfig;
const hasChartData = useMemo(() => chartData.length > 0, [chartData]);
const resetState = () => {
setAplcnYm(''); // 상수사용월 초기화
setRdiiAnlss([]); // 강우유입수분석 초기화
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS); // 강우유입수분석정보 초기화
setRdiiAnls(null); // 강우유입수분석정보 초기화
setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화
setChartData([]);
setRdiiQty('0'); // 강우유입수량 초기화
setRainTotal('0');// 적용강우량 초기화
setTotalVolume('0');// 총체적 m³/mm 초기화
};
useEffect(() => {
if (isModalOpen === false) {
setPopupOpen(true); // 모달이 닫힐 때 팝업은
setAplcnYm(""); // 상수사용월 초기화
setRdiiAnlss([]); // 강우유입수분석 초기화
setRdiiAnlsInfos([{ val: 0, text: '선택' }]); // 강우유입수분석정보 초기화
setRdiiAnls(null); // 강우유입수분석정보 초기화
setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화
setChartData([]); // 차트 데이터 초기화
setRdiiQty("0"); // 강우유입수량 초기화
setRainTotal("0"); // 적용강우량 초기화
setTotalVolume("0"); // 총체적 m³/mm 초기화
if (!isModalOpen) {
resetState();
}
}, [isModalOpen]);
// 상수사용월 변경시 분석 정보 리스트 가져오기
useEffect(() => {
// aplcnYm이 비어있거나 "0"이면 API 호출하지 않음
if (!aplcnYm || aplcnYm === "0" || aplcnYm === "") {
setRainInfwtAnlsSn(0); // 초기화
setRdiiAnlsInfos([]);
const handleAplcnYmChange = async (val: string | number) => {
const selectedValue = String(val);
setAplcnYm(selectedValue);
setRdiiAnlss([]);
setRdiiAnls(null);
setRainInfwtAnlsSn(0);
setChartData([]);
setRdiiQty('0');
setRainTotal('0');
setTotalVolume('0');
if (!selectedValue || selectedValue === '0') {
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
return;
}
fn.selectRdiiAnls(aplcnYm)
.then((data) => {
setRdiiAnlss(data ?? []); // 선택한 월의 분석성공한 RDII분석데이터[]
setRdiiAnlsInfos(fn.getOptionsRdiiAnlsInfo(data));
setRdiiAnls(null); // 오수전환율 이후 초기화
setRainInfwtAnlsSn(0); // 초기화
})
.catch((error) => {
console.error("selectrdiiAnls 에러:", error);
setRdiiAnlss([]);
setRdiiAnlsInfos([]);
});
}, [aplcnYm]);
// 값 표시
useEffect(() => {
// 선택된 강우유입수분석 정보에 대한 처리 로직
if (rainInfwtAnlsSn === 0) return;
const selectedAnls = rdiiAnlss.find(anls => anls.rainInfwtAnlsSn === rainInfwtAnlsSn);
if (selectedAnls) {
setRdiiAnls(selectedAnls);
try {
const data = await fn.selectRdiiAnls(selectedValue); // 상수사용월 변경시 분석 정보 리스트 가져오기
const list = data ?? [];
setRdiiAnlss(list);
setRdiiAnlsInfos(fn.getOptionsRdiiAnlsInfo(list));
} catch (error) {
console.error('selectRdiiAnls 에러:', error);
setRdiiAnlss([]);
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
}
}, [rainInfwtAnlsSn, rdiiAnlss]);
const closePopup = () => { // 팝업 닫기
setPopupOpen(false);
}
const rdiiRsltChart = () => { // 그래프 & 팝업 닫기
fetchFilteredData();
fn.selectPplRdiiQty(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수량 조회
setRdiiQty(Math.round(data?.rdiih*24).toLocaleString('ko-KR')); // cmd: m³/d 단위로 변환
if (data?.raintotal && data?.rainTotal !== 0) {
setRainTotal(data?.raintotal.toLocaleString('ko-KR')); // 총강우량mm
setTotalVolume(Math.round(data?.rdiih * data?.sustaintime / data?.raintotal).toLocaleString('ko-KR')); // 총체적 m³/mm
}
});
setPopupOpen(false);
};
const fetchFilteredData: () => Promise<void> = async () => {
const rsltData = await fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, pplMngNo);
const rsltDataformatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []); // format 변경
const rainData = await fn.selectRainData(rainInfwtAnlsSn);
const rainDataformatted = fn.getChartDataRainData(rainData ?? []); // format 변경
const rainMap = new Map(rainDataformatted.map(item => [item.label, item.barVal0]));
const handleRdiiAnlsSnChange = (val: string | number) => {
const selectedValue = Number(val);
setRainInfwtAnlsSn(selectedValue);
setChartData([]);
setRdiiQty('0');
setRainTotal('0');
setTotalVolume('0');
if (selectedValue === 0) {
setRdiiAnls(null);
return;
}
const selectedAnls = rdiiAnlss.find((anls) => anls.rainInfwtAnlsSn === selectedValue) ?? null;
setRdiiAnls(selectedAnls);
};
const fetchFilteredData = async () => {
if (!rainInfwtAnlsSn || !normalizedPplMngNo) {
setChartData([]);
setRdiiQty('0');
setRainTotal('0');
setTotalVolume('0');
return;
}
try {
const [rsltData, rainData, qtyData] = await Promise.all([
fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, normalizedPplMngNo),
fn.selectRainData(rainInfwtAnlsSn),
fn.selectPplRdiiQty(rainInfwtAnlsSn, normalizedPplMngNo),
]);
const rsltDataFormatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []);
const rainDataFormatted = fn.getChartDataRainData(rainData ?? []);
const rainMap = new Map(rainDataFormatted.map((item) => [item.label, item.barVal0]));
rsltDataFormatted.forEach((item) => {
const matchedVal = rainMap.get(item.label);
if (matchedVal !== undefined) {
item.barVal0 = matchedVal;
}
});
setChartData(rsltDataFormatted);// 차트 데이터 설정
const avgRdii = Number(qtyData?.rdiih ?? 0);
const appliedRainfall = Number(qtyData?.raintotal ?? 0);
const rdiiPerRainfall =
qtyData?.raintotal && qtyData?.raintotal !== 0
? Math.round(avgRdii * Number(qtyData?.sustaintime ?? 0) / qtyData.raintotal)
: 0;
setRdiiQty(Math.round(avgRdii * 24).toLocaleString('ko-KR'));
setRainTotal(appliedRainfall.toLocaleString('ko-KR'));
setTotalVolume(rdiiPerRainfall.toLocaleString('ko-KR'));
} catch (error) {
console.error('rdiiRsltChart 에러:', error);
setChartData([]);
setRdiiQty('0');
setRainTotal('0');
setTotalVolume('0');
}
};
const renderInfoInput = (label: string, value?: string | number | null) => (
<div className="grid grid-cols-[125px_minmax(0,1fr)] items-center gap-3">
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
{label}
</Label>
<Input
className="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white px-3 text-xl text-[#2b3440]"
placeholder=""
value={value ?? ''}
readOnly
/>
</div>
);
rsltDataformatted.forEach(item => {
const matchedVal = rainMap.get(item.label);
if (matchedVal !== undefined) {
item.barVal0 = matchedVal;
}
});
setChartData(rsltDataformatted); // 차트 데이터 설정
}
const renderBody = () => (
<>
<div className="text-black text-3xl font-bold mb-5">RDII분석결과</div>
<div className='grid grid-cols-[1fr_3fr] gap-4'>
<div className="grid grid-rows-4 gap-4 mb-4">
<Button className="bg-white text-white w-[130px] font-bold px-4 mt-2 py-2" onClick={openPopup}>
분석 입력현황 조회
</Button>
{/* 상수사용량 적용연월 */}
<div className="bg-white px-4 py-2 ">
<span className="text-2xl text-red-600 font-bold">● 상수사용량 적용연월</span>
<div className="flex items-center mt-2">
<span className="ml-5 text-black text-xl font-bold">
{(aplcnYm)? moment(aplcnYm, "YYYYMM").format("YYYY-MM"):""}
</span>
</div>
</div>
{/* 강우유입수량 분석결과 */}
<div className="bg-white px-4 py-2 ">
<span className="text-2xl text-red-600 font-bold">● 강우유입수량 분석결과</span>
<div className="flex items-center mt-2">
<span className="w-[95px] inline-block text-right text-black text-xl">평균RDII : </span>
<span className="w-[45px] inline-block text-right text-black text-xl font-bold"> {rdiiQty}</span>
<span className="ml-2 text-black"> m³/d</span>
</div>
<div className="flex items-center mt-2">
<span className="w-[95px] inline-block text-right text-black text-xl">적용강우량 : </span>
<span className="w-[45px] inline-block text-right text-black text-xl font-bold"> {rainTotal}</span>
<span className="ml-2 text-black"> mm</span>
</div>
<div className="flex items-center mt-2">
<span className="w-[95px] inline-block text-right text-black text-xl">강우량대비RDII : </span>
<span className="w-[45px] inline-block text-right text-black text-xl font-bold"> {totalVolume}</span>
<span className="ml-2 text-black"> m³/mm</span>
</div>
</div>
</div>
{/* 차트 */}
<div className="h-[90%] bg-white text-2xl">
<MainChart chartConfig={chartConfig} chartForm={chartForm} chartData={chartData} chartTitle={'RDII 그래프'}
yFieldText2="강수량(mm)" yFieldText="유량(m³/h)" />
</div>
<div className="text-black">
<div className="mb-4 flex items-start justify-between">
<div className="text-[18px] font-semibold text-[#111827]" />
</div>
{popupOpen && (
<div
className="absolute bg-gray-100 border border-gray-300 w-[440px] shadow-lg p-6 rounded text-2xl"
style={{
top: popupPosition.top,
left: popupPosition.left,
zIndex: 1000,
}}
>
<div className="font-bold mb-6 text-3xl">강우유입수분석 입력현황</div>
<div className='flex flex-col'>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">상수사용월:</Label>
<SelectBox
className="w-[220px] mr-8 z-[51] mb-2"
selectClass="rounded-lg bg-white"
label=''
selectArray={aplcnYms}
value={aplcnYm}
onSelectValue={(val) => {
setAplcnYm(String(val));
}}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">분석정보:</Label>
<SelectBox
className="w-[220px] mr-8 z-[51] mb-2"
selectClass="rounded-lg bg-white"
label=''
selectArray={rdiiAnlsInfos}
value={rainInfwtAnlsSn}
onSelectValue={(val) => {
setRainInfwtAnlsSn(Number(val));
}}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">오수전환율:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={rdiiAnls?.drtwSwtcRt ?? ""}
readOnly={true}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">수용가-맨홀 매칭:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={rdiiAnls?.atflMpngNm ?? ""}
readOnly={true}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">관망도 파일명:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={rdiiAnls?.atflInpNm ?? ""}
readOnly={true}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">강우 파일명:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={rdiiAnls?.atflRainNm ?? ""}
readOnly={true}
/>
</div>
<div className='flex items-center justify-between'>
<Label className="mr-2 w-auto text-2xl text-gray-600 whitespace-nowrap">RTK계수 출처:</Label>
<Input
className="mr-8 w-[220px] !text-xl placeholder:text-center mb-2"
placeholder=""
value={rdiiAnls?.srcNm ?? ""}
readOnly={true}
/>
<div className="grid min-h-[68vh] grid-cols-[440px_minmax(0,1fr)] gap-5">
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
강우유입수분석 선택 조건
</h3>
<div className="rounded-[6px] border border-[#d9dde3] bg-[#eff1f4] px-4 py-6">
<div className="space-y-3">
<div className="grid grid-cols-[125px_minmax(0,1fr)] items-center gap-3">
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
상수사용월:
</Label>
<SelectBox
className="w-full"
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
label=""
selectArray={aplcnYms}
value={aplcnYm}
onSelectValue={handleAplcnYmChange}
/>
</div>
<div className="grid grid-cols-[125px_minmax(0,1fr)] items-center gap-3">
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
분석정보:
</Label>
<SelectBox
className="w-full"
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
label=""
selectArray={rdiiAnlsInfos}
value={rainInfwtAnlsSn}
onSelectValue={handleRdiiAnlsSnChange}
/>
</div>
{renderInfoInput('오수전환율:', rdiiAnls?.drtwSwtcRt ?? '')}
{renderInfoInput('수용가-맨홀 매칭:', rdiiAnls?.atflMpngNm ?? '')}
{renderInfoInput('관망도 파일명:', rdiiAnls?.atflInpNm ?? '')}
{renderInfoInput('강우 파일명:', rdiiAnls?.atflRainNm ?? '')}
{renderInfoInput('RTK계수 출처:', rdiiAnls?.srcNm ?? '')}
</div>
</div>
<div className="flex justify-end mt-4 space-x-2">
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closePopup}>
닫기
</button>
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={rdiiRsltChart}>
선택조건 적용
</button>
<div className="absolute inset-x-0 bottom-0 mb-[40px] flex justify-center">
<Button size="lg" onClick={fetchFilteredData}>
조회
</Button>
&nbsp;&nbsp;
<Button size="lg" color="gray" onClick={resetState}>
초기화
</Button>
</div>
</div>)}
{/* </Modal > */}
</>
)
</section>
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
강우유입수분석 결과
</h3>
<div className="mb-6 grid grid-cols-2 gap-5">
{/* 좌측 카드 */}
<div className="flex min-h-[98px] items-center justify-between rounded-[16px] border border-[#5fa3ea] bg-white px-6 py-4">
<div className="text-[16px] font-bold text-[#ef4444]">
상수사용량 적용연월
</div>
<div className="text-[24px] font-extrabold leading-none text-[#111]">
{aplcnYm ? moment(aplcnYm, 'YYYYMM').format('YYYY-MM') : '-'}
</div>
</div>
{/* 우측 카드 */}
<div className="flex min-h-[98px] min-w-0 items-center gap-5 rounded-[16px] border border-[#5fa3ea] bg-white px-6 py-4">
{/* 좌측: 제목 (세로 가운데 정렬) */}
<div className="flex min-w-0 basis-[32%] items-center">
<div className="text-[16px] font-bold text-[#ef4444] break-keep">
강우유입수량 분석결과
</div>
</div>
{/* 우측: 지표 영역 */}
<div className="flex min-w-0 basis-[68%] flex-col gap-[2px]">
<div className="flex min-w-0 items-center gap-3">
<div className="min-w-0 basis-[42%] text-right text-[15px] text-[#555]">
평균RDII
</div>
<div className="min-w-0 basis-[38%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">
{rdiiQty}
</div>
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">
m³/d
</div>
</div>
<div className="flex min-w-0 items-center gap-3">
<div className="min-w-0 basis-[42%] text-right text-[15px] text-[#555]">
적용 강우량
</div>
<div className="min-w-0 basis-[38%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">
{rainTotal}
</div>
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">
mm
</div>
</div>
<div className="flex min-w-0 items-center gap-3">
<div className="min-w-0 basis-[42%] text-right text-[15px] text-[#555]">
강우량 대비 RDII
</div>
<div className="min-w-0 basis-[38%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">
{totalVolume}
</div>
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">
m³/mm
</div>
</div>
</div>
</div>
</div>
<div className="rounded-[10px] bg-white px-2 pb-2 pt-3">
{hasChartData ? (
<MainChart
chartConfig={chartConfig}
chartForm={chartForm}
chartData={chartData}
chartTitle={'RDII 그래프'}
yFieldText2="강수량(mm)"
yFieldText="유량(m³/h)"
/>
) : (
<div className="flex h-[464px] items-center justify-center text-[18px] text-[#2c2f33]">
데이터가 없습니다.
</div>
)}
</div>
</section>
</div>
</div>
);
return (
<UiModal
@@ -284,17 +341,17 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => {
onOpenChange={setIsModalOpen}
headerTitle=""
bodyClass={`
min-h-[50vh]
w-[55vw]
max-w-none
max-h-none
overflow-visible
z-[50]
`}
w-[78vw]
max-w-none
min-h-[74vh]
max-h-none
overflow-visible
z-[50]
`}
body={renderBody()}
noFooterYn = 'Y'
noFooterYn="Y"
/>
)
}
);
};
export default React.memo(RdiiChart);
export default React.memo(RdiiChart);