617 lines
21 KiB
TypeScript
617 lines
21 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import {
|
|
StandardPopupLayout,
|
|
type UiHeadProps,
|
|
} from "@common/business_components";
|
|
import { Button } from "@common/components";
|
|
|
|
import { getData, getRawJson } from "@/components/lib/api";
|
|
import { appendApiSearchParams } from "@/shared/util/util";
|
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
|
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
|
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
|
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
|
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
|
import { useAlertStore } from "@/shared/lib";
|
|
import useStore from "@/store/useStore";
|
|
import { createPredictionAnalysisTimeRange } from "../model/analysisTimeRange";
|
|
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
|
import { downloadFloodPredictionStatusExcel } from "../lib/fldng-predc-prst-excel";
|
|
|
|
type ApiRow = {
|
|
sggCd: string;
|
|
ctpvCd: string;
|
|
ctpvNm: string;
|
|
sggNm: string;
|
|
rainBeginningDateTime: string;
|
|
analysisDateTime: string;
|
|
observedAccumulatedRainfall: number | null;
|
|
predictionHour: number | null;
|
|
predictedAccumulatedRainfall: number | null;
|
|
exceededFrequencyYear: number | null;
|
|
floodRiskGrade: string;
|
|
};
|
|
|
|
type TableRow = ApiRow &
|
|
Record<string, unknown> & {
|
|
rowNo: number;
|
|
analysisDateTimeDisplay: string;
|
|
rainBeginningDateTimeDisplay: string;
|
|
observedAccumulatedRainfallDisplay: string;
|
|
predictionHourDisplay: string;
|
|
predictedAccumulatedRainfallDisplay: string;
|
|
exceededFrequencyYearDisplay: string;
|
|
};
|
|
|
|
type Filters = {
|
|
ctpvCd: string;
|
|
sggCd: string;
|
|
rainBgngFrom: string;
|
|
rainBgngTo: string;
|
|
analysisFrom: string;
|
|
analysisTo: string;
|
|
frequencyYear: string;
|
|
};
|
|
|
|
const PAGE_SIZE = 10;
|
|
const EXCEL_DOWNLOAD_PAGE_SIZE = 500;
|
|
const MAP_FREQUENCIES = new Set([30, 50, 80, 100]);
|
|
const FREQUENCY_OPTIONS = [30, 50, 80, 100] as const;
|
|
|
|
/** 도시침수지도 DB의 3자리 빈도 코드로 변환한다. 30년 이하는 030, 100년 초과는 100으로 제한한다. */
|
|
const toFloodMapFrequencyCode = (value: number) => {
|
|
if (!Number.isFinite(value) || value <= 0) return null;
|
|
const normalized = value <= 30 ? 30 : value > 100 ? 100 : value;
|
|
if (!MAP_FREQUENCIES.has(normalized)) return null;
|
|
return String(normalized).padStart(3, "0");
|
|
};
|
|
|
|
const HEADS: UiHeadProps[] = [
|
|
{ id: "rowNo", title: "순번", widthPercent: 6 },
|
|
{ id: "ctpvNm", title: "시도", widthPercent: 8 },
|
|
{ id: "sggNm", title: "시군구", widthPercent: 9 },
|
|
{
|
|
id: "analysisDateTimeDisplay",
|
|
title: "분석시각",
|
|
widthPercent: 13,
|
|
},
|
|
{
|
|
id: "rainBeginningDateTimeDisplay",
|
|
title: "강우시작시각",
|
|
widthPercent: 16,
|
|
},
|
|
{
|
|
id: "observedAccumulatedRainfallDisplay",
|
|
title: "관측 누적강수량(mm)",
|
|
widthPercent: 13,
|
|
align: "right",
|
|
},
|
|
{
|
|
id: "predictionHourDisplay",
|
|
title: "예측시간",
|
|
widthPercent: 9,
|
|
},
|
|
{
|
|
id: "predictedAccumulatedRainfallDisplay",
|
|
title: "예측 누적강수량(mm)",
|
|
widthPercent: 15,
|
|
align: "right",
|
|
},
|
|
{
|
|
id: "exceededFrequencyYearDisplay",
|
|
title: "초과 빈도(년)",
|
|
widthPercent: 10,
|
|
align: "right",
|
|
},
|
|
{ id: "floodRiskGrade", title: "침수위험도", widthPercent: 10 },
|
|
];
|
|
|
|
/** 최초·초기화 조회조건은 금일 분석결과를 기준으로 생성한다. */
|
|
const buildInitialFilters = (): Filters => {
|
|
const range = createPredictionAnalysisTimeRange();
|
|
const toDateTimeLocal = (value: string) =>
|
|
`${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}T${value.slice(8, 10)}:${value.slice(10, 12)}`;
|
|
return {
|
|
ctpvCd: "0",
|
|
sggCd: "0",
|
|
rainBgngFrom: "",
|
|
rainBgngTo: "",
|
|
analysisFrom: toDateTimeLocal(range.analysisFrom),
|
|
analysisTo: toDateTimeLocal(range.analysisTo),
|
|
frequencyYear: "0",
|
|
};
|
|
};
|
|
|
|
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 predictionHourText = (value: number | null) => {
|
|
const hour = Number(value ?? 0);
|
|
return hour > 0 ? `${hour}시간 후` : "-";
|
|
};
|
|
|
|
const formatNumber = (value: number | null, digits: number) =>
|
|
value == null || !Number.isFinite(Number(value))
|
|
? "-"
|
|
: Number(value).toFixed(digits);
|
|
|
|
/** 화면 조회조건을 목록·Excel API 쿼리로 변환한다. */
|
|
const buildQuery = (filters: Filters, page?: number, size = PAGE_SIZE) => {
|
|
const params = new URLSearchParams();
|
|
if (page !== undefined) {
|
|
params.set("page", String(page - 1));
|
|
params.set("size", String(size));
|
|
}
|
|
appendApiSearchParams(params, {
|
|
ctpvCd: filters.ctpvCd === "0" ? "0" : filters.ctpvCd.slice(0, 2),
|
|
sggCd: filters.sggCd === "0" ? "0" : filters.sggCd.slice(0, 5),
|
|
rainBgngFrom: apiDate(filters.rainBgngFrom),
|
|
rainBgngTo: apiDate(filters.rainBgngTo),
|
|
analysisFrom: apiDate(filters.analysisFrom),
|
|
analysisTo: apiDate(filters.analysisTo),
|
|
frequencyYear: filters.frequencyYear,
|
|
});
|
|
return params.toString();
|
|
};
|
|
|
|
/** 시작·종료시각과 빈도 조건을 API 호출 전에 검증한다. */
|
|
const validateFilters = (filters: Filters) => {
|
|
if (
|
|
filters.rainBgngFrom &&
|
|
filters.rainBgngTo &&
|
|
filters.rainBgngFrom > filters.rainBgngTo
|
|
) {
|
|
return "강우 시작시각의 시작은 종료보다 늦을 수 없습니다.";
|
|
}
|
|
if (
|
|
filters.analysisFrom &&
|
|
filters.analysisTo &&
|
|
filters.analysisFrom > filters.analysisTo
|
|
) {
|
|
return "분석시각의 시작은 종료보다 늦을 수 없습니다.";
|
|
}
|
|
if (
|
|
filters.frequencyYear != "0" &&
|
|
!FREQUENCY_OPTIONS.includes(
|
|
Number(filters.frequencyYear) as 30 | 50 | 80 | 100,
|
|
)
|
|
) {
|
|
return "초과 빈도는 30년, 50년, 80년, 100년 중에서 선택해야 합니다.";
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const buildRowId = (row: ApiRow) =>
|
|
`${row.ctpvCd}:${row.sggCd}:${row.rainBeginningDateTime}:${row.analysisDateTime}:${row.predictionHour}`;
|
|
|
|
/** 침수위험도 등급을 목록 배지의 고정 의미색으로 변환한다. */
|
|
const getFloodRiskGradeClassName = (value: string) => {
|
|
const grade = String(value ?? "")
|
|
.trim()
|
|
.toUpperCase();
|
|
if (grade.includes("위험") || grade === "DANGER") {
|
|
return "bg-[#dc2626] text-white";
|
|
}
|
|
if (grade.includes("경계") || grade === "WARNING") {
|
|
return "bg-[#f97316] text-white";
|
|
}
|
|
if (grade.includes("주의") || grade === "CAUTION") {
|
|
return "bg-[#facc15] text-[#713f12]";
|
|
}
|
|
return "sn-popup-badge-normal";
|
|
};
|
|
|
|
export default function FldngPredcPrstPopupContent() {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
|
const [filters, setFilters] = useState<Filters>(() => buildInitialFilters());
|
|
const [appliedFilters, setAppliedFilters] = useState<Filters>(() =>
|
|
buildInitialFilters(),
|
|
);
|
|
const [rows, setRows] = useState<TableRow[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [loading, setLoading] = useState(false);
|
|
const [selectedRowId, setSelectedRowId] = useState("");
|
|
// 개발 StrictMode에서도 최초 목록 API를 한 번만 호출하기 위한 ref다.
|
|
const didInitialLoadRef = useRef(false);
|
|
|
|
// 부모 지도에서 도시침수지도 레이어가 비활성 상태임을 알려오면 팝업에서 안내한다.
|
|
useEffect(() => {
|
|
const handleMapMessage = (event: MessageEvent) => {
|
|
if (event.origin !== window.location.origin) return;
|
|
if (
|
|
event.data?.type ===
|
|
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_FLOOD_LAYER_REQUIRED
|
|
) {
|
|
alert("지도의 좌측 도시침수지도를 선택하여야 지도에 보입니다.");
|
|
}
|
|
};
|
|
window.addEventListener("message", handleMapMessage);
|
|
return () => window.removeEventListener("message", handleMapMessage);
|
|
}, [alert]);
|
|
|
|
useEffect(() => {
|
|
if (!ctpv.length) void fetchCtpv();
|
|
if (!sgg.length) void fetchSgg();
|
|
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
|
|
|
const sggOptions = useMemo(
|
|
() =>
|
|
filters.ctpvCd === "0"
|
|
? []
|
|
: sgg.filter((item) =>
|
|
String(item.val).startsWith(filters.ctpvCd.slice(0, 2)),
|
|
),
|
|
[filters.ctpvCd, sgg],
|
|
);
|
|
|
|
/** 조회조건과 페이지에 맞는 침수예측현황 목록을 불러온다. */
|
|
const loadRows = useCallback(
|
|
async (targetPage: number, condition: Filters) => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await getRawJson(
|
|
`/sewerage-service/api/v1/fldng/predc/prst?${buildQuery(condition, targetPage)}`,
|
|
);
|
|
const payload = response?.data ?? response;
|
|
const content: ApiRow[] = Array.isArray(payload?.content)
|
|
? payload.content
|
|
: [];
|
|
setRows(
|
|
content.map((item) => ({
|
|
...item,
|
|
rowNo: 0,
|
|
analysisDateTimeDisplay: displayDate(item.analysisDateTime),
|
|
rainBeginningDateTimeDisplay: displayDate(
|
|
item.rainBeginningDateTime,
|
|
),
|
|
observedAccumulatedRainfallDisplay: formatNumber(
|
|
item.observedAccumulatedRainfall,
|
|
1,
|
|
),
|
|
predictionHourDisplay: predictionHourText(item.predictionHour),
|
|
predictedAccumulatedRainfallDisplay: formatNumber(
|
|
item.predictedAccumulatedRainfall,
|
|
1,
|
|
),
|
|
exceededFrequencyYearDisplay:
|
|
item.exceededFrequencyYear == null
|
|
? "-"
|
|
: String(item.exceededFrequencyYear),
|
|
})),
|
|
);
|
|
setTotal(Number(payload?.totalElements ?? 0));
|
|
setPage(targetPage);
|
|
setAppliedFilters(condition);
|
|
setSelectedRowId("");
|
|
} catch (error) {
|
|
console.error("침수예측현황 조회 중 오류:", error);
|
|
setRows([]);
|
|
setTotal(0);
|
|
alert("침수예측현황 조회에 실패했습니다.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[alert],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (didInitialLoadRef.current) return;
|
|
didInitialLoadRef.current = true;
|
|
const defaults = buildInitialFilters();
|
|
setFilters(defaults);
|
|
void loadRows(1, defaults);
|
|
}, [loadRows]);
|
|
|
|
const handleSearch = () => {
|
|
const message = validateFilters(filters);
|
|
if (message) {
|
|
alert(message);
|
|
return;
|
|
}
|
|
void loadRows(1, filters);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
const defaults = buildInitialFilters();
|
|
setFilters(defaults);
|
|
void loadRows(1, defaults);
|
|
};
|
|
|
|
/** 선택 행과 동일한 시군구·빈도의 도시침수지도 범위를 부모 지도에 전달한다. */
|
|
const handleRowClick = async (row: TableRow) => {
|
|
const frequencyYear = Number(row.exceededFrequencyYear ?? 0);
|
|
const sbmrgFrqExpln = toFloodMapFrequencyCode(frequencyYear);
|
|
if (!sbmrgFrqExpln) {
|
|
alert(
|
|
`${frequencyYear || "해당"}년 빈도 도시침수지도는 제공되지 않습니다. (30·50·80·100년 빈도만 제공)`,
|
|
);
|
|
return;
|
|
}
|
|
if (!row.sggCd) {
|
|
alert("지도 이동에 필요한 시군구 정보가 없습니다.");
|
|
return;
|
|
}
|
|
if (!window.opener || window.opener.closed) {
|
|
alert("하수관망도 분석도구에서 침수예측현황을 다시 열어주세요.");
|
|
return;
|
|
}
|
|
|
|
setSelectedRowId(buildRowId(row));
|
|
setLoading(true);
|
|
try {
|
|
const params = new URLSearchParams({
|
|
sggCd: row.sggCd,
|
|
sbmrgFrqExpln,
|
|
});
|
|
// 백엔드 공통 응답({ data: ... })의 data 영역을 반환하는 API 헬퍼를 사용한다.
|
|
const extentData = await getData(
|
|
`/sewerage-service/api/v1/fldng/floodDngrMap/group/extent?${params.toString()}`,
|
|
);
|
|
const extent = [
|
|
extentData?.minX,
|
|
extentData?.minY,
|
|
extentData?.maxX,
|
|
extentData?.maxY,
|
|
].map(Number);
|
|
if (extent.some((value) => !Number.isFinite(value))) {
|
|
throw new Error("도시침수지도 위치 정보가 없습니다.");
|
|
}
|
|
window.opener.postMessage(
|
|
{
|
|
type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_FLOOD_GROUP,
|
|
payload: {
|
|
extent,
|
|
sggCd: row.sggCd,
|
|
sbmrgFrqExpln,
|
|
analysisDateTime: row.analysisDateTime,
|
|
},
|
|
},
|
|
window.location.origin,
|
|
);
|
|
window.opener.focus();
|
|
} catch (error) {
|
|
console.error("침수예측현황 지도 이동 중 오류:", error);
|
|
alert(
|
|
error instanceof Error && error.message.includes("위치 정보")
|
|
? error.message
|
|
: "도시침수지도 이동에 실패하였습니다.",
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDownload = async () => {
|
|
if (!total) {
|
|
alert("다운로드할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const result = await fetchAllPages<
|
|
{ content: ApiRow[]; totalElements: number },
|
|
ApiRow
|
|
>({
|
|
batchSize: EXCEL_DOWNLOAD_PAGE_SIZE,
|
|
fetchPage: async (pageIndex, size) => {
|
|
const response = await getRawJson(
|
|
`/sewerage-service/api/v1/fldng/predc/prst?${buildQuery(appliedFilters, pageIndex + 1, size)}`,
|
|
);
|
|
const payload = response?.data ?? response;
|
|
return {
|
|
content: Array.isArray(payload?.content) ? payload.content : [],
|
|
totalElements: Number(payload?.totalElements ?? 0),
|
|
};
|
|
},
|
|
getItems: (response) => response.content,
|
|
getTotal: (response) => response.totalElements,
|
|
});
|
|
await downloadFloodPredictionStatusExcel(result.items);
|
|
} catch (error) {
|
|
console.error("침수예측현황 Excel 다운로드 중 오류:", error);
|
|
alert("침수예측현황 Excel 다운로드에 실패했습니다.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const renderDateTimeRange = (
|
|
label: string,
|
|
fromKey: "rainBgngFrom" | "analysisFrom",
|
|
toKey: "rainBgngTo" | "analysisTo",
|
|
) => (
|
|
<label className="flex shrink-0 items-center gap-2">
|
|
<span className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
|
{label}
|
|
</span>
|
|
<input
|
|
className="h-[3.2rem] w-[17rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
|
type="datetime-local"
|
|
value={filters[fromKey]}
|
|
aria-label={`${label} 시작`}
|
|
onChange={(event) =>
|
|
setFilters((current) => ({
|
|
...current,
|
|
[fromKey]: event.target.value,
|
|
}))
|
|
}
|
|
/>
|
|
<span className="text-xl font-medium text-label">~</span>
|
|
<input
|
|
className="h-[3.2rem] w-[17rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
|
type="datetime-local"
|
|
value={filters[toKey]}
|
|
aria-label={`${label} 종료`}
|
|
onChange={(event) =>
|
|
setFilters((current) => ({
|
|
...current,
|
|
[toKey]: event.target.value,
|
|
}))
|
|
}
|
|
/>
|
|
</label>
|
|
);
|
|
|
|
return (
|
|
<StandardPopupLayout
|
|
title={windowPopupDefinitions.fldngPredcPrst.title}
|
|
bodyClassName="overflow-x-hidden"
|
|
>
|
|
<section
|
|
className={`relative flex min-h-0 flex-1 flex-col gap-[1.2rem] ${
|
|
rows.length === 0
|
|
? "[&_.standard-data-table>.card]:!flex [&_.standard-data-table>.card]:!min-h-0 [&_.standard-data-table>.card]:!flex-1 [&_.standard-data-table>.card]:!flex-col [&_.standard-data-table]:!flex [&_.standard-data-table]:!min-h-0 [&_.standard-data-table]:!flex-1 [&_.standard-data-table]:!flex-col"
|
|
: ""
|
|
}`}
|
|
>
|
|
<LoadingOverlay isLoading={loading} />
|
|
<PopupSearchPanel contentClassName="flex flex-wrap items-center gap-x-4 gap-y-3 2xl:flex-nowrap">
|
|
<label className="flex shrink-0 items-center gap-2">
|
|
<span className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
|
시도
|
|
</span>
|
|
<select
|
|
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"
|
|
value={filters.ctpvCd}
|
|
aria-label="시도"
|
|
onChange={(event) =>
|
|
setFilters((current) => ({
|
|
...current,
|
|
ctpvCd: event.target.value,
|
|
sggCd: "0",
|
|
}))
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{ctpv.map((option) => (
|
|
<option key={option.val} value={String(option.val)}>
|
|
{option.text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="flex shrink-0 items-center gap-2">
|
|
<span className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
|
시군구
|
|
</span>
|
|
<select
|
|
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"
|
|
value={filters.sggCd}
|
|
aria-label="시군구"
|
|
onChange={(event) =>
|
|
setFilters((current) => ({
|
|
...current,
|
|
sggCd: event.target.value,
|
|
}))
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{sggOptions.map((option) => (
|
|
<option key={option.val} value={String(option.val)}>
|
|
{option.text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
{renderDateTimeRange("분석시각", "analysisFrom", "analysisTo")}
|
|
{renderDateTimeRange("강우시각", "rainBgngFrom", "rainBgngTo")}
|
|
|
|
<label className="flex shrink-0 items-center gap-2">
|
|
<span className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
|
초과 빈도
|
|
</span>
|
|
<select
|
|
className="h-[3.2rem] w-[12rem] rounded-md border border-input bg-field px-3 text-xl text-foreground outline-none focus:ring-2 focus:ring-ring"
|
|
value={filters.frequencyYear}
|
|
aria-label="초과 빈도"
|
|
onChange={(event) =>
|
|
setFilters((current) => ({
|
|
...current,
|
|
frequencyYear: event.target.value,
|
|
}))
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{FREQUENCY_OPTIONS.map((frequency) => (
|
|
<option key={frequency} value={String(frequency)}>
|
|
{frequency}년
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<div className="flex min-w-fit flex-1 items-center justify-end gap-2">
|
|
<Button type="button" color="primary" onClick={handleSearch}>
|
|
조회
|
|
</Button>
|
|
<Button type="button" color="white" onClick={handleReset}>
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
</PopupSearchPanel>
|
|
|
|
<MapPopupDataTable
|
|
rows={rows}
|
|
headlist={HEADS}
|
|
title="침수예측현황 목록"
|
|
total={total}
|
|
page={page}
|
|
pageSize={PAGE_SIZE}
|
|
onPageChange={(nextPage) => void loadRows(nextPage, appliedFilters)}
|
|
rowId={(row) => buildRowId(row)}
|
|
selectedRowId={selectedRowId}
|
|
onRowClick={(row) => void handleRowClick(row)}
|
|
renderCell={(row, headId) =>
|
|
headId === "floodRiskGrade" ? (
|
|
<span
|
|
className={`sn-popup-badge min-w-[5.6rem] ${getFloodRiskGradeClassName(row.floodRiskGrade)}`}
|
|
>
|
|
{row.floodRiskGrade || "-"}
|
|
</span>
|
|
) : undefined
|
|
}
|
|
scrollMode="horizontal"
|
|
minTableWidth="100rem"
|
|
fillAvailableHeight={rows.length === 0}
|
|
emptyMessage="조회 조건에 해당하는 침수예측현황이 없습니다."
|
|
actions={
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
onClick={() => void handleDownload()}
|
|
disabled={!total || loading}
|
|
>
|
|
엑셀다운로드
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<div className="space-y-1 px-1 text-xl text-muted-foreground">
|
|
<p>
|
|
30년빈도 이상인 경우{" "}
|
|
<span className="font-semibold text-[#dc2626]">위험</span>, 10년빈도
|
|
이상인 경우{" "}
|
|
<span className="font-semibold text-[#f97316]">경계</span>, 2년빈도
|
|
이상인 경우{" "}
|
|
<span className="font-semibold text-[#ca8a04]">주의</span>
|
|
</p>
|
|
<p>행을 선택하면 해당 시군구·빈도의 도시침수지도로 이동합니다.</p>
|
|
</div>
|
|
</section>
|
|
</StandardPopupLayout>
|
|
);
|
|
}
|