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

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);