345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useMemo, useState } from "react";
|
|
import moment from "moment";
|
|
|
|
import {
|
|
CHART_FORM,
|
|
type ChartConfig,
|
|
MainChart,
|
|
StandardFormField,
|
|
StandardFormSection,
|
|
StandardPopupLayout,
|
|
} from "@common/business_components";
|
|
import { SelectBox } from "@common/business_components/ui";
|
|
import { Button, Input } from "@common/components";
|
|
|
|
import type { TRdiiAnls } from "./aplcnYms";
|
|
import * as fn from "./aplcnYms";
|
|
|
|
type ModalOpenProp = {
|
|
pplMngNo?: string;
|
|
rainInfwtAnlsSn?: number;
|
|
aplcnYm?: string;
|
|
analysisInfo?: string;
|
|
resultOnly?: boolean;
|
|
};
|
|
|
|
type LineBarChartData = {
|
|
label: string;
|
|
lineVal0: number;
|
|
barVal0: number;
|
|
};
|
|
|
|
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: "선택" }];
|
|
|
|
const normalizePplMngNo = (value: string) => {
|
|
const trimmed = String(value ?? "").trim();
|
|
if (!trimmed) return "";
|
|
return trimmed.startsWith("L") ? trimmed : `L${trimmed}`;
|
|
};
|
|
|
|
const RdiiChart = ({
|
|
pplMngNo = "",
|
|
rainInfwtAnlsSn = 0,
|
|
aplcnYm = "",
|
|
analysisInfo = "",
|
|
resultOnly = false,
|
|
}: ModalOpenProp) => {
|
|
const [selectedAplcnYm, setSelectedAplcnYm] = useState("");
|
|
const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]);
|
|
const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<
|
|
{ text: string; val: number; hidden?: boolean }[]
|
|
>(INITIAL_ANALYSIS_OPTIONS);
|
|
const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null);
|
|
const [selectedRainInfwtAnlsSn, setSelectedRainInfwtAnlsSn] =
|
|
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");
|
|
|
|
const normalizedPplMngNo = normalizePplMngNo(pplMngNo);
|
|
const isResultOnlyMode =
|
|
resultOnly && rainInfwtAnlsSn > 0 && Boolean(normalizedPplMngNo);
|
|
const aplcnYms = fn.useAplcnYms(isResultOnlyMode ? "" : "rdiiAnlsRslt");
|
|
|
|
const chartForm = CHART_FORM.LINEBAR2;
|
|
const chartConfig = {
|
|
barVal0: {
|
|
label: "강수량",
|
|
color: "#33AAFF",
|
|
},
|
|
lineVal0: {
|
|
label: "RDII",
|
|
color: "#FF0000",
|
|
},
|
|
} as ChartConfig;
|
|
|
|
const hasChartData = useMemo(() => chartData.length > 0, [chartData]);
|
|
|
|
const resetState = () => {
|
|
setSelectedAplcnYm("");
|
|
setRdiiAnlss([]);
|
|
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
|
setRdiiAnls(null);
|
|
setSelectedRainInfwtAnlsSn(0);
|
|
setChartData([]);
|
|
setRdiiQty("0");
|
|
setRainTotal("0");
|
|
setTotalVolume("0");
|
|
};
|
|
|
|
const loadResultChart = async (targetRainInfwtAnlsSn: number) => {
|
|
if (!targetRainInfwtAnlsSn || !normalizedPplMngNo) {
|
|
setChartData([]);
|
|
setRdiiQty("0");
|
|
setRainTotal("0");
|
|
setTotalVolume("0");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const [rsltData, rainData, qtyData] = await Promise.all([
|
|
fn.selectRdiiAnlsRslt(targetRainInfwtAnlsSn, normalizedPplMngNo),
|
|
fn.selectRainData(targetRainInfwtAnlsSn),
|
|
fn.selectPplRdiiQty(targetRainInfwtAnlsSn, 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("RDII 분석결과 차트 조회 중 오류:", error);
|
|
setChartData([]);
|
|
setRdiiQty("0");
|
|
setRainTotal("0");
|
|
setTotalVolume("0");
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!isResultOnlyMode) return;
|
|
|
|
setSelectedAplcnYm(aplcnYm);
|
|
setSelectedRainInfwtAnlsSn(rainInfwtAnlsSn);
|
|
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
|
void loadResultChart(rainInfwtAnlsSn);
|
|
}, [aplcnYm, isResultOnlyMode, normalizedPplMngNo, rainInfwtAnlsSn]);
|
|
|
|
const handleAplcnYmChange = async (val: string | number) => {
|
|
const selectedValue = String(val);
|
|
|
|
setSelectedAplcnYm(selectedValue);
|
|
setRdiiAnlss([]);
|
|
setRdiiAnls(null);
|
|
setSelectedRainInfwtAnlsSn(0);
|
|
setChartData([]);
|
|
setRdiiQty("0");
|
|
setRainTotal("0");
|
|
setTotalVolume("0");
|
|
|
|
if (!selectedValue || selectedValue === "0") {
|
|
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
|
return;
|
|
}
|
|
|
|
const data = await fn.selectRdiiAnls(selectedValue);
|
|
const list = data ?? [];
|
|
setRdiiAnlss(list);
|
|
setRdiiAnlsInfos(fn.getOptionsRdiiAnlsInfo(list));
|
|
};
|
|
|
|
const handleRdiiAnlsSnChange = (val: string | number) => {
|
|
const selectedValue = Number(val);
|
|
|
|
setSelectedRainInfwtAnlsSn(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 (!selectedRainInfwtAnlsSn || !normalizedPplMngNo) {
|
|
setChartData([]);
|
|
setRdiiQty("0");
|
|
setRainTotal("0");
|
|
setTotalVolume("0");
|
|
return;
|
|
}
|
|
|
|
await loadResultChart(selectedRainInfwtAnlsSn);
|
|
};
|
|
|
|
/** 분석 메타데이터를 공통 표형 폼의 읽기 전용 셀로 표시한다. */
|
|
const renderInfoField = (
|
|
label: string,
|
|
value?: string | number | null,
|
|
span: 1 | 2 = 1,
|
|
) => (
|
|
<StandardFormField label={label} columns={2} span={span}>
|
|
<Input className="w-full" value={value ?? ""} readOnly />
|
|
</StandardFormField>
|
|
);
|
|
|
|
const renderResultPanel = () => (
|
|
<section className="card card-border !m-0 min-w-0 overflow-hidden !p-[1.2rem]">
|
|
<StandardFormSection title="RDII 분석결과" columns={2}>
|
|
<StandardFormField label="상수사용 적용연월" columns={2}>
|
|
<div className="flex min-h-[3.2rem] items-center justify-end text-[1.35rem] font-semibold tabular-nums text-foreground">
|
|
{selectedAplcnYm
|
|
? moment(selectedAplcnYm, "YYYYMM").format("YYYY-MM")
|
|
: "-"}
|
|
</div>
|
|
</StandardFormField>
|
|
<StandardFormField label="강우유입수량 분석결과" columns={2}>
|
|
<div className="grid min-h-[3.2rem] grid-cols-[minmax(0,1fr)_7rem_5rem] items-center gap-x-3 gap-y-1 text-[1.1rem] text-foreground">
|
|
<span className="text-right text-muted-foreground">평균 RDII</span>
|
|
<strong className="text-right text-[1.35rem] font-semibold tabular-nums">
|
|
{rdiiQty}
|
|
</strong>
|
|
<span>m³/d</span>
|
|
<span className="text-right text-muted-foreground">적용 강우량</span>
|
|
<strong className="text-right text-[1.35rem] font-semibold tabular-nums">
|
|
{rainTotal}
|
|
</strong>
|
|
<span>mm</span>
|
|
<span className="text-right text-muted-foreground">
|
|
강우량 대비 RDII
|
|
</span>
|
|
<strong className="text-right text-[1.35rem] font-semibold tabular-nums">
|
|
{totalVolume}
|
|
</strong>
|
|
<span>m³/mm</span>
|
|
</div>
|
|
</StandardFormField>
|
|
</StandardFormSection>
|
|
|
|
<section className="mt-[1.2rem] overflow-hidden rounded-sm border border-border bg-background">
|
|
<h4 className="border-b border-border bg-container px-4 py-3 text-[1.4rem] font-semibold text-label">
|
|
RDII 그래프
|
|
</h4>
|
|
<div className="min-h-[30rem] p-3">
|
|
{hasChartData ? (
|
|
<MainChart
|
|
chartConfig={chartConfig}
|
|
chartForm={chartForm}
|
|
chartData={chartData}
|
|
chartTitle=""
|
|
yFieldText2="강수량(mm)"
|
|
yFieldText="유량(m³/h)"
|
|
/>
|
|
) : (
|
|
<div
|
|
className="flex min-h-[30rem] items-center justify-center text-[1.3rem] text-muted-foreground"
|
|
role="status"
|
|
>
|
|
조회된 데이터가 없습니다.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</section>
|
|
);
|
|
|
|
const renderDefaultBody = () => (
|
|
<div className="flex min-h-0 flex-1 flex-col gap-[1.2rem]">
|
|
<StandardFormSection title="RDII 분석 조건" columns={2}>
|
|
<StandardFormField label="상수사용월" columns={2}>
|
|
<SelectBox
|
|
className="w-full"
|
|
selectClass="w-full"
|
|
label=""
|
|
selectArray={aplcnYms}
|
|
value={selectedAplcnYm}
|
|
onSelectValue={handleAplcnYmChange}
|
|
/>
|
|
</StandardFormField>
|
|
<StandardFormField label="분석정보" columns={2}>
|
|
<SelectBox
|
|
className="w-full"
|
|
selectClass="w-full"
|
|
label=""
|
|
selectArray={rdiiAnlsInfos}
|
|
value={selectedRainInfwtAnlsSn}
|
|
onSelectValue={handleRdiiAnlsSnChange}
|
|
/>
|
|
</StandardFormField>
|
|
{renderInfoField("오수전환율", rdiiAnls?.drtwSwtcRt ?? "")}
|
|
{renderInfoField("수용가-맨홀 매칭", rdiiAnls?.atflMpngNm ?? "")}
|
|
{renderInfoField("관망도 파일명", rdiiAnls?.atflInpNm ?? "")}
|
|
{renderInfoField("강우 파일명", rdiiAnls?.atflRainNm ?? "")}
|
|
{renderInfoField("RTK계수 출처", rdiiAnls?.srcNm ?? "", 2)}
|
|
</StandardFormSection>
|
|
|
|
{renderResultPanel()}
|
|
</div>
|
|
);
|
|
|
|
/** 조회·초기화 뒤에 창 닫기 성격의 취소 액션을 항상 마지막에 배치한다. */
|
|
const renderFooterActions = () => (
|
|
<div className="flex w-full items-center justify-center gap-2">
|
|
{!isResultOnlyMode ? (
|
|
<>
|
|
<Button type="button" onClick={fetchFilteredData}>
|
|
조회
|
|
</Button>
|
|
<Button type="button" color="white" onClick={resetState}>
|
|
초기화
|
|
</Button>
|
|
</>
|
|
) : null}
|
|
<Button type="button" color="white" onClick={() => window.close()}>
|
|
취소
|
|
</Button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<StandardPopupLayout
|
|
title={isResultOnlyMode ? "RDII 분석결과" : "RDII 분석"}
|
|
bodyClassName="overflow-x-hidden"
|
|
footer={renderFooterActions()}
|
|
showDefaultCancelAction={false}
|
|
>
|
|
{isResultOnlyMode ? renderResultPanel() : renderDefaultBody()}
|
|
</StandardPopupLayout>
|
|
);
|
|
};
|
|
|
|
export default React.memo(RdiiChart);
|