Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev
This commit is contained in:
@@ -72,8 +72,8 @@ const ANALYSIS_POPUP_ITEMS: Array<{
|
||||
icon: ChartColumnBig,
|
||||
},
|
||||
{
|
||||
key: "floodPredictionMapPopup",
|
||||
label: windowPopupDefinitions.floodPredictionMapPopup.title,
|
||||
key: "fldngPredcPrst",
|
||||
label: windowPopupDefinitions.fldngPredcPrst.title,
|
||||
icon: Waves,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
WINDOW_POPUP_MESSAGE_TYPE,
|
||||
type WindowPopupContentProps,
|
||||
} from "@/features/windowPopup/model/types";
|
||||
|
||||
import {
|
||||
fetchWaterRiskAreas,
|
||||
fetchWaterRiskMapView,
|
||||
@@ -45,6 +46,7 @@ type ResultFilters = {
|
||||
const WATER_RISK_PAGE_SIZE = 10;
|
||||
const EMPTY_RESULT_PAGE: WaterRiskResultPage = {
|
||||
items: [],
|
||||
analysisBaseAt: null,
|
||||
total: 0,
|
||||
page: 0,
|
||||
size: WATER_RISK_PAGE_SIZE,
|
||||
@@ -57,73 +59,44 @@ type WaterRiskTableRow = Record<string, unknown> & {
|
||||
sidoName: string;
|
||||
sggName: string;
|
||||
areaName: string;
|
||||
rainStartedAt: string;
|
||||
currentAccumRainQty: string;
|
||||
forecastRainQty: string;
|
||||
maxRainIntensity: string;
|
||||
runoffFlowQty: string;
|
||||
conveyQty: string;
|
||||
analysisDisplay: string;
|
||||
maxFloodingAt: string;
|
||||
maxFloodingVolume: string;
|
||||
riskStatus: WaterRiskResultRow["riskStatus"];
|
||||
};
|
||||
|
||||
const CURRENT_RESULT_HEADLIST: UiHeadProps[] = [
|
||||
const WATER_RISK_RESULT_HEADLIST: UiHeadProps[] = [
|
||||
{ id: "rowNo", title: "순번", widthPercent: 6 },
|
||||
{ id: "sidoName", title: "시도", widthPercent: 9 },
|
||||
{ id: "sggName", title: "시군구", widthPercent: 9 },
|
||||
{ id: "sidoName", title: "시도", widthPercent: 8 },
|
||||
{ id: "sggName", title: "시군구", widthPercent: 8 },
|
||||
{ id: "areaName", title: "중점관리지역", widthPercent: 14 },
|
||||
{ id: "rainStartedAt", title: "강우시작시간", widthPercent: 13 },
|
||||
{
|
||||
id: "currentAccumRainQty",
|
||||
title: "현재누적강우량(mm)",
|
||||
widthPercent: 13,
|
||||
title: "누적강우량(mm)",
|
||||
widthPercent: 11,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
id: "forecastRainQty",
|
||||
title: "예측강우량(mm)",
|
||||
widthPercent: 12,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
id: "runoffFlowQty",
|
||||
title: "유출유량(㎥/s)",
|
||||
widthPercent: 11,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
id: "conveyQty",
|
||||
title: "통수량(㎥/s)",
|
||||
id: "maxFloodingAt",
|
||||
title: "최대 침수 예상시점",
|
||||
widthPercent: 13,
|
||||
},
|
||||
{
|
||||
id: "maxFloodingVolume",
|
||||
title: "최대침수량(m³)",
|
||||
widthPercent: 10,
|
||||
align: "right",
|
||||
},
|
||||
{ id: "analysisDisplay", title: "분석일", widthPercent: 10 },
|
||||
{ id: "riskStatus", title: "통수위험", widthPercent: 7 },
|
||||
];
|
||||
|
||||
const HISTORY_RESULT_HEADLIST: UiHeadProps[] = [
|
||||
{ id: "rowNo", title: "순번", widthPercent: 6 },
|
||||
{ id: "sidoName", title: "시도", widthPercent: 9 },
|
||||
{ id: "sggName", title: "시군구", widthPercent: 9 },
|
||||
{ id: "areaName", title: "중점관리지역", widthPercent: 15 },
|
||||
{
|
||||
id: "maxRainIntensity",
|
||||
title: "최대강우강도(mm/hr)",
|
||||
widthPercent: 15,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
id: "runoffFlowQty",
|
||||
title: "유출유량(㎥/s)",
|
||||
widthPercent: 12,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
id: "conveyQty",
|
||||
title: "통수량(㎥/s)",
|
||||
widthPercent: 11,
|
||||
align: "right",
|
||||
},
|
||||
{ id: "analysisDisplay", title: "분석일", widthPercent: 12 },
|
||||
{ id: "riskStatus", title: "통수위험", widthPercent: 11 },
|
||||
{ id: "riskStatus", title: "위험 여부", widthPercent: 7 },
|
||||
];
|
||||
|
||||
const formatOptionalNumber = (
|
||||
@@ -139,12 +112,26 @@ const formatLocalDate = (date: Date) => {
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
/** 화면을 연 날짜를 현재 위험 기본 조회일로 사용한다. */
|
||||
/** 현재 로컬시각을 API가 조회할 정시 yyyy-MM-ddTHH:00:00으로 내림한다. */
|
||||
const formatCurrentAnalysisHour = (date: Date) =>
|
||||
`${formatLocalDate(date)}T${String(date.getHours()).padStart(2, "0")}:00:00`;
|
||||
|
||||
/** API 분석기준시간을 화면의 yyyy.MM.dd HH:mm 형식으로 표시한다. */
|
||||
const formatAnalysisBaseAt = (value?: string | null) => {
|
||||
if (!value || value === "-") return "-";
|
||||
const normalized = value.replace("T", " ");
|
||||
const [date = "", time = ""] = normalized.split(" ");
|
||||
return `${date.replaceAll("-", ".")} ${time.slice(0, 5)}`.trim();
|
||||
};
|
||||
|
||||
/** 개발 DB의 과거 시험자료와 운영 최신자료를 함께 찾도록 최근 1년을 기본 조회기간으로 사용한다. */
|
||||
const buildDefaultResultFilters = (): ResultFilters => {
|
||||
const today = formatLocalDate(new Date());
|
||||
const endDate = new Date();
|
||||
const startDate = new Date(endDate);
|
||||
startDate.setDate(startDate.getDate() - 365);
|
||||
return {
|
||||
startDate: today,
|
||||
endDate: today,
|
||||
startDate: formatLocalDate(startDate),
|
||||
endDate: formatLocalDate(endDate),
|
||||
ctpvCd: "ALL",
|
||||
sggCd: "ALL",
|
||||
areaId: "ALL",
|
||||
@@ -183,6 +170,9 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
const [selectedResultKey, setSelectedResultKey] = useState("");
|
||||
const [resultCurrentPage, setResultCurrentPage] = useState(1);
|
||||
const [reloadVersion, setReloadVersion] = useState(0);
|
||||
const [currentAnalysisBaseAt, setCurrentAnalysisBaseAt] = useState(() =>
|
||||
formatCurrentAnalysisHour(new Date()),
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isMapLoading, setIsMapLoading] = useState(false);
|
||||
const [noticeMessage, setNoticeMessage] = useState("");
|
||||
@@ -214,12 +204,14 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
fetchWaterRiskResults({
|
||||
startDate: appliedResultFilters.startDate,
|
||||
endDate: appliedResultFilters.endDate,
|
||||
analysisBaseAt:
|
||||
resultMode === "current" ? currentAnalysisBaseAt : undefined,
|
||||
ctpvCd:
|
||||
appliedResultFilters.ctpvCd === "ALL"
|
||||
resultMode === "current" || appliedResultFilters.ctpvCd === "ALL"
|
||||
? undefined
|
||||
: appliedResultFilters.ctpvCd,
|
||||
sggCd:
|
||||
appliedResultFilters.sggCd === "ALL"
|
||||
resultMode === "current" || appliedResultFilters.sggCd === "ALL"
|
||||
? undefined
|
||||
: appliedResultFilters.sggCd,
|
||||
areaId:
|
||||
@@ -227,9 +219,10 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
? undefined
|
||||
: appliedResultFilters.areaId,
|
||||
riskStatus:
|
||||
appliedResultFilters.riskStatus === "ALL"
|
||||
resultMode === "current" || appliedResultFilters.riskStatus === "ALL"
|
||||
? undefined
|
||||
: appliedResultFilters.riskStatus,
|
||||
rainEventStatus: resultMode === "current" ? "IN_PROGRESS" : "ENDED",
|
||||
page: resultCurrentPage - 1,
|
||||
size: WATER_RISK_PAGE_SIZE,
|
||||
})
|
||||
@@ -257,7 +250,13 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [appliedResultFilters, reloadVersion, resultCurrentPage]);
|
||||
}, [
|
||||
appliedResultFilters,
|
||||
currentAnalysisBaseAt,
|
||||
reloadVersion,
|
||||
resultCurrentPage,
|
||||
resultMode,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
@@ -284,9 +283,34 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !window.resizeTo) return;
|
||||
window.resizeTo(1180, 720);
|
||||
window.resizeTo(1360, 600);
|
||||
}, []);
|
||||
|
||||
/** 현재 탭의 표시·조회 기준시간을 매 정시에 새 현재시간으로 변경한다. */
|
||||
useEffect(() => {
|
||||
if (resultMode !== "current" || typeof window === "undefined") return;
|
||||
|
||||
let intervalId: number | undefined;
|
||||
const applyCurrentHour = () =>
|
||||
setCurrentAnalysisBaseAt(formatCurrentAnalysisHour(new Date()));
|
||||
applyCurrentHour();
|
||||
const now = new Date();
|
||||
const nextRefreshAt = new Date(now);
|
||||
nextRefreshAt.setHours(now.getHours() + 1, 0, 0, 0);
|
||||
const timeoutId = window.setTimeout(
|
||||
() => {
|
||||
applyCurrentHour();
|
||||
intervalId = window.setInterval(applyCurrentHour, 60 * 60 * 1000);
|
||||
},
|
||||
Math.max(0, nextRefreshAt.getTime() - now.getTime()),
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
if (intervalId != null) window.clearInterval(intervalId);
|
||||
};
|
||||
}, [resultMode]);
|
||||
|
||||
const ctpvOptions = useMemo(() => {
|
||||
const unique = new Map<string, string>();
|
||||
areaOptions.forEach((option) => {
|
||||
@@ -334,25 +358,18 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
const tableRows = useMemo<WaterRiskTableRow[]>(
|
||||
() =>
|
||||
resultPage.items.map((row) => {
|
||||
const representativePipe =
|
||||
row.pipeSummaries.find((pipe) => pipe.riskStatus === "DANGER") ??
|
||||
row.pipeSummaries[0];
|
||||
|
||||
return {
|
||||
sourceRow: row,
|
||||
rowNo: 0,
|
||||
sidoName: row.sidoName || "-",
|
||||
sggName: row.sggName || "-",
|
||||
areaName: row.areaName,
|
||||
rainStartedAt: formatAnalysisBaseAt(row.rainStartedAt),
|
||||
currentAccumRainQty: formatOptionalNumber(row.currentAccumRainQty, 1),
|
||||
forecastRainQty: formatOptionalNumber(row.forecastRainQty, 1),
|
||||
maxRainIntensity: formatOptionalNumber(row.maxRainIntensity, 1),
|
||||
runoffFlowQty: formatOptionalNumber(
|
||||
representativePipe?.runoffFlowQty,
|
||||
2,
|
||||
),
|
||||
conveyQty: formatOptionalNumber(representativePipe?.conveyQty, 3),
|
||||
analysisDisplay: row.maxFloodingAt || row.analysisDate || "-",
|
||||
maxFloodingAt: formatAnalysisBaseAt(row.maxFloodingAt),
|
||||
// 목록 API에는 최대 침수유량만 있고 침수량 합계는 아직 제공하지 않는다.
|
||||
maxFloodingVolume: "-",
|
||||
riskStatus: row.riskStatus,
|
||||
};
|
||||
}),
|
||||
@@ -376,10 +393,12 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
setNoticeMessage("통수위험 분석 결과를 조회했습니다.");
|
||||
};
|
||||
|
||||
/** 현재 위험과 과거 이력 표시열만 전환하고 같은 API 검색조건은 유지한다. */
|
||||
/** 강우이벤트 상태에 따라 진행중 현재 위험과 종료된 과거 이력을 다시 조회한다. */
|
||||
const handleChangeResultMode = (mode: WaterRiskResultMode) => {
|
||||
setResultMode(mode);
|
||||
setResultCurrentPage(1);
|
||||
setResultPage(EMPTY_RESULT_PAGE);
|
||||
setSelectedResultKey("");
|
||||
setNoticeMessage("");
|
||||
};
|
||||
|
||||
@@ -417,102 +436,116 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
}`}
|
||||
>
|
||||
<LoadingOverlay isLoading={isLoading || isMapLoading} />
|
||||
<UITab
|
||||
variant="underline"
|
||||
defaultValue="current"
|
||||
tabList={[
|
||||
{ value: "current", label: "현재 위험" },
|
||||
{ value: "history", label: "과거 이력" },
|
||||
]}
|
||||
onValueChange={(value) =>
|
||||
handleChangeResultMode(value as WaterRiskResultMode)
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<UITab
|
||||
variant="underline"
|
||||
defaultValue="current"
|
||||
tabList={[
|
||||
{ value: "current", label: "현재 위험" },
|
||||
{ value: "history", label: "과거 이력" },
|
||||
]}
|
||||
onValueChange={(value) =>
|
||||
handleChangeResultMode(value as WaterRiskResultMode)
|
||||
}
|
||||
/>
|
||||
{resultMode === "current" ? (
|
||||
<div className="flex items-center gap-2 text-xl">
|
||||
<span className="font-medium text-label">분석기준시간</span>
|
||||
<strong className="font-semibold text-primary">
|
||||
{formatAnalysisBaseAt(currentAnalysisBaseAt)}
|
||||
</strong>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<PopupSearchPanel contentClassName="flex flex-wrap items-center gap-x-5 gap-y-3">
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
{resultMode === "current" ? "조회기간" : "강우발생일"}
|
||||
</div>
|
||||
<span className="flex shrink-0 items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={draftResultFilters.startDate}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
startDate: event.target.value,
|
||||
}))
|
||||
}
|
||||
aria-label="조회 시작일"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<span className="text-xl font-medium text-label">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={draftResultFilters.endDate}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
endDate: event.target.value,
|
||||
}))
|
||||
}
|
||||
aria-label="조회 종료일"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
{resultMode === "history" ? (
|
||||
<>
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
강우발생일
|
||||
</div>
|
||||
<span className="flex shrink-0 items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={draftResultFilters.startDate}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
startDate: event.target.value,
|
||||
}))
|
||||
}
|
||||
aria-label="조회 시작일"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<span className="text-xl font-medium text-label">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={draftResultFilters.endDate}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
endDate: event.target.value,
|
||||
}))
|
||||
}
|
||||
aria-label="조회 종료일"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
시도
|
||||
</div>
|
||||
<select
|
||||
value={draftResultFilters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "ALL",
|
||||
areaId: "ALL",
|
||||
}))
|
||||
}
|
||||
aria-label="시도"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="ALL">전체</option>
|
||||
{ctpvOptions.map((option) => (
|
||||
<option key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
시도
|
||||
</div>
|
||||
<select
|
||||
value={draftResultFilters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "ALL",
|
||||
areaId: "ALL",
|
||||
}))
|
||||
}
|
||||
aria-label="시도"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="ALL">전체</option>
|
||||
{ctpvOptions.map((option) => (
|
||||
<option key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
시군구
|
||||
</div>
|
||||
<select
|
||||
value={draftResultFilters.sggCd}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
sggCd: event.target.value,
|
||||
areaId: "ALL",
|
||||
}))
|
||||
}
|
||||
aria-label="시군구"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="ALL">전체</option>
|
||||
{sggOptions.map((option) => (
|
||||
<option key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
시군구
|
||||
</div>
|
||||
<select
|
||||
value={draftResultFilters.sggCd}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
sggCd: event.target.value,
|
||||
areaId: "ALL",
|
||||
}))
|
||||
}
|
||||
aria-label="시군구"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="ALL">전체</option>
|
||||
{sggOptions.map((option) => (
|
||||
<option key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
@@ -530,7 +563,10 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
className="h-[3.2rem] w-[18rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="ALL">전체</option>
|
||||
{filteredAreaOptions.map((option) => (
|
||||
{(resultMode === "current"
|
||||
? areaOptions
|
||||
: filteredAreaOptions
|
||||
).map((option) => (
|
||||
<option
|
||||
key={`${option.ctpvCd}:${option.sggCd}:${option.areaId}`}
|
||||
value={option.areaId}
|
||||
@@ -541,26 +577,29 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
위험상태
|
||||
</div>
|
||||
<select
|
||||
value={draftResultFilters.riskStatus}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
riskStatus: event.target.value as ResultFilters["riskStatus"],
|
||||
}))
|
||||
}
|
||||
aria-label="위험상태"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="ALL">전체</option>
|
||||
<option value="DANGER">위험</option>
|
||||
<option value="NORMAL">정상</option>
|
||||
</select>
|
||||
</label>
|
||||
{resultMode === "history" ? (
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
위험상태
|
||||
</div>
|
||||
<select
|
||||
value={draftResultFilters.riskStatus}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
riskStatus: event.target
|
||||
.value as ResultFilters["riskStatus"],
|
||||
}))
|
||||
}
|
||||
aria-label="위험상태"
|
||||
className="h-[3.2rem] w-[14rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="ALL">전체</option>
|
||||
<option value="DANGER">위험</option>
|
||||
<option value="NORMAL">정상</option>
|
||||
</select>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<div className="flex min-w-fit flex-1 items-center justify-end gap-2">
|
||||
<Button
|
||||
@@ -588,11 +627,7 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
|
||||
<MapPopupDataTable
|
||||
rows={tableRows}
|
||||
headlist={
|
||||
resultMode === "current"
|
||||
? CURRENT_RESULT_HEADLIST
|
||||
: HISTORY_RESULT_HEADLIST
|
||||
}
|
||||
headlist={WATER_RISK_RESULT_HEADLIST}
|
||||
title={resultListTitle}
|
||||
total={resultPage.total}
|
||||
page={resultCurrentPage}
|
||||
@@ -611,11 +646,11 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
) : undefined
|
||||
}
|
||||
scrollMode="horizontal"
|
||||
minTableWidth={resultMode === "current" ? "114rem" : "106rem"}
|
||||
minTableWidth="124rem"
|
||||
emptyMessage="조회 조건에 해당하는 통수위험 결과가 없습니다."
|
||||
actions={
|
||||
<span className="text-xl font-normal text-muted-foreground">
|
||||
합리식 유출유량과 Manning 통수량 비교 결과
|
||||
SWMM 시뮬레이션 기반 통수위험 분석 결과
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -11,10 +11,12 @@ import type {
|
||||
export type WaterRiskResultQuery = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
analysisBaseAt?: string;
|
||||
ctpvCd?: string;
|
||||
sggCd?: string;
|
||||
areaId?: string;
|
||||
riskStatus?: WaterRiskStatus;
|
||||
rainEventStatus?: "IN_PROGRESS" | "ENDED";
|
||||
page: number;
|
||||
size: number;
|
||||
};
|
||||
@@ -48,9 +50,11 @@ export const fetchWaterRiskResults = async (
|
||||
size: String(query.size),
|
||||
});
|
||||
if (query.ctpvCd) params.set("ctpvCd", query.ctpvCd);
|
||||
if (query.analysisBaseAt) params.set("analysisBaseAt", query.analysisBaseAt);
|
||||
if (query.sggCd) params.set("sggCd", query.sggCd);
|
||||
if (query.areaId) params.set("areaId", query.areaId);
|
||||
if (query.riskStatus) params.set("riskStatus", query.riskStatus);
|
||||
if (query.rainEventStatus) params.set("rainEventStatus", query.rainEventStatus);
|
||||
|
||||
const response = await getData(`v1/waterRisk/results?${params.toString()}`);
|
||||
const items = Array.isArray(response?.items)
|
||||
@@ -58,6 +62,7 @@ export const fetchWaterRiskResults = async (
|
||||
: [];
|
||||
return {
|
||||
items,
|
||||
analysisBaseAt: response?.analysisBaseAt ?? null,
|
||||
total: Number(response?.total ?? 0),
|
||||
page: Number(response?.page ?? query.page),
|
||||
size: Number(response?.size ?? query.size),
|
||||
@@ -76,7 +81,12 @@ export const fetchWaterRiskMapView = async (
|
||||
ctpvCd: row.ctpvCd,
|
||||
sggCd: row.sggCd,
|
||||
});
|
||||
return getData(
|
||||
const response = await getData(
|
||||
`v1/waterRisk/results/${encodeURIComponent(row.analysisDate)}/areas/${encodeURIComponent(row.areaId)}/mapView?${params.toString()}`,
|
||||
);
|
||||
return {
|
||||
...response,
|
||||
pipes: Array.isArray(response?.pipes) ? response.pipes : [],
|
||||
manholes: Array.isArray(response?.manholes) ? response.manholes : [],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
"use client";
|
||||
|
||||
export type WaterRiskStatus = "DANGER" | "NORMAL";
|
||||
export type WaterRiskCalculationMethod = "METHOD_1" | "METHOD_2" | "RATIONAL_MANNING";
|
||||
export type WaterRiskCalculationMethod = "SWMM";
|
||||
|
||||
/** 대표실행에서 capacity가 가장 큰 관로의 실제 SWMM 결과이다. */
|
||||
export type WaterRiskPipeSummary = {
|
||||
pipeId: string;
|
||||
runoffFlowQty: number;
|
||||
conveyQty: number;
|
||||
runoffFlowQty: number | null;
|
||||
conveyQty: number | null;
|
||||
riskStatus?: WaterRiskStatus;
|
||||
};
|
||||
|
||||
/** 강우이벤트 대표실행 한 건을 팝업 목록에 표시하는 모델이다. */
|
||||
export type WaterRiskResultRow = {
|
||||
analysisDate?: string;
|
||||
analysisBaseAt?: string | null;
|
||||
rainStartedAt?: string | null;
|
||||
areaId: string;
|
||||
areaName: string;
|
||||
ctpvCd?: string;
|
||||
sidoName: string;
|
||||
sggCd?: string;
|
||||
sggName: string;
|
||||
// 기존 물리 테이블에 누적강우량이 없으므로 값이 없을 때는 화면에서 '-'로 표시한다.
|
||||
currentAccumRainQty: number | null;
|
||||
forecastRainQty: number | null;
|
||||
maxRainIntensity: number | null;
|
||||
@@ -28,26 +31,7 @@ export type WaterRiskResultRow = {
|
||||
pipeSummaries: WaterRiskPipeSummary[];
|
||||
};
|
||||
|
||||
export type WaterRiskGridCell = {
|
||||
gridId: string;
|
||||
rainQty: number;
|
||||
colorLevel: "LEVEL_1" | "LEVEL_2" | "LEVEL_3" | "LEVEL_4";
|
||||
polygonCoordinates: number[][][];
|
||||
};
|
||||
|
||||
export type WaterRiskAreaMapData = {
|
||||
areaId: string;
|
||||
areaName: string;
|
||||
polygonCoordinates: number[][][];
|
||||
gridCells: WaterRiskGridCell[];
|
||||
};
|
||||
|
||||
export type WaterRiskMapPipe = {
|
||||
pipeId: string;
|
||||
displayLabel: string;
|
||||
lineCoordinates: number[][];
|
||||
};
|
||||
|
||||
/** 부모 지도와 별도 팝업 사이에서 공유하는 최소 선택 상태이다. */
|
||||
export type WaterRiskSessionSnapshot = {
|
||||
selectedAreaId: string | null;
|
||||
};
|
||||
@@ -56,650 +40,9 @@ export const EMPTY_WATER_RISK_SESSION_SNAPSHOT: WaterRiskSessionSnapshot = {
|
||||
selectedAreaId: null,
|
||||
};
|
||||
|
||||
const buildRectanglePolygon = (
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
halfWidth: number,
|
||||
halfHeight: number,
|
||||
) => {
|
||||
return [[
|
||||
[centerX - halfWidth, centerY - halfHeight],
|
||||
[centerX + halfWidth, centerY - halfHeight],
|
||||
[centerX + halfWidth, centerY + halfHeight],
|
||||
[centerX - halfWidth, centerY + halfHeight],
|
||||
[centerX - halfWidth, centerY - halfHeight],
|
||||
]];
|
||||
};
|
||||
|
||||
const buildGridCells = (
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
values: number[],
|
||||
): WaterRiskGridCell[] => {
|
||||
const cellHalfSize = 180;
|
||||
const offsets = [
|
||||
[-190, 190],
|
||||
[190, 190],
|
||||
[-190, -190],
|
||||
[190, -190],
|
||||
] as const;
|
||||
|
||||
return offsets.map(([offsetX, offsetY], index) => {
|
||||
const rainQty = values[index] ?? values[values.length - 1] ?? 0;
|
||||
const colorLevel = rainQty >= 40
|
||||
? "LEVEL_4"
|
||||
: rainQty >= 30
|
||||
? "LEVEL_3"
|
||||
: rainQty >= 20
|
||||
? "LEVEL_2"
|
||||
: "LEVEL_1";
|
||||
|
||||
return {
|
||||
gridId: `GRID-${index + 1}`,
|
||||
rainQty,
|
||||
colorLevel,
|
||||
polygonCoordinates: buildRectanglePolygon(
|
||||
centerX + offsetX,
|
||||
centerY + offsetY,
|
||||
cellHalfSize,
|
||||
cellHalfSize,
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const WATER_RISK_RESULT_ROWS: WaterRiskResultRow[] = [
|
||||
{
|
||||
areaId: "AREA-001",
|
||||
areaName: "A 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 18.6,
|
||||
forecastRainQty: 42.0,
|
||||
maxRainIntensity: 50.2,
|
||||
maxFloodingAt: "2026-05-18 14:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-10284", runoffFlowQty: 18.5, conveyQty: 15.2 },
|
||||
{ pipeId: "PIPE-10291", runoffFlowQty: 11.3, conveyQty: 12.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-002",
|
||||
areaName: "B 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 12.4,
|
||||
forecastRainQty: 31.4,
|
||||
maxRainIntensity: 42.1,
|
||||
maxFloodingAt: "2026-05-18 13:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-45103", runoffFlowQty: 9.8, conveyQty: 14.7 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-003",
|
||||
areaName: "C 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 15.2,
|
||||
forecastRainQty: 37.8,
|
||||
maxRainIntensity: 48.7,
|
||||
maxFloodingAt: "2026-05-18 14:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-60014", runoffFlowQty: 16.1, conveyQty: 12.9 },
|
||||
{ pipeId: "PIPE-60018", runoffFlowQty: 10.2, conveyQty: 11.1 },
|
||||
{ pipeId: "PIPE-60021", runoffFlowQty: 8.7, conveyQty: 9.5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-004",
|
||||
areaName: "D 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 8.9,
|
||||
forecastRainQty: 24.3,
|
||||
maxRainIntensity: 35.4,
|
||||
maxFloodingAt: "2026-05-18 12:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-72010", runoffFlowQty: 7.2, conveyQty: 11.4 },
|
||||
{ pipeId: "PIPE-72018", runoffFlowQty: 6.1, conveyQty: 9.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-005",
|
||||
areaName: "E 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 10.7,
|
||||
forecastRainQty: 28.6,
|
||||
maxRainIntensity: 39.8,
|
||||
maxFloodingAt: "2026-05-18 13:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-83014", runoffFlowQty: 8.3, conveyQty: 12.1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-006",
|
||||
areaName: "F 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 19.8,
|
||||
forecastRainQty: 45.1,
|
||||
maxRainIntensity: 54.2,
|
||||
maxFloodingAt: "2026-05-18 15:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-83018", runoffFlowQty: 17.4, conveyQty: 12.3 },
|
||||
{ pipeId: "PIPE-83019", runoffFlowQty: 13.6, conveyQty: 10.1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-007",
|
||||
areaName: "G 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 13.5,
|
||||
forecastRainQty: 33.2,
|
||||
maxRainIntensity: 44.6,
|
||||
maxFloodingAt: "2026-05-18 14:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-91004", runoffFlowQty: 9.7, conveyQty: 12.5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-008",
|
||||
areaName: "H 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 16.4,
|
||||
forecastRainQty: 39.4,
|
||||
maxRainIntensity: 51.3,
|
||||
maxFloodingAt: "2026-05-18 15:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-91010", runoffFlowQty: 15.5, conveyQty: 11.6 },
|
||||
{ pipeId: "PIPE-91012", runoffFlowQty: 12.4, conveyQty: 10.8 },
|
||||
{ pipeId: "PIPE-91014", runoffFlowQty: 9.2, conveyQty: 9.0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-009",
|
||||
areaName: "I 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 9.6,
|
||||
forecastRainQty: 26.1,
|
||||
maxRainIntensity: 37.2,
|
||||
maxFloodingAt: "2026-05-18 12:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-99001", runoffFlowQty: 6.7, conveyQty: 10.4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-010",
|
||||
areaName: "J 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 17.1,
|
||||
forecastRainQty: 41.8,
|
||||
maxRainIntensity: 52.7,
|
||||
maxFloodingAt: "2026-05-18 15:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-99007", runoffFlowQty: 14.8, conveyQty: 10.9 },
|
||||
{ pipeId: "PIPE-99009", runoffFlowQty: 10.1, conveyQty: 9.7 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-011",
|
||||
areaName: "K 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 11.2,
|
||||
forecastRainQty: 29.7,
|
||||
maxRainIntensity: 40.1,
|
||||
maxFloodingAt: "2026-05-18 13:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-11001", runoffFlowQty: 8.8, conveyQty: 12.5 },
|
||||
{ pipeId: "PIPE-11002", runoffFlowQty: 7.9, conveyQty: 11.4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-012",
|
||||
areaName: "L 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 18.9,
|
||||
forecastRainQty: 43.6,
|
||||
maxRainIntensity: 53.8,
|
||||
maxFloodingAt: "2026-05-18 15:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-12001", runoffFlowQty: 16.7, conveyQty: 12.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-013",
|
||||
areaName: "M 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 14.3,
|
||||
forecastRainQty: 35.2,
|
||||
maxRainIntensity: 46.5,
|
||||
maxFloodingAt: "2026-05-18 14:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-13001", runoffFlowQty: 14.4, conveyQty: 11.6 },
|
||||
{ pipeId: "PIPE-13002", runoffFlowQty: 10.5, conveyQty: 10.9 },
|
||||
{ pipeId: "PIPE-13003", runoffFlowQty: 8.6, conveyQty: 9.3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-014",
|
||||
areaName: "N 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 7.6,
|
||||
forecastRainQty: 21.9,
|
||||
maxRainIntensity: 32.8,
|
||||
maxFloodingAt: "2026-05-18 11:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-14001", runoffFlowQty: 5.8, conveyQty: 8.7 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-015",
|
||||
areaName: "O 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 20.4,
|
||||
forecastRainQty: 47.3,
|
||||
maxRainIntensity: 56.9,
|
||||
maxFloodingAt: "2026-05-18 16:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-15001", runoffFlowQty: 18.2, conveyQty: 13.4 },
|
||||
{ pipeId: "PIPE-15002", runoffFlowQty: 12.7, conveyQty: 10.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-016",
|
||||
areaName: "P 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 10.1,
|
||||
forecastRainQty: 27.5,
|
||||
maxRainIntensity: 38.6,
|
||||
maxFloodingAt: "2026-05-18 13:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-16001", runoffFlowQty: 7.1, conveyQty: 10.3 },
|
||||
{ pipeId: "PIPE-16002", runoffFlowQty: 6.2, conveyQty: 9.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-017",
|
||||
areaName: "Q 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 12.8,
|
||||
forecastRainQty: 32.8,
|
||||
maxRainIntensity: 43.7,
|
||||
maxFloodingAt: "2026-05-18 14:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-17001", runoffFlowQty: 9.9, conveyQty: 11.7 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-018",
|
||||
areaName: "R 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 18.2,
|
||||
forecastRainQty: 44.9,
|
||||
maxRainIntensity: 55.1,
|
||||
maxFloodingAt: "2026-05-18 16:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-18001", runoffFlowQty: 17.1, conveyQty: 12.4 },
|
||||
{ pipeId: "PIPE-18002", runoffFlowQty: 13.4, conveyQty: 10.6 },
|
||||
{ pipeId: "PIPE-18003", runoffFlowQty: 9.8, conveyQty: 9.1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-019",
|
||||
areaName: "S 중점관리지역",
|
||||
sidoName: "서울특별시",
|
||||
sggName: "강남구",
|
||||
currentAccumRainQty: 8.4,
|
||||
forecastRainQty: 23.4,
|
||||
maxRainIntensity: 34.9,
|
||||
maxFloodingAt: "2026-05-18 12:00",
|
||||
riskStatus: "NORMAL",
|
||||
calculationMethod: "METHOD_1",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-19001", runoffFlowQty: 6.4, conveyQty: 9.6 },
|
||||
],
|
||||
},
|
||||
{
|
||||
areaId: "AREA-020",
|
||||
areaName: "T 중점관리지역",
|
||||
sidoName: "부산광역시",
|
||||
sggName: "해운대구",
|
||||
currentAccumRainQty: 16.7,
|
||||
forecastRainQty: 40.7,
|
||||
maxRainIntensity: 51.9,
|
||||
maxFloodingAt: "2026-05-18 15:00",
|
||||
riskStatus: "DANGER",
|
||||
calculationMethod: "METHOD_2",
|
||||
pipeSummaries: [
|
||||
{ pipeId: "PIPE-20001", runoffFlowQty: 15.2, conveyQty: 11.4 },
|
||||
{ pipeId: "PIPE-20002", runoffFlowQty: 11.9, conveyQty: 10.1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const WATER_RISK_MAP_AREAS: WaterRiskAreaMapData[] = [
|
||||
{
|
||||
areaId: "AREA-001",
|
||||
areaName: "A 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(955300, 1948770, 360, 240),
|
||||
gridCells: buildGridCells(955300, 1948770, [38.2, 42.0, 31.5, 28.4]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-002",
|
||||
areaName: "B 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(954480, 1948350, 320, 210),
|
||||
gridCells: buildGridCells(954480, 1948350, [22.4, 24.1, 20.5, 18.9]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-003",
|
||||
areaName: "C 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(956260, 1948220, 390, 250),
|
||||
gridCells: buildGridCells(956260, 1948220, [35.1, 37.8, 32.4, 29.6]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-004",
|
||||
areaName: "D 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(957020, 1948660, 300, 210),
|
||||
gridCells: buildGridCells(957020, 1948660, [19.3, 24.3, 21.7, 17.8]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-005",
|
||||
areaName: "E 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(954900, 1949020, 290, 200),
|
||||
gridCells: buildGridCells(954900, 1949020, [24.8, 28.6, 22.5, 20.7]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-006",
|
||||
areaName: "F 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(955980, 1949040, 330, 220),
|
||||
gridCells: buildGridCells(955980, 1949040, [41.7, 45.1, 36.2, 31.5]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-007",
|
||||
areaName: "G 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(956760, 1947990, 280, 190),
|
||||
gridCells: buildGridCells(956760, 1947990, [27.4, 33.2, 24.2, 21.1]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-008",
|
||||
areaName: "H 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(957420, 1948090, 340, 230),
|
||||
gridCells: buildGridCells(957420, 1948090, [34.7, 39.4, 32.8, 28.6]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-009",
|
||||
areaName: "I 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(954020, 1947900, 260, 180),
|
||||
gridCells: buildGridCells(954020, 1947900, [18.4, 26.1, 17.6, 15.9]),
|
||||
},
|
||||
{
|
||||
areaId: "AREA-010",
|
||||
areaName: "J 중점관리지역",
|
||||
polygonCoordinates: buildRectanglePolygon(957980, 1948900, 300, 210),
|
||||
gridCells: buildGridCells(957980, 1948900, [37.9, 41.8, 33.8, 29.4]),
|
||||
},
|
||||
];
|
||||
|
||||
export const WATER_RISK_MAP_PIPES_BY_AREA: Record<string, WaterRiskMapPipe[]> = {
|
||||
"AREA-001": [
|
||||
{
|
||||
pipeId: "PIPE-10284",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[955020, 1948450],
|
||||
[955210, 1948570],
|
||||
[955320, 1948690],
|
||||
],
|
||||
},
|
||||
{
|
||||
pipeId: "PIPE-10291",
|
||||
displayLabel: "관로 ID 2",
|
||||
lineCoordinates: [
|
||||
[955350, 1948820],
|
||||
[955470, 1948680],
|
||||
[955560, 1948550],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-002": [
|
||||
{
|
||||
pipeId: "PIPE-45103",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[954230, 1948500],
|
||||
[954360, 1948420],
|
||||
[954520, 1948340],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-003": [
|
||||
{
|
||||
pipeId: "PIPE-60014",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[956000, 1948420],
|
||||
[956110, 1948320],
|
||||
[956220, 1948210],
|
||||
],
|
||||
},
|
||||
{
|
||||
pipeId: "PIPE-60018",
|
||||
displayLabel: "관로 ID 2",
|
||||
lineCoordinates: [
|
||||
[956180, 1948480],
|
||||
[956270, 1948360],
|
||||
[956410, 1948240],
|
||||
],
|
||||
},
|
||||
{
|
||||
pipeId: "PIPE-60021",
|
||||
displayLabel: "관로 ID 3",
|
||||
lineCoordinates: [
|
||||
[956420, 1948420],
|
||||
[956520, 1948340],
|
||||
[956650, 1948240],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-004": [
|
||||
{
|
||||
pipeId: "PIPE-72010",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[956770, 1948790],
|
||||
[956900, 1948720],
|
||||
[957050, 1948650],
|
||||
],
|
||||
},
|
||||
{
|
||||
pipeId: "PIPE-72018",
|
||||
displayLabel: "관로 ID 2",
|
||||
lineCoordinates: [
|
||||
[956980, 1948840],
|
||||
[957090, 1948740],
|
||||
[957210, 1948640],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-005": [
|
||||
{
|
||||
pipeId: "PIPE-83014",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[954710, 1949160],
|
||||
[954840, 1949080],
|
||||
[954980, 1949000],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-006": [
|
||||
{
|
||||
pipeId: "PIPE-83018",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[955760, 1949180],
|
||||
[955910, 1949100],
|
||||
[956030, 1949020],
|
||||
],
|
||||
},
|
||||
{
|
||||
pipeId: "PIPE-83019",
|
||||
displayLabel: "관로 ID 2",
|
||||
lineCoordinates: [
|
||||
[955980, 1949240],
|
||||
[956120, 1949140],
|
||||
[956240, 1949040],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-007": [
|
||||
{
|
||||
pipeId: "PIPE-91004",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[956520, 1948120],
|
||||
[956660, 1948050],
|
||||
[956820, 1947980],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-008": [
|
||||
{
|
||||
pipeId: "PIPE-91010",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[957150, 1948220],
|
||||
[957300, 1948160],
|
||||
[957470, 1948100],
|
||||
],
|
||||
},
|
||||
{
|
||||
pipeId: "PIPE-91012",
|
||||
displayLabel: "관로 ID 2",
|
||||
lineCoordinates: [
|
||||
[957350, 1948290],
|
||||
[957480, 1948200],
|
||||
[957610, 1948110],
|
||||
],
|
||||
},
|
||||
{
|
||||
pipeId: "PIPE-91014",
|
||||
displayLabel: "관로 ID 3",
|
||||
lineCoordinates: [
|
||||
[957540, 1948340],
|
||||
[957670, 1948220],
|
||||
[957790, 1948100],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-009": [
|
||||
{
|
||||
pipeId: "PIPE-99001",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[953810, 1948010],
|
||||
[953940, 1947960],
|
||||
[954080, 1947910],
|
||||
],
|
||||
},
|
||||
],
|
||||
"AREA-010": [
|
||||
{
|
||||
pipeId: "PIPE-99007",
|
||||
displayLabel: "관로 ID 1",
|
||||
lineCoordinates: [
|
||||
[957750, 1949040],
|
||||
[957900, 1948970],
|
||||
[958040, 1948900],
|
||||
],
|
||||
},
|
||||
{
|
||||
pipeId: "PIPE-99009",
|
||||
displayLabel: "관로 ID 2",
|
||||
lineCoordinates: [
|
||||
[957980, 1949120],
|
||||
[958120, 1949010],
|
||||
[958260, 1948900],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const WATER_RISK_SIDO_OPTIONS = ["전체", "서울특별시", "부산광역시"];
|
||||
|
||||
export const WATER_RISK_SGG_OPTIONS_BY_SIDO: Record<string, string[]> = {
|
||||
전체: ["전체", "강남구", "해운대구"],
|
||||
서울특별시: ["전체", "강남구"],
|
||||
부산광역시: ["전체", "해운대구"],
|
||||
};
|
||||
|
||||
export const WATER_RISK_COLOR_LABELS: Record<WaterRiskGridCell["colorLevel"], string> = {
|
||||
LEVEL_1: "0.0~20.0 미만",
|
||||
LEVEL_2: "20.0~30.0 미만",
|
||||
LEVEL_3: "30.0~40.0 미만",
|
||||
LEVEL_4: "40.0 이상",
|
||||
};
|
||||
|
||||
export const buildWaterRiskStatusLabel = (status: WaterRiskStatus) =>
|
||||
status === "DANGER" ? "위험" : "정상";
|
||||
|
||||
export const buildWaterRiskMethodLabel = (method: WaterRiskCalculationMethod) =>
|
||||
method === "RATIONAL_MANNING"
|
||||
? "합리식·Manning"
|
||||
: method === "METHOD_1"
|
||||
? "방법 1"
|
||||
: "방법 2";
|
||||
|
||||
export type WaterRiskAreaOption = {
|
||||
areaId: string;
|
||||
areaName: string;
|
||||
@@ -711,6 +54,7 @@ export type WaterRiskAreaOption = {
|
||||
|
||||
export type WaterRiskResultPage = {
|
||||
items: WaterRiskResultRow[];
|
||||
analysisBaseAt: string | null;
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
@@ -722,6 +66,7 @@ export type WaterRiskGeoJsonGeometry = {
|
||||
coordinates: unknown;
|
||||
};
|
||||
|
||||
/** 선택한 대표실행의 실제 중점관리지역 및 관로 지도 결과이다. */
|
||||
export type WaterRiskMapView = {
|
||||
analysisDate: string;
|
||||
areaId: string;
|
||||
@@ -731,14 +76,27 @@ export type WaterRiskMapView = {
|
||||
sggCd: string;
|
||||
sggName: string;
|
||||
riskStatus: WaterRiskStatus;
|
||||
currentAccumRainQty: number | null;
|
||||
forecastRainQty: number | null;
|
||||
maxFloodingFlow: number | null;
|
||||
projection: "EPSG:5179" | string;
|
||||
areaGeometry: WaterRiskGeoJsonGeometry | null;
|
||||
pipes: Array<{
|
||||
pipeId: string;
|
||||
gisFacilityId: string | null;
|
||||
displayLabel: string;
|
||||
geometry: WaterRiskGeoJsonGeometry | null;
|
||||
runoffFlowQty: number;
|
||||
conveyQty: number;
|
||||
runoffFlowQty: number | null;
|
||||
conveyQty: number | null;
|
||||
capacityRatio: number | null;
|
||||
riskStatus: WaterRiskStatus;
|
||||
}>;
|
||||
manholes: Array<{
|
||||
manholeId: string;
|
||||
gisFacilityId: string | null;
|
||||
displayLabel: string;
|
||||
geometry: WaterRiskGeoJsonGeometry | null;
|
||||
floodingFlow: number | null;
|
||||
riskStatus: WaterRiskStatus;
|
||||
}>;
|
||||
};
|
||||
|
||||
@@ -168,9 +168,6 @@ import {
|
||||
import {
|
||||
buildWaterRiskStatusLabel,
|
||||
EMPTY_WATER_RISK_SESSION_SNAPSHOT,
|
||||
WATER_RISK_MAP_AREAS,
|
||||
WATER_RISK_MAP_PIPES_BY_AREA,
|
||||
WATER_RISK_RESULT_ROWS,
|
||||
type WaterRiskMapView,
|
||||
type WaterRiskResultRow,
|
||||
type WaterRiskSessionSnapshot,
|
||||
@@ -244,8 +241,8 @@ const GIS_EDIT_PIPE_SNAP_BBOX_BUFFER_RATIO = 0.15;
|
||||
const GIS_EDIT_PREVIEW_LAYER_ID = "SN_GIS_EDIT_PREVIEW";
|
||||
const GIS_EDIT_VERTEX_LAYER_ID = "SN_GIS_EDIT_VERTEX_HANDLES";
|
||||
const WATER_RISK_AREA_LAYER_ID = "SN_WATER_RISK_AREA";
|
||||
const WATER_RISK_GRID_LAYER_ID = "SN_WATER_RISK_GRID";
|
||||
const WATER_RISK_PIPE_LAYER_ID = "SN_WATER_RISK_PIPE";
|
||||
const WATER_RISK_MANHOLE_LAYER_ID = "SN_WATER_RISK_MANHOLE";
|
||||
const FLOOD_HISTORY_SELECTION_PADDING: [number, number, number, number] = [
|
||||
100, 380, 120, 120,
|
||||
];
|
||||
@@ -918,22 +915,6 @@ const expandGisEditPipeSnapExtent = (extent: number[]) => {
|
||||
const geometryObjectToKey = (geometry: Record<string, unknown> | null) =>
|
||||
geometry ? JSON.stringify(geometry) : "";
|
||||
|
||||
const getWaterRiskAreaMapData = (areaId: string | null) =>
|
||||
WATER_RISK_MAP_AREAS.find((area) => area.areaId === areaId) ?? null;
|
||||
|
||||
const getWaterRiskResultRow = (areaId: string | null) =>
|
||||
WATER_RISK_RESULT_ROWS.find((row) => row.areaId === areaId) ?? null;
|
||||
|
||||
const buildWaterRiskInfoAnchor = (areaId: string | null) => {
|
||||
const areaData = getWaterRiskAreaMapData(areaId);
|
||||
if (!areaData) return null;
|
||||
|
||||
const polygon = new Polygon(areaData.polygonCoordinates);
|
||||
const extent = polygon.getExtent();
|
||||
const [, minY = 0, maxX = 0, maxY = 0] = extent;
|
||||
return [maxX, (minY + maxY) / 2] as [number, number];
|
||||
};
|
||||
|
||||
const buildWaterRiskStatusFillColor = (riskStatus: "DANGER" | "NORMAL") =>
|
||||
riskStatus === "DANGER"
|
||||
? "rgba(198, 40, 40, 0.24)"
|
||||
@@ -944,17 +925,13 @@ const buildWaterRiskStatusStrokeColor = (riskStatus: "DANGER" | "NORMAL") =>
|
||||
? "rgba(198, 40, 40, 0.92)"
|
||||
: "rgba(21, 101, 192, 0.92)";
|
||||
|
||||
const buildWaterRiskGridFillColor = (colorLevel: string) => {
|
||||
switch (colorLevel) {
|
||||
case "LEVEL_4":
|
||||
return "rgba(198, 40, 40, 0.28)";
|
||||
case "LEVEL_3":
|
||||
return "rgba(245, 124, 0, 0.24)";
|
||||
case "LEVEL_2":
|
||||
return "rgba(251, 192, 45, 0.22)";
|
||||
default:
|
||||
return "rgba(33, 150, 243, 0.18)";
|
||||
}
|
||||
/** capacity를 첨부 범례의 다섯 고정 구간색으로 변환한다. */
|
||||
const buildWaterRiskCapacityColor = (capacityRatio: number) => {
|
||||
if (capacityRatio <= 0.25) return "#0000ff";
|
||||
if (capacityRatio <= 0.5) return "#00d9ff";
|
||||
if (capacityRatio <= 0.75) return "#00d95f";
|
||||
if (capacityRatio < 1) return "#ffe100";
|
||||
return "#ff1c15";
|
||||
};
|
||||
|
||||
const createWaterRiskAreaStyle = (feature: any) => {
|
||||
@@ -982,27 +959,9 @@ const createWaterRiskAreaStyle = (feature: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
const createWaterRiskGridStyle = (feature: any) => {
|
||||
const rainQty = Number(feature.get("rainQty") ?? 0);
|
||||
const colorLevel = String(feature.get("colorLevel") ?? "LEVEL_1");
|
||||
return new Style({
|
||||
fill: new Fill({ color: buildWaterRiskGridFillColor(colorLevel) }),
|
||||
stroke: new Stroke({
|
||||
color: "rgba(71, 85, 105, 0.36)",
|
||||
width: 1,
|
||||
}),
|
||||
text: new Text({
|
||||
text: `${rainQty.toFixed(1)}mm`,
|
||||
font: "600 11px sans-serif",
|
||||
fill: new Fill({ color: "#0f172a" }),
|
||||
overflow: true,
|
||||
}),
|
||||
zIndex: 9995,
|
||||
});
|
||||
};
|
||||
|
||||
const createWaterRiskPipeStyle = (feature: any) => {
|
||||
const displayLabel = String(feature.get("displayLabel") ?? "");
|
||||
const capacityRatio = Number(feature.get("capacityRatio") ?? 0);
|
||||
const capacityColor = buildWaterRiskCapacityColor(capacityRatio);
|
||||
return [
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
@@ -1015,24 +974,30 @@ const createWaterRiskPipeStyle = (feature: any) => {
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(205, 52, 52, 0.96)",
|
||||
color: capacityColor,
|
||||
width: 4,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
}),
|
||||
zIndex: 9998,
|
||||
text: new Text({
|
||||
text: displayLabel,
|
||||
font: "700 12px sans-serif",
|
||||
fill: new Fill({ color: "#ffffff" }),
|
||||
backgroundFill: new Fill({ color: "rgba(60, 60, 60, 0.9)" }),
|
||||
padding: [3, 6, 3, 6],
|
||||
overflow: true,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
/** 맨홀 Flooding이 0이면 파랑, 양수이면 빨강으로 표시한다. */
|
||||
const createWaterRiskManholeStyle = (feature: any) => {
|
||||
const floodingFlow = Number(feature.get("floodingFlow") ?? 0);
|
||||
const isFlooding = floodingFlow > 0;
|
||||
return new Style({
|
||||
image: new CircleStyle({
|
||||
radius: isFlooding ? 7 : 4,
|
||||
fill: new Fill({ color: isFlooding ? "#ff1c15" : "#1677ff" }),
|
||||
stroke: new Stroke({ color: "#ffffff", width: 1.5 }),
|
||||
}),
|
||||
zIndex: 9999,
|
||||
});
|
||||
};
|
||||
|
||||
const getSelectedFeatureLayerTypeName = (data: any) => {
|
||||
const previewLayerId = String(data?.get?.("layerId") ?? "").trim();
|
||||
if (String(data?.get?.("changeId") ?? "").trim() && previewLayerId) {
|
||||
@@ -3224,10 +3189,10 @@ const Map = () => {
|
||||
const gisEditVertexSourceRef = useRef<VectorSource | null>(null);
|
||||
const waterRiskAreaLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
const waterRiskAreaSourceRef = useRef<VectorSource | null>(null);
|
||||
const waterRiskGridLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
const waterRiskGridSourceRef = useRef<VectorSource | null>(null);
|
||||
const waterRiskPipeLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
const waterRiskPipeSourceRef = useRef<VectorSource | null>(null);
|
||||
const waterRiskManholeLayerRef = useRef<VectorLayer<any> | null>(null);
|
||||
const waterRiskManholeSourceRef = useRef<VectorSource | null>(null);
|
||||
const gisEditDrawRef = useRef<any | null>(null);
|
||||
const gisEditDrawSnapRef = useRef<any | null>(null);
|
||||
const gisEditTranslateRef = useRef<any | null>(null);
|
||||
@@ -3447,12 +3412,11 @@ const Map = () => {
|
||||
}, [waterRiskSessionSnapshot]);
|
||||
|
||||
const selectedWaterRiskResultRow = useMemo(
|
||||
() =>
|
||||
selectedWaterRiskApiResult ??
|
||||
getWaterRiskResultRow(waterRiskSession.selectedAreaId),
|
||||
[selectedWaterRiskApiResult, waterRiskSession.selectedAreaId],
|
||||
() => selectedWaterRiskApiResult,
|
||||
[selectedWaterRiskApiResult],
|
||||
);
|
||||
|
||||
/** 선택 영역 오른쪽에 흰색 강우·침수 요약카드를 배치한다. */
|
||||
const syncWaterRiskInfoCardPosition = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map || !waterRiskInfoAnchor) {
|
||||
@@ -3464,7 +3428,7 @@ const Map = () => {
|
||||
const [pixelX = 0, pixelY = 0] =
|
||||
map.getPixelFromCoordinate?.(waterRiskInfoAnchor) ?? [];
|
||||
const cardWidth = 244;
|
||||
const cardHeight = 236;
|
||||
const cardHeight = 180;
|
||||
const left = pixelX + 22;
|
||||
const top = pixelY - cardHeight / 2;
|
||||
|
||||
@@ -3482,10 +3446,7 @@ const Map = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setWaterRiskInfoCardStyle({
|
||||
left,
|
||||
top,
|
||||
});
|
||||
setWaterRiskInfoCardStyle({ left, top });
|
||||
}, [waterRiskInfoAnchor]);
|
||||
|
||||
const setGisEditPreviewCursor = useCallback((cursor: string) => {
|
||||
@@ -3887,29 +3848,6 @@ const Map = () => {
|
||||
return source;
|
||||
}, []);
|
||||
|
||||
const ensureWaterRiskGridLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (waterRiskGridLayerRef.current && waterRiskGridSourceRef.current) {
|
||||
return waterRiskGridSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({
|
||||
source,
|
||||
zIndex: 9995,
|
||||
});
|
||||
|
||||
layer.set("id", WATER_RISK_GRID_LAYER_ID);
|
||||
layer.set("lyrNm", "통수위험 예측강우 격자");
|
||||
layer.set("lyrTyNm", WATER_RISK_GRID_LAYER_ID);
|
||||
map.addLayer(layer);
|
||||
waterRiskGridLayerRef.current = layer;
|
||||
waterRiskGridSourceRef.current = source;
|
||||
return source;
|
||||
}, []);
|
||||
|
||||
const ensureWaterRiskPipeLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
@@ -3933,10 +3871,29 @@ const Map = () => {
|
||||
return source;
|
||||
}, []);
|
||||
|
||||
const ensureWaterRiskManholeLayer = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return null;
|
||||
|
||||
if (waterRiskManholeLayerRef.current && waterRiskManholeSourceRef.current) {
|
||||
return waterRiskManholeSourceRef.current;
|
||||
}
|
||||
|
||||
const source = new VectorSource();
|
||||
const layer = new VectorLayer({ source, zIndex: 9999 });
|
||||
layer.set("id", WATER_RISK_MANHOLE_LAYER_ID);
|
||||
layer.set("lyrNm", "통수위험 맨홀 Flooding");
|
||||
layer.set("lyrTyNm", WATER_RISK_MANHOLE_LAYER_ID);
|
||||
map.addLayer(layer);
|
||||
waterRiskManholeLayerRef.current = layer;
|
||||
waterRiskManholeSourceRef.current = source;
|
||||
return source;
|
||||
}, []);
|
||||
|
||||
const clearWaterRiskLayers = useCallback(() => {
|
||||
waterRiskAreaSourceRef.current?.clear();
|
||||
waterRiskGridSourceRef.current?.clear();
|
||||
waterRiskPipeSourceRef.current?.clear();
|
||||
waterRiskManholeSourceRef.current?.clear();
|
||||
}, []);
|
||||
|
||||
const renderWaterRiskSelection = useCallback(
|
||||
@@ -3945,107 +3902,75 @@ const Map = () => {
|
||||
if (!map) return;
|
||||
|
||||
const areaSource = ensureWaterRiskAreaLayer();
|
||||
const gridSource = ensureWaterRiskGridLayer();
|
||||
if (!areaSource || !gridSource) return;
|
||||
if (!areaSource) return;
|
||||
|
||||
areaSource.clear();
|
||||
gridSource.clear();
|
||||
const pipeSource = ensureWaterRiskPipeLayer();
|
||||
pipeSource?.clear();
|
||||
const manholeSource = ensureWaterRiskManholeLayer();
|
||||
manholeSource?.clear();
|
||||
|
||||
if (!areaId) return;
|
||||
if (!areaId || !mapView || mapView.areaId !== areaId) return;
|
||||
|
||||
if (mapView && mapView.areaId === areaId) {
|
||||
const geoJson = new GeoJSON();
|
||||
const projection = mapView.projection || "EPSG:5179";
|
||||
let focusGeometry: any | null = null;
|
||||
const geoJson = new GeoJSON();
|
||||
const projection = mapView.projection || "EPSG:5179";
|
||||
let focusGeometry: any | null = null;
|
||||
|
||||
if (mapView.areaGeometry) {
|
||||
const geometry = geoJson.readGeometry(mapView.areaGeometry as any, {
|
||||
dataProjection: projection,
|
||||
featureProjection: "EPSG:5179",
|
||||
});
|
||||
const areaFeature = new Feature({
|
||||
geometry,
|
||||
areaId: mapView.areaId,
|
||||
areaName: mapView.areaName,
|
||||
riskStatus: mapView.riskStatus,
|
||||
});
|
||||
areaFeature.setStyle(createWaterRiskAreaStyle(areaFeature));
|
||||
areaSource.addFeature(areaFeature);
|
||||
focusGeometry = geometry;
|
||||
}
|
||||
|
||||
mapView.pipes.forEach((pipe) => {
|
||||
if (!pipeSource || !pipe.geometry) return;
|
||||
const geometry = geoJson.readGeometry(pipe.geometry as any, {
|
||||
dataProjection: projection,
|
||||
featureProjection: "EPSG:5179",
|
||||
});
|
||||
const pipeFeature = new Feature({
|
||||
geometry,
|
||||
pipeId: pipe.pipeId,
|
||||
displayLabel: pipe.displayLabel || pipe.pipeId,
|
||||
});
|
||||
pipeFeature.setStyle(createWaterRiskPipeStyle(pipeFeature));
|
||||
pipeSource.addFeature(pipeFeature);
|
||||
if (!focusGeometry) focusGeometry = geometry;
|
||||
if (mapView.areaGeometry) {
|
||||
const geometry = geoJson.readGeometry(mapView.areaGeometry as any, {
|
||||
dataProjection: projection,
|
||||
featureProjection: "EPSG:5179",
|
||||
});
|
||||
|
||||
if (!focusGeometry) return;
|
||||
const extent = focusGeometry.getExtent();
|
||||
const [, minY = 0, maxX = 0, maxY = 0] = extent;
|
||||
setWaterRiskInfoAnchor([maxX, (minY + maxY) / 2]);
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [120, 120, 120, 120],
|
||||
maxZoom: 15,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
const areaFeature = new Feature({
|
||||
geometry,
|
||||
areaId: mapView.areaId,
|
||||
areaName: mapView.areaName,
|
||||
riskStatus: mapView.riskStatus,
|
||||
});
|
||||
return;
|
||||
areaFeature.setStyle(createWaterRiskAreaStyle(areaFeature));
|
||||
areaSource.addFeature(areaFeature);
|
||||
focusGeometry = geometry;
|
||||
}
|
||||
|
||||
const areaData = getWaterRiskAreaMapData(areaId);
|
||||
const resultRow = getWaterRiskResultRow(areaId);
|
||||
if (!areaData || !resultRow) return;
|
||||
|
||||
const areaFeature = new Feature({
|
||||
geometry: new Polygon(areaData.polygonCoordinates),
|
||||
areaId: areaData.areaId,
|
||||
areaName: areaData.areaName,
|
||||
riskStatus: resultRow.riskStatus,
|
||||
});
|
||||
areaFeature.setStyle(createWaterRiskAreaStyle(areaFeature));
|
||||
areaSource.addFeature(areaFeature);
|
||||
|
||||
areaData.gridCells.forEach((cell) => {
|
||||
const cellFeature = new Feature({
|
||||
geometry: new Polygon(cell.polygonCoordinates),
|
||||
gridId: cell.gridId,
|
||||
rainQty: cell.rainQty,
|
||||
colorLevel: cell.colorLevel,
|
||||
mapView.pipes.forEach((pipe) => {
|
||||
if (!pipeSource || !pipe.geometry) return;
|
||||
const geometry = geoJson.readGeometry(pipe.geometry as any, {
|
||||
dataProjection: projection,
|
||||
featureProjection: "EPSG:5179",
|
||||
});
|
||||
cellFeature.setStyle(createWaterRiskGridStyle(cellFeature));
|
||||
gridSource.addFeature(cellFeature);
|
||||
});
|
||||
|
||||
const pipes = WATER_RISK_MAP_PIPES_BY_AREA[areaId] ?? [];
|
||||
pipes.forEach((pipe) => {
|
||||
if (!pipeSource) return;
|
||||
const pipeFeature = new Feature({
|
||||
geometry: new LineString(pipe.lineCoordinates),
|
||||
geometry,
|
||||
pipeId: pipe.pipeId,
|
||||
displayLabel: pipe.displayLabel,
|
||||
displayLabel: pipe.displayLabel || pipe.pipeId,
|
||||
capacityRatio: pipe.capacityRatio ?? 0,
|
||||
});
|
||||
pipeFeature.setStyle(createWaterRiskPipeStyle(pipeFeature));
|
||||
pipeSource.addFeature(pipeFeature);
|
||||
if (!focusGeometry) focusGeometry = geometry;
|
||||
});
|
||||
|
||||
const areaGeometry = areaFeature.getGeometry();
|
||||
if (!areaGeometry) return;
|
||||
mapView.manholes.forEach((manhole) => {
|
||||
if (!manholeSource || !manhole.geometry) return;
|
||||
const geometry = geoJson.readGeometry(manhole.geometry as any, {
|
||||
dataProjection: projection,
|
||||
featureProjection: "EPSG:5179",
|
||||
});
|
||||
const manholeFeature = new Feature({
|
||||
geometry,
|
||||
manholeId: manhole.manholeId,
|
||||
displayLabel: manhole.displayLabel || manhole.manholeId,
|
||||
floodingFlow: manhole.floodingFlow ?? 0,
|
||||
});
|
||||
manholeFeature.setStyle(createWaterRiskManholeStyle(manholeFeature));
|
||||
manholeSource.addFeature(manholeFeature);
|
||||
if (!focusGeometry) focusGeometry = geometry;
|
||||
});
|
||||
|
||||
map.getView().fit(areaGeometry.getExtent(), {
|
||||
if (!focusGeometry) return;
|
||||
const extent = focusGeometry.getExtent();
|
||||
const [, minY = 0, maxX = 0, maxY = 0] = extent;
|
||||
setWaterRiskInfoAnchor([maxX, (minY + maxY) / 2]);
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [120, 120, 120, 120],
|
||||
maxZoom: 15,
|
||||
@@ -4055,7 +3980,7 @@ const Map = () => {
|
||||
},
|
||||
[
|
||||
ensureWaterRiskAreaLayer,
|
||||
ensureWaterRiskGridLayer,
|
||||
ensureWaterRiskManholeLayer,
|
||||
ensureWaterRiskPipeLayer,
|
||||
],
|
||||
);
|
||||
@@ -4082,11 +4007,8 @@ const Map = () => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedWaterRiskMapView) return;
|
||||
setWaterRiskInfoAnchor(
|
||||
buildWaterRiskInfoAnchor(waterRiskSession.selectedAreaId),
|
||||
);
|
||||
}, [selectedWaterRiskMapView, waterRiskSession.selectedAreaId]);
|
||||
if (!selectedWaterRiskMapView) setWaterRiskInfoAnchor(null);
|
||||
}, [selectedWaterRiskMapView]);
|
||||
|
||||
useEffect(() => {
|
||||
const popup = waterRiskPopupWindowRef.current;
|
||||
@@ -4098,10 +4020,7 @@ const Map = () => {
|
||||
if (!mapReady || !gisMaps.current || !waterRiskInfoAnchor) return;
|
||||
|
||||
const map = gisMaps.current;
|
||||
const syncDuringMapRender = () => {
|
||||
syncWaterRiskInfoCardPosition();
|
||||
};
|
||||
|
||||
const syncDuringMapRender = () => syncWaterRiskInfoCardPosition();
|
||||
syncWaterRiskInfoCardPosition();
|
||||
map.on?.("postrender", syncDuringMapRender);
|
||||
map.on?.("moveend", syncDuringMapRender);
|
||||
@@ -8859,7 +8778,11 @@ const Map = () => {
|
||||
|
||||
{selectedWaterRiskResultRow.pipeSummaries.map(
|
||||
(pipe, index) => {
|
||||
const isDanger = pipe.runoffFlowQty > pipe.conveyQty;
|
||||
const isDanger =
|
||||
pipe.riskStatus === "DANGER" ||
|
||||
(pipe.runoffFlowQty != null &&
|
||||
pipe.conveyQty != null &&
|
||||
pipe.runoffFlowQty > pipe.conveyQty);
|
||||
const isLast =
|
||||
index ===
|
||||
selectedWaterRiskResultRow.pipeSummaries.length - 1;
|
||||
@@ -8899,7 +8822,9 @@ const Map = () => {
|
||||
(㎥/s)
|
||||
</td>
|
||||
<td className="border-b border-border px-[9px] py-[7px] text-right text-[11px] font-medium text-foreground">
|
||||
{pipe.conveyQty.toFixed(1)}
|
||||
{pipe.conveyQty == null
|
||||
? "-"
|
||||
: pipe.conveyQty.toFixed(1)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
@@ -8915,7 +8840,9 @@ const Map = () => {
|
||||
<td
|
||||
className={`px-[9px] py-[7px] text-right text-[11px] ${isDanger ? "font-bold text-[#d32f2f]" : "font-medium text-foreground"}`}
|
||||
>
|
||||
{pipe.runoffFlowQty.toFixed(1)}
|
||||
{pipe.runoffFlowQty == null
|
||||
? "-"
|
||||
: pipe.runoffFlowQty.toFixed(1)}
|
||||
</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
@@ -8928,6 +8855,36 @@ const Map = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedWaterRiskMapView ? (
|
||||
// Capacity와 Flooding 색상은 SWMM 결과의 업무 의미색이므로 테마와 무관하게 유지한다.
|
||||
<div className="absolute bottom-4 left-[270px] z-[61] rounded-md border border-border bg-background px-3 py-2 text-[10px] text-foreground shadow-md">
|
||||
<div className="mb-1 font-bold">Capacity</div>
|
||||
<div className="space-y-1 font-semibold">
|
||||
{[
|
||||
["#0000ff", "≤ 0.25"],
|
||||
["#00d9ff", "≤ 0.50"],
|
||||
["#00d95f", "≤ 0.75"],
|
||||
["#ffe100", "< 1.00"],
|
||||
["#ff1c15", "≥ 1.00"],
|
||||
].map(([color, label]) => (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block h-[10px] w-[10px] border border-foreground"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 border-t border-border pt-2">
|
||||
<div className="font-bold">Flooding(m³/s)</div>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<span className="text-[#1677ff]">● 0</span>
|
||||
<span className="text-[#ff1c15]">● > 0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedFldngHstryDetail && fldngHstryDetailPopupPosition ? (
|
||||
<FldngHstryDetailPanel
|
||||
detail={selectedFldngHstryDetail}
|
||||
|
||||
@@ -114,3 +114,48 @@
|
||||
- dev 서버 또는 로컬 미리보기 진입 가능
|
||||
- mock 데이터 또는 초기 API 응답 구조 준비
|
||||
- 인증 가능한 브라우저 환경에서 지도 조작 수동 검증 가능
|
||||
|
||||
## 6. 2026-08-18 강우이벤트 상태별 탭 연동 검증
|
||||
|
||||
- Java `WaterRiskServiceTest` 4건과 `compileJava`가 성공했다.
|
||||
- 개발 DB 직접 API에서 `IN_PROGRESS=0건`, `ENDED=6건`을 확인했다.
|
||||
- 종료 이력 6건은 위험 4건·정상 2건이며 기존 6개 성공모델 시험결과와 일치한다.
|
||||
- 프론트 3002 미들웨어 경유 API도 현재 위험 0건·과거 이력 6건으로 동일하게 응답했다.
|
||||
- 변경한 FE 4개 파일의 ESLint는 오류·경고 없이 통과했다.
|
||||
- TypeScript 단독 검사는 기존 `tsconfig.json`의 `ignoreDeprecations` 값과 실행 TypeScript 버전 불일치로 중단됐으며 변경 파일 오류는 ESLint와 실행 중 Next 개발서버 컴파일로 분리 확인했다.
|
||||
- 브라우저 자동검증 환경에는 로그인 세션이 없어 로그인 화면까지만 확인했다. 사용자의 로그인된 Chrome에서 탭 전환과 과거 6건 표시는 최종 수동 확인 대상으로 남긴다.
|
||||
- 사용자 소유 프론트 3002·백엔드 8883은 종료하거나 재시작하지 않았다.
|
||||
|
||||
## 7. 2026-08-18 임시 GIS 관로·맨홀 매핑 검증
|
||||
|
||||
- KH_HM 대표실행의 SWMM 관로 184건과 실제 GIS 관로 geometry 184건이 모두 연결됐다.
|
||||
- SWMM 맨홀 180건과 실제 GIS 맨홀 geometry 180건이 모두 연결됐다.
|
||||
- 관로 capacity 범위는 `0.00420~0.83100`이며 0.25/0.50/0.75/1.00 고정 색상구간 적용을 확인했다.
|
||||
- 맨홀 180건의 Flooding 양수 건수는 0건이므로 현재 시험화면에서는 모두 정상 파랑으로 표시된다.
|
||||
- 지도요약은 누적강우 52.3mm, 예측강우 39.89mm, 최대 침수유량 NULL(`-`)이다.
|
||||
- 프론트 3002 프록시에서도 관로·맨홀 geometry가 각각 184/184, 180/180으로 반환됐다.
|
||||
- Java Service 테스트와 FE 대상 ESLint가 통과했다.
|
||||
|
||||
## 10. 2026-08-18 팝업 크기·시간갱신 정적검증
|
||||
|
||||
- 팝업 정의와 컴포넌트 크기가 `1360×600`으로 일치한다.
|
||||
- 현재 탭 전용 자동갱신은 다음 `HH:00`에 시작하고 이후 1시간 간격이며 effect cleanup에서 timeout·interval을 모두 해제한다.
|
||||
- 현재 탭 설명문 제거와 FE 대상 ESLint 통과를 확인했다.
|
||||
- 과거분석이력에서 현재로 전환할 때 결과페이지는 비우되 분석기준시간은 현재 정시를 계속 표시하며 DB 0건에도 사라지지 않는다.
|
||||
- 분석기준시간 라벨·값이 현재 탭 행 오른쪽 끝에 표시되고 검색패널에서 제거된 것을 정적 확인했다.
|
||||
|
||||
## 9. 2026-08-18 결과표 헤더·강우시작시간 검증
|
||||
|
||||
- `rainStartedAt`이 실제 강우이벤트 시작시각으로 반환되는 것을 확인했다.
|
||||
- 사천 `2026-07-01 03:00`, 완도 `2026-07-01 01:00`과 누적·예측강우 값이 같은 행에 표시되는 계약을 확인했다.
|
||||
- 2단 그룹헤더는 총 10개 데이터 열과 일치하며 로딩·빈 결과 colSpan도 10으로 통일했다.
|
||||
- Java Service 테스트와 FE 대상 ESLint가 통과했다.
|
||||
- 관리번호 15 완도는 MS 중점관리지역 geometry가 없어 임시 공간매핑이 불가능하다.
|
||||
|
||||
## 8. 2026-08-18 탭별 검색조건·분석기준시간 검증
|
||||
|
||||
- 목록 API에서 대표실행 `ANLS_CRTR_DT`가 `analysisBaseAt`으로 반환되는 것을 확인했다.
|
||||
- 개발 DB 현재 진행중 이벤트가 0건이어도 현재 탭 분석기준시간은 현재 정시를 표시하고 그리드만 0건이다.
|
||||
- 과거 종료결과에서 실제 `2026-07-01 01:00`, `03:00`, `05:00` 등의 분석기준시간이 반환됐다.
|
||||
- 과거 종료결과 전체의 상단 최신 분석기준시간은 `2026-07-06 08:00`으로 반환됐다.
|
||||
- Java Service 테스트와 FE 대상 ESLint가 통과했다.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# SFR-011-008 침수예측현황 FE 작업계획
|
||||
|
||||
## 작업 목적
|
||||
|
||||
지도 분석도구의 기존 침수예측현황 placeholder를 검색·목록·Excel·지도연계가 가능한 실제 WindowPopup으로 전환한다.
|
||||
|
||||
## 근거
|
||||
|
||||
- 요구사항: `SFR-011-008`
|
||||
- 화면: `SN_MN010205 침수예측현황`
|
||||
- route: `/popup/fldngPredcPrst`
|
||||
- 참고: `PipeAnalysisPopupPage`, `FloodDngrMapPopupPage`, `/oper/obsltPplBiz`
|
||||
|
||||
## 수정 예정 범위
|
||||
|
||||
- `src/features/fldngPredcPrst/{api,model,ui}/**`
|
||||
- `src/features/windowPopup/model/popupDefinitions.ts`
|
||||
- `src/features/windowPopup/registry.tsx`
|
||||
- `app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx`
|
||||
- 공통 `app/(popupLayout)/popup/[popupKey]` route는 재사용하고 popup key를 `fldngPredcPrst`로 변경한다.
|
||||
- 공통 패키지는 수정하지 않는다.
|
||||
|
||||
## 화면·상태 계약
|
||||
|
||||
- 강우시작시각은 최초 진입 시 미입력으로 두고, 분석일시는 오늘 00:00~현재 KST를 기본값으로 설정한다.
|
||||
- 시도 변경 시 시군구를 전체로 초기화한다.
|
||||
- 입력 조건과 실제 적용 조건을 분리하여 페이지 이동이 마지막 조회조건을 유지한다.
|
||||
- 20건 서버 페이징, 허용 컬럼 서버 정렬, 전체건수 기반 no를 제공한다.
|
||||
- 시도 헤더는 `ctpvCd` 서버 정렬키를 사용한다.
|
||||
- 강수량은 소수점 한 자리로 표시하고 `초과빈도년도` 값은 `년` 접미사 없이 숫자로 표시한다.
|
||||
- `ListDiv` 내부에서 최소 테이블 폭과 가로 스크롤을 제공한다.
|
||||
- Excel은 현재 적용 검색조건을 동일하게 전달한다.
|
||||
- 신규 파일과 함수에 화면/API 역할 한글 주석을 작성한다.
|
||||
|
||||
## 지도 연계
|
||||
|
||||
- 행 선택 시 기존 도시침수지도 group extent API를 호출한다.
|
||||
- 부모 창에 기존 `MAP_SEARCH_SHOW_FLOOD_GROUP` 메시지를 보내 범위 이동·레이어 표시를 요청한다.
|
||||
- 도시침수지도에 없는 빈도는 자동 변환하지 않고 화면 안내를 표시한다.
|
||||
|
||||
## 검증
|
||||
|
||||
- TypeScript 정적검사 및 `sn` 빌드(실행 중 개발서버가 없거나 사용자 확인 후)
|
||||
- `/popup/fldngPredcPrst` 직접 진입
|
||||
- 분석도구 클릭 별도 창 진입
|
||||
- 기본 날짜·종속 시군구·조회·페이지·정렬·가로스크롤
|
||||
- Excel 파일명/열/검색조건
|
||||
- 행 클릭 후 부모 지도 Network·extent·레이어 동작
|
||||
|
||||
## 롤백과 위험
|
||||
|
||||
- 신규 route/feature를 제거하고 registry를 placeholder로 복원한다.
|
||||
- 팝업 경로와 기존 popup key가 다르므로 route 연결 누락을 회귀 확인한다.
|
||||
- 행정구역 코드와 도시침수지도 빈도 자료가 없는 행은 지도 이동이 제한될 수 있다.
|
||||
|
||||
## 구현 결과 (2026-08-13)
|
||||
|
||||
- 분석도구 메뉴와 WindowPopup key를 `fldngPredcPrst`로 변경하고 placeholder를 실제 화면으로 교체했다.
|
||||
- 강우시작시각 미입력·분석일시 당일 00:00~현재 기본값, 시도·시군구 종속조건, 두 날짜 범위, 빈도년도 검색을 구현했다.
|
||||
- 20건 서버 페이징·정렬, `ListDiv` 가로 스크롤, 적용조건 기준 Excel 다운로드를 구현했다.
|
||||
- 행 클릭 시 기존 extent API와 `MAP_SEARCH_SHOW_FLOOD_GROUP` 메시지를 사용한다.
|
||||
- 도시침수지도가 없는 빈도는 30·50·80년 중 임의 근사하지 않고 안내한다.
|
||||
- 2026-08-18 시도 헤더를 정렬 가능 상태로 전환하고 `ctpvCd` 서버 정렬을 연결했다.
|
||||
- `pnpm --filter sn run build`를 통과했다.
|
||||
- 독립 `tsc --noEmit`은 현재 수정과 무관한 조사사업·침수이력·지도·노후관로·팝업 registry 등의 기존 타입 오류로 실패했다. 이번 변경 파일에서는 오류가 보고되지 않았다.
|
||||
- 2026-08-18 목록 강수량을 소수점 한 자리로, 헤더를 `초과빈도년도`로 변경하고 값의 `년` 접미사를 제거했다.
|
||||
- 기존 `.next` 산출물을 재생성한 뒤 `pnpm --filter sn run build`를 최종 통과했다.
|
||||
- TypeScript 정적검사는 저장소 기존 `ignoreDeprecations` 설정과 실행 중 산출물 잠금 때문에 완료하지 못했고, Next 빌드는 최적화 단계 장기 대기 후 중단했다.
|
||||
@@ -174,3 +174,44 @@
|
||||
- 실제 API 응답 구조 확정
|
||||
- BE 저장/다운로드 계약 확정
|
||||
- 인증 가능한 브라우저 환경에서 수동 검증 가능 상태 확보
|
||||
|
||||
## 9. 2026-08-18 SWMM 현재 위험·과거 이력 실 DB 연동
|
||||
|
||||
- 기존 팝업 레이아웃과 검색조건은 유지하고 탭별 조회조건을 강우이벤트 상태로 분리했다.
|
||||
- `현재 위험`은 `RAIN_SEVNT_STTS_CD=IN_PROGRESS`, `과거 이력`은 `RAIN_SEVNT_STTS_CD=ENDED`를 `GET /api/v1/waterRisk/results`에 전달한다.
|
||||
- 기본 조회기간을 최근 365일에서 오늘까지로 설정해 개발 DB의 과거 시험자료와 운영 최신자료를 함께 조회할 수 있게 했다.
|
||||
- 합리식·Manning 안내문과 통수량 열을 제거하고 `대표관로 예측유량`, `분석일`, `최대침수예상시점` 등 SWMM 결과 용어로 교체했다.
|
||||
- 부모 지도 정보카드도 6시간 예측강우량과 대표관로 예측유량을 표시하며 위험판정은 API의 관로 위험상태를 사용한다.
|
||||
- `waterRiskTypes.ts`에 남아 있던 mock 결과·영역·격자·관로 자료와 합리식 계산방법 타입을 제거했다.
|
||||
- 부모 지도는 선택행의 실제 `mapView` 응답만 사용하며 mock polygon·grid·pipe fallback을 사용하지 않는다.
|
||||
|
||||
## 10. 2026-08-18 실제 GIS 자산 임시 매핑 지도
|
||||
|
||||
- 선택한 중점관리지역의 실제 GIS 관로·맨홀 geometry를 확장된 `mapView`로 수신한다.
|
||||
- 관로는 capacity 0.25/0.50/0.75/1.00 경계의 고정 구간색으로, 맨홀은 Flooding 0/양수 기준으로 지도에 표시한다.
|
||||
- 지도에 capacity 세로 범례와 맨홀 Flooding 범례를 함께 표시한다.
|
||||
- 지역 정보카드는 대표관로 상세 대신 현재 누적강우량, 예측강우량, 최대 침수유량 3개 항목으로 단순화했다.
|
||||
- 매핑은 화면검증용 임시 순번 연결이며 정식 매핑테이블 도입 후 교체 대상이다.
|
||||
- 중점관리지역 중앙 빨간 라벨은 제거하고 흰색 강우·침수 요약카드는 유지했다.
|
||||
|
||||
## 11. 2026-08-18 탭명·검색조건 분리
|
||||
|
||||
- 탭명을 `현재`, `과거분석이력`으로 변경했다.
|
||||
- 현재 검색영역은 중점관리지역 선택, 브라우저 현재시각을 정시로 내린 분석기준시간, 조회 버튼만 표시한다.
|
||||
- 과거분석이력 검색영역은 기존 검색조건을 유지하고 날짜 라벨을 `강우 발생기간`으로 변경했다.
|
||||
- 현재 탭에서는 숨겨진 시도·시군구·위험상태 조건을 API에 전달하지 않아 이전 과거검색 조건이 현재조회에 영향을 주지 않게 했다.
|
||||
|
||||
## 12. 2026-08-18 화면설계서 기준 2단 결과표 헤더
|
||||
|
||||
- 관측강우 2개 열과 통수위험 3개 열을 묶는 2단 그룹 헤더로 변경했다.
|
||||
- 목록 행은 강우시작시간, 누적강우량, 예측강우량, 최대 침수 예상시점, 최대침수량, 위험 여부 순으로 표시한다.
|
||||
- 최대침수량 체적값은 현재 API 원천이 없어 `-`로 표시한다.
|
||||
|
||||
## 13. 2026-08-18 팝업 크기·현재 분석기준시간 자동갱신
|
||||
|
||||
- 팝업 정의와 화면 `resizeTo`를 모두 `1360×600`으로 통일했다.
|
||||
- 현재 탭 결과 제목 우측의 별도 SWMM 설명문을 제거했다.
|
||||
- 현재 탭이 열린 동안 매시간 `HH:00`에 현재 정시를 새 분석기준시간으로 적용하고 동일 `ANLS_CRTR_DT` 결과를 자동 재조회한다.
|
||||
- 탭을 과거분석이력으로 전환하거나 팝업을 닫으면 자동갱신 타이머를 해제한다.
|
||||
- 분석기준시간은 DB 결과와 독립된 현재 정시이므로 진행중 데이터가 없어도 사라지지 않는다. 입력창으로 오해하지 않도록 테두리·배경 없이 글씨만 표시한다.
|
||||
- `분석기준시간` 라벨과 시각은 현재 탭 행의 오른쪽 끝으로 이동하고 검색패널에는 중점관리지역 선택과 우측 조회 버튼만 유지한다.
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@common/components";
|
||||
import { UiTable } from "@common/business_components/ui/table";
|
||||
import type { UiHeadProps, UiTableSort } from "@common/business_components/ui";
|
||||
import ListDiv from "@common/business_components/ui/list_div";
|
||||
import { getBlobData, getRawJson } from "@/components/lib/api";
|
||||
import { fileDownload } from "@/components/lib/utils/fileDownload";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||
import useStore from "@/store/useStore";
|
||||
|
||||
type ApiRow = {
|
||||
sggCd: string;
|
||||
ctpvCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
rainBeginningDateTime: string;
|
||||
analysisDateTime: string;
|
||||
observedAccumulatedRainfall: number;
|
||||
rainHour: number;
|
||||
rainfall: number;
|
||||
exceededFrequencyYear: number;
|
||||
floodRiskGrade: string;
|
||||
};
|
||||
type TableRow = ApiRow & {
|
||||
rowNo: number;
|
||||
rainBeginningDateTimeDisplay: string;
|
||||
analysisDateTimeDisplay: string;
|
||||
observedAccumulatedRainfallDisplay: string;
|
||||
rainHourDisplay: string;
|
||||
rainfallDisplay: string;
|
||||
exceededFrequencyYearDisplay: string;
|
||||
};
|
||||
type Filters = {
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
rainBgngFrom: string;
|
||||
rainBgngTo: string;
|
||||
analysisFrom: string;
|
||||
analysisTo: string;
|
||||
frequencyYear: string;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
const MAP_FREQUENCIES = new Set([30, 50, 80]);
|
||||
const HEADS: UiHeadProps[] = [
|
||||
{ id: "rowNo", title: "NO", widthPercent: 5, sortable: false },
|
||||
{
|
||||
id: "ctpvNm",
|
||||
title: "시도",
|
||||
widthPercent: 9,
|
||||
sortable: true,
|
||||
sortKey: "ctpvCd",
|
||||
},
|
||||
{
|
||||
id: "sggNm",
|
||||
title: "시군구",
|
||||
widthPercent: 10,
|
||||
sortable: true,
|
||||
sortKey: "sggCd",
|
||||
},
|
||||
{
|
||||
id: "rainBeginningDateTimeDisplay",
|
||||
title: "강우 시작시각",
|
||||
widthPercent: 13,
|
||||
sortable: true,
|
||||
sortKey: "rainBeginningDateTime",
|
||||
},
|
||||
{
|
||||
id: "analysisDateTimeDisplay",
|
||||
title: "분석시각",
|
||||
widthPercent: 13,
|
||||
sortable: true,
|
||||
sortKey: "analysisDateTime",
|
||||
},
|
||||
{
|
||||
id: "observedAccumulatedRainfallDisplay",
|
||||
title: "관측 누적강수량(mm)",
|
||||
widthPercent: 13,
|
||||
sortable: true,
|
||||
sortKey: "observedAccumulatedRainfall",
|
||||
},
|
||||
{
|
||||
id: "rainHourDisplay",
|
||||
title: "강우시간",
|
||||
widthPercent: 9,
|
||||
sortable: true,
|
||||
sortKey: "rainHour",
|
||||
},
|
||||
{
|
||||
id: "rainfallDisplay",
|
||||
title: "강수량(mm)",
|
||||
widthPercent: 10,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
id: "exceededFrequencyYearDisplay",
|
||||
title: "초과빈도년도",
|
||||
widthPercent: 9,
|
||||
sortable: true,
|
||||
sortKey: "exceededFrequencyYear",
|
||||
},
|
||||
{
|
||||
id: "floodRiskGrade",
|
||||
title: "위험등급",
|
||||
widthPercent: 9,
|
||||
sortable: true,
|
||||
sortKey: "floodRiskGrade",
|
||||
},
|
||||
];
|
||||
|
||||
const localDateTime = (date: Date) =>
|
||||
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}T${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
||||
const initialFilters = (): Filters => {
|
||||
const now = new Date();
|
||||
const from = new Date(now);
|
||||
from.setHours(0, 0, 0, 0);
|
||||
return {
|
||||
ctpvCd: "0",
|
||||
sggCd: "0",
|
||||
// 최초 조회에서는 강우 시작시각을 제한하지 않고 전체 강우사상을 대상으로 한다.
|
||||
rainBgngFrom: "",
|
||||
rainBgngTo: "",
|
||||
analysisFrom: localDateTime(from),
|
||||
analysisTo: localDateTime(now),
|
||||
frequencyYear: "",
|
||||
};
|
||||
};
|
||||
const apiDate = (value: string) => value.replace(/[^0-9]/g, "").slice(0, 12);
|
||||
const displayDate = (value: string) =>
|
||||
/^\d{12}$/.test(value)
|
||||
? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)} ${value.slice(8, 10)}:${value.slice(10, 12)}`
|
||||
: value;
|
||||
const rainHourText = (value: number) =>
|
||||
value === 0 ? "현재" : `${value > 0 ? "+" : "-"}${Math.abs(value)}시간`;
|
||||
const query = (filters: Filters, page?: number, sort?: UiTableSort) => {
|
||||
const p = new URLSearchParams();
|
||||
if (page !== undefined) {
|
||||
p.set("page", String(page - 1));
|
||||
p.set("size", String(PAGE_SIZE));
|
||||
}
|
||||
if (filters.ctpvCd !== "0") p.set("ctpvCd", filters.ctpvCd.slice(0, 2));
|
||||
if (filters.sggCd !== "0") p.set("sggCd", filters.sggCd.slice(0, 5));
|
||||
["rainBgngFrom", "rainBgngTo", "analysisFrom", "analysisTo"].forEach(
|
||||
(key) => {
|
||||
const value = apiDate(filters[key as keyof Filters]);
|
||||
if (value) p.set(key, value);
|
||||
},
|
||||
);
|
||||
if (filters.frequencyYear) p.set("frequencyYear", filters.frequencyYear);
|
||||
if (sort?.key && sort.direction)
|
||||
p.set("sort", `${sort.key},${sort.direction}`);
|
||||
return p.toString();
|
||||
};
|
||||
|
||||
export default function FldngPredcPrstPopupContent() {
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const [filters, setFilters] = useState<Filters>(() => initialFilters());
|
||||
const [applied, setApplied] = useState<Filters>(() => initialFilters());
|
||||
const [rows, setRows] = useState<TableRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [sort, setSort] = useState<UiTableSort>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selected, setSelected] = useState(-1);
|
||||
useEffect(() => {
|
||||
if (!ctpv.length) void fetchCtpv();
|
||||
if (!sgg.length) void fetchSgg();
|
||||
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
||||
const sggOptions = useMemo(
|
||||
() =>
|
||||
filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) =>
|
||||
String(item.val).startsWith(filters.ctpvCd.slice(0, 2)),
|
||||
),
|
||||
[filters.ctpvCd, sgg],
|
||||
);
|
||||
const load = useCallback(
|
||||
async (targetPage: number, condition: Filters, nextSort?: UiTableSort) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getRawJson(
|
||||
`/sewerage-service/api/v1/fldng/predc/prst?${query(condition, targetPage, nextSort)}`,
|
||||
);
|
||||
const payload = response?.data ?? response;
|
||||
const content: ApiRow[] = Array.isArray(payload?.content)
|
||||
? payload.content
|
||||
: [];
|
||||
setRows(
|
||||
content.map((item, index) => ({
|
||||
...item,
|
||||
rowNo: (targetPage - 1) * PAGE_SIZE + index + 1,
|
||||
rainBeginningDateTimeDisplay: displayDate(
|
||||
item.rainBeginningDateTime,
|
||||
),
|
||||
analysisDateTimeDisplay: displayDate(item.analysisDateTime),
|
||||
observedAccumulatedRainfallDisplay: Number(
|
||||
item.observedAccumulatedRainfall ?? 0,
|
||||
).toFixed(4),
|
||||
rainHourDisplay: rainHourText(Number(item.rainHour ?? 0)),
|
||||
rainfallDisplay: Number(item.rainfall ?? 0).toFixed(1),
|
||||
exceededFrequencyYearDisplay: String(
|
||||
item.exceededFrequencyYear ?? "",
|
||||
),
|
||||
})),
|
||||
);
|
||||
setTotal(Number(payload?.totalElements ?? 0));
|
||||
setPage(targetPage);
|
||||
setApplied(condition);
|
||||
setSort(nextSort);
|
||||
setSelected(-1);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "침수예측현황 조회에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
useEffect(() => {
|
||||
const defaults = initialFilters();
|
||||
setFilters(defaults);
|
||||
void load(1, defaults);
|
||||
}, [load]);
|
||||
const mapRow = async (index?: number) => {
|
||||
if (typeof index !== "number") return;
|
||||
const row = rows[index];
|
||||
if (!row) return;
|
||||
setSelected(index);
|
||||
if (!MAP_FREQUENCIES.has(row.exceededFrequencyYear)) {
|
||||
alert(
|
||||
`${row.exceededFrequencyYear}년 빈도 도시침수지도는 제공되지 않습니다. (30·50·80년 빈도만 제공)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const sbmrgFrqExpln = `${row.exceededFrequencyYear}년`;
|
||||
const p = new URLSearchParams({ sggCd: row.sggCd, sbmrgFrqExpln });
|
||||
const extentData = await getRawJson(
|
||||
`/sewerage-service/api/v1/fldng/flood-dngr-map/group/extent?${p}`,
|
||||
);
|
||||
const extent = [
|
||||
extentData.minX,
|
||||
extentData.minY,
|
||||
extentData.maxX,
|
||||
extentData.maxY,
|
||||
].map(Number);
|
||||
if (extent.some((v) => !Number.isFinite(v)))
|
||||
throw new Error("도시침수지도 위치 정보가 없습니다.");
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_FLOOD_GROUP,
|
||||
payload: { extent, sggCd: row.sggCd, sbmrgFrqExpln },
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
} catch (error) {
|
||||
alert(
|
||||
error instanceof Error ? error.message : "지도 이동에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const download = async () => {
|
||||
if (!total) return alert("다운로드할 데이터가 없습니다.");
|
||||
setLoading(true);
|
||||
try {
|
||||
const blob = await getBlobData(
|
||||
`/sewerage-service/api/v1/fldng/predc/prst/excel?${query(applied, undefined, sort)}`,
|
||||
);
|
||||
fileDownload(blob, "침수예측현황.xlsx");
|
||||
} catch (error) {
|
||||
alert(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Excel 다운로드에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const field = (key: keyof Filters, label: string) => (
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">{label}</div>
|
||||
<input
|
||||
className="sn-popup-input"
|
||||
type="datetime-local"
|
||||
value={filters[key]}
|
||||
onChange={(e) => setFilters((v) => ({ ...v, [key]: e.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
return (
|
||||
<main className="sn-popup-page w-full max-w-full overflow-hidden">
|
||||
<LoadingOverlay isLoading={loading} />
|
||||
<header className="sn-popup-header">
|
||||
<h1 className="sn-popup-title">침수예측현황</h1>
|
||||
<div />
|
||||
</header>
|
||||
<section className="flex min-h-0 flex-1 flex-col px-4 pb-3 pt-3">
|
||||
<div className="sn-popup-filter-panel-compact grid grid-cols-4 gap-3">
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">시도</div>
|
||||
<select
|
||||
className="sn-popup-input"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(e) =>
|
||||
setFilters((v) => ({
|
||||
...v,
|
||||
ctpvCd: e.target.value,
|
||||
sggCd: "0",
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((x) => (
|
||||
<option key={x.val} value={String(x.val)}>
|
||||
{x.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">시군구</div>
|
||||
<select
|
||||
className="sn-popup-input"
|
||||
value={filters.sggCd}
|
||||
onChange={(e) =>
|
||||
setFilters((v) => ({ ...v, sggCd: e.target.value }))
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{sggOptions.map((x) => (
|
||||
<option key={x.val} value={String(x.val)}>
|
||||
{x.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{field("rainBgngFrom", "강우 시작시각 From")}
|
||||
{field("rainBgngTo", "강우 시작시각 To")}
|
||||
{field("analysisFrom", "분석시각 From")}
|
||||
{field("analysisTo", "분석시각 To")}
|
||||
<label className="sn-popup-field">
|
||||
<div className="sn-popup-field-label">초과 빈도년도</div>
|
||||
<input
|
||||
className="sn-popup-input"
|
||||
type="number"
|
||||
min="0"
|
||||
value={filters.frequencyYear}
|
||||
onChange={(e) =>
|
||||
setFilters((v) => ({ ...v, frequencyYear: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<div className="sn-popup-actions self-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={() => void load(1, filters, sort)}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button size="sm" color="primary" onClick={() => void download()}>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ListDiv className="mt-3 min-h-0 flex-1 overflow-x-auto">
|
||||
<div className="min-w-[1450px]">
|
||||
<UiTable<TableRow>
|
||||
headName={
|
||||
<>침수예측현황 조회 결과 총 {total.toLocaleString()}건</>
|
||||
}
|
||||
tableData={rows}
|
||||
headlist={HEADS}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={total}
|
||||
curPage={page}
|
||||
onPageChange={(p) => void load(p, applied, sort)}
|
||||
cellClick={(i) => void mapRow(i)}
|
||||
index={selected}
|
||||
widthFixed={true}
|
||||
isHideCount={true}
|
||||
sort={sort}
|
||||
onSortChange={(s) => void load(1, applied, s)}
|
||||
/>
|
||||
</div>
|
||||
</ListDiv>
|
||||
<div className="sn-popup-help-text">
|
||||
행 클릭 시 해당 시군구·빈도의 도시침수지도로 이동합니다.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -182,11 +182,11 @@ export const windowPopupDefinitions = {
|
||||
height: 760,
|
||||
requiredParams: ["zoneId"],
|
||||
},
|
||||
floodPredictionMapPopup: {
|
||||
key: "floodPredictionMapPopup",
|
||||
fldngPredcPrst: {
|
||||
key: "fldngPredcPrst",
|
||||
title: "침수예측현황",
|
||||
width: 1080,
|
||||
height: 760,
|
||||
width: 1500,
|
||||
height: 860,
|
||||
},
|
||||
agedPipeStatusByCtpvPopup: {
|
||||
key: "agedPipeStatusByCtpvPopup",
|
||||
@@ -223,8 +223,8 @@ export const windowPopupDefinitions = {
|
||||
flowRiskPopup: {
|
||||
key: "flowRiskPopup",
|
||||
title: "통수 위험",
|
||||
width: 1180,
|
||||
height: 820,
|
||||
width: 1360,
|
||||
height: 600,
|
||||
},
|
||||
} as const satisfies Record<string, WindowPopupDefinition>;
|
||||
|
||||
|
||||
@@ -123,13 +123,8 @@ const RepairHistoryPopupContent = loadPopupContent(
|
||||
const WaterRiskPopupContent = loadPopupContent(
|
||||
() => import("./ui/WaterRiskPopupContent"),
|
||||
);
|
||||
|
||||
const FloodPredictionPlaceholderPopupContent = (
|
||||
_props: WindowPopupContentProps,
|
||||
) => (
|
||||
<FeaturePlaceholderPopupContent
|
||||
title={windowPopupDefinitions.floodPredictionMapPopup.title}
|
||||
/>
|
||||
const FldngPredcPrstPopupContent = loadPopupContent(
|
||||
() => import("../fldngPredcPrst/ui/FldngPredcPrstPopupContent"),
|
||||
);
|
||||
|
||||
const GisEditPopupContent = (_props: WindowPopupContentProps) => (
|
||||
@@ -243,9 +238,9 @@ export const windowPopupRegistry = {
|
||||
...windowPopupDefinitions.repairHistoryPopup,
|
||||
component: RepairHistoryPopupContent,
|
||||
},
|
||||
floodPredictionMapPopup: {
|
||||
...windowPopupDefinitions.floodPredictionMapPopup,
|
||||
component: FloodPredictionPlaceholderPopupContent,
|
||||
fldngPredcPrst: {
|
||||
...windowPopupDefinitions.fldngPredcPrst,
|
||||
component: FldngPredcPrstPopupContent,
|
||||
},
|
||||
agedPipeStatusByCtpvPopup: {
|
||||
...windowPopupDefinitions.agedPipeStatusByCtpvPopup,
|
||||
|
||||
Reference in New Issue
Block a user