RDII 분석결과 팝업 지도연계 및 결과전용 팝업 적용
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
||||
import {
|
||||
ChartColumnBig,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Droplets,
|
||||
PanelsTopLeft,
|
||||
PencilRuler,
|
||||
Search,
|
||||
@@ -23,10 +22,40 @@ type Props = {
|
||||
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<{
|
||||
key: WindowPopupKey | "infoSearch";
|
||||
label: string;
|
||||
icon: typeof ChartColumnBig;
|
||||
icon?: ComponentType<{ className?: string }>;
|
||||
renderIcon?: (props: MenuIconProps) => ReactNode;
|
||||
}> = [
|
||||
{ key: "infoSearch", label: "정보검색", icon: Search },
|
||||
{ key: "pipeAnalysisPopup", label: "관로분석", icon: ChartColumnBig },
|
||||
@@ -34,7 +63,8 @@ const ANALYSIS_POPUP_ITEMS: Array<{
|
||||
{ key: "gisEditPopup", label: "GIS 편집", icon: PencilRuler },
|
||||
{ key: "gisManagementPopup", label: "GIS 관리", icon: Settings2 },
|
||||
{ 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({
|
||||
@@ -63,7 +93,7 @@ export default function AnalysisPopupMenu({
|
||||
<div className="absolute right-[0.55rem] top-[20px] z-[45]">
|
||||
<div className="relative">
|
||||
{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 items-center gap-2">
|
||||
<PanelsTopLeft className="h-4 w-4 text-[#1e88e5]" />
|
||||
@@ -116,12 +146,15 @@ export default function AnalysisPopupMenu({
|
||||
}, 300);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className={`h-[15px] w-[15px] shrink-0 ${
|
||||
isActive ? "text-[#1e88e5]" : "text-[#64748b]"
|
||||
}`}
|
||||
/>
|
||||
<span className="break-keep whitespace-normal">{item.label}</span>
|
||||
{item.renderIcon ? item.renderIcon({ isActive }) : null}
|
||||
{Icon ? (
|
||||
<Icon
|
||||
className={`h-[15px] w-[15px] shrink-0 ${
|
||||
isActive ? "text-[#1e88e5]" : "text-[#64748b]"
|
||||
}`}
|
||||
/>
|
||||
) : null}
|
||||
<span className="whitespace-nowrap">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -141,4 +174,4 @@ export default function AnalysisPopupMenu({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
import { getData } from '@/components/lib/api';
|
||||
import { useState, useEffect } from "react";
|
||||
import moment from "moment";
|
||||
import { getData } from "@/components/lib/api";
|
||||
|
||||
export type TInftwtAnls = {
|
||||
inftwtAnlsSn:number;
|
||||
inftwtAnlsSn: number;
|
||||
aplcnYm: string;
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
@@ -15,18 +15,20 @@ export type TInftwtAnls = {
|
||||
atflMpngNm: string;
|
||||
atflOilqNm: string;
|
||||
atflNm: string;
|
||||
drtwSwtcRt:number;
|
||||
drtwSwtcRt: number;
|
||||
anlsInfo: string;
|
||||
scsYn: string;
|
||||
};
|
||||
|
||||
export type TInftwtAnlsRslt = {
|
||||
inftwtAnlsSn: number;
|
||||
pplMngNo: string;
|
||||
anlsTm: string;
|
||||
predcOilq:number;
|
||||
}
|
||||
predcOilq: number;
|
||||
};
|
||||
|
||||
export type TRdiiAnls = {
|
||||
rainInfwtAnlsSn:number;
|
||||
rainInfwtAnlsSn: number;
|
||||
aplcnYm: string;
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
@@ -38,234 +40,259 @@ export type TRdiiAnls = {
|
||||
atflMpngNm: string;
|
||||
atflRainNm: string;
|
||||
atflNm: string;
|
||||
drtwSwtcRt:number;
|
||||
drtwSwtcRt: number;
|
||||
anlsInfo: string;
|
||||
scsYn: string;
|
||||
srcNm: string; // 출처이름
|
||||
srcNm: string;
|
||||
};
|
||||
|
||||
export type TRdiiAnlsRslt = {
|
||||
rainInfwtAnlsSn: number;
|
||||
pplMngNo: string;
|
||||
anlsTm: string;
|
||||
predcOilq:number;
|
||||
minq:number;
|
||||
}
|
||||
predcOilq: number;
|
||||
minq: number;
|
||||
};
|
||||
|
||||
export type TOilqData = {
|
||||
inftwtAnlsSn: number; // 침입수분석 일련번호
|
||||
pplMngNo: string; // 관로관리번호
|
||||
mtrngDt: string; // 계측일시
|
||||
oilq: number; // 유량
|
||||
}
|
||||
inftwtAnlsSn: number;
|
||||
pplMngNo: string;
|
||||
mtrngDt: string;
|
||||
oilq: number;
|
||||
};
|
||||
|
||||
export type TRainData = {
|
||||
rainInfwtAnlsSn: number;
|
||||
mtrngDt: string;
|
||||
amrf: number; // 강우량
|
||||
}
|
||||
amrf: number;
|
||||
};
|
||||
|
||||
function roundTo(n: number, digits: number): number {
|
||||
const factor = Math.pow(10, digits);
|
||||
return Math.round(n * factor) / factor;
|
||||
}
|
||||
|
||||
const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string, hidden?: boolean }[] => {
|
||||
const yyyymms1 : { text: string; val: string; hidden?: boolean }[] = yyyymms.map(dateStr => {
|
||||
const year = dateStr.slice(0, 4);
|
||||
const month = dateStr.slice(4, 6);
|
||||
return {
|
||||
text: `${year}-${month}`,
|
||||
val: dateStr
|
||||
};
|
||||
});
|
||||
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);
|
||||
}
|
||||
const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string; hidden?: boolean }[] => {
|
||||
const mapped: { text: string; val: string; hidden?: boolean }[] = yyyymms.map((dateStr) => ({
|
||||
text: `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}`,
|
||||
val: dateStr,
|
||||
}));
|
||||
mapped.unshift({ val: "0", text: "선택", hidden: true });
|
||||
return mapped;
|
||||
};
|
||||
|
||||
export const useAplcnYms = (gubun:string) => {
|
||||
const [data, setData] = useState<{ text: string; val: string }[]>([]);
|
||||
const selectAplcnYms = async (gubun: 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(() => {
|
||||
const fetch = async (gubun:string) => {
|
||||
if (!gubun) {
|
||||
setData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetch = async () => {
|
||||
const result = await selectAplcnYms(gubun);
|
||||
if (result) setData(result);
|
||||
setData(result ?? []);
|
||||
};
|
||||
fetch(gubun);
|
||||
|
||||
void fetch();
|
||||
}, [gubun]);
|
||||
|
||||
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,
|
||||
val: anls.inftwtAnlsSn,
|
||||
}));
|
||||
options.unshift({ val: 0, text: '선택', hidden: true });
|
||||
options.unshift({ val: 0, text: "선택", hidden: true });
|
||||
return options;
|
||||
};
|
||||
|
||||
export const selectInftwtAnls = async(aplcnYm:string) => { // 침입수분석
|
||||
export const selectInftwtAnls = async (aplcnYm: string) => {
|
||||
try {
|
||||
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : '';
|
||||
const result = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`);
|
||||
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : "";
|
||||
const result = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ""}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return result.content;
|
||||
}
|
||||
} catch {
|
||||
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시"),
|
||||
val: roundTo(anlsRslt.predcOilq, 2),
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
export const selectInftwtAnlsRslt = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수분석 결과
|
||||
|
||||
export const selectInftwtAnlsRslt = async (inftwtAnlsSn: number, pplMngNo: string) => {
|
||||
try {
|
||||
if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) {
|
||||
console.warn("inftwtAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
if (!inftwtAnlsSn || !pplMngNo) return [];
|
||||
const result = await getData(`v1/inftwtAnlsRslt/ppl/${inftwtAnlsSn}/${pplMngNo}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return result.content;
|
||||
}
|
||||
} catch {
|
||||
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,
|
||||
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 }[] => {
|
||||
const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({
|
||||
export const selectOilq = async (inftwtAnlsSn: number, pplMngNo: string) => {
|
||||
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,
|
||||
val: anls.rainInfwtAnlsSn,
|
||||
}));
|
||||
options.unshift({ val: 0, text: '선택', hidden: true });
|
||||
options.unshift({ val: 0, text: "선택", hidden: true });
|
||||
return options;
|
||||
};
|
||||
|
||||
export const selectRdiiAnls = async(aplcnYm:string) => { // 침입수분석
|
||||
export const selectRdiiAnls = async (aplcnYm: string) => {
|
||||
try {
|
||||
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : '';
|
||||
const result = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`);
|
||||
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : "";
|
||||
const result = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ""}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return result.content;
|
||||
}
|
||||
} catch {
|
||||
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시"),
|
||||
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시"),
|
||||
val: roundTo(anlsRslt.minq, 2),
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // RDII분석 결과
|
||||
try {
|
||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) {
|
||||
console.warn("RdiiAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${pplMngNo}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
|
||||
export const selectRdiiAnlsRslt = async (rainInfwtAnlsSn: number, pplMngNo: string) => {
|
||||
if (!rainInfwtAnlsSn || !pplMngNo) return [];
|
||||
|
||||
for (const candidate of buildPplMngNoCandidates(pplMngNo)) {
|
||||
try {
|
||||
const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${candidate}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const selectRainData = async(rainInfwtAnlsSn:number) => { // 강우데이터
|
||||
try {
|
||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0)) {
|
||||
console.warn("RdiiAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
} catch {
|
||||
// resultOnly mode fallback candidates should not raise console noise
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const selectRainData = async (rainInfwtAnlsSn: number) => {
|
||||
try {
|
||||
if (!rainInfwtAnlsSn) return [];
|
||||
const result = await getData(`v1/rdiiAnlsRslt/rainData/${rainInfwtAnlsSn}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return result.content;
|
||||
}
|
||||
} catch {
|
||||
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시"),
|
||||
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 moment from 'moment';
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import moment from "moment";
|
||||
|
||||
import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
} from "@common/business_components";
|
||||
import { SelectBox } from "@common/business_components/ui";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import type { TRdiiAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import type { TRdiiAnls } from "./aplcnYms";
|
||||
import * as fn from "./aplcnYms";
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
pplMngNo?: string;
|
||||
rainInfwtAnlsSn?: number;
|
||||
aplcnYm?: string;
|
||||
analysisInfo?: string;
|
||||
resultOnly?: boolean;
|
||||
};
|
||||
|
||||
type LineBarChartData = {
|
||||
@@ -24,112 +28,77 @@ type LineBarChartData = {
|
||||
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 [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>(
|
||||
INITIAL_ANALYSIS_OPTIONS,
|
||||
);
|
||||
const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null); // 강우유입수분석정보
|
||||
const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState<number>(0);// 강우유입수분석 정보 선택시 사용
|
||||
const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null);
|
||||
const [selectedRainInfwtAnlsSn, setSelectedRainInfwtAnlsSn] = useState<number>(0);
|
||||
const [chartData, setChartData] = useState<LineBarChartData[]>([]);
|
||||
const [rdiiQty, setRdiiQty] = useState<string>('0'); // 강우유입수량
|
||||
const [rainTotal, setRainTotal] = useState<string>('0'); // 적용강우량
|
||||
const [totalVolume, setTotalVolume] = useState<string>('0');// 총체적 m³/mm
|
||||
const [rdiiQty, setRdiiQty] = useState<string>("0");
|
||||
const [rainTotal, setRainTotal] = useState<string>("0");
|
||||
const [totalVolume, setTotalVolume] = useState<string>("0");
|
||||
|
||||
const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기
|
||||
const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : '';
|
||||
const normalizedPplMngNo = normalizePplMngNo(pplMngNo);
|
||||
const isResultOnlyMode = resultOnly && rainInfwtAnlsSn > 0 && Boolean(normalizedPplMngNo);
|
||||
const aplcnYms = fn.useAplcnYms(isResultOnlyMode ? "" : "rdiiAnlsRslt");
|
||||
|
||||
const chartForm = CHART_FORM.LINEBAR2;
|
||||
const chartConfig = {
|
||||
barVal0: {
|
||||
label: '강수량',
|
||||
color: '#33AAFF',
|
||||
label: "강수량",
|
||||
color: "#33AAFF",
|
||||
},
|
||||
lineVal0: {
|
||||
label: 'RDII',
|
||||
color: '#FF0000',
|
||||
label: "RDII",
|
||||
color: "#FF0000",
|
||||
},
|
||||
} as ChartConfig;
|
||||
|
||||
const hasChartData = useMemo(() => chartData.length > 0, [chartData]);
|
||||
|
||||
const resetState = () => {
|
||||
setAplcnYm(''); // 상수사용월 초기화
|
||||
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);
|
||||
setSelectedAplcnYm("");
|
||||
setRdiiAnlss([]);
|
||||
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||
setRdiiAnls(null);
|
||||
setRainInfwtAnlsSn(0);
|
||||
setSelectedRainInfwtAnlsSn(0);
|
||||
setChartData([]);
|
||||
setRdiiQty('0');
|
||||
setRainTotal('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);
|
||||
}
|
||||
setRdiiQty("0");
|
||||
setRainTotal("0");
|
||||
setTotalVolume("0");
|
||||
};
|
||||
|
||||
const handleRdiiAnlsSnChange = (val: string | number) => {
|
||||
const selectedValue = Number(val);
|
||||
|
||||
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) {
|
||||
const loadResultChart = async (targetRainInfwtAnlsSn: number) => {
|
||||
if (!targetRainInfwtAnlsSn || !normalizedPplMngNo) {
|
||||
setChartData([]);
|
||||
setRdiiQty('0');
|
||||
setRainTotal('0');
|
||||
setTotalVolume('0');
|
||||
setRdiiQty("0");
|
||||
setRainTotal("0");
|
||||
setTotalVolume("0");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [rsltData, rainData, qtyData] = await Promise.all([
|
||||
fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, normalizedPplMngNo),
|
||||
fn.selectRainData(rainInfwtAnlsSn),
|
||||
fn.selectPplRdiiQty(rainInfwtAnlsSn, normalizedPplMngNo),
|
||||
fn.selectRdiiAnlsRslt(targetRainInfwtAnlsSn, normalizedPplMngNo),
|
||||
fn.selectRainData(targetRainInfwtAnlsSn),
|
||||
fn.selectPplRdiiQty(targetRainInfwtAnlsSn, normalizedPplMngNo),
|
||||
]);
|
||||
|
||||
const rsltDataFormatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []);
|
||||
@@ -143,7 +112,7 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
}
|
||||
});
|
||||
|
||||
setChartData(rsltDataFormatted);// 차트 데이터 설정
|
||||
setChartData(rsltDataFormatted);
|
||||
|
||||
const avgRdii = Number(qtyData?.rdiih ?? 0);
|
||||
const appliedRainfall = Number(qtyData?.raintotal ?? 0);
|
||||
@@ -152,18 +121,80 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
? Math.round(avgRdii * Number(qtyData?.sustaintime ?? 0) / qtyData.raintotal)
|
||||
: 0;
|
||||
|
||||
setRdiiQty(Math.round(avgRdii * 24).toLocaleString('ko-KR'));
|
||||
setRainTotal(appliedRainfall.toLocaleString('ko-KR'));
|
||||
setTotalVolume(rdiiPerRainfall.toLocaleString('ko-KR'));
|
||||
setRdiiQty(Math.round(avgRdii * 24).toLocaleString("ko-KR"));
|
||||
setRainTotal(appliedRainfall.toLocaleString("ko-KR"));
|
||||
setTotalVolume(rdiiPerRainfall.toLocaleString("ko-KR"));
|
||||
} catch (error) {
|
||||
console.error('rdiiRsltChart 에러:', error);
|
||||
console.error("RDII 분석결과 차트 조회 중 오류:", error);
|
||||
setChartData([]);
|
||||
setRdiiQty('0');
|
||||
setRainTotal('0');
|
||||
setTotalVolume('0');
|
||||
setRdiiQty("0");
|
||||
setRainTotal("0");
|
||||
setTotalVolume("0");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResultOnlyMode) return;
|
||||
|
||||
setSelectedAplcnYm(aplcnYm);
|
||||
setSelectedRainInfwtAnlsSn(rainInfwtAnlsSn);
|
||||
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||
void loadResultChart(rainInfwtAnlsSn);
|
||||
}, [aplcnYm, isResultOnlyMode, normalizedPplMngNo, rainInfwtAnlsSn]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
setSelectedAplcnYm(selectedValue);
|
||||
setRdiiAnlss([]);
|
||||
setRdiiAnls(null);
|
||||
setSelectedRainInfwtAnlsSn(0);
|
||||
setChartData([]);
|
||||
setRdiiQty("0");
|
||||
setRainTotal("0");
|
||||
setTotalVolume("0");
|
||||
|
||||
if (!selectedValue || selectedValue === "0") {
|
||||
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await fn.selectRdiiAnls(selectedValue);
|
||||
const list = data ?? [];
|
||||
setRdiiAnlss(list);
|
||||
setRdiiAnlsInfos(fn.getOptionsRdiiAnlsInfo(list));
|
||||
};
|
||||
|
||||
const handleRdiiAnlsSnChange = (val: string | number) => {
|
||||
const selectedValue = Number(val);
|
||||
|
||||
setSelectedRainInfwtAnlsSn(selectedValue);
|
||||
setChartData([]);
|
||||
setRdiiQty("0");
|
||||
setRainTotal("0");
|
||||
setTotalVolume("0");
|
||||
|
||||
if (selectedValue === 0) {
|
||||
setRdiiAnls(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedAnls = rdiiAnlss.find((anls) => anls.rainInfwtAnlsSn === selectedValue) ?? null;
|
||||
setRdiiAnls(selectedAnls);
|
||||
};
|
||||
|
||||
const fetchFilteredData = async () => {
|
||||
if (!selectedRainInfwtAnlsSn || !normalizedPplMngNo) {
|
||||
setChartData([]);
|
||||
setRdiiQty("0");
|
||||
setRainTotal("0");
|
||||
setTotalVolume("0");
|
||||
return;
|
||||
}
|
||||
|
||||
await loadResultChart(selectedRainInfwtAnlsSn);
|
||||
};
|
||||
|
||||
const renderInfoInput = (label: string, value?: string | number | null) => (
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
|
||||
@@ -172,166 +203,128 @@ const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
<Input
|
||||
className="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white px-3 text-xl text-[#2b3440]"
|
||||
placeholder=""
|
||||
value={value ?? ''}
|
||||
value={value ?? ""}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderBody = () => (
|
||||
<div className="text-black">
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div className="text-[18px] font-semibold text-[#111827]" />
|
||||
const renderResultPanel = () => (
|
||||
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-4">
|
||||
<h3 className="mb-4 text-[20px] font-extrabold tracking-[-0.02em] text-black">RDII 분석 결과</h3>
|
||||
<div className="mb-4 grid grid-cols-2 gap-4">
|
||||
<div className="flex min-h-[98px] items-center justify-between rounded-[14px] border border-[#4f88c6] bg-white px-6 py-4">
|
||||
<div className="flex items-center gap-2 text-[16px] font-extrabold text-[#ef4444]">
|
||||
<span>상수사용 적용연월</span>
|
||||
</div>
|
||||
<div className="text-[20px] font-extrabold text-[#222]">
|
||||
{selectedAplcnYm ? moment(selectedAplcnYm, "YYYYMM").format("YYYY-MM") : "-"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[98px] items-center justify-between rounded-[14px] border border-[#4f88c6] bg-white px-6 py-4">
|
||||
<div className="shrink-0 whitespace-nowrap text-[14px] font-extrabold text-[#ef4444]">
|
||||
<span>강우유입수량 분석결과</span>
|
||||
</div>
|
||||
<div className="ml-4 flex min-w-0 basis-[60%] flex-col gap-[2px]">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="min-w-0 basis-[48%] text-right text-[15px] text-[#555]">평균RDII</div>
|
||||
<div className="min-w-0 basis-[32%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">{rdiiQty}</div>
|
||||
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">m³/d</div>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="min-w-0 basis-[48%] text-right text-[15px] text-[#555]">적용 강우량</div>
|
||||
<div className="min-w-0 basis-[32%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">{rainTotal}</div>
|
||||
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">mm</div>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="min-w-0 basis-[48%] text-right text-[15px] text-[#555]">강우량 대비 RDII</div>
|
||||
<div className="min-w-0 basis-[32%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">{totalVolume}</div>
|
||||
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">m³/mm</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[10px] bg-white px-2 pb-2 pt-3">
|
||||
{hasChartData ? (
|
||||
<MainChart
|
||||
chartConfig={chartConfig}
|
||||
chartForm={chartForm}
|
||||
chartData={chartData}
|
||||
chartTitle={"RDII 그래프"}
|
||||
yFieldText2="강수량(mm)"
|
||||
yFieldText="유량(m³/h)"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-[464px] items-center justify-center text-[18px] text-[#2c2f33]">데이터가 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
const renderDefaultBody = () => (
|
||||
<div className="text-black">
|
||||
<div className="grid min-h-[68vh] grid-cols-[minmax(0,36%)_minmax(0,64%)] gap-5">
|
||||
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
|
||||
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
|
||||
강우유입수분석 선택 조건
|
||||
</h3>
|
||||
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">강우유입수량 선택 조건</h3>
|
||||
|
||||
<div className="rounded-[6px] border border-[#d9dde3] bg-[#eff1f4] px-4 py-6">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
|
||||
상수사용월:
|
||||
</Label>
|
||||
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">상수사용월</Label>
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
||||
label=""
|
||||
selectArray={aplcnYms}
|
||||
value={aplcnYm}
|
||||
value={selectedAplcnYm}
|
||||
onSelectValue={handleAplcnYmChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
|
||||
분석정보:
|
||||
</Label>
|
||||
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">분석정보:</Label>
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
||||
label=""
|
||||
selectArray={rdiiAnlsInfos}
|
||||
value={rainInfwtAnlsSn}
|
||||
value={selectedRainInfwtAnlsSn}
|
||||
onSelectValue={handleRdiiAnlsSnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{renderInfoInput('오수전환율:', rdiiAnls?.drtwSwtcRt ?? '')}
|
||||
{renderInfoInput('수용가-맨홀 매칭:', rdiiAnls?.atflMpngNm ?? '')}
|
||||
{renderInfoInput('관망도 파일명:', rdiiAnls?.atflInpNm ?? '')}
|
||||
{renderInfoInput('강우 파일명:', rdiiAnls?.atflRainNm ?? '')}
|
||||
{renderInfoInput('RTK계수 출처:', rdiiAnls?.srcNm ?? '')}
|
||||
{renderInfoInput("오수전환율", rdiiAnls?.drtwSwtcRt ?? "")}
|
||||
{renderInfoInput("수용가-맨홀 매칭:", rdiiAnls?.atflMpngNm ?? "")}
|
||||
{renderInfoInput("관망도 파일명", rdiiAnls?.atflInpNm ?? "")}
|
||||
{renderInfoInput("강우 파일명", rdiiAnls?.atflRainNm ?? "")}
|
||||
{renderInfoInput("RTK계수 출처:", rdiiAnls?.srcNm ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 mb-[40px] flex justify-center">
|
||||
<Button size="lg" onClick={fetchFilteredData}>
|
||||
조회
|
||||
</Button>
|
||||
<Button size="lg" onClick={fetchFilteredData}>조회</Button>
|
||||
|
||||
<Button size="lg" color="gray" onClick={resetState}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button size="lg" color="gray" onClick={resetState}>초기화</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
|
||||
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
|
||||
강우유입수분석 결과
|
||||
</h3>
|
||||
|
||||
<div className="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>
|
||||
{renderResultPanel()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isResultOnlyMode) {
|
||||
return <div className="min-h-dvh bg-white p-3">{renderResultPanel()}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-white p-4">
|
||||
<h1 className="mb-3 text-2xl font-semibold text-gray-900">RDII 분석</h1>
|
||||
{renderBody()}
|
||||
{renderDefaultBody()}
|
||||
</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";
|
||||
import { downloadAgedPipeCsv } from "./components/agedPipe/agedPipeExport";
|
||||
import { useInftwtResultMap } from "./components/inftwtResult/useInftwtResultMap";
|
||||
import { useRdiiResultMap } from "./components/rdiiResult/useRdiiResultMap";
|
||||
import SinkholeDetailPanel from "./components/sinkhole/SinkholeDetailPanel";
|
||||
import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettingPanel";
|
||||
import FloodHistoryDetailPanel from "./components/floodHistory/FloodHistoryDetailPanel";
|
||||
@@ -2459,6 +2460,12 @@ const Map = () => {
|
||||
setIsOpen,
|
||||
setIsSearchModalOpen,
|
||||
});
|
||||
const rdiiResultMap = useRdiiResultMap({
|
||||
gisMaps,
|
||||
setFeatureData,
|
||||
setIsOpen,
|
||||
setIsSearchModalOpen,
|
||||
});
|
||||
const shouldFocusAgedPipeOnNextSelectRef = useRef(false);
|
||||
const agedPipePopupWindowRef = useRef<Window | null>(null);
|
||||
const sinkholePopupWindowRef = useRef<Window | null>(null);
|
||||
@@ -3584,6 +3591,12 @@ const Map = () => {
|
||||
const clickedLayerTypeName = getSelectedFeatureLayerTypeName(data);
|
||||
// 지형에 있는 피처를 직접 선택하면 지반침하 반경 내에서 만든 임시 관로 강조는 해제한다.
|
||||
clearSinkholeFocusedPipeLayer();
|
||||
if (rdiiResultMap.handleOverlayFeatureSelect(data)) {
|
||||
setFeatureData(null);
|
||||
setIsOpen(false);
|
||||
setIsSearchModalOpen(false);
|
||||
return;
|
||||
}
|
||||
if (inftwtResultMap.handleOverlayFeatureSelect(data)) {
|
||||
setFeatureData(null);
|
||||
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 분석 > 침입수 분석결과`, `RDII 분석결과`는 지도 popup 흐름으로 대체하고 레거시 라우트는 유지한 채 LNB에서만 숨긴다. */
|
||||
[data-sidebar="menu"] > *:nth-child(2) [data-sidebar="menu-sub"] > li:nth-child(2) {
|
||||
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,
|
||||
height: 900,
|
||||
},
|
||||
rdiiResultPopup: {
|
||||
key: "rdiiResultPopup",
|
||||
title: "RDII 분석결과",
|
||||
width: 1280,
|
||||
height: 900,
|
||||
},
|
||||
infilChart: {
|
||||
key: "infilChart",
|
||||
title: "침입수 분석",
|
||||
@@ -66,8 +72,12 @@ export const windowPopupDefinitions = {
|
||||
rdiiChart: {
|
||||
key: "rdiiChart",
|
||||
title: "RDII 분석",
|
||||
width: 1360,
|
||||
height: 980,
|
||||
width: 940,
|
||||
height: 860,
|
||||
placement: "browser-left",
|
||||
alignTop: true,
|
||||
offsetLeft: 0,
|
||||
offsetTop: 70,
|
||||
requiredParams: ["pplMngNo"],
|
||||
},
|
||||
sscDetail: {
|
||||
|
||||
@@ -8,6 +8,7 @@ import InftwtResultPopupContent from "./ui/InftwtResultPopupContent";
|
||||
import OldPpUploadPopupContent from "./ui/OldPpUploadPopupContent";
|
||||
import OldPpTablePopupContent from "./ui/OldPpTablePopupContent";
|
||||
import RdiiChartPopupContent from "./ui/RdiiChartPopupContent";
|
||||
import RdiiResultPopupContent from "./ui/RdiiResultPopupContent";
|
||||
import ReportFilesPopupContent from "./ui/ReportFilesPopupContent";
|
||||
import ReportTabPopupContent from "./ui/ReportTabPopupContent";
|
||||
import SearchModalPopupContent from "./ui/SearchModalPopupContent";
|
||||
@@ -65,6 +66,10 @@ export const windowPopupRegistry = {
|
||||
...windowPopupDefinitions.inftwtResultPopup,
|
||||
component: InftwtResultPopupContent,
|
||||
},
|
||||
rdiiResultPopup: {
|
||||
...windowPopupDefinitions.rdiiResultPopup,
|
||||
component: RdiiResultPopupContent,
|
||||
},
|
||||
infilChart: {
|
||||
...windowPopupDefinitions.infilChart,
|
||||
component: InfilChartPopupContent,
|
||||
|
||||
@@ -8,6 +8,10 @@ export default function RdiiChartPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
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) {
|
||||
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