RDII 분석결과 팝업 지도연계 및 결과전용 팝업 적용
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
ChartColumnBig,
|
ChartColumnBig,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Droplets,
|
|
||||||
PanelsTopLeft,
|
PanelsTopLeft,
|
||||||
PencilRuler,
|
PencilRuler,
|
||||||
Search,
|
Search,
|
||||||
@@ -23,10 +22,40 @@ type Props = {
|
|||||||
isInfoSearchActive?: boolean;
|
isInfoSearchActive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MenuIconProps = {
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InftwtMenuIcon = ({ isActive }: MenuIconProps) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className={`h-[15px] w-[15px] shrink-0 ${isActive ? "text-[#1e88e5]" : "text-[#64748b]"}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2.8C9.5 6.5 6.7 9.8 6.7 13.3A5.3 5.3 0 0 0 12 18.6a5.3 5.3 0 0 0 5.3-5.3c0-3.5-2.8-6.8-5.3-10.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
fillOpacity="0.14"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9.5 13.4c0-1.2.8-2.6 1.9-4.2"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
opacity="0.9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const ANALYSIS_POPUP_ITEMS: Array<{
|
const ANALYSIS_POPUP_ITEMS: Array<{
|
||||||
key: WindowPopupKey | "infoSearch";
|
key: WindowPopupKey | "infoSearch";
|
||||||
label: string;
|
label: string;
|
||||||
icon: typeof ChartColumnBig;
|
icon?: ComponentType<{ className?: string }>;
|
||||||
|
renderIcon?: (props: MenuIconProps) => ReactNode;
|
||||||
}> = [
|
}> = [
|
||||||
{ key: "infoSearch", label: "정보검색", icon: Search },
|
{ key: "infoSearch", label: "정보검색", icon: Search },
|
||||||
{ key: "pipeAnalysisPopup", label: "관로분석", icon: ChartColumnBig },
|
{ key: "pipeAnalysisPopup", label: "관로분석", icon: ChartColumnBig },
|
||||||
@@ -34,7 +63,8 @@ const ANALYSIS_POPUP_ITEMS: Array<{
|
|||||||
{ key: "gisEditPopup", label: "GIS 편집", icon: PencilRuler },
|
{ key: "gisEditPopup", label: "GIS 편집", icon: PencilRuler },
|
||||||
{ key: "gisManagementPopup", label: "GIS 관리", icon: Settings2 },
|
{ key: "gisManagementPopup", label: "GIS 관리", icon: Settings2 },
|
||||||
{ key: "flowRiskPopup", label: "통수 위험", icon: TriangleAlert },
|
{ key: "flowRiskPopup", label: "통수 위험", icon: TriangleAlert },
|
||||||
{ key: "inftwtResultPopup", label: "침입수 분석", icon: Droplets },
|
{ key: "inftwtResultPopup", label: "침입수 분석결과", renderIcon: InftwtMenuIcon },
|
||||||
|
{ key: "rdiiResultPopup", label: "RDII 분석결과", icon: ChartColumnBig },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AnalysisPopupMenu({
|
export default function AnalysisPopupMenu({
|
||||||
@@ -63,7 +93,7 @@ export default function AnalysisPopupMenu({
|
|||||||
<div className="absolute right-[0.55rem] top-[20px] z-[45]">
|
<div className="absolute right-[0.55rem] top-[20px] z-[45]">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<div className="absolute right-[28px] w-[132px] overflow-hidden rounded-[6px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] shadow-[0_8px_18px_rgba(15,23,42,0.14)]">
|
<div className="absolute right-[28px] w-[154px] overflow-hidden rounded-[6px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] shadow-[0_8px_18px_rgba(15,23,42,0.14)]">
|
||||||
<div className="flex h-[36px] items-center gap-2 border-b border-[#e5eaf1] bg-[#f8fafc] px-[10px] text-[13px] font-bold text-[#1f2937]">
|
<div className="flex h-[36px] items-center gap-2 border-b border-[#e5eaf1] bg-[#f8fafc] px-[10px] text-[13px] font-bold text-[#1f2937]">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<PanelsTopLeft className="h-4 w-4 text-[#1e88e5]" />
|
<PanelsTopLeft className="h-4 w-4 text-[#1e88e5]" />
|
||||||
@@ -116,12 +146,15 @@ export default function AnalysisPopupMenu({
|
|||||||
}, 300);
|
}, 300);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
{item.renderIcon ? item.renderIcon({ isActive }) : null}
|
||||||
className={`h-[15px] w-[15px] shrink-0 ${
|
{Icon ? (
|
||||||
isActive ? "text-[#1e88e5]" : "text-[#64748b]"
|
<Icon
|
||||||
}`}
|
className={`h-[15px] w-[15px] shrink-0 ${
|
||||||
/>
|
isActive ? "text-[#1e88e5]" : "text-[#64748b]"
|
||||||
<span className="break-keep whitespace-normal">{item.label}</span>
|
}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className="whitespace-nowrap">{item.label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -141,4 +174,4 @@ export default function AnalysisPopupMenu({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {useState, useEffect} from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { getData } from '@/components/lib/api';
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { getData } from "@/components/lib/api";
|
||||||
|
|
||||||
export type TInftwtAnls = {
|
export type TInftwtAnls = {
|
||||||
inftwtAnlsSn:number;
|
inftwtAnlsSn: number;
|
||||||
aplcnYm: string;
|
aplcnYm: string;
|
||||||
ctpvCd: string;
|
ctpvCd: string;
|
||||||
sggCd: string;
|
sggCd: string;
|
||||||
@@ -15,18 +15,20 @@ export type TInftwtAnls = {
|
|||||||
atflMpngNm: string;
|
atflMpngNm: string;
|
||||||
atflOilqNm: string;
|
atflOilqNm: string;
|
||||||
atflNm: string;
|
atflNm: string;
|
||||||
drtwSwtcRt:number;
|
drtwSwtcRt: number;
|
||||||
anlsInfo: string;
|
anlsInfo: string;
|
||||||
scsYn: string;
|
scsYn: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TInftwtAnlsRslt = {
|
export type TInftwtAnlsRslt = {
|
||||||
inftwtAnlsSn: number;
|
inftwtAnlsSn: number;
|
||||||
pplMngNo: string;
|
pplMngNo: string;
|
||||||
anlsTm: string;
|
anlsTm: string;
|
||||||
predcOilq:number;
|
predcOilq: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TRdiiAnls = {
|
export type TRdiiAnls = {
|
||||||
rainInfwtAnlsSn:number;
|
rainInfwtAnlsSn: number;
|
||||||
aplcnYm: string;
|
aplcnYm: string;
|
||||||
ctpvCd: string;
|
ctpvCd: string;
|
||||||
sggCd: string;
|
sggCd: string;
|
||||||
@@ -38,234 +40,259 @@ export type TRdiiAnls = {
|
|||||||
atflMpngNm: string;
|
atflMpngNm: string;
|
||||||
atflRainNm: string;
|
atflRainNm: string;
|
||||||
atflNm: string;
|
atflNm: string;
|
||||||
drtwSwtcRt:number;
|
drtwSwtcRt: number;
|
||||||
anlsInfo: string;
|
anlsInfo: string;
|
||||||
scsYn: string;
|
scsYn: string;
|
||||||
srcNm: string; // 출처이름
|
srcNm: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TRdiiAnlsRslt = {
|
export type TRdiiAnlsRslt = {
|
||||||
rainInfwtAnlsSn: number;
|
rainInfwtAnlsSn: number;
|
||||||
pplMngNo: string;
|
pplMngNo: string;
|
||||||
anlsTm: string;
|
anlsTm: string;
|
||||||
predcOilq:number;
|
predcOilq: number;
|
||||||
minq:number;
|
minq: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TOilqData = {
|
export type TOilqData = {
|
||||||
inftwtAnlsSn: number; // 침입수분석 일련번호
|
inftwtAnlsSn: number;
|
||||||
pplMngNo: string; // 관로관리번호
|
pplMngNo: string;
|
||||||
mtrngDt: string; // 계측일시
|
mtrngDt: string;
|
||||||
oilq: number; // 유량
|
oilq: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TRainData = {
|
export type TRainData = {
|
||||||
rainInfwtAnlsSn: number;
|
rainInfwtAnlsSn: number;
|
||||||
mtrngDt: string;
|
mtrngDt: string;
|
||||||
amrf: number; // 강우량
|
amrf: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
function roundTo(n: number, digits: number): number {
|
function roundTo(n: number, digits: number): number {
|
||||||
const factor = Math.pow(10, digits);
|
const factor = Math.pow(10, digits);
|
||||||
return Math.round(n * factor) / factor;
|
return Math.round(n * factor) / factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string, hidden?: boolean }[] => {
|
const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string; hidden?: boolean }[] => {
|
||||||
const yyyymms1 : { text: string; val: string; hidden?: boolean }[] = yyyymms.map(dateStr => {
|
const mapped: { text: string; val: string; hidden?: boolean }[] = yyyymms.map((dateStr) => ({
|
||||||
const year = dateStr.slice(0, 4);
|
text: `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}`,
|
||||||
const month = dateStr.slice(4, 6);
|
val: dateStr,
|
||||||
return {
|
}));
|
||||||
text: `${year}-${month}`,
|
mapped.unshift({ val: "0", text: "선택", hidden: true });
|
||||||
val: dateStr
|
return mapped;
|
||||||
};
|
|
||||||
});
|
|
||||||
yyyymms1.unshift({ val: '0', text: '선택', hidden: true });
|
|
||||||
return yyyymms1;
|
|
||||||
};
|
|
||||||
const selectAplcnYms = async(gubun:string) => { // 상수사용월
|
|
||||||
try {
|
|
||||||
const queryString = '';
|
|
||||||
const result = await getData(`v1/${gubun}/aplcnYm${queryString ? `?${queryString}` : ''}`);
|
|
||||||
if (result && result.length > 0) {
|
|
||||||
return getOptionsAplcnYm(result);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAplcnYms = (gubun:string) => {
|
const selectAplcnYms = async (gubun: string) => {
|
||||||
const [data, setData] = useState<{ text: string; val: string }[]>([]);
|
if (!gubun) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getData(`v1/${gubun}/aplcnYm`);
|
||||||
|
if (Array.isArray(result) && result.length > 0) {
|
||||||
|
return getOptionsAplcnYm(result);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAplcnYms = (gubun: string) => {
|
||||||
|
const [data, setData] = useState<{ text: string; val: string; hidden?: boolean }[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetch = async (gubun:string) => {
|
if (!gubun) {
|
||||||
|
setData([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetch = async () => {
|
||||||
const result = await selectAplcnYms(gubun);
|
const result = await selectAplcnYms(gubun);
|
||||||
if (result) setData(result);
|
setData(result ?? []);
|
||||||
};
|
};
|
||||||
fetch(gubun);
|
|
||||||
|
void fetch();
|
||||||
}, [gubun]);
|
}, [gubun]);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
export const getOptionsInftwtAnlsInfo = (anlsArr: TInftwtAnls[]): { text: string; val: number, hidden?: boolean }[] => {
|
|
||||||
const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({
|
export const getOptionsInftwtAnlsInfo = (anlsArr: TInftwtAnls[]): { text: string; val: number; hidden?: boolean }[] => {
|
||||||
|
const options: { text: string; val: number; hidden?: boolean }[] = anlsArr.map((anls) => ({
|
||||||
text: anls.anlsInfo,
|
text: anls.anlsInfo,
|
||||||
val: anls.inftwtAnlsSn,
|
val: anls.inftwtAnlsSn,
|
||||||
}));
|
}));
|
||||||
options.unshift({ val: 0, text: '선택', hidden: true });
|
options.unshift({ val: 0, text: "선택", hidden: true });
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectInftwtAnls = async(aplcnYm:string) => { // 침입수분석
|
export const selectInftwtAnls = async (aplcnYm: string) => {
|
||||||
try {
|
try {
|
||||||
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : '';
|
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : "";
|
||||||
const result = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`);
|
const result = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ""}`);
|
||||||
if (result && result.numberOfElements > 0) {
|
if (result && result.numberOfElements > 0) {
|
||||||
return result.content;
|
return result.content;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error(error);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
export const getChartDataInftwtAnlsRslt = (arr: TInftwtAnlsRslt[]): { label: string; val: number }[] => {
|
|
||||||
const options = arr.map(anlsRslt => ({
|
export const getChartDataInftwtAnlsRslt = (arr: TInftwtAnlsRslt[]) => {
|
||||||
|
return arr.map((anlsRslt) => ({
|
||||||
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("HH시"),
|
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("HH시"),
|
||||||
val: roundTo(anlsRslt.predcOilq, 2),
|
val: roundTo(anlsRslt.predcOilq, 2),
|
||||||
}));
|
}));
|
||||||
return options;
|
|
||||||
};
|
};
|
||||||
export const selectInftwtAnlsRslt = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수분석 결과
|
|
||||||
|
export const selectInftwtAnlsRslt = async (inftwtAnlsSn: number, pplMngNo: string) => {
|
||||||
try {
|
try {
|
||||||
if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) {
|
if (!inftwtAnlsSn || !pplMngNo) return [];
|
||||||
console.warn("inftwtAnlsSn or pplMngNo is not provided");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const result = await getData(`v1/inftwtAnlsRslt/ppl/${inftwtAnlsSn}/${pplMngNo}`);
|
const result = await getData(`v1/inftwtAnlsRslt/ppl/${inftwtAnlsSn}/${pplMngNo}`);
|
||||||
if (result && result.numberOfElements > 0) {
|
if (result && result.numberOfElements > 0) {
|
||||||
return result.content;
|
return result.content;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error(error);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
export const getChartDataOilq = (arr: TOilqData[]): { label: string; val: number }[] => {
|
|
||||||
const options = arr.map(anlsRslt => ({
|
export const getChartDataOilq = (arr: TOilqData[]) => {
|
||||||
|
return arr.map((anlsRslt) => ({
|
||||||
label: anlsRslt.mtrngDt,
|
label: anlsRslt.mtrngDt,
|
||||||
val: roundTo(anlsRslt.oilq, 2),
|
val: roundTo(anlsRslt.oilq, 2),
|
||||||
}));
|
}));
|
||||||
return options;
|
|
||||||
};
|
|
||||||
export const selectOilq = async(inftwtAnlsSn:number, pplMngNo:string) => { // 유량
|
|
||||||
try {
|
|
||||||
if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) {
|
|
||||||
console.warn("inftwtAnlsSn or pplMngNo is not provided");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const result = await getData(`v1/inftwtAnlsRslt/oilqPpl/${inftwtAnlsSn}/${pplMngNo}`);
|
|
||||||
if (result && result.numberOfElements > 0) {
|
|
||||||
return result.content;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export const selectPplInftwtQty = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수량
|
|
||||||
try {
|
|
||||||
if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) {
|
|
||||||
console.warn("inftwtAnlsSn or pplMngNo is not provided");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const result = await getData(`v1/inftwtAnlsRslt/pplInftwtQty/${inftwtAnlsSn}/${pplMngNo}`);
|
|
||||||
if (result.length > 0) {
|
|
||||||
return result[0].avg_day_flow;
|
|
||||||
} else {
|
|
||||||
return 0; // 데이터가 없을 경우 0 반환
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
//----------
|
|
||||||
export const selectPplRdiiQty = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // rdii값
|
|
||||||
try {
|
|
||||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) {
|
|
||||||
console.warn("rainInfwtAnlsSn or pplMngNo is not provided");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const result = await getData(`v1/rdiiAnlsRslt/pplRdiiQty/${rainInfwtAnlsSn}/${pplMngNo}`);
|
|
||||||
if (result?.sustaintime > 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOptionsRdiiAnlsInfo = (anlsArr: TRdiiAnls[]): { text: string; val: number, hidden?: boolean }[] => {
|
export const selectOilq = async (inftwtAnlsSn: number, pplMngNo: string) => {
|
||||||
const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({
|
try {
|
||||||
|
if (!inftwtAnlsSn || !pplMngNo) return [];
|
||||||
|
const result = await getData(`v1/inftwtAnlsRslt/oilqPpl/${inftwtAnlsSn}/${pplMngNo}`);
|
||||||
|
if (result && result.numberOfElements > 0) {
|
||||||
|
return result.content;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectPplInftwtQty = async (inftwtAnlsSn: number, pplMngNo: string) => {
|
||||||
|
try {
|
||||||
|
if (!inftwtAnlsSn || !pplMngNo) return 0;
|
||||||
|
const result = await getData(`v1/inftwtAnlsRslt/pplInftwtQty/${inftwtAnlsSn}/${pplMngNo}`);
|
||||||
|
if (Array.isArray(result) && result.length > 0) {
|
||||||
|
return result[0].avg_day_flow;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildPplMngNoCandidates = (pplMngNo: string) => {
|
||||||
|
const normalized = String(pplMngNo ?? "").trim();
|
||||||
|
if (!normalized) return [];
|
||||||
|
const withoutPrefix = normalized.replace(/^L/, "");
|
||||||
|
const withPrefix = withoutPrefix ? `L${withoutPrefix}` : "";
|
||||||
|
return Array.from(new Set([normalized, withPrefix, withoutPrefix].filter(Boolean)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectPplRdiiQty = async (rainInfwtAnlsSn: number, pplMngNo: string) => {
|
||||||
|
if (!rainInfwtAnlsSn || !pplMngNo) return null;
|
||||||
|
|
||||||
|
for (const candidate of buildPplMngNoCandidates(pplMngNo)) {
|
||||||
|
try {
|
||||||
|
const result = await getData(`v1/rdiiAnlsRslt/pplRdiiQty/${rainInfwtAnlsSn}/${candidate}`);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// resultOnly mode fallback candidates should not raise console noise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOptionsRdiiAnlsInfo = (anlsArr: TRdiiAnls[]): { text: string; val: number; hidden?: boolean }[] => {
|
||||||
|
const options: { text: string; val: number; hidden?: boolean }[] = anlsArr.map((anls) => ({
|
||||||
text: anls.anlsInfo,
|
text: anls.anlsInfo,
|
||||||
val: anls.rainInfwtAnlsSn,
|
val: anls.rainInfwtAnlsSn,
|
||||||
}));
|
}));
|
||||||
options.unshift({ val: 0, text: '선택', hidden: true });
|
options.unshift({ val: 0, text: "선택", hidden: true });
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectRdiiAnls = async(aplcnYm:string) => { // 침입수분석
|
export const selectRdiiAnls = async (aplcnYm: string) => {
|
||||||
try {
|
try {
|
||||||
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : '';
|
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : "";
|
||||||
const result = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`);
|
const result = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ""}`);
|
||||||
if (result && result.numberOfElements > 0) {
|
if (result && result.numberOfElements > 0) {
|
||||||
return result.content;
|
return result.content;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error(error);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; lineVal0: number, barVal0:number }[] => {
|
|
||||||
const options = arr.map(anlsRslt => ({
|
export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]) => {
|
||||||
|
return arr.map((anlsRslt) => ({
|
||||||
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"),
|
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"),
|
||||||
lineVal0: roundTo(anlsRslt.predcOilq, 2),
|
lineVal0: roundTo(anlsRslt.predcOilq, 2),
|
||||||
barVal0: 0
|
barVal0: 0,
|
||||||
}));
|
}));
|
||||||
return options;
|
|
||||||
};
|
};
|
||||||
export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => {
|
|
||||||
const options = arr.map(anlsRslt => ({
|
export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]) => {
|
||||||
|
return arr.map((anlsRslt) => ({
|
||||||
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"),
|
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"),
|
||||||
val: roundTo(anlsRslt.minq, 2),
|
val: roundTo(anlsRslt.minq, 2),
|
||||||
}));
|
}));
|
||||||
return options;
|
|
||||||
};
|
};
|
||||||
export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // RDII분석 결과
|
|
||||||
try {
|
export const selectRdiiAnlsRslt = async (rainInfwtAnlsSn: number, pplMngNo: string) => {
|
||||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) {
|
if (!rainInfwtAnlsSn || !pplMngNo) return [];
|
||||||
console.warn("RdiiAnlsSn or pplMngNo is not provided");
|
|
||||||
return [];
|
for (const candidate of buildPplMngNoCandidates(pplMngNo)) {
|
||||||
}
|
try {
|
||||||
const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${pplMngNo}`);
|
const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${candidate}`);
|
||||||
if (result && result.numberOfElements > 0) {
|
if (result && result.numberOfElements > 0) {
|
||||||
return result.content;
|
return result.content;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error(error);
|
// resultOnly mode fallback candidates should not raise console noise
|
||||||
}
|
|
||||||
};
|
|
||||||
export const selectRainData = async(rainInfwtAnlsSn:number) => { // 강우데이터
|
|
||||||
try {
|
|
||||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0)) {
|
|
||||||
console.warn("RdiiAnlsSn or pplMngNo is not provided");
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectRainData = async (rainInfwtAnlsSn: number) => {
|
||||||
|
try {
|
||||||
|
if (!rainInfwtAnlsSn) return [];
|
||||||
const result = await getData(`v1/rdiiAnlsRslt/rainData/${rainInfwtAnlsSn}`);
|
const result = await getData(`v1/rdiiAnlsRslt/rainData/${rainInfwtAnlsSn}`);
|
||||||
if (result && result.numberOfElements > 0) {
|
if (result && result.numberOfElements > 0) {
|
||||||
return result.content;
|
return result.content;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error(error);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
export const getChartDataRainData = (arr: TRainData[]): { label: string; lineVal0: number, barVal0: number }[] => {
|
|
||||||
const options = arr.map(data => ({
|
export const getChartDataRainData = (arr: TRainData[]) => {
|
||||||
|
return arr.map((data) => ({
|
||||||
label: moment(data.mtrngDt, "YYYY-MM-DD HH:mm:ss").format("MM-DD HH시"),
|
label: moment(data.mtrngDt, "YYYY-MM-DD HH:mm:ss").format("MM-DD HH시"),
|
||||||
lineVal0: 0,
|
lineVal0: 0,
|
||||||
barVal0: roundTo(data.amrf,2),
|
barVal0: roundTo(data.amrf, 2),
|
||||||
}));
|
}));
|
||||||
return options;
|
|
||||||
};
|
};
|
||||||
@@ -1,21 +1,25 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import moment from 'moment';
|
import moment from "moment";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CHART_FORM,
|
CHART_FORM,
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
MainChart,
|
MainChart,
|
||||||
} from '@common/business_components';
|
} from "@common/business_components";
|
||||||
import { SelectBox } from '@common/business_components/ui';
|
import { SelectBox } from "@common/business_components/ui";
|
||||||
import { Button, Input, Label } from '@common/components';
|
import { Button, Input, Label } from "@common/components";
|
||||||
|
|
||||||
import type { TRdiiAnls } from './aplcnYms';
|
import type { TRdiiAnls } from "./aplcnYms";
|
||||||
import * as fn from './aplcnYms';
|
import * as fn from "./aplcnYms";
|
||||||
|
|
||||||
type ModalOpenProp = {
|
type ModalOpenProp = {
|
||||||
pplMngNo?: string; // 관로관리번호
|
pplMngNo?: string;
|
||||||
|
rainInfwtAnlsSn?: number;
|
||||||
|
aplcnYm?: string;
|
||||||
|
analysisInfo?: string;
|
||||||
|
resultOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LineBarChartData = {
|
type LineBarChartData = {
|
||||||
@@ -24,112 +28,77 @@ type LineBarChartData = {
|
|||||||
barVal0: number;
|
barVal0: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: "선택" }];
|
||||||
|
|
||||||
const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
const normalizePplMngNo = (value: string) => {
|
||||||
|
const trimmed = String(value ?? "").trim();
|
||||||
|
if (!trimmed) return "";
|
||||||
|
return trimmed.startsWith("L") ? trimmed : `L${trimmed}`;
|
||||||
|
};
|
||||||
|
|
||||||
const [aplcnYm, setAplcnYm] = useState('');
|
const RdiiChart = ({
|
||||||
|
pplMngNo = "",
|
||||||
|
rainInfwtAnlsSn = 0,
|
||||||
|
aplcnYm = "",
|
||||||
|
analysisInfo = "",
|
||||||
|
resultOnly = false,
|
||||||
|
}: ModalOpenProp) => {
|
||||||
|
const [selectedAplcnYm, setSelectedAplcnYm] = useState("");
|
||||||
const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]);
|
const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]);
|
||||||
const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>(
|
const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>(
|
||||||
INITIAL_ANALYSIS_OPTIONS,
|
INITIAL_ANALYSIS_OPTIONS,
|
||||||
);
|
);
|
||||||
const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null); // 강우유입수분석정보
|
const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null);
|
||||||
const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState<number>(0);// 강우유입수분석 정보 선택시 사용
|
const [selectedRainInfwtAnlsSn, setSelectedRainInfwtAnlsSn] = useState<number>(0);
|
||||||
const [chartData, setChartData] = useState<LineBarChartData[]>([]);
|
const [chartData, setChartData] = useState<LineBarChartData[]>([]);
|
||||||
const [rdiiQty, setRdiiQty] = useState<string>('0'); // 강우유입수량
|
const [rdiiQty, setRdiiQty] = useState<string>("0");
|
||||||
const [rainTotal, setRainTotal] = useState<string>('0'); // 적용강우량
|
const [rainTotal, setRainTotal] = useState<string>("0");
|
||||||
const [totalVolume, setTotalVolume] = useState<string>('0');// 총체적 m³/mm
|
const [totalVolume, setTotalVolume] = useState<string>("0");
|
||||||
|
|
||||||
const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기
|
const normalizedPplMngNo = normalizePplMngNo(pplMngNo);
|
||||||
const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : '';
|
const isResultOnlyMode = resultOnly && rainInfwtAnlsSn > 0 && Boolean(normalizedPplMngNo);
|
||||||
|
const aplcnYms = fn.useAplcnYms(isResultOnlyMode ? "" : "rdiiAnlsRslt");
|
||||||
|
|
||||||
const chartForm = CHART_FORM.LINEBAR2;
|
const chartForm = CHART_FORM.LINEBAR2;
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
barVal0: {
|
barVal0: {
|
||||||
label: '강수량',
|
label: "강수량",
|
||||||
color: '#33AAFF',
|
color: "#33AAFF",
|
||||||
},
|
},
|
||||||
lineVal0: {
|
lineVal0: {
|
||||||
label: 'RDII',
|
label: "RDII",
|
||||||
color: '#FF0000',
|
color: "#FF0000",
|
||||||
},
|
},
|
||||||
} as ChartConfig;
|
} as ChartConfig;
|
||||||
|
|
||||||
const hasChartData = useMemo(() => chartData.length > 0, [chartData]);
|
const hasChartData = useMemo(() => chartData.length > 0, [chartData]);
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
setAplcnYm(''); // 상수사용월 초기화
|
setSelectedAplcnYm("");
|
||||||
setRdiiAnlss([]); // 강우유입수분석 초기화
|
|
||||||
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS); // 강우유입수분석정보 초기화
|
|
||||||
setRdiiAnls(null); // 강우유입수분석정보 초기화
|
|
||||||
setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화
|
|
||||||
setChartData([]);
|
|
||||||
setRdiiQty('0'); // 강우유입수량 초기화
|
|
||||||
setRainTotal('0');// 적용강우량 초기화
|
|
||||||
setTotalVolume('0');// 총체적 m³/mm 초기화
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAplcnYmChange = async (val: string | number) => {
|
|
||||||
const selectedValue = String(val);
|
|
||||||
|
|
||||||
setAplcnYm(selectedValue);
|
|
||||||
setRdiiAnlss([]);
|
setRdiiAnlss([]);
|
||||||
|
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||||
setRdiiAnls(null);
|
setRdiiAnls(null);
|
||||||
setRainInfwtAnlsSn(0);
|
setSelectedRainInfwtAnlsSn(0);
|
||||||
setChartData([]);
|
setChartData([]);
|
||||||
setRdiiQty('0');
|
setRdiiQty("0");
|
||||||
setRainTotal('0');
|
setRainTotal("0");
|
||||||
setTotalVolume('0');
|
setTotalVolume("0");
|
||||||
|
|
||||||
if (!selectedValue || selectedValue === '0') {
|
|
||||||
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRdiiAnlsSnChange = (val: string | number) => {
|
const loadResultChart = async (targetRainInfwtAnlsSn: number) => {
|
||||||
const selectedValue = Number(val);
|
if (!targetRainInfwtAnlsSn || !normalizedPplMngNo) {
|
||||||
|
|
||||||
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([]);
|
setChartData([]);
|
||||||
setRdiiQty('0');
|
setRdiiQty("0");
|
||||||
setRainTotal('0');
|
setRainTotal("0");
|
||||||
setTotalVolume('0');
|
setTotalVolume("0");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rsltData, rainData, qtyData] = await Promise.all([
|
const [rsltData, rainData, qtyData] = await Promise.all([
|
||||||
fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, normalizedPplMngNo),
|
fn.selectRdiiAnlsRslt(targetRainInfwtAnlsSn, normalizedPplMngNo),
|
||||||
fn.selectRainData(rainInfwtAnlsSn),
|
fn.selectRainData(targetRainInfwtAnlsSn),
|
||||||
fn.selectPplRdiiQty(rainInfwtAnlsSn, normalizedPplMngNo),
|
fn.selectPplRdiiQty(targetRainInfwtAnlsSn, normalizedPplMngNo),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const rsltDataFormatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []);
|
const rsltDataFormatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []);
|
||||||
@@ -143,7 +112,7 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setChartData(rsltDataFormatted);// 차트 데이터 설정
|
setChartData(rsltDataFormatted);
|
||||||
|
|
||||||
const avgRdii = Number(qtyData?.rdiih ?? 0);
|
const avgRdii = Number(qtyData?.rdiih ?? 0);
|
||||||
const appliedRainfall = Number(qtyData?.raintotal ?? 0);
|
const appliedRainfall = Number(qtyData?.raintotal ?? 0);
|
||||||
@@ -152,18 +121,80 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
|||||||
? Math.round(avgRdii * Number(qtyData?.sustaintime ?? 0) / qtyData.raintotal)
|
? Math.round(avgRdii * Number(qtyData?.sustaintime ?? 0) / qtyData.raintotal)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
setRdiiQty(Math.round(avgRdii * 24).toLocaleString('ko-KR'));
|
setRdiiQty(Math.round(avgRdii * 24).toLocaleString("ko-KR"));
|
||||||
setRainTotal(appliedRainfall.toLocaleString('ko-KR'));
|
setRainTotal(appliedRainfall.toLocaleString("ko-KR"));
|
||||||
setTotalVolume(rdiiPerRainfall.toLocaleString('ko-KR'));
|
setTotalVolume(rdiiPerRainfall.toLocaleString("ko-KR"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('rdiiRsltChart 에러:', error);
|
console.error("RDII 분석결과 차트 조회 중 오류:", error);
|
||||||
setChartData([]);
|
setChartData([]);
|
||||||
setRdiiQty('0');
|
setRdiiQty("0");
|
||||||
setRainTotal('0');
|
setRainTotal("0");
|
||||||
setTotalVolume('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) => (
|
const renderInfoInput = (label: string, value?: string | number | null) => (
|
||||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-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 className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
|
||||||
@@ -172,166 +203,128 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
|||||||
<Input
|
<Input
|
||||||
className="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white px-3 text-xl text-[#2b3440]"
|
className="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white px-3 text-xl text-[#2b3440]"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
value={value ?? ''}
|
value={value ?? ""}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderBody = () => (
|
const renderResultPanel = () => (
|
||||||
<div className="text-black">
|
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-4">
|
||||||
<div className="mb-4 flex items-start justify-between">
|
<h3 className="mb-4 text-[20px] font-extrabold tracking-[-0.02em] text-black">RDII 분석 결과</h3>
|
||||||
<div className="text-[18px] font-semibold text-[#111827]" />
|
<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>
|
||||||
|
|
||||||
|
<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">
|
<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">
|
<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 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">강우유입수량 선택 조건</h3>
|
||||||
강우유입수분석 선택 조건
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="rounded-[6px] border border-[#d9dde3] bg-[#eff1f4] px-4 py-6">
|
<div className="rounded-[6px] border border-[#d9dde3] bg-[#eff1f4] px-4 py-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-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 className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">상수사용월</Label>
|
||||||
상수사용월:
|
|
||||||
</Label>
|
|
||||||
<SelectBox
|
<SelectBox
|
||||||
className="w-full"
|
className="w-full"
|
||||||
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
||||||
label=""
|
label=""
|
||||||
selectArray={aplcnYms}
|
selectArray={aplcnYms}
|
||||||
value={aplcnYm}
|
value={selectedAplcnYm}
|
||||||
onSelectValue={handleAplcnYmChange}
|
onSelectValue={handleAplcnYmChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-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 className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">분석정보:</Label>
|
||||||
분석정보:
|
|
||||||
</Label>
|
|
||||||
<SelectBox
|
<SelectBox
|
||||||
className="w-full"
|
className="w-full"
|
||||||
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
||||||
label=""
|
label=""
|
||||||
selectArray={rdiiAnlsInfos}
|
selectArray={rdiiAnlsInfos}
|
||||||
value={rainInfwtAnlsSn}
|
value={selectedRainInfwtAnlsSn}
|
||||||
onSelectValue={handleRdiiAnlsSnChange}
|
onSelectValue={handleRdiiAnlsSnChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderInfoInput('오수전환율:', rdiiAnls?.drtwSwtcRt ?? '')}
|
{renderInfoInput("오수전환율", rdiiAnls?.drtwSwtcRt ?? "")}
|
||||||
{renderInfoInput('수용가-맨홀 매칭:', rdiiAnls?.atflMpngNm ?? '')}
|
{renderInfoInput("수용가-맨홀 매칭:", rdiiAnls?.atflMpngNm ?? "")}
|
||||||
{renderInfoInput('관망도 파일명:', rdiiAnls?.atflInpNm ?? '')}
|
{renderInfoInput("관망도 파일명", rdiiAnls?.atflInpNm ?? "")}
|
||||||
{renderInfoInput('강우 파일명:', rdiiAnls?.atflRainNm ?? '')}
|
{renderInfoInput("강우 파일명", rdiiAnls?.atflRainNm ?? "")}
|
||||||
{renderInfoInput('RTK계수 출처:', rdiiAnls?.srcNm ?? '')}
|
{renderInfoInput("RTK계수 출처:", rdiiAnls?.srcNm ?? "")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute inset-x-0 bottom-0 mb-[40px] flex justify-center">
|
<div className="absolute inset-x-0 bottom-0 mb-[40px] flex justify-center">
|
||||||
<Button size="lg" onClick={fetchFilteredData}>
|
<Button size="lg" onClick={fetchFilteredData}>조회</Button>
|
||||||
조회
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button size="lg" color="gray" onClick={resetState}>
|
<Button size="lg" color="gray" onClick={resetState}>초기화</Button>
|
||||||
초기화
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
|
{renderResultPanel()}
|
||||||
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
|
|
||||||
강우유입수분석 결과
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="mb-6 flex gap-5">
|
|
||||||
{/* 좌측 카드 */}
|
|
||||||
<div className="flex min-h-[98px] min-w-0 basis-2/5 grow shrink-[1.4] 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 basis-3/5 grow shrink 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-[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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isResultOnlyMode) {
|
||||||
|
return <div className="min-h-dvh bg-white p-3">{renderResultPanel()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh bg-white p-4">
|
<div className="min-h-dvh bg-white p-4">
|
||||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">RDII 분석</h1>
|
<h1 className="mb-3 text-2xl font-semibold text-gray-900">RDII 분석</h1>
|
||||||
{renderBody()}
|
{renderDefaultBody()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(RdiiChart);
|
export default React.memo(RdiiChart);
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { DateMonthPicker, UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||||
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
|
import { getData } from "@/components/lib/api";
|
||||||
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||||
|
import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox";
|
||||||
|
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||||
|
import useStore from "@/store/useStore";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RDII_RESULT_ROW_SELECT_MESSAGE,
|
||||||
|
type RdiiResultRow,
|
||||||
|
} from "./rdiiResultTypes";
|
||||||
|
|
||||||
|
const RDII_RESULT_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
const RDII_RESULT_HEADLIST = [
|
||||||
|
{ id: "rowNumber", title: "일련번호" },
|
||||||
|
{ id: "ctpvNm", title: "시도" },
|
||||||
|
{ id: "sggNm", title: "시군구" },
|
||||||
|
{ id: "swtrmtFcltNm", title: "하수처리시설" },
|
||||||
|
{ id: "aplcnYm", title: "상수사용연월" },
|
||||||
|
{ id: "drtwSwtcRt", title: "오수 전환율" },
|
||||||
|
{ id: "srcNm", title: "RTK 계수 출처" },
|
||||||
|
{ id: "cntbArea", title: "RDII 기여면적" },
|
||||||
|
{ id: "anlsInfo", title: "RDII 분석정보" },
|
||||||
|
{ id: "regDt", title: "분석일자" },
|
||||||
|
{ id: "rgtrNm", title: "분석자" },
|
||||||
|
{ id: "scsYn", title: "분석성공여부" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type RdiiResultFilters = {
|
||||||
|
ctpvCd: string;
|
||||||
|
sggCd: string;
|
||||||
|
sggName: string;
|
||||||
|
swtrmtFcltNm: string;
|
||||||
|
aplcnYmFrom: string;
|
||||||
|
aplcnYmTo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDefaultAplcnYm = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
|
return `${year}${month}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_FILTERS: RdiiResultFilters = {
|
||||||
|
ctpvCd: "0",
|
||||||
|
sggCd: "0",
|
||||||
|
sggName: "",
|
||||||
|
swtrmtFcltNm: "",
|
||||||
|
aplcnYmFrom: `${new Date().getFullYear()}01`,
|
||||||
|
aplcnYmTo: createDefaultAplcnYm(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const postRowSelectMessage = (row: RdiiResultRow) => {
|
||||||
|
if (typeof window === "undefined" || !window.opener) return;
|
||||||
|
window.opener.postMessage(
|
||||||
|
{
|
||||||
|
type: RDII_RESULT_ROW_SELECT_MESSAGE,
|
||||||
|
row,
|
||||||
|
},
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RdiiResultPopupPage() {
|
||||||
|
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||||
|
const [filters, setFilters] = useState<RdiiResultFilters>(DEFAULT_FILTERS);
|
||||||
|
const [selectedSwagePrcsFclt, setSelectedSwagePrcsFclt] = useState("0");
|
||||||
|
const [selectedRow, setSelectedRow] = useState<RdiiResultRow | null>(null);
|
||||||
|
const [rows, setRows] = useState<RdiiResultRow[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ctpv.length === 0) void fetchCtpv();
|
||||||
|
if (sgg.length === 0) void fetchSgg();
|
||||||
|
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
||||||
|
|
||||||
|
const fetchRows = useCallback(
|
||||||
|
async (page: number, nextFilters: RdiiResultFilters) => {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
page: String(page - 1),
|
||||||
|
size: String(RDII_RESULT_PAGE_SIZE),
|
||||||
|
sort: "rainInfwtAnlsSn,desc",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nextFilters.aplcnYmFrom) queryParams.set("aplcnYmFrom", nextFilters.aplcnYmFrom);
|
||||||
|
if (nextFilters.aplcnYmTo) queryParams.set("aplcnYmTo", nextFilters.aplcnYmTo);
|
||||||
|
if (nextFilters.ctpvCd !== "0") queryParams.set("ctpvCd", nextFilters.ctpvCd);
|
||||||
|
if (nextFilters.sggCd !== "0") queryParams.set("sggCd", nextFilters.sggCd);
|
||||||
|
if (nextFilters.swtrmtFcltNm.trim()) queryParams.set("swtrmtFcltNm", nextFilters.swtrmtFcltNm.trim());
|
||||||
|
|
||||||
|
const response = await getData(`v1/rdiiAnls?${queryParams.toString()}`);
|
||||||
|
const content = Array.isArray(response?.content) ? response.content : [];
|
||||||
|
|
||||||
|
const nextRows = content
|
||||||
|
.map((item: Record<string, unknown>, index: number) => {
|
||||||
|
const formattedItem = formatDisplayData(item);
|
||||||
|
const ctpvOption = ctpv.find((code) => code.val.trim() === String(item?.ctpvCd ?? "").trim());
|
||||||
|
const sggOption = sgg.find((code) => code.val === item?.sggCd);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(formattedItem as Record<string, unknown>),
|
||||||
|
rowNumber: (page - 1) * RDII_RESULT_PAGE_SIZE + index + 1,
|
||||||
|
ctpvNm: ctpvOption ? ctpvOption.text : String(item?.ctpvCd ?? "-"),
|
||||||
|
sggNm: sggOption ? sggOption.text : String(item?.sggCd ?? "-"),
|
||||||
|
scsYn: String(item?.scsYn ?? "N"),
|
||||||
|
} as RdiiResultRow;
|
||||||
|
})
|
||||||
|
.filter((item: RdiiResultRow) => item.scsYn === "Y");
|
||||||
|
|
||||||
|
setRows(nextRows);
|
||||||
|
setTotal(Number(response?.totalElements ?? nextRows.length));
|
||||||
|
setCurrentPage(page);
|
||||||
|
},
|
||||||
|
[ctpv, sgg],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadRows = useCallback(
|
||||||
|
async (page = 1, nextFilters = filters) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await fetchRows(page, nextFilters);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("RDII 분석결과 목록 조회 중 오류:", error);
|
||||||
|
alert("RDII 분석결과 목록을 불러오지 못했습니다.");
|
||||||
|
setRows([]);
|
||||||
|
setTotal(0);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchRows, filters],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadRows(1, DEFAULT_FILTERS);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = async (page = 1) => {
|
||||||
|
setSelectedIndex(-1);
|
||||||
|
setSelectedRow(null);
|
||||||
|
await loadRows(page, filters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = (index?: number) => {
|
||||||
|
if (typeof index !== "number") return;
|
||||||
|
const row = rows[index];
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
setSelectedIndex(index);
|
||||||
|
setSelectedRow(row);
|
||||||
|
postRowSelectMessage(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (!selectedRow?.rainInfwtAnlsSn) {
|
||||||
|
alert("다운로드할 분석결과를 먼저 선택해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await getData(`v1/rdiiAnlsRslt/ppl/${selectedRow.rainInfwtAnlsSn}`);
|
||||||
|
const excelRows = Array.isArray(response?.content) ? response.content : [];
|
||||||
|
|
||||||
|
await downloadExcelWithKoreanHeaders({
|
||||||
|
data: excelRows as JSON[],
|
||||||
|
fileName: `RDII분석결과_${selectedRow.swtrmtFcltNm}_${selectedRow.aplcnYm}_${selectedRow.rainInfwtAnlsSn}`,
|
||||||
|
sheetNameArray: ["RDII 분석결과"],
|
||||||
|
headerRange: "A1:D1",
|
||||||
|
headerMapping: {
|
||||||
|
rainInfwtAnlsSn: "RDII분석\n일련번호",
|
||||||
|
pplMngNo: "관로\n관리번호",
|
||||||
|
anlsTm: "분석시각",
|
||||||
|
predcOilq: "예측유량\n(m³/h)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("RDII 분석결과 다운로드 중 오류:", error);
|
||||||
|
alert("RDII 분석결과 다운로드 중 오류가 발생했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex h-screen flex-col overflow-hidden bg-white">
|
||||||
|
<LoadingOverlay isLoading={isLoading} />
|
||||||
|
<header className="flex min-h-[44px] items-center justify-between border-b border-[#dbe2e6] bg-primary px-5 py-3">
|
||||||
|
<h1 className="text-[1.45rem] font-semibold text-white">RDII 분석결과</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="overflow-y-auto p-4">
|
||||||
|
<div className="rounded-md border border-slate-200 bg-slate-50 p-4">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="whitespace-nowrap text-[1.05rem] font-medium text-slate-700">상수사용기간</span>
|
||||||
|
<DateMonthPicker
|
||||||
|
field={{
|
||||||
|
value: filters.aplcnYmFrom,
|
||||||
|
onChange: (value: string) => {
|
||||||
|
setFilters((current) => ({
|
||||||
|
...current,
|
||||||
|
aplcnYmFrom: value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
selectClass="w-[145px]"
|
||||||
|
startYear={1900}
|
||||||
|
/>
|
||||||
|
<span className="text-[1.1rem] font-semibold text-slate-500">~</span>
|
||||||
|
<DateMonthPicker
|
||||||
|
field={{
|
||||||
|
value: filters.aplcnYmTo,
|
||||||
|
onChange: (value: string) => {
|
||||||
|
setFilters((current) => ({
|
||||||
|
...current,
|
||||||
|
aplcnYmTo: value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
selectClass="w-[145px]"
|
||||||
|
startYear={1900}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1 overflow-x-auto">
|
||||||
|
<CtpvSggPrcsFcltSelectBox
|
||||||
|
sido={filters.ctpvCd}
|
||||||
|
sigungu={filters.sggCd}
|
||||||
|
sggName={filters.sggName}
|
||||||
|
onSidoChange={(value) => {
|
||||||
|
setSelectedSwagePrcsFclt("0");
|
||||||
|
setFilters((current) => ({
|
||||||
|
...current,
|
||||||
|
ctpvCd: value,
|
||||||
|
sggCd: "0",
|
||||||
|
sggName: "",
|
||||||
|
swtrmtFcltNm: "",
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
onSigunguChange={(value) => {
|
||||||
|
setSelectedSwagePrcsFclt("0");
|
||||||
|
setFilters((current) => ({
|
||||||
|
...current,
|
||||||
|
sggCd: value,
|
||||||
|
swtrmtFcltNm: "",
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
setSggName={(value) => {
|
||||||
|
setSelectedSwagePrcsFclt("0");
|
||||||
|
setFilters((current) => ({
|
||||||
|
...current,
|
||||||
|
sggName: value ?? "",
|
||||||
|
swtrmtFcltNm: "",
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
selectedSwagePrcsFclt={selectedSwagePrcsFclt}
|
||||||
|
onSwagePrcsFcltChange={setSelectedSwagePrcsFclt}
|
||||||
|
setSwtrmtFcltNm={(value) =>
|
||||||
|
setFilters((current) => ({
|
||||||
|
...current,
|
||||||
|
swtrmtFcltNm: value ?? "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
setIsLoading={setIsLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
className="h-10 px-5 !text-[1.1rem]"
|
||||||
|
onClick={() => void handleSearch(1)}
|
||||||
|
>
|
||||||
|
조회
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="green"
|
||||||
|
size="sm"
|
||||||
|
className="h-10 px-5 !text-[1.1rem]"
|
||||||
|
onClick={() => void handleDownload()}
|
||||||
|
>
|
||||||
|
다운로드
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<UiTable<RdiiResultRow>
|
||||||
|
headName={<>{`분석이력 총 ${total.toLocaleString()}건`}</>}
|
||||||
|
tableData={rows}
|
||||||
|
headlist={RDII_RESULT_HEADLIST as unknown as { id: string; title: string }[]}
|
||||||
|
pageSize={RDII_RESULT_PAGE_SIZE}
|
||||||
|
total={total}
|
||||||
|
cellClick={(index) => handleRowClick(index)}
|
||||||
|
onPageChange={(page) => void handleSearch(page)}
|
||||||
|
curPage={currentPage}
|
||||||
|
index={selectedIndex}
|
||||||
|
widthFixed={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedRow ? (
|
||||||
|
<div className="mt-3 rounded-md border border-slate-200 bg-slate-50 px-4 py-3 text-[1rem] text-slate-600">
|
||||||
|
{"선택 분석: "}
|
||||||
|
<span className="font-semibold text-slate-800">{selectedRow.anlsInfo}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
export const RDII_RESULT_ROW_SELECT_MESSAGE = "SN_RDII_RESULT_ROW_SELECT";
|
||||||
|
export const RDII_RESULT_OVERLAY_TYPE = "RDII_RESULT";
|
||||||
|
|
||||||
|
export type RdiiResultRow = {
|
||||||
|
rowNumber: number;
|
||||||
|
rainInfwtAnlsSn: number;
|
||||||
|
ctpvCd?: string;
|
||||||
|
ctpvNm: string;
|
||||||
|
sggCd?: string;
|
||||||
|
sggNm: string;
|
||||||
|
swtrmtFcltNm: string;
|
||||||
|
aplcnYm: string;
|
||||||
|
drtwSwtcRt?: number | string;
|
||||||
|
srcNm?: string;
|
||||||
|
cntbArea?: number | string;
|
||||||
|
anlsInfo: string;
|
||||||
|
regDt?: string;
|
||||||
|
rgtrNm?: string;
|
||||||
|
scsYn?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RdiiResultMapViewPipe = {
|
||||||
|
pplMngNo: string;
|
||||||
|
fcltMngNo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RdiiResultMapViewResponse = {
|
||||||
|
analysis: {
|
||||||
|
rainInfwtAnlsSn: number;
|
||||||
|
aplcnYm: string;
|
||||||
|
ctpvCd?: string;
|
||||||
|
sggCd?: string;
|
||||||
|
swtrmtFcltNm?: string;
|
||||||
|
anlsInfo?: string;
|
||||||
|
};
|
||||||
|
focusTargetType: "PIPE_EXTENT";
|
||||||
|
focusTargetName: string;
|
||||||
|
focusLayerType?: "tb_ms_swage_ppl";
|
||||||
|
focusFid?: string;
|
||||||
|
focusFallbackApplied?: boolean;
|
||||||
|
pipeGeometry: Record<string, unknown>;
|
||||||
|
pipes: RdiiResultMapViewPipe[];
|
||||||
|
pipeCount: number;
|
||||||
|
};
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
|
||||||
|
import GeoJSON from "ol/format/GeoJSON.js";
|
||||||
|
import VectorLayer from "ol/layer/Vector.js";
|
||||||
|
import VectorSource from "ol/source/Vector.js";
|
||||||
|
import Stroke from "ol/style/Stroke.js";
|
||||||
|
import Style from "ol/style/Style.js";
|
||||||
|
|
||||||
|
import { getRawJson } from "@/components/lib/api";
|
||||||
|
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||||
|
|
||||||
|
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||||
|
import { getSigunguCenter, moveMapToCenter, sidoCenter } from "@common/business_components/ui/map/searchBar/utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RDII_RESULT_OVERLAY_TYPE,
|
||||||
|
RDII_RESULT_ROW_SELECT_MESSAGE,
|
||||||
|
type RdiiResultMapViewResponse,
|
||||||
|
type RdiiResultRow,
|
||||||
|
} from "./rdiiResultTypes";
|
||||||
|
|
||||||
|
type UseRdiiResultMapParams = {
|
||||||
|
gisMaps: MutableRefObject<any | null>;
|
||||||
|
setFeatureData: (value: any) => void;
|
||||||
|
setIsOpen: (value: boolean) => void;
|
||||||
|
setIsSearchModalOpen: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RDII_PIPE_LAYER_ID = "SN_RDII_RESULT_PIPE_LAYER";
|
||||||
|
const DEFAULT_RESULT_FIT_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
||||||
|
const PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH";
|
||||||
|
|
||||||
|
const pipeStyle = [
|
||||||
|
new Style({
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: "rgba(15, 23, 42, 0.56)",
|
||||||
|
width: 9,
|
||||||
|
lineCap: "round",
|
||||||
|
lineJoin: "round",
|
||||||
|
}),
|
||||||
|
zIndex: 20020,
|
||||||
|
}),
|
||||||
|
new Style({
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: "rgba(255, 255, 255, 0.98)",
|
||||||
|
width: 5,
|
||||||
|
lineCap: "round",
|
||||||
|
lineJoin: "round",
|
||||||
|
}),
|
||||||
|
zIndex: 20021,
|
||||||
|
}),
|
||||||
|
new Style({
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: "rgba(59, 130, 246, 0.98)",
|
||||||
|
width: 2.5,
|
||||||
|
lineCap: "round",
|
||||||
|
lineJoin: "round",
|
||||||
|
}),
|
||||||
|
zIndex: 20022,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const getFeatureCollection = (payload: any) => {
|
||||||
|
if (payload?.type === "FeatureCollection") return payload;
|
||||||
|
if (payload?.data?.type === "FeatureCollection") return payload.data;
|
||||||
|
return { type: "FeatureCollection", features: [] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeProjectionName = (value: string) => {
|
||||||
|
const trimmed = String(value ?? "").trim();
|
||||||
|
if (!trimmed) return "EPSG:4326";
|
||||||
|
if (trimmed.startsWith("EPSG:")) return trimmed;
|
||||||
|
const match = trimmed.match(/EPSG[:/]{1,2}(\d{4,5})/i) || trimmed.match(/(\d{4,5})$/);
|
||||||
|
return match?.[1] ? `EPSG:${match[1]}` : trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveFeatureCollectionProjection = (payload: any) => {
|
||||||
|
const featureCollection = getFeatureCollection(payload);
|
||||||
|
const crsName = String(featureCollection?.crs?.properties?.name ?? "").trim();
|
||||||
|
return normalizeProjectionName(crsName || GisApp.TO_PROJ);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOverlayFlag = (data: any) =>
|
||||||
|
String(
|
||||||
|
data?.get?.("analysisOverlayType") ??
|
||||||
|
data?.values_?.analysisOverlayType ??
|
||||||
|
data?.analysisOverlayType ??
|
||||||
|
"",
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
const getOverlayBasePplMngNo = (data: any) =>
|
||||||
|
String(
|
||||||
|
data?.get?.("fcltMngNo") ??
|
||||||
|
data?.values_?.fcltMngNo ??
|
||||||
|
data?.values_?.fclt_mng_no ??
|
||||||
|
data?.fcltMngNo ??
|
||||||
|
data?.fclt_mng_no ??
|
||||||
|
"",
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
const getOverlayAnalysisPplMngNo = (data: any) =>
|
||||||
|
String(
|
||||||
|
data?.get?.("pplMngNo") ??
|
||||||
|
data?.values_?.pplMngNo ??
|
||||||
|
data?.pplMngNo ??
|
||||||
|
"",
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
const getOverlayAnalysisSn = (data: any) =>
|
||||||
|
Number(
|
||||||
|
data?.get?.("rainInfwtAnlsSn") ??
|
||||||
|
data?.values_?.rainInfwtAnlsSn ??
|
||||||
|
data?.rainInfwtAnlsSn ??
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getOverlayAplcnYm = (data: any) =>
|
||||||
|
String(
|
||||||
|
data?.get?.("aplcnYm") ??
|
||||||
|
data?.values_?.aplcnYm ??
|
||||||
|
data?.aplcnYm ??
|
||||||
|
"",
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
const getOverlayAnalysisInfo = (data: any) =>
|
||||||
|
String(
|
||||||
|
data?.get?.("analysisInfo") ??
|
||||||
|
data?.values_?.analysisInfo ??
|
||||||
|
data?.analysisInfo ??
|
||||||
|
"",
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
export const useRdiiResultMap = ({
|
||||||
|
gisMaps,
|
||||||
|
setFeatureData,
|
||||||
|
setIsOpen,
|
||||||
|
setIsSearchModalOpen,
|
||||||
|
}: UseRdiiResultMapParams) => {
|
||||||
|
const activeResultRowRef = useRef<RdiiResultRow | null>(null);
|
||||||
|
const pipeLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||||
|
const pipeSourceRef = useRef<VectorSource | null>(null);
|
||||||
|
const mapViewCacheRef = useRef<Map<number, RdiiResultMapViewResponse>>(new Map());
|
||||||
|
const [activeMapView, setActiveMapView] = useState<RdiiResultMapViewResponse | null>(null);
|
||||||
|
|
||||||
|
const clearLayers = useCallback(() => {
|
||||||
|
pipeSourceRef.current?.clear?.();
|
||||||
|
pipeLayerRef.current?.changed?.();
|
||||||
|
gisMaps.current?.renderSync?.();
|
||||||
|
}, [gisMaps]);
|
||||||
|
|
||||||
|
const ensurePipeLayer = useCallback(() => {
|
||||||
|
const map = gisMaps.current;
|
||||||
|
if (!map) return null;
|
||||||
|
|
||||||
|
if (pipeLayerRef.current && pipeSourceRef.current) {
|
||||||
|
return pipeSourceRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = new VectorSource();
|
||||||
|
const layer = new VectorLayer({
|
||||||
|
source,
|
||||||
|
zIndex: 20019,
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.set("id", RDII_PIPE_LAYER_ID);
|
||||||
|
layer.set("lyrNm", "RDII 결과관로");
|
||||||
|
layer.set("lyrTyNm", "tb_ms_swage_ppl");
|
||||||
|
layer.set("clickPsblYn", "Y");
|
||||||
|
layer.setVisible?.(true);
|
||||||
|
map.addLayer(layer);
|
||||||
|
|
||||||
|
pipeLayerRef.current = layer;
|
||||||
|
pipeSourceRef.current = source;
|
||||||
|
return source;
|
||||||
|
}, [gisMaps]);
|
||||||
|
|
||||||
|
const fitToRenderedFeatures = useCallback(() => {
|
||||||
|
const map = gisMaps.current;
|
||||||
|
const pipeExtent = pipeSourceRef.current?.getExtent?.();
|
||||||
|
if (!map || !pipeExtent || pipeExtent.some((value: number) => !Number.isFinite(value))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.getView?.().fit?.(pipeExtent, {
|
||||||
|
duration: 350,
|
||||||
|
padding: DEFAULT_RESULT_FIT_PADDING,
|
||||||
|
maxZoom: 16,
|
||||||
|
constrainResolution: true,
|
||||||
|
nearest: true,
|
||||||
|
});
|
||||||
|
}, [gisMaps]);
|
||||||
|
|
||||||
|
const moveByRegionCodes = useCallback(
|
||||||
|
(ctpvCd?: string, sggCd?: string) => {
|
||||||
|
const normalizedSggCd = String(sggCd ?? "").trim();
|
||||||
|
if (normalizedSggCd && normalizedSggCd !== "0") {
|
||||||
|
const sigunguCenter = getSigunguCenter(normalizedSggCd);
|
||||||
|
if (sigunguCenter) {
|
||||||
|
moveMapToCenter(gisMaps, sigunguCenter, 12);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedCtpvCd = String(ctpvCd ?? "").trim();
|
||||||
|
if (normalizedCtpvCd && normalizedCtpvCd !== "0") {
|
||||||
|
const sidoCenterCoordinate = sidoCenter[normalizedCtpvCd.slice(0, 2)];
|
||||||
|
if (sidoCenterCoordinate) {
|
||||||
|
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[gisMaps],
|
||||||
|
);
|
||||||
|
|
||||||
|
const moveToStableFocus = useCallback(
|
||||||
|
async (mapView: RdiiResultMapViewResponse) => {
|
||||||
|
const activeRow = activeResultRowRef.current;
|
||||||
|
const movedByRegion = moveByRegionCodes(
|
||||||
|
activeRow?.ctpvCd ?? mapView.analysis?.ctpvCd,
|
||||||
|
activeRow?.sggCd ?? mapView.analysis?.sggCd,
|
||||||
|
);
|
||||||
|
if (movedByRegion) return;
|
||||||
|
|
||||||
|
fitToRenderedFeatures();
|
||||||
|
},
|
||||||
|
[fitToRenderedFeatures, moveByRegionCodes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderMapView = useCallback(
|
||||||
|
async (mapView: RdiiResultMapViewResponse) => {
|
||||||
|
const pipeSource = ensurePipeLayer();
|
||||||
|
if (!pipeSource) return;
|
||||||
|
|
||||||
|
clearLayers();
|
||||||
|
|
||||||
|
const pipeDataProjection = resolveFeatureCollectionProjection(mapView.pipeGeometry);
|
||||||
|
const pipeFeatures = new GeoJSON().readFeatures(getFeatureCollection(mapView.pipeGeometry), {
|
||||||
|
dataProjection: pipeDataProjection,
|
||||||
|
featureProjection: GisApp.TO_PROJ,
|
||||||
|
});
|
||||||
|
pipeFeatures.forEach((feature: any) => {
|
||||||
|
feature.setStyle(pipeStyle);
|
||||||
|
});
|
||||||
|
pipeSource.addFeatures(pipeFeatures);
|
||||||
|
pipeSource.changed?.();
|
||||||
|
pipeLayerRef.current?.setVisible?.(true);
|
||||||
|
pipeLayerRef.current?.changed?.();
|
||||||
|
gisMaps.current?.renderSync?.();
|
||||||
|
|
||||||
|
await moveToStableFocus(mapView);
|
||||||
|
},
|
||||||
|
[clearLayers, ensurePipeLayer, gisMaps, moveToStableFocus],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowSelect = useCallback(
|
||||||
|
async (row: RdiiResultRow) => {
|
||||||
|
try {
|
||||||
|
activeResultRowRef.current = row;
|
||||||
|
setFeatureData(null);
|
||||||
|
setIsOpen(false);
|
||||||
|
setIsSearchModalOpen(false);
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(PROC_SEARCH_EVENT, {
|
||||||
|
detail: {
|
||||||
|
ctpvCd: row.ctpvCd ?? "0",
|
||||||
|
sggCd: row.sggCd ?? "0",
|
||||||
|
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedMapView = mapViewCacheRef.current.get(row.rainInfwtAnlsSn);
|
||||||
|
if (cachedMapView) {
|
||||||
|
setActiveMapView(cachedMapView);
|
||||||
|
void renderMapView(cachedMapView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await getRawJson(`/sewerage-service/api/v1/rdiiAnlsRslt/mapView/${row.rainInfwtAnlsSn}`);
|
||||||
|
const mapView = (payload?.data ?? payload) as RdiiResultMapViewResponse;
|
||||||
|
mapViewCacheRef.current.set(row.rainInfwtAnlsSn, mapView);
|
||||||
|
setActiveMapView(mapView);
|
||||||
|
void renderMapView(mapView);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("RDII 분석결과 지도 연계 중 오류:", error);
|
||||||
|
alert("RDII 분석결과를 지도에 연계하지 못했습니다.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[renderMapView, setFeatureData, setIsOpen, setIsSearchModalOpen],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopupMessage = (event: MessageEvent) => {
|
||||||
|
if (event.origin !== window.location.origin) return;
|
||||||
|
if (event.data?.type !== RDII_RESULT_ROW_SELECT_MESSAGE || !event.data?.row) return;
|
||||||
|
void handleRowSelect(event.data.row as RdiiResultRow);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handlePopupMessage);
|
||||||
|
return () => window.removeEventListener("message", handlePopupMessage);
|
||||||
|
}, [handleRowSelect]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearLayers();
|
||||||
|
};
|
||||||
|
}, [clearLayers]);
|
||||||
|
|
||||||
|
const handleOverlayFeatureSelect = useCallback((data: any) => {
|
||||||
|
if (getOverlayFlag(data) !== RDII_RESULT_OVERLAY_TYPE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayAnalysisSn = getOverlayAnalysisSn(data);
|
||||||
|
const basePplMngNo = getOverlayBasePplMngNo(data);
|
||||||
|
const analysisPplMngNo = getOverlayAnalysisPplMngNo(data);
|
||||||
|
const currentRow = activeResultRowRef.current;
|
||||||
|
const popupWindow = openWindowPopup("rdiiChart", {
|
||||||
|
pplMngNo: basePplMngNo || analysisPplMngNo.replace(/^L/, ""),
|
||||||
|
rainInfwtAnlsSn: overlayAnalysisSn || currentRow?.rainInfwtAnlsSn || 0,
|
||||||
|
aplcnYm: getOverlayAplcnYm(data) || currentRow?.aplcnYm || activeMapView?.analysis?.aplcnYm || "",
|
||||||
|
analysisInfo:
|
||||||
|
getOverlayAnalysisInfo(data) ||
|
||||||
|
currentRow?.anlsInfo ||
|
||||||
|
activeMapView?.analysis?.anlsInfo ||
|
||||||
|
"",
|
||||||
|
resultOnly: "Y",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!popupWindow) {
|
||||||
|
alert("팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [activeMapView]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeMapView,
|
||||||
|
clearLayers,
|
||||||
|
handleOverlayFeatureSelect,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -79,6 +79,7 @@ import {
|
|||||||
} from "./components/agedPipe/agedPipeUtils";
|
} from "./components/agedPipe/agedPipeUtils";
|
||||||
import { downloadAgedPipeCsv } from "./components/agedPipe/agedPipeExport";
|
import { downloadAgedPipeCsv } from "./components/agedPipe/agedPipeExport";
|
||||||
import { useInftwtResultMap } from "./components/inftwtResult/useInftwtResultMap";
|
import { useInftwtResultMap } from "./components/inftwtResult/useInftwtResultMap";
|
||||||
|
import { useRdiiResultMap } from "./components/rdiiResult/useRdiiResultMap";
|
||||||
import SinkholeDetailPanel from "./components/sinkhole/SinkholeDetailPanel";
|
import SinkholeDetailPanel from "./components/sinkhole/SinkholeDetailPanel";
|
||||||
import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettingPanel";
|
import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettingPanel";
|
||||||
import FloodHistoryDetailPanel from "./components/floodHistory/FloodHistoryDetailPanel";
|
import FloodHistoryDetailPanel from "./components/floodHistory/FloodHistoryDetailPanel";
|
||||||
@@ -2459,6 +2460,12 @@ const Map = () => {
|
|||||||
setIsOpen,
|
setIsOpen,
|
||||||
setIsSearchModalOpen,
|
setIsSearchModalOpen,
|
||||||
});
|
});
|
||||||
|
const rdiiResultMap = useRdiiResultMap({
|
||||||
|
gisMaps,
|
||||||
|
setFeatureData,
|
||||||
|
setIsOpen,
|
||||||
|
setIsSearchModalOpen,
|
||||||
|
});
|
||||||
const shouldFocusAgedPipeOnNextSelectRef = useRef(false);
|
const shouldFocusAgedPipeOnNextSelectRef = useRef(false);
|
||||||
const agedPipePopupWindowRef = useRef<Window | null>(null);
|
const agedPipePopupWindowRef = useRef<Window | null>(null);
|
||||||
const sinkholePopupWindowRef = useRef<Window | null>(null);
|
const sinkholePopupWindowRef = useRef<Window | null>(null);
|
||||||
@@ -3584,6 +3591,12 @@ const Map = () => {
|
|||||||
const clickedLayerTypeName = getSelectedFeatureLayerTypeName(data);
|
const clickedLayerTypeName = getSelectedFeatureLayerTypeName(data);
|
||||||
// 지형에 있는 피처를 직접 선택하면 지반침하 반경 내에서 만든 임시 관로 강조는 해제한다.
|
// 지형에 있는 피처를 직접 선택하면 지반침하 반경 내에서 만든 임시 관로 강조는 해제한다.
|
||||||
clearSinkholeFocusedPipeLayer();
|
clearSinkholeFocusedPipeLayer();
|
||||||
|
if (rdiiResultMap.handleOverlayFeatureSelect(data)) {
|
||||||
|
setFeatureData(null);
|
||||||
|
setIsOpen(false);
|
||||||
|
setIsSearchModalOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (inftwtResultMap.handleOverlayFeatureSelect(data)) {
|
if (inftwtResultMap.handleOverlayFeatureSelect(data)) {
|
||||||
setFeatureData(null);
|
setFeatureData(null);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# SN RDII분석결과 지도연계 유지보수 FE 통합구현기록
|
||||||
|
|
||||||
|
## 1. 작업 목적
|
||||||
|
- RDII 분석결과를 좌측 LNB 대신 하수관망도 `분석도구` popup으로 옮기고, 침입수 분석결과와 같은 지도 연계 흐름으로 정리한다.
|
||||||
|
|
||||||
|
## 2. 실제 수정 파일
|
||||||
|
- `FE_APP_ROOT/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx`
|
||||||
|
- `FE_APP_ROOT/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx`
|
||||||
|
- `FE_APP_ROOT/app/(map-layout)/map/components/rdiiResult/**`
|
||||||
|
- `FE_APP_ROOT/app/(map-layout)/map/page.tsx`
|
||||||
|
- `FE_APP_ROOT/src/features/windowPopup/**`
|
||||||
|
- `FE_APP_ROOT/src/app/styles/components.css`
|
||||||
|
|
||||||
|
## 3. 구현 내용
|
||||||
|
- `rdiiResultPopup` popup 정의와 registry를 추가했다.
|
||||||
|
- `RdiiResultPopupPage`를 만들어 기존 `ii/rdiiResult`의 조회/다운로드 흐름을 popup에서 재사용하도록 옮겼다.
|
||||||
|
- row click 시 `SN_RDII_RESULT_ROW_SELECT` 메시지를 부모 창으로 전달하고, `useRdiiResultMap`에서 `/sewerage-service/api/v1/rdiiAnlsRslt/mapView/{rainInfwtAnlsSn}`를 호출해 결과 관로 overlay를 렌더하도록 연결했다.
|
||||||
|
- row click 직후 지도 이동은 `SN_INFTWT_PROC_SEARCH` 검색바 동기화 이벤트를 재사용해 시도/시군구 중심 이동 규칙을 그대로 따르도록 맞췄다.
|
||||||
|
- overlay 관로 click 시 `rdiiChart` popup을 `resultOnly` 모드로 열고, `rainInfwtAnlsSn/aplcnYm/analysisInfo`를 함께 넘겨 좌측 선택조건 없이 결과만 보이도록 확장했다.
|
||||||
|
- `분석도구` 메뉴에는 popup key를 `rdiiResultPopup`으로 연결하되, 실제 노출 라벨은 `RDII 분석`으로 맞췄고 legacy `ii/rdiiResult`는 CSS로만 숨겨 route는 유지했다.
|
||||||
|
|
||||||
|
## 4. 검증
|
||||||
|
- FE 변경분 기준 `node node_modules\typescript\bin\tsc --noEmit --ignoreDeprecations 5.0 -p tsconfig.json`을 실행했다.
|
||||||
|
- 이번 변경으로 새로 생긴 오류는 없었고, 아래 선행 타입 오류 때문에 전체 통과는 하지 못했다.
|
||||||
|
- `app/(map-layout)/map/components/common/index.ts`
|
||||||
|
- `app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts`
|
||||||
|
- `app/(map-layout)/map/page.tsx`
|
||||||
|
- `app/html/(map-layout)/map/components/common/index.ts`
|
||||||
|
- `app/html/(with-base-layout)/layout.tsx`
|
||||||
|
- `packages/common/business_components/src/ui/chart/*`
|
||||||
|
|
||||||
|
## 5. 2026-07-02 후속 UI 보정
|
||||||
|
- 사용자의 피드백에 따라 침입수 분석 메뉴 아이콘은 기존 물방울 표시로 되돌렸다.
|
||||||
|
- RDII 분석결과 메뉴 문구는 실제 노출 기준 RDII 분석으로 조정했다.
|
||||||
|
|
||||||
|
## 6. 남은 이슈
|
||||||
|
- 브라우저 실동작 검증은 이번 세션에서 수행하지 못했다.
|
||||||
|
- LNB 숨김 규칙은 현재 메뉴 순서 기준 `nth-child(4)`를 사용하므로, 추후 I/I 메뉴 순서가 바뀌면 selector를 함께 조정해야 한다.
|
||||||
|
## 7. 2026-07-02 resultOnly popup 후속 보정
|
||||||
|
- `rdiiChart` resultOnly 우측 요약 카드의 제목 라벨을 `shrink-0 + whitespace-nowrap` 기준으로 보정해 `강우유입수량 분석결과`가 한 줄로 유지되도록 맞췄다.
|
||||||
|
- 차트/요약 조회 Promise 묶음에 예외 방어를 추가해 조회 실패 시 콘솔 오류를 남기고 상태를 빈 값으로 명확히 초기화하도록 정리했다.
|
||||||
|
- FE가 계속 호출하던 `/api/v1/rdiiAnlsRslt/ppl/{rainInfwtAnlsSn}/{pplMngNo}`, `/api/v1/rdiiAnlsRslt/ppl/{rainInfwtAnlsSn}` 경로가 BE에 실제로 필요함을 기준으로 연동 계약을 다시 점검했다.
|
||||||
|
- `node node_modules\typescript\bin\tsc --noEmit --ignoreDeprecations 5.0 -p tsconfig.json` 재실행 결과, 이번 변경과 무관한 기존 선행 오류만 남고 RDII 변경 파일 기준 신규 오류는 확인되지 않았다.
|
||||||
|
## 8. 최종 확인 메모
|
||||||
|
- 사용자 실동작 기준으로 `분석도구 > RDII 분석` popup, row click 지도 이동, 관로 highlight, resultOnly popup 흐름이 정상 동작하는 것으로 마감한다.
|
||||||
|
- FE 전체 타입 오류는 이번 RDII 변경과 무관한 기존 선행 이슈만 남아 별도 정리 대상으로 유지한다.
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* `I/I 분석 > 침입수 분석결과`는 나중에 다시 사용할 수 있도록 라우트는 유지하고 LNB에서만 임시 숨김 처리한다. */
|
/* `I/I 분석 > 침입수 분석결과`는 나중에 다시 사용할 수 있도록 라우트는 유지하고 LNB에서만 임시 숨김 처리한다. */
|
||||||
|
/* `I/I 분석 > 침입수 분석결과`, `RDII 분석결과`는 지도 popup 흐름으로 대체하고 레거시 라우트는 유지한 채 LNB에서만 숨긴다. */
|
||||||
[data-sidebar="menu"] > *:nth-child(2) [data-sidebar="menu-sub"] > li:nth-child(2) {
|
[data-sidebar="menu"] > *:nth-child(2) [data-sidebar="menu-sub"] > li:nth-child(2) {
|
||||||
display: none;
|
display: none;
|
||||||
|
}
|
||||||
|
[data-sidebar="menu"] > *:nth-child(2) [data-sidebar="menu-sub"] > li:nth-child(4) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,12 @@ export const windowPopupDefinitions = {
|
|||||||
width: 1280,
|
width: 1280,
|
||||||
height: 900,
|
height: 900,
|
||||||
},
|
},
|
||||||
|
rdiiResultPopup: {
|
||||||
|
key: "rdiiResultPopup",
|
||||||
|
title: "RDII 분석결과",
|
||||||
|
width: 1280,
|
||||||
|
height: 900,
|
||||||
|
},
|
||||||
infilChart: {
|
infilChart: {
|
||||||
key: "infilChart",
|
key: "infilChart",
|
||||||
title: "침입수 분석",
|
title: "침입수 분석",
|
||||||
@@ -66,8 +72,12 @@ export const windowPopupDefinitions = {
|
|||||||
rdiiChart: {
|
rdiiChart: {
|
||||||
key: "rdiiChart",
|
key: "rdiiChart",
|
||||||
title: "RDII 분석",
|
title: "RDII 분석",
|
||||||
width: 1360,
|
width: 940,
|
||||||
height: 980,
|
height: 860,
|
||||||
|
placement: "browser-left",
|
||||||
|
alignTop: true,
|
||||||
|
offsetLeft: 0,
|
||||||
|
offsetTop: 70,
|
||||||
requiredParams: ["pplMngNo"],
|
requiredParams: ["pplMngNo"],
|
||||||
},
|
},
|
||||||
sscDetail: {
|
sscDetail: {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import InftwtResultPopupContent from "./ui/InftwtResultPopupContent";
|
|||||||
import OldPpUploadPopupContent from "./ui/OldPpUploadPopupContent";
|
import OldPpUploadPopupContent from "./ui/OldPpUploadPopupContent";
|
||||||
import OldPpTablePopupContent from "./ui/OldPpTablePopupContent";
|
import OldPpTablePopupContent from "./ui/OldPpTablePopupContent";
|
||||||
import RdiiChartPopupContent from "./ui/RdiiChartPopupContent";
|
import RdiiChartPopupContent from "./ui/RdiiChartPopupContent";
|
||||||
|
import RdiiResultPopupContent from "./ui/RdiiResultPopupContent";
|
||||||
import ReportFilesPopupContent from "./ui/ReportFilesPopupContent";
|
import ReportFilesPopupContent from "./ui/ReportFilesPopupContent";
|
||||||
import ReportTabPopupContent from "./ui/ReportTabPopupContent";
|
import ReportTabPopupContent from "./ui/ReportTabPopupContent";
|
||||||
import SearchModalPopupContent from "./ui/SearchModalPopupContent";
|
import SearchModalPopupContent from "./ui/SearchModalPopupContent";
|
||||||
@@ -65,6 +66,10 @@ export const windowPopupRegistry = {
|
|||||||
...windowPopupDefinitions.inftwtResultPopup,
|
...windowPopupDefinitions.inftwtResultPopup,
|
||||||
component: InftwtResultPopupContent,
|
component: InftwtResultPopupContent,
|
||||||
},
|
},
|
||||||
|
rdiiResultPopup: {
|
||||||
|
...windowPopupDefinitions.rdiiResultPopup,
|
||||||
|
component: RdiiResultPopupContent,
|
||||||
|
},
|
||||||
infilChart: {
|
infilChart: {
|
||||||
...windowPopupDefinitions.infilChart,
|
...windowPopupDefinitions.infilChart,
|
||||||
component: InfilChartPopupContent,
|
component: InfilChartPopupContent,
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export default function RdiiChartPopupContent({
|
|||||||
searchParams,
|
searchParams,
|
||||||
}: WindowPopupContentProps) {
|
}: WindowPopupContentProps) {
|
||||||
const pplMngNo = getPopupParamValue(searchParams.pplMngNo);
|
const pplMngNo = getPopupParamValue(searchParams.pplMngNo);
|
||||||
|
const resultOnly = getPopupParamValue(searchParams.resultOnly) === "Y";
|
||||||
|
const aplcnYm = getPopupParamValue(searchParams.aplcnYm);
|
||||||
|
const analysisInfo = getPopupParamValue(searchParams.analysisInfo);
|
||||||
|
const rainInfwtAnlsSn = Number(getPopupParamValue(searchParams.rainInfwtAnlsSn) || "0");
|
||||||
|
|
||||||
if (!pplMngNo) {
|
if (!pplMngNo) {
|
||||||
return (
|
return (
|
||||||
@@ -18,5 +22,13 @@ export default function RdiiChartPopupContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <RdiiChart pplMngNo={pplMngNo} />;
|
return (
|
||||||
}
|
<RdiiChart
|
||||||
|
pplMngNo={pplMngNo}
|
||||||
|
rainInfwtAnlsSn={Number.isFinite(rainInfwtAnlsSn) ? rainInfwtAnlsSn : 0}
|
||||||
|
aplcnYm={aplcnYm}
|
||||||
|
analysisInfo={analysisInfo}
|
||||||
|
resultOnly={resultOnly}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import RdiiResultPopupPage from "@/app/(map-layout)/map/components/rdiiResult/RdiiResultPopupPage";
|
||||||
|
|
||||||
|
export default function RdiiResultPopupContent() {
|
||||||
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isMounted) {
|
||||||
|
return <div className="h-dvh bg-white" suppressHydrationWarning />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RdiiResultPopupPage />;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user