Files
frontend/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx
T

330 lines
12 KiB
TypeScript

"use client";
import React, { useEffect, useMemo, useState } from "react";
import moment from "moment";
import {
CHART_FORM,
type ChartConfig,
MainChart,
} from "@common/business_components";
import { SelectBox } from "@common/business_components/ui";
import { Button, Input, Label } 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 renderInfoInput = (label: string, value?: string | number | null) => (
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] 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>
);
const renderResultPanel = () => (
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-4">
<h3 className="mb-4 text-[20px] font-extrabold tracking-[-0.02em] text-black">RDII 분석 결과</h3>
<div className="mb-4 grid grid-cols-2 gap-4">
<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>상수사용 적용연월</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="shrink-0 whitespace-nowrap text-[14px] font-extrabold text-[#ef4444]">
<span>강우유입수량 분석결과</span>
</div>
<div className="ml-4 flex min-w-0 basis-[60%] flex-col gap-[2px]">
<div className="flex min-w-0 items-center gap-3">
<div className="min-w-0 basis-[48%] text-right text-[15px] text-[#555]">평균RDII</div>
<div className="min-w-0 basis-[32%] 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-[48%] text-right text-[15px] text-[#555]">적용 강우량</div>
<div className="min-w-0 basis-[32%] 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-[48%] text-right text-[15px] text-[#555]">강우량 대비 RDII</div>
<div className="min-w-0 basis-[32%] 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>
);
const renderDefaultBody = () => (
<div className="text-black">
<div className="grid min-h-[68vh] grid-cols-[minmax(0,36%)_minmax(0,64%)] 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-[minmax(0,32%)_minmax(0,68%)] 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={selectedAplcnYm}
onSelectValue={handleAplcnYmChange}
/>
</div>
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] 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={selectedRainInfwtAnlsSn}
onSelectValue={handleRdiiAnlsSnChange}
/>
</div>
{renderInfoInput("오수전환율", rdiiAnls?.drtwSwtcRt ?? "")}
{renderInfoInput("수용가-맨홀 매칭:", rdiiAnls?.atflMpngNm ?? "")}
{renderInfoInput("관망도 파일명", rdiiAnls?.atflInpNm ?? "")}
{renderInfoInput("강우 파일명", rdiiAnls?.atflRainNm ?? "")}
{renderInfoInput("RTK계수 출처:", rdiiAnls?.srcNm ?? "")}
</div>
</div>
<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>
</section>
{renderResultPanel()}
</div>
</div>
);
if (isResultOnlyMode) {
return <div className="min-h-dvh bg-white p-3">{renderResultPanel()}</div>;
}
return (
<div className="min-h-dvh bg-white p-4">
<h1 className="mb-3 text-2xl font-semibold text-gray-900">RDII 분석</h1>
{renderDefaultBody()}
</div>
);
};
export default React.memo(RdiiChart);