366 lines
11 KiB
TypeScript
366 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 { TInftwtAnls, TInftwtAnlsRslt, TOilqData } from "./aplcnYms";
|
|
import * as fn from "./aplcnYms";
|
|
|
|
type ModalOpenProp = {
|
|
pplMngNo?: string;
|
|
inftwtAnlsSn?: number;
|
|
aplcnYm?: string;
|
|
analysisInfo?: string;
|
|
resultOnly?: boolean;
|
|
};
|
|
|
|
type DataType = {
|
|
label: string;
|
|
val: number;
|
|
};
|
|
|
|
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: "선택" }];
|
|
|
|
const InfilChart = ({
|
|
pplMngNo = "",
|
|
inftwtAnlsSn = 0,
|
|
aplcnYm = "",
|
|
analysisInfo = "",
|
|
resultOnly = false,
|
|
}: ModalOpenProp) => {
|
|
const [selectedAplcnYm, setSelectedAplcnYm] = useState("");
|
|
const [inftwtAnlss, setInftwtAnlss] = useState<TInftwtAnls[]>([]);
|
|
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: [],
|
|
});
|
|
const [inftwtQty, setInftwtQty] = useState<string>("0");
|
|
|
|
const aplcnYms = fn.useAplcnYms("inftwtAnlsRslt");
|
|
const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : "";
|
|
const isResultOnlyMode =
|
|
resultOnly && inftwtAnlsSn > 0 && Boolean(normalizedPplMngNo);
|
|
|
|
const chartForm = CHART_FORM.LINE;
|
|
const chartConfig = {
|
|
chartData1: { label: "순오수량", color: "red" },
|
|
chartData2: { label: "실측하수량", color: "orange" },
|
|
} as ChartConfig;
|
|
|
|
const hasChartData = useMemo(() => {
|
|
return chartData.chartData1.length > 0 || chartData.chartData2.length > 0;
|
|
}, [chartData]);
|
|
|
|
const calculateFallbackInftwtQty = (
|
|
inftwtAnlsRslt: TInftwtAnlsRslt[],
|
|
oilqRows: TOilqData[],
|
|
) => {
|
|
const predcValues = inftwtAnlsRslt
|
|
.map((item) => Number(item.predcOilq))
|
|
.filter((value) => Number.isFinite(value));
|
|
const oilqValues = oilqRows
|
|
.map((item) => Number(item.oilq))
|
|
.filter((value) => Number.isFinite(value));
|
|
|
|
if (predcValues.length === 0 || oilqValues.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
const avgPredc =
|
|
predcValues.reduce((sum, value) => sum + value, 0) / predcValues.length;
|
|
const avgOilq =
|
|
oilqValues.reduce((sum, value) => sum + value, 0) / oilqValues.length;
|
|
const dailyQty = (avgOilq - avgPredc) * 24;
|
|
return Math.round(dailyQty / 10) * 10;
|
|
};
|
|
|
|
const resetState = () => {
|
|
setSelectedAplcnYm("");
|
|
setInftwtAnlss([]);
|
|
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
|
setInftwtAnls(null);
|
|
setSelectedInftwtAnls(0);
|
|
setChartData({ chartData1: [], chartData2: [] });
|
|
setInftwtQty("0");
|
|
};
|
|
|
|
const loadResultChart = async (targetInftwtAnlsSn: number) => {
|
|
if (!targetInftwtAnlsSn || !normalizedPplMngNo) {
|
|
setChartData({ chartData1: [], chartData2: [] });
|
|
setInftwtQty("0");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const [inftwtAnlsRslt, oilq, qty] = await Promise.all([
|
|
fn.selectInftwtAnlsRslt(targetInftwtAnlsSn, normalizedPplMngNo),
|
|
fn.selectOilq(targetInftwtAnlsSn, normalizedPplMngNo).catch((err) => {
|
|
console.error("selectOilq 에러:", err);
|
|
return [];
|
|
}),
|
|
fn
|
|
.selectPplInftwtQty(targetInftwtAnlsSn, normalizedPplMngNo)
|
|
.catch((err) => {
|
|
console.error("selectPplInftwtQty 에러:", err);
|
|
return 0;
|
|
}),
|
|
]);
|
|
|
|
setChartData({
|
|
chartData1: fn.getChartDataInftwtAnlsRslt(inftwtAnlsRslt ?? []),
|
|
chartData2: fn.getChartDataOilq(oilq ?? []),
|
|
});
|
|
|
|
const fallbackQty = calculateFallbackInftwtQty(
|
|
inftwtAnlsRslt ?? [],
|
|
oilq ?? [],
|
|
);
|
|
const normalizedQty = Number(qty ?? Number.NaN);
|
|
const resolvedQty =
|
|
Number.isFinite(normalizedQty) &&
|
|
!(normalizedQty === 0 && fallbackQty !== 0)
|
|
? normalizedQty
|
|
: fallbackQty;
|
|
|
|
if (
|
|
!Number.isFinite(normalizedQty) ||
|
|
(normalizedQty === 0 && fallbackQty !== 0)
|
|
) {
|
|
console.warn(
|
|
"Inftwt qty API returned empty or zero; using chart-data fallback qty.",
|
|
{
|
|
apiQty: qty,
|
|
fallbackQty,
|
|
resultCount: inftwtAnlsRslt?.length ?? 0,
|
|
oilqCount: oilq?.length ?? 0,
|
|
},
|
|
);
|
|
}
|
|
|
|
setInftwtQty(resolvedQty.toLocaleString("ko-KR"));
|
|
} catch (error) {
|
|
console.error("침입수 분석결과 차트 조회 중 오류:", error);
|
|
setChartData({ chartData1: [], chartData2: [] });
|
|
setInftwtQty("0");
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!isResultOnlyMode) return;
|
|
|
|
setSelectedAplcnYm(aplcnYm);
|
|
setSelectedInftwtAnls(inftwtAnlsSn);
|
|
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
|
void loadResultChart(inftwtAnlsSn);
|
|
}, [aplcnYm, inftwtAnlsSn, isResultOnlyMode, normalizedPplMngNo]);
|
|
|
|
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") {
|
|
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = await fn.selectInftwtAnls(selectedValue);
|
|
const list = data ?? [];
|
|
setInftwtAnlss(list);
|
|
setInftwtAnlsInfos(fn.getOptionsInftwtAnlsInfo(list));
|
|
} catch (error) {
|
|
console.error("selectInftwtAnls 에러:", error);
|
|
setInftwtAnlss([]);
|
|
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
|
}
|
|
};
|
|
|
|
const handleInftwtAnlsSnChange = (val: string | number) => {
|
|
const selectedValue = Number(val);
|
|
|
|
setSelectedInftwtAnls(selectedValue);
|
|
setChartData({ chartData1: [], chartData2: [] });
|
|
setInftwtQty("0");
|
|
|
|
if (selectedValue === 0) {
|
|
setInftwtAnls(null);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
await loadResultChart(selectedInftwtAnls);
|
|
};
|
|
|
|
/** 분석 메타데이터를 공통 표형 폼의 읽기 전용 셀로 표시한다. */
|
|
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 handleReset = () => {
|
|
resetState();
|
|
};
|
|
|
|
const renderResultPanel = () => (
|
|
<section className="card card-border !m-0 min-w-0 overflow-hidden !p-[1.2rem]">
|
|
<StandardFormSection title="침입수 분석결과" 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="flex min-h-[3.2rem] items-center justify-end gap-3 whitespace-nowrap text-foreground">
|
|
<span className="text-[1.15rem] text-muted-foreground">
|
|
침입수량
|
|
</span>
|
|
<div className="flex items-baseline gap-2">
|
|
<strong className="text-[1.5rem] font-semibold tabular-nums">
|
|
{inftwtQty}
|
|
</strong>
|
|
<span className="text-[1.1rem]">m³/d</span>
|
|
</div>
|
|
</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">
|
|
침입수량 그래프
|
|
</h4>
|
|
<div className="min-h-[30rem] p-3">
|
|
{hasChartData ? (
|
|
<MainChart
|
|
chartConfig={chartConfig}
|
|
chartForm={chartForm}
|
|
chartData={chartData}
|
|
chartTitle=""
|
|
yFieldText="유량(m³/h)"
|
|
yFieldText2=""
|
|
/>
|
|
) : (
|
|
<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="침입수 분석 조건" 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={inftwtAnlsInfos}
|
|
value={selectedInftwtAnls}
|
|
onSelectValue={handleInftwtAnlsSnChange}
|
|
/>
|
|
</StandardFormField>
|
|
{renderInfoField("오수전환율", inftwtAnls?.drtwSwtcRt ?? "")}
|
|
{renderInfoField(
|
|
"시간패턴 파일명",
|
|
inftwtAnls?.atflHrPttnNm ?? "",
|
|
)}
|
|
{renderInfoField("수용가-맨홀 매칭", inftwtAnls?.atflMpngNm ?? "")}
|
|
{renderInfoField("관망도 파일명", inftwtAnls?.atflInpNm ?? "")}
|
|
{renderInfoField("유량 파일명", inftwtAnls?.atflOilqNm ?? "", 2)}
|
|
</StandardFormSection>
|
|
|
|
{renderResultPanel()}
|
|
</div>
|
|
);
|
|
|
|
/** 조회·초기화 뒤에 창 닫기 성격의 취소 액션을 항상 마지막에 배치한다. */
|
|
const renderFooterActions = () => (
|
|
<div className="flex w-full items-center justify-center gap-2">
|
|
{!isResultOnlyMode ? (
|
|
<>
|
|
<Button type="button" onClick={infilRsltChart}>
|
|
조회
|
|
</Button>
|
|
<Button type="button" color="white" onClick={handleReset}>
|
|
초기화
|
|
</Button>
|
|
</>
|
|
) : null}
|
|
<Button type="button" color="white" onClick={() => window.close()}>
|
|
취소
|
|
</Button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<StandardPopupLayout
|
|
title={isResultOnlyMode ? "침입수 분석결과" : "침입수 분석"}
|
|
bodyClassName="overflow-x-hidden"
|
|
footer={renderFooterActions()}
|
|
showDefaultCancelAction={false}
|
|
>
|
|
{isResultOnlyMode ? renderResultPanel() : renderDefaultBody()}
|
|
</StandardPopupLayout>
|
|
);
|
|
};
|
|
|
|
export default React.memo(InfilChart);
|