sn: 통수위험 위험여부란 수정 외
This commit is contained in:
+4
-4
@@ -5,7 +5,7 @@
|
||||
"use client";
|
||||
|
||||
import { SelectBox } from "@common/business_components/ui";
|
||||
import { FLDNG_HSTRY_MIN_YEAR, type FldngHstryYearRange } from "@/entities/flood-history/model/flood-history-types";
|
||||
import { FLDNG_HSTRY_MIN_YEAR, FLDNG_HSTRY_MAX_YEAR, type FldngHstryYearRange } from "@/entities/flood-history/model/flood-history-types";
|
||||
import { normalizeFldngHstryYearRange } from "@/entities/flood-history/lib/flood-history-utils";
|
||||
import LayerSettingCard from "../layer-setting/layer-setting-card";
|
||||
|
||||
@@ -26,10 +26,10 @@ export default function FldngHstryLayerSettingPanel({
|
||||
onClose,
|
||||
}: Props) {
|
||||
const normalizedRange = normalizeFldngHstryYearRange(yearRange);
|
||||
const currentYear = new Date().getFullYear();
|
||||
// 제공 자료의 최대 연도부터 최소 연도까지 선택 목록을 구성한다.
|
||||
const yearOptions = Array.from(
|
||||
{ length: currentYear - FLDNG_HSTRY_MIN_YEAR + 1 },
|
||||
(_, index) => String(currentYear - index),
|
||||
{ length: FLDNG_HSTRY_MAX_YEAR - FLDNG_HSTRY_MIN_YEAR + 1 },
|
||||
(_, index) => String(FLDNG_HSTRY_MAX_YEAR - index),
|
||||
);
|
||||
const startYearOptions = yearOptions
|
||||
.filter((year) => Number(year) <= Number(normalizedRange.endYear))
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useAlertStore } from "@/shared/lib";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import { FLDNG_HSTRY_LAYER_TYPE, FLDNG_HSTRY_DEFAULT_FILTERS, FLDNG_HSTRY_LIST_HEADLIST, FLDNG_HSTRY_PAGE_SIZE, FLDNG_HSTRY_ROW_SELECT_MESSAGE, type FldngHstryApiItem, type FldngHstryFilters, type FldngHstryTableRow } from "@/entities/flood-history/model/flood-history-types";
|
||||
import { attachFldngHstryGeometry, buildFldngHstryListQueryString, buildFldngHstryYearOptions, normalizeFldngHstryFilters, toFldngHstryTableRows } from "@/entities/flood-history/lib/flood-history-utils";
|
||||
@@ -421,9 +422,13 @@ export default function FldngHstryPopupPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="rounded-sm bg-secondary px-4 py-3 text-[1.2rem] text-muted-foreground">
|
||||
행을 클릭하면 메인 지도에서 해당 침수흔적을 표시합니다.
|
||||
</div>
|
||||
{/* 기존 행 클릭 안내에 인수테스트 요구(자료 원천 표시) 출처 문구를 글머리표로 더한다. */}
|
||||
<PopupFooterNote
|
||||
items={[
|
||||
"행을 클릭하면 메인 지도에서 해당 침수흔적을 표시합니다.",
|
||||
"출처 : 행정안전부 재난보험과(2002년~2025년)",
|
||||
]}
|
||||
/>
|
||||
</StandardPopupLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getData } from "@/shared/api/sewerage-api";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import {
|
||||
@@ -398,6 +399,11 @@ export default function FloodDngrMapPopupPage() {
|
||||
onRowClick={(row) => void handleRowClick(row)}
|
||||
rowId={(row, index) => `${row.sggCd}-${row.sbmrgFrqExpln}-${index}`}
|
||||
/>
|
||||
|
||||
{/* 인수테스트 요구: 팝업 자료의 원천을 목록 아래에 표시한다. */}
|
||||
<PopupFooterNote
|
||||
items={["출처 : 한강홍수통제소, 행정구역별 도시침수지도(2025.09)"]}
|
||||
/>
|
||||
</StandardPopupLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useUserStore } from "@/shared/lib/use-user-store";
|
||||
import { openWindowPopup } from "@/features/window-popup/lib/open-window-popup";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import PipeAnalysisSummaryTable from "./pipe-analysis-summary-table";
|
||||
import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import {
|
||||
PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS,
|
||||
@@ -522,6 +523,13 @@ export default function PipeAnalysisPopupPage() {
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 인수테스트 요구: 관로분석의 기반 자료(원천)를 목록 아래에 표시한다. */}
|
||||
<PopupFooterNote
|
||||
items={[
|
||||
"국토교통부로부터 제공받은 GIS 관로정보(2025년)를 기반으로 분석하고 있음",
|
||||
]}
|
||||
/>
|
||||
</StandardPopupLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { StandardPopupLayout } from "@common/business_components";
|
||||
import { getRawJson } from "@/shared/api/sewerage-api";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
@@ -309,6 +310,11 @@ export default function SbmrgDngrMapPopupPage() {
|
||||
].join(":")
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 인수테스트 요구: 팝업 자료의 원천을 목록 아래에 표시한다. */}
|
||||
<PopupFooterNote
|
||||
items={["출처 : 하수도정비 중점관리지역 20개소 과업 산출물"]}
|
||||
/>
|
||||
</StandardPopupLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note";
|
||||
import PopupSearchPanel, {
|
||||
PopupSearchActions,
|
||||
PopupSearchField,
|
||||
@@ -514,6 +515,13 @@ export default function SinkholePopupPage() {
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 인수테스트 요구: 지반침하 정보의 연계 원천과 연계 주기를 목록 아래에 표시한다. */}
|
||||
<PopupFooterNote
|
||||
items={[
|
||||
"출처 : 지하안전정보시스템 지반침하사고 정보 연계 표출(연계 주기 : 매 시간 연계)",
|
||||
]}
|
||||
/>
|
||||
</StandardPopupLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,19 @@ import type {
|
||||
export type WaterRiskResultQuery = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
/** 현재 탭 분석기준시간(정시) 정확 일치 조건. 분석시각 기간과 함께 보내지 않는다. */
|
||||
analysisBaseAt?: string;
|
||||
/** 과거이력 분석시각 기간 시작(yyyy-MM-ddTHH:00:00, 포함). 종료와 함께 보낸다. */
|
||||
analysisBaseFrom?: string;
|
||||
/** 과거이력 분석시각 기간 종료(yyyy-MM-ddTHH:00:00, 포함). 시작과 함께 보낸다. */
|
||||
analysisBaseTo?: string;
|
||||
/**
|
||||
* 과거이력 강우시각(강우 시작 일시) 기간 시작(yyyy-MM-ddTHH:00:00, 포함). 종료와 함께 보낸다.
|
||||
* 보내면 백엔드가 startDate·endDate보다 이 기간을 우선한다.
|
||||
*/
|
||||
rainStartedFrom?: string;
|
||||
/** 과거이력 강우시각(강우 시작 일시) 기간 종료(yyyy-MM-ddTHH:00:00, 포함). 시작과 함께 보낸다. */
|
||||
rainStartedTo?: string;
|
||||
ctpvCd?: string;
|
||||
sggCd?: string;
|
||||
areaId?: string;
|
||||
@@ -54,6 +66,14 @@ export const fetchWaterRiskResults = async (
|
||||
});
|
||||
if (query.ctpvCd) params.set("ctpvCd", query.ctpvCd);
|
||||
if (query.analysisBaseAt) params.set("analysisBaseAt", query.analysisBaseAt);
|
||||
if (query.analysisBaseFrom && query.analysisBaseTo) {
|
||||
params.set("analysisBaseFrom", query.analysisBaseFrom);
|
||||
params.set("analysisBaseTo", query.analysisBaseTo);
|
||||
}
|
||||
if (query.rainStartedFrom && query.rainStartedTo) {
|
||||
params.set("rainStartedFrom", query.rainStartedFrom);
|
||||
params.set("rainStartedTo", query.rainStartedTo);
|
||||
}
|
||||
if (query.sggCd) params.set("sggCd", query.sggCd);
|
||||
if (query.areaId) params.set("areaId", query.areaId);
|
||||
if (query.riskStatus) params.set("riskStatus", query.riskStatus);
|
||||
@@ -78,15 +98,12 @@ export const fetchWaterRiskResults = async (
|
||||
export const fetchWaterRiskMapView = async (
|
||||
row: WaterRiskResultRow,
|
||||
): Promise<WaterRiskMapView> => {
|
||||
if (!row.analysisDate || !row.ctpvCd || !row.sggCd) {
|
||||
throw new Error("지도 조회에 필요한 분석일·행정구역 정보가 없습니다.");
|
||||
// 날짜·지역이 아니라 누른 행의 SWMM 실행 ID로 조회해야 같은 날 같은 지역 결과가 여러 건이어도 같은 결과를 그린다.
|
||||
if (!row.executionId) {
|
||||
throw new Error("지도 조회에 필요한 SWMM 실행 ID가 없습니다.");
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
ctpvCd: row.ctpvCd,
|
||||
sggCd: row.sggCd,
|
||||
});
|
||||
const response = await getData(
|
||||
`v1/waterRisk/results/${encodeURIComponent(row.analysisDate)}/areas/${encodeURIComponent(row.areaId)}/mapView?${params.toString()}`,
|
||||
`v1/waterRisk/executions/${encodeURIComponent(row.executionId)}/mapView`,
|
||||
);
|
||||
return {
|
||||
...response,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** 백엔드의 양 끝 날짜 포함 최대 366일 계약을 캘린더와 조회 검증에서 함께 사용한다. */
|
||||
/** 백엔드의 양 끝 날짜 포함 최대 366일 계약을 기본값과 조회 검증에서 함께 사용한다. */
|
||||
export const WATER_RISK_MAX_SEARCH_DAYS = 366;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -24,31 +24,82 @@ export const getWaterRiskMinStartDate = (
|
||||
.slice(0, 10);
|
||||
};
|
||||
|
||||
/** 달력 제한을 우회한 직접 입력도 API 요청 전에 검증한다. */
|
||||
export const validateWaterRiskDateRange = (
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
today: string,
|
||||
/**
|
||||
* 시간 단위 검색 입력(`datetime-local`, `step=3600`)의 값 형식 `yyyy-MM-ddTHH:00`이다.
|
||||
* 분은 항상 00이며, 첫 번째 그룹은 날짜(yyyy-MM-dd)다.
|
||||
*/
|
||||
const HOUR_VALUE_PATTERN = /^(\d{4}-\d{2}-\d{2})T\d{2}:00$/;
|
||||
|
||||
/**
|
||||
* `datetime-local` 입력값의 분을 00으로 맞춘다.
|
||||
* 브라우저는 값의 분이 00이면 분 칸을 고정하지만, 빈 값에서 처음 입력하거나 직접 입력해 분이 들어온 경우는
|
||||
* 스스로 고치지 않으므로 저장 전에 시간 단위로 내린다. 형식이 아니면(빈 값 포함) 그대로 둔다.
|
||||
*/
|
||||
export const toWaterRiskHourValue = (value: string) =>
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)
|
||||
? `${value.slice(0, 13)}:00`
|
||||
: value;
|
||||
|
||||
/** 로컬 시각을 시간 단위 입력값(yyyy-MM-ddTHH:00)으로 내린다. 기본 종료값과 미래 시각 제한에 쓴다. */
|
||||
export const formatWaterRiskHourValue = (date: Date) => {
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`;
|
||||
};
|
||||
|
||||
/** 시간 단위 입력값을 API 정시 일시(yyyy-MM-ddTHH:00:00)로 바꾼다. 빈 값은 빈 문자열이다. */
|
||||
export const toWaterRiskApiDateTime = (value: string) =>
|
||||
value ? `${value}:00` : "";
|
||||
|
||||
/**
|
||||
* 시간 단위 기간을 백엔드 계약(시작·종료 함께, 정시, 시작 ≤ 종료, 날짜 기준 최대 366일)과
|
||||
* 화면 조건(종료 ≤ 현재 정시)으로 검증한다. 오류가 없으면 빈 문자열을 반환한다.
|
||||
* `required`가 아니면 시작·종료를 모두 비웠을 때 조건 없이 조회한다.
|
||||
*/
|
||||
const validateHourPeriod = (
|
||||
from: string,
|
||||
to: string,
|
||||
currentHourValue: string,
|
||||
{ label, required }: { label: string; required: boolean },
|
||||
): string => {
|
||||
if (!startDate || !endDate) return "조회 시작일과 종료일을 입력해야 합니다.";
|
||||
const start = parseDate(startDate);
|
||||
const end = parseDate(endDate);
|
||||
if (start === null || end === null)
|
||||
return "올바른 조회 날짜를 입력해 주세요.";
|
||||
if (endDate > today) return "조회 종료일은 오늘보다 늦을 수 없습니다.";
|
||||
if (start > end) return "조회 시작일은 종료일보다 늦을 수 없습니다.";
|
||||
if (!from && !to) {
|
||||
return required ? `${label} 시작과 종료를 입력해야 합니다.` : "";
|
||||
}
|
||||
if (!from || !to) {
|
||||
return required
|
||||
? `${label} 시작과 종료를 입력해야 합니다.`
|
||||
: `${label}은 시작과 종료를 모두 입력하거나 모두 비워야 합니다.`;
|
||||
}
|
||||
const fromMatch = HOUR_VALUE_PATTERN.exec(from);
|
||||
const toMatch = HOUR_VALUE_PATTERN.exec(to);
|
||||
const start = fromMatch ? parseDate(fromMatch[1]!) : null;
|
||||
const end = toMatch ? parseDate(toMatch[1]!) : null;
|
||||
if (start === null || end === null) return `올바른 ${label}을 입력해 주세요.`;
|
||||
if (to > currentHourValue) return `${label} 종료는 현재 시각보다 늦을 수 없습니다.`;
|
||||
if (from > to) return `${label} 시작은 종료보다 늦을 수 없습니다.`;
|
||||
if ((end - start) / DAY_MS + 1 > WATER_RISK_MAX_SEARCH_DAYS) {
|
||||
return "강우 발생기간은 종료일 기준 최대 1년(366일)까지 조회할 수 있습니다.";
|
||||
return `${label}은 최대 1년(366일)까지 조회할 수 있습니다.`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
/** 종료일 변경으로 기존 시작일이 허용 범위를 벗어난 경우 최근 1년 시작일로 맞춘다. */
|
||||
export const adjustWaterRiskStartDate = (
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
): string => {
|
||||
const min = getWaterRiskMinStartDate(endDate);
|
||||
if (!min) return startDate;
|
||||
return !startDate || startDate < min || startDate > endDate ? min : startDate;
|
||||
};
|
||||
/** 과거이력 강우시각(강우 시작 일시) 기간을 검증한다. 필수 조건이다. */
|
||||
export const validateWaterRiskRainStartedPeriod = (
|
||||
from: string,
|
||||
to: string,
|
||||
currentHourValue: string,
|
||||
) =>
|
||||
validateHourPeriod(from, to, currentHourValue, {
|
||||
label: "강우시각",
|
||||
required: true,
|
||||
});
|
||||
|
||||
/** 과거이력 분석시각 기간을 검증한다. 선택 조건이며 모두 비우면 적용하지 않는다. */
|
||||
export const validateWaterRiskAnalysisPeriod = (
|
||||
from: string,
|
||||
to: string,
|
||||
currentHourValue: string,
|
||||
) =>
|
||||
validateHourPeriod(from, to, currentHourValue, {
|
||||
label: "분석시각",
|
||||
required: false,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type WindowPopupContentProps,
|
||||
} from "@/features/window-popup/model/types";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import {
|
||||
StandardPopupLayout,
|
||||
@@ -21,9 +22,12 @@ import {
|
||||
import type { UiTableSort } from "@common/business_components/ui/table";
|
||||
import { Button } from "@common/components";
|
||||
import {
|
||||
adjustWaterRiskStartDate,
|
||||
formatWaterRiskHourValue,
|
||||
getWaterRiskMinStartDate,
|
||||
validateWaterRiskDateRange,
|
||||
toWaterRiskApiDateTime,
|
||||
toWaterRiskHourValue,
|
||||
validateWaterRiskAnalysisPeriod,
|
||||
validateWaterRiskRainStartedPeriod,
|
||||
} from "./water-risk-date-range";
|
||||
|
||||
import {
|
||||
@@ -34,25 +38,42 @@ import {
|
||||
import {
|
||||
buildWaterRiskStatusLabel,
|
||||
EMPTY_WATER_RISK_SESSION_SNAPSHOT,
|
||||
isWaterRiskDanger,
|
||||
type WaterRiskAreaOption,
|
||||
type WaterRiskResultPage,
|
||||
type WaterRiskResultRow,
|
||||
type WaterRiskSessionSnapshot,
|
||||
type WaterRiskStatus,
|
||||
} from "./water-risk-types";
|
||||
|
||||
import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
|
||||
type WaterRiskResultMode = "current" | "history";
|
||||
|
||||
/**
|
||||
* 과거이력 검색조건이다. 시각 값은 모두 시간 단위 입력값(yyyy-MM-ddTHH:00)이며
|
||||
* 시작·종료 정시를 모두 포함해 조회한다. 현재 탭은 중점관리지역만 이 값을 쓴다.
|
||||
*/
|
||||
type ResultFilters = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
areaId: string;
|
||||
riskStatus: "ALL" | "DANGER" | "NORMAL";
|
||||
riskStatus: "ALL" | WaterRiskStatus;
|
||||
/** 과거이력 분석시각 기간 시작. 비우면 분석시각 조건을 적용하지 않는다. */
|
||||
analysisFrom: string;
|
||||
/** 과거이력 분석시각 기간 종료. 시작과 함께 입력한다. */
|
||||
analysisTo: string;
|
||||
/** 과거이력 강우시각(강우 시작 일시) 기간 시작. 필수이며 기본은 최근 1년 시작일 00시다. */
|
||||
rainStartedFrom: string;
|
||||
/** 과거이력 강우시각(강우 시작 일시) 기간 종료. 필수이며 기본은 현재 정시다. */
|
||||
rainStartedTo: string;
|
||||
};
|
||||
|
||||
/** 과거이력 시각 범위 입력 키. 분석시각·강우시각 입력을 같은 모양으로 그릴 때 쓴다. */
|
||||
type HourRangeKeys =
|
||||
| { fromKey: "analysisFrom"; toKey: "analysisTo" }
|
||||
| { fromKey: "rainStartedFrom"; toKey: "rainStartedTo" };
|
||||
|
||||
const WATER_RISK_PAGE_SIZE = 10;
|
||||
const EMPTY_RESULT_PAGE: WaterRiskResultPage = {
|
||||
items: [],
|
||||
@@ -69,6 +90,8 @@ type WaterRiskTableRow = Record<string, unknown> & {
|
||||
sidoName: string;
|
||||
sggName: string;
|
||||
areaName: string;
|
||||
/** 대표실행 분석기준시각(excn.anls_crtr_dt)을 yyyy.MM.dd HH:mm으로 표시한 값 */
|
||||
analysisBaseAt: string;
|
||||
rainStartedAt: string;
|
||||
currentAccumRainQty: string;
|
||||
forecastRainQty: string;
|
||||
@@ -81,53 +104,56 @@ const WATER_RISK_RESULT_HEADLIST: UiHeadProps[] = [
|
||||
{
|
||||
id: "rowNo",
|
||||
title: "순번",
|
||||
widthPercent: 6,
|
||||
widthPercent: 5,
|
||||
sortable: true,
|
||||
sortKey: "rowNumber",
|
||||
},
|
||||
{
|
||||
id: "sidoName",
|
||||
title: "시도",
|
||||
widthPercent: 8,
|
||||
widthPercent: 7,
|
||||
sortable: true,
|
||||
sortKey: "ctpvName",
|
||||
},
|
||||
{ id: "sggName", title: "시군구", widthPercent: 8, sortable: true },
|
||||
{ id: "areaName", title: "중점관리지역", widthPercent: 14, sortable: true },
|
||||
{ id: "sggName", title: "시군구", widthPercent: 7, sortable: true },
|
||||
{ id: "areaName", title: "중점관리지역", widthPercent: 12, sortable: true },
|
||||
// 대표실행 분석기준시각. 컬럼 id가 백엔드 정렬 키(analysisBaseAt)와 같다.
|
||||
{ id: "analysisBaseAt", title: "분석시각", widthPercent: 11, sortable: true },
|
||||
{
|
||||
id: "rainStartedAt",
|
||||
title: "강우시작시간",
|
||||
widthPercent: 13,
|
||||
title: "강우시작시각",
|
||||
widthPercent: 11,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
id: "currentAccumRainQty",
|
||||
title: "누적강우량(mm)",
|
||||
widthPercent: 11,
|
||||
widthPercent: 9,
|
||||
align: "right",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
id: "forecastRainQty",
|
||||
title: "예측강우량(mm)",
|
||||
widthPercent: 11,
|
||||
widthPercent: 9,
|
||||
align: "right",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
id: "maxFloodingAt",
|
||||
title: "최대 침수 예상시점",
|
||||
widthPercent: 13,
|
||||
widthPercent: 11,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
id: "maxFloodingFlow",
|
||||
title: "최대 침수유량(m³/s)",
|
||||
widthPercent: 10,
|
||||
widthPercent: 9,
|
||||
align: "right",
|
||||
sortable: true,
|
||||
},
|
||||
{ id: "riskStatus", title: "위험 여부", widthPercent: 7 },
|
||||
// "위험(매핑관로없음)" 문구가 들어가도록 기존보다 넓게 둔다.
|
||||
{ id: "riskStatus", title: "위험 여부", widthPercent: 9 },
|
||||
];
|
||||
|
||||
const formatOptionalNumber = (
|
||||
@@ -155,17 +181,20 @@ const formatAnalysisBaseAt = (value?: string | null) => {
|
||||
return `${date.replaceAll("-", ".")} ${time.slice(0, 5)}`.trim();
|
||||
};
|
||||
|
||||
/** 현재 위험 조회와 검색 초기화에 사용하는 최근 1년의 기본 조회조건이다. */
|
||||
/** 최초 진입과 검색 초기화에 사용하는 기본 조회조건이다. 강우시각은 최근 1년이다. */
|
||||
const buildDefaultResultFilters = (): ResultFilters => {
|
||||
const endDate = new Date();
|
||||
const endDateValue = formatLocalDate(endDate);
|
||||
const now = new Date();
|
||||
return {
|
||||
startDate: getWaterRiskMinStartDate(endDateValue)!,
|
||||
endDate: endDateValue,
|
||||
ctpvCd: "ALL",
|
||||
sggCd: "ALL",
|
||||
areaId: "ALL",
|
||||
riskStatus: "ALL",
|
||||
// 분석시각 기간은 선택 조건이므로 기본·초기화 상태는 비워 둔다.
|
||||
analysisFrom: "",
|
||||
analysisTo: "",
|
||||
// 강우시각은 필수 조건이다. 365일 전 00시 ~ 현재 정시(종료일 포함 최대 366일)를 기본으로 한다.
|
||||
rainStartedFrom: `${getWaterRiskMinStartDate(formatLocalDate(now))!}T00:00`,
|
||||
rainStartedTo: formatWaterRiskHourValue(now),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -178,12 +207,16 @@ const postMessageToOpener = (
|
||||
window.opener.postMessage({ type, payload }, window.location.origin);
|
||||
};
|
||||
|
||||
/** 위험 여부에 맞는 기존 팝업 badge 스타일을 반환한다. */
|
||||
const getRiskBadgeClassName = (riskStatus: "DANGER" | "NORMAL") =>
|
||||
riskStatus === "DANGER" ? "sn-popup-badge-danger" : "sn-popup-badge-normal";
|
||||
/** 위험 여부에 맞는 기존 팝업 badge 스타일을 반환한다. 위험(매핑관로없음)도 위험 배지를 쓴다. */
|
||||
const getRiskBadgeClassName = (riskStatus: WaterRiskStatus) =>
|
||||
isWaterRiskDanger(riskStatus) ? "sn-popup-badge-danger" : "sn-popup-badge-normal";
|
||||
|
||||
/** 동일 날짜·지역에 여러 강우이벤트가 있어도 실행별 행 키가 겹치지 않게 식별한다. */
|
||||
/**
|
||||
* 목록 행을 대표 SWMM 실행 ID로 식별한다. 행 하나가 실행 하나이므로 같은 날·지역 행도 겹치지 않는다.
|
||||
* 실행 ID가 없는 응답(구 BE)에서는 기존 날짜·시각·지역 조합으로 대체한다.
|
||||
*/
|
||||
const buildResultRowKey = (row: WaterRiskResultRow) =>
|
||||
row.executionId ||
|
||||
[
|
||||
row.analysisDate ?? "",
|
||||
row.rainStartedAt ?? "",
|
||||
@@ -195,10 +228,10 @@ const buildResultRowKey = (row: WaterRiskResultRow) =>
|
||||
|
||||
/** 실제 통수위험 API를 조회하고 선택 결과를 부모 지도에 전달하는 팝업이다. */
|
||||
export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
// 최초 진입과 초기화 모두 오늘을 종료일로 하는 최근 1년을 사용한다.
|
||||
// 최초 진입과 초기화 모두 현재 정시를 종료로 하는 최근 1년 강우시각을 사용한다.
|
||||
const initialFilters = useMemo(() => buildDefaultResultFilters(), []);
|
||||
// 과거분석은 오늘까지 선택 가능하며, 시작일의 범위는 선택 종료일에 따라 달라진다.
|
||||
const today = formatLocalDate(new Date());
|
||||
// 과거이력 시각 입력은 현재 정시까지 고를 수 있다. 렌더링마다 다시 계산해 정시가 바뀌면 따라간다.
|
||||
const currentHourValue = formatWaterRiskHourValue(new Date());
|
||||
const [resultMode, setResultMode] = useState<WaterRiskResultMode>("current");
|
||||
const [draftResultFilters, setDraftResultFilters] =
|
||||
useState<ResultFilters>(initialFilters);
|
||||
@@ -241,15 +274,38 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
let active = true;
|
||||
setIsLoading(true);
|
||||
setErrorMessage("");
|
||||
// 과거 이력의 고정 기본기간이 현재 진행 중인 강우 조회를 제한하지 않게 분리한다.
|
||||
const dateFilters = resultMode === "current"
|
||||
? buildDefaultResultFilters()
|
||||
: appliedResultFilters;
|
||||
const isHistory = resultMode === "history";
|
||||
// 현재 탭은 진행 중 강우를 오늘까지 최근 1년 강우시작일로 찾고, 과거이력의 적용 기간이 이를 제한하지 않게 분리한다.
|
||||
// 과거이력은 적용 시 검증을 통과한 강우시각 기간을 보내며, 강우시작일도 그 날짜로 맞춘다.
|
||||
const requestToday = formatLocalDate(new Date());
|
||||
const startDate = isHistory
|
||||
? appliedResultFilters.rainStartedFrom.slice(0, 10)
|
||||
: getWaterRiskMinStartDate(requestToday)!;
|
||||
const endDate = isHistory
|
||||
? appliedResultFilters.rainStartedTo.slice(0, 10)
|
||||
: requestToday;
|
||||
// 과거이력의 분석시각 기간은 적용 시 검증을 통과한 값이며, 비어 있으면 조건 없이 조회한다.
|
||||
const analysisBaseFrom = isHistory
|
||||
? toWaterRiskApiDateTime(appliedResultFilters.analysisFrom)
|
||||
: "";
|
||||
const analysisBaseTo = isHistory
|
||||
? toWaterRiskApiDateTime(appliedResultFilters.analysisTo)
|
||||
: "";
|
||||
const rainStartedFrom = isHistory
|
||||
? toWaterRiskApiDateTime(appliedResultFilters.rainStartedFrom)
|
||||
: "";
|
||||
const rainStartedTo = isHistory
|
||||
? toWaterRiskApiDateTime(appliedResultFilters.rainStartedTo)
|
||||
: "";
|
||||
fetchWaterRiskResults({
|
||||
startDate: dateFilters.startDate,
|
||||
endDate: dateFilters.endDate,
|
||||
startDate,
|
||||
endDate,
|
||||
analysisBaseAt:
|
||||
resultMode === "current" ? currentAnalysisBaseAt : undefined,
|
||||
analysisBaseFrom: analysisBaseFrom || undefined,
|
||||
analysisBaseTo: analysisBaseTo || undefined,
|
||||
rainStartedFrom: rainStartedFrom || undefined,
|
||||
rainStartedTo: rainStartedTo || undefined,
|
||||
ctpvCd:
|
||||
resultMode === "current" || appliedResultFilters.ctpvCd === "ALL"
|
||||
? undefined
|
||||
@@ -313,7 +369,12 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
const nextSnapshot = (event.data.payload ??
|
||||
EMPTY_WATER_RISK_SESSION_SNAPSHOT) as WaterRiskSessionSnapshot;
|
||||
if (nextSnapshot.selectedAreaId) {
|
||||
const matchingRow = resultPage.items.find(
|
||||
// 지도에 표시 중인 실행 ID가 있으면 그 행만 선택한다. 같은 지역 첫 행으로 바꾸면 다른 결과가 선택돼 보인다.
|
||||
const matchingRow = nextSnapshot.selectedExecutionId
|
||||
? resultPage.items.find(
|
||||
(row) => row.executionId === nextSnapshot.selectedExecutionId,
|
||||
)
|
||||
: resultPage.items.find(
|
||||
(row) => row.areaId === nextSnapshot.selectedAreaId,
|
||||
);
|
||||
if (matchingRow) setSelectedResultKey(buildResultRowKey(matchingRow));
|
||||
@@ -418,6 +479,7 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
sidoName: row.sidoName || "-",
|
||||
sggName: row.sggName || "-",
|
||||
areaName: row.areaName,
|
||||
analysisBaseAt: formatAnalysisBaseAt(row.analysisBaseAt),
|
||||
rainStartedAt: formatAnalysisBaseAt(row.rainStartedAt),
|
||||
currentAccumRainQty: formatOptionalNumber(row.currentAccumRainQty, 1),
|
||||
forecastRainQty: formatOptionalNumber(row.forecastRainQty, 1),
|
||||
@@ -432,11 +494,25 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
/** 검증된 검색조건을 적용하고 첫 페이지를 새로 조회한다. */
|
||||
const handleApplyResultFilters = () => {
|
||||
if (resultMode === "history") {
|
||||
const dateError = validateWaterRiskDateRange(
|
||||
draftResultFilters.startDate, draftResultFilters.endDate, today,
|
||||
const nowHourValue = formatWaterRiskHourValue(new Date());
|
||||
// 분석시각 기간은 선택 조건이다. 입력한 경우에만 시작·종료 짝, 순서, 기간, 현재 정시 이하를 확인한다.
|
||||
const analysisPeriodError = validateWaterRiskAnalysisPeriod(
|
||||
draftResultFilters.analysisFrom,
|
||||
draftResultFilters.analysisTo,
|
||||
nowHourValue,
|
||||
);
|
||||
if (dateError) {
|
||||
setErrorMessage(dateError);
|
||||
if (analysisPeriodError) {
|
||||
setErrorMessage(analysisPeriodError);
|
||||
return;
|
||||
}
|
||||
// 강우시각 기간은 필수 조건이다. 시작·종료 짝, 순서, 최대 366일, 현재 정시 이하를 확인한다.
|
||||
const rainStartedPeriodError = validateWaterRiskRainStartedPeriod(
|
||||
draftResultFilters.rainStartedFrom,
|
||||
draftResultFilters.rainStartedTo,
|
||||
nowHourValue,
|
||||
);
|
||||
if (rainStartedPeriodError) {
|
||||
setErrorMessage(rainStartedPeriodError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -496,6 +572,44 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 침수예측현황과 같은 형태(라벨 + 시작 `~` 종료 `datetime-local`)로 과거이력 시각 범위를 그린다.
|
||||
* `step=3600`이라 값의 분이 00이면 브라우저가 분 칸을 고정하고, 분이 들어온 경우는 저장 전에 00으로 내린다.
|
||||
* 현재 정시 이후는 고를 수 없게 `max`만 둔다(`min`은 step 기준점을 바꾸므로 두지 않는다).
|
||||
*/
|
||||
const renderHourRange = (label: string, { fromKey, toKey }: HourRangeKeys) => (
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
{label}
|
||||
</div>
|
||||
<input
|
||||
type="datetime-local"
|
||||
step={3600}
|
||||
max={currentHourValue}
|
||||
value={draftResultFilters[fromKey]}
|
||||
onChange={(event) => {
|
||||
const nextValue = toWaterRiskHourValue(event.target.value);
|
||||
setDraftResultFilters((current) => ({ ...current, [fromKey]: nextValue }));
|
||||
}}
|
||||
aria-label={`${label} 시작`}
|
||||
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"
|
||||
/>
|
||||
<span className="text-xl font-medium text-label">~</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
step={3600}
|
||||
max={currentHourValue}
|
||||
value={draftResultFilters[toKey]}
|
||||
onChange={(event) => {
|
||||
const nextValue = toWaterRiskHourValue(event.target.value);
|
||||
setDraftResultFilters((current) => ({ ...current, [toKey]: nextValue }));
|
||||
}}
|
||||
aria-label={`${label} 종료`}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StandardPopupLayout title={windowPopupDefinitions.flowRiskPopup.title}>
|
||||
<section
|
||||
@@ -529,45 +643,9 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
</div>
|
||||
|
||||
<PopupSearchPanel contentClassName="flex flex-wrap items-center gap-x-5 gap-y-3">
|
||||
{/* 과거이력 검색조건 순서: 시도 → 시군구 → 중점관리지역 → 분석시각 → 강우시각 → 위험상태 */}
|
||||
{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}
|
||||
min={getWaterRiskMinStartDate(draftResultFilters.endDate)}
|
||||
max={draftResultFilters.endDate || today}
|
||||
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}
|
||||
max={today}
|
||||
onChange={(event) =>
|
||||
setDraftResultFilters((current) => ({
|
||||
...current,
|
||||
startDate: adjustWaterRiskStartDate(current.startDate, event.target.value),
|
||||
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">
|
||||
시도
|
||||
@@ -652,6 +730,16 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
</label>
|
||||
|
||||
{resultMode === "history" ? (
|
||||
<>
|
||||
{/* 분석시각(선택)·강우시각(필수): 시간 단위로만 고르며 시작·종료 정시를 모두 포함해 조회한다. */}
|
||||
{renderHourRange("분석시각", {
|
||||
fromKey: "analysisFrom",
|
||||
toKey: "analysisTo",
|
||||
})}
|
||||
{renderHourRange("강우시각", {
|
||||
fromKey: "rainStartedFrom",
|
||||
toKey: "rainStartedTo",
|
||||
})}
|
||||
<label className="flex shrink-0 items-center gap-2">
|
||||
<div className="shrink-0 whitespace-nowrap text-xl font-medium text-label">
|
||||
위험상태
|
||||
@@ -670,9 +758,11 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
>
|
||||
<option value="ALL">전체</option>
|
||||
<option value="DANGER">위험</option>
|
||||
<option value="DANGER_UNMAPPED">위험(매핑관로없음)</option>
|
||||
<option value="NORMAL">정상</option>
|
||||
</select>
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="flex min-w-fit flex-1 items-center justify-end gap-2">
|
||||
@@ -722,25 +812,38 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
renderCell={(row, headId) =>
|
||||
headId === "riskStatus" ? (
|
||||
<span
|
||||
className={`sn-popup-badge min-w-[5.6rem] ${getRiskBadgeClassName(row.riskStatus)}`}
|
||||
className={`sn-popup-badge min-w-[5.6rem] whitespace-nowrap ${getRiskBadgeClassName(row.riskStatus)}`}
|
||||
>
|
||||
{buildWaterRiskStatusLabel(row.riskStatus)}
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
scrollMode="horizontal"
|
||||
minTableWidth="124rem"
|
||||
// 분석시각 컬럼 추가로 기존 124rem보다 넓힌다.
|
||||
minTableWidth="136rem"
|
||||
emptyMessage="조회된 결과가 없습니다."
|
||||
sort={resultSort}
|
||||
onSortChange={handleSortChange}
|
||||
actions={
|
||||
<span className="text-xl font-normal text-muted-foreground">
|
||||
SWMM 시뮬레이션 기반 통수위험 분석 결과 (GIS 매핑 관로 중
|
||||
Capacity 0.8 이상이 1개라도 있으면{" "}
|
||||
<span className="font-bold text-[#ff1c15]">위험</span>, 없으면{" "}
|
||||
<span className="font-bold text-[#1677ff]">정상</span>)
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
{/*
|
||||
인수테스트 요구: 판정 기준을 목록 머리글 오른쪽에서 침수예측현황과 같은 하단 위치로 옮기고,
|
||||
분석 원천·실행 주기·침수 항목 설명을 글머리표로 함께 표시한다.
|
||||
*/}
|
||||
<PopupFooterNote
|
||||
items={[
|
||||
<>
|
||||
분석 결과 관로의 Capacity(통수능) 값이 0.8 이상이 1개라도 있으면{" "}
|
||||
<span className="font-bold text-[#ff1c15]">위험</span>, 매핑되지
|
||||
않은 관로만 0.8 이상이면{" "}
|
||||
<span className="font-bold text-[#ff1c15]">위험(매핑관로없음)</span>,
|
||||
없으면 <span className="font-bold text-[#1677ff]">정상</span>
|
||||
</>,
|
||||
"통수능이 위험에 해당하지만 지도화면에 해당 관로를 매핑할 수 없을 때, 빨간색 관로는 보이지 않을 수 있습니다.",
|
||||
"분석 실행 시기 : 통수위험은 관측강우가 0mm이상으로 강우가 발생시 1시간 주기로 분석 자동실행",
|
||||
"분석 방법 : 기상청 향후 6시간 예측강우를 20개소 중점관리지역의 관망모델(SWMM모델)에 연동하여 관로 통수량을 분석하여 통수위험여부를 분석",
|
||||
"최대 침수예상시점과 최대침수유량은 맨홀 월류량이 있을때 그 시점과 값이 나타나는것입니다.",
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
</StandardPopupLayout>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/** 통수위험 분석 화면에서 사용하는 타입·상수와 표시값 변환을 정의한다. */
|
||||
"use client";
|
||||
|
||||
export type WaterRiskStatus = "DANGER" | "NORMAL";
|
||||
/**
|
||||
* 백엔드 위험상태 코드이다.
|
||||
* DANGER: 유효 GIS 매핑 관로 중 Capacity 기준 이상 관로가 있음.
|
||||
* DANGER_UNMAPPED: 매핑 위험 관로는 없고, 유효 GIS 매핑이 없는 관로만 기준 이상임.
|
||||
* NORMAL: 기준 이상 관로가 없음.
|
||||
*/
|
||||
export type WaterRiskStatus = "DANGER" | "DANGER_UNMAPPED" | "NORMAL";
|
||||
export type WaterRiskCalculationMethod = "SWMM";
|
||||
|
||||
/** 대표실행에서 capacity가 가장 큰 관로의 실제 SWMM 결과이다. */
|
||||
@@ -14,6 +20,9 @@ export type WaterRiskPipeSummary = {
|
||||
|
||||
/** 강우이벤트 대표실행 한 건을 팝업 목록에 표시하는 모델이다. */
|
||||
export type WaterRiskResultRow = {
|
||||
/** 대표 SWMM 실행 ID(UUID). 목록 행 식별과 지도 조회(executions/{executionId}/mapView) 키다. */
|
||||
executionId: string;
|
||||
/** 강우시작일(yyyy-MM-dd). 분석시각(analysisBaseAt)과 다른 값이다. */
|
||||
analysisDate?: string;
|
||||
analysisBaseAt?: string | null;
|
||||
rainStartedAt?: string | null;
|
||||
@@ -36,14 +45,25 @@ export type WaterRiskResultRow = {
|
||||
/** 부모 지도와 별도 팝업 사이에서 공유하는 최소 선택 상태이다. */
|
||||
export type WaterRiskSessionSnapshot = {
|
||||
selectedAreaId: string | null;
|
||||
/** 지도에 표시 중인 SWMM 실행 ID. 같은 지역 행이 여러 개여도 팝업이 정확한 행을 다시 선택하게 한다. */
|
||||
selectedExecutionId: string | null;
|
||||
};
|
||||
|
||||
export const EMPTY_WATER_RISK_SESSION_SNAPSHOT: WaterRiskSessionSnapshot = {
|
||||
selectedAreaId: null,
|
||||
selectedExecutionId: null,
|
||||
};
|
||||
|
||||
export const buildWaterRiskStatusLabel = (status: WaterRiskStatus) =>
|
||||
status === "DANGER" ? "위험" : "정상";
|
||||
/** 위험상태 코드를 목록 배지·지도 라벨·정보카드에 쓰는 화면 문구로 바꾼다. */
|
||||
export const buildWaterRiskStatusLabel = (status: WaterRiskStatus) => {
|
||||
if (status === "DANGER") return "위험";
|
||||
if (status === "DANGER_UNMAPPED") return "위험(매핑관로없음)";
|
||||
return "정상";
|
||||
};
|
||||
|
||||
/** 배지·지도 색을 위험 계열(빨강)로 표시할 상태인지 판별한다. 매핑관로없음도 위험 색을 쓴다. */
|
||||
export const isWaterRiskDanger = (status: WaterRiskStatus | null | undefined) =>
|
||||
status === "DANGER" || status === "DANGER_UNMAPPED";
|
||||
|
||||
export type WaterRiskAreaOption = {
|
||||
areaId: string;
|
||||
@@ -70,7 +90,14 @@ export type WaterRiskGeoJsonGeometry = {
|
||||
|
||||
/** 선택한 대표실행의 실제 중점관리지역 및 관로 지도 결과이다. */
|
||||
export type WaterRiskMapView = {
|
||||
/** 지도에 표시한 SWMM 실행 ID. 요청한 목록 행의 executionId와 같다. */
|
||||
executionId?: string;
|
||||
/** 강우시작일(yyyy-MM-dd) */
|
||||
analysisDate: string;
|
||||
/** 분석기준시각 */
|
||||
analysisBaseAt?: string | null;
|
||||
/** 강우시작시각 */
|
||||
rainStartedAt?: string | null;
|
||||
areaId: string;
|
||||
areaName: string;
|
||||
ctpvCd: string;
|
||||
|
||||
@@ -101,7 +101,8 @@ export function useWaterRiskLayer({
|
||||
[selectedWaterRiskApiResult],
|
||||
);
|
||||
|
||||
// 지도카드는 실제 GIS geometry까지 결합된 mapView 위험상태를 목록 상태보다 우선한다.
|
||||
// 지도카드는 mapView 위험상태를 우선하고, mapView 응답 전에는 목록 상태를 쓴다.
|
||||
// 2026-10-20부터 mapView 위험상태는 목록과 같은 대표결과 판정값이므로 두 값이 다르지 않다.
|
||||
const selectedWaterRiskDisplayStatus = useMemo<
|
||||
WaterRiskResultRow["riskStatus"]
|
||||
>(
|
||||
@@ -419,11 +420,20 @@ export function useWaterRiskLayer({
|
||||
| undefined;
|
||||
const mapView = data.payload?.mapView as WaterRiskMapView | undefined;
|
||||
if (!resultRow || !mapView || mapView.areaId !== areaId) return;
|
||||
// 지도 응답이 누른 행과 다른 실행이면 표시하지 않는다(실행 ID 기준 조회의 안전장치).
|
||||
if (
|
||||
mapView.executionId &&
|
||||
resultRow.executionId &&
|
||||
mapView.executionId !== resultRow.executionId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSelectedWaterRiskApiResult(resultRow);
|
||||
setSelectedWaterRiskMapView(mapView);
|
||||
setWaterRiskSession((current) => ({
|
||||
...current,
|
||||
selectedAreaId: areaId,
|
||||
selectedExecutionId: resultRow.executionId || null,
|
||||
}));
|
||||
onSelectResult();
|
||||
return;
|
||||
|
||||
@@ -4,19 +4,21 @@ import CircleStyle from "ol/style/Circle.js";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import Text from "ol/style/Text.js";
|
||||
import { buildWaterRiskStatusLabel } from "@/app/(map-layout)/map/components/water-risk/water-risk-types";
|
||||
import {
|
||||
buildWaterRiskStatusLabel,
|
||||
isWaterRiskDanger,
|
||||
type WaterRiskStatus,
|
||||
} from "@/app/(map-layout)/map/components/water-risk/water-risk-types";
|
||||
|
||||
export const buildWaterRiskStatusFillColor = (
|
||||
riskStatus: "DANGER" | "NORMAL",
|
||||
) =>
|
||||
riskStatus === "DANGER"
|
||||
/** 위험·위험(매핑관로없음)은 빨강, 정상은 파랑 반투명으로 중점관리지역 면을 채운다(화면설계서 21쪽). */
|
||||
export const buildWaterRiskStatusFillColor = (riskStatus: WaterRiskStatus) =>
|
||||
isWaterRiskDanger(riskStatus)
|
||||
? "rgba(198, 40, 40, 0.24)"
|
||||
: "rgba(21, 101, 192, 0.2)";
|
||||
|
||||
export const buildWaterRiskStatusStrokeColor = (
|
||||
riskStatus: "DANGER" | "NORMAL",
|
||||
) =>
|
||||
riskStatus === "DANGER"
|
||||
/** 중점관리지역 경계선·라벨 배경색. 위험 계열 판정은 면 색과 같다. */
|
||||
export const buildWaterRiskStatusStrokeColor = (riskStatus: WaterRiskStatus) =>
|
||||
isWaterRiskDanger(riskStatus)
|
||||
? "rgba(198, 40, 40, 0.92)"
|
||||
: "rgba(21, 101, 192, 0.92)";
|
||||
|
||||
@@ -31,9 +33,9 @@ export const buildWaterRiskCapacityColor = (capacityRatio: number) => {
|
||||
|
||||
export const createWaterRiskAreaStyle = (feature: any) => {
|
||||
const areaName = String(feature.get("areaName") ?? "");
|
||||
const riskStatus = String(feature.get("riskStatus") ?? "NORMAL") as
|
||||
| "DANGER"
|
||||
| "NORMAL";
|
||||
const riskStatus = String(
|
||||
feature.get("riskStatus") ?? "NORMAL",
|
||||
) as WaterRiskStatus;
|
||||
return new Style({
|
||||
fill: new Fill({ color: buildWaterRiskStatusFillColor(riskStatus) }),
|
||||
stroke: new Stroke({
|
||||
|
||||
@@ -182,7 +182,10 @@ import {
|
||||
WATER_DEPTH_LAYER_TYPES,
|
||||
extractWaterDepthLegendItems,
|
||||
} from "../water-depth/water-depth";
|
||||
import { buildWaterRiskStatusLabel } from "../components/water-risk/water-risk-types";
|
||||
import {
|
||||
buildWaterRiskStatusLabel,
|
||||
isWaterRiskDanger,
|
||||
} from "../components/water-risk/water-risk-types";
|
||||
|
||||
import GisToolButtons from "../components/gis-tools/gis-tool-buttons";
|
||||
|
||||
@@ -3152,7 +3155,8 @@ const MapPageClient = () => {
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex min-w-[48px] shrink-0 items-center justify-center rounded-full px-[7px] py-[5px] text-[10px] font-bold ${
|
||||
selectedWaterRiskDisplayStatus === "DANGER"
|
||||
// 위험(매핑관로없음)도 위험과 같은 빨간 배지로 표시하고 문구로 구분한다.
|
||||
isWaterRiskDanger(selectedWaterRiskDisplayStatus)
|
||||
? "bg-[#ff1c15] text-white"
|
||||
: "bg-[#e8f6f4] text-[#0f766e]"
|
||||
}`}
|
||||
|
||||
@@ -41,3 +41,41 @@
|
||||
- 롤백: `waterRiskClient.ts`를 제거하고 popup의 실제 조회/부모 메시지 변경 및 `map/page.tsx` 통수위험 mapView 처리를 되돌린다.
|
||||
- 자동화 도구가 새 창 popup을 부모 탭과 동시에 제어하는 데 제한이 있어, popup의 실제 mapView 요청과 부모 메시지 처리 코드를 각각 확인했다. 최종 사용자 회귀 시 부모 지도에서 popup을 열고 한 행을 선택하는 시나리오를 한 번 더 수행할 수 있다.
|
||||
- 현재누적강우량은 백엔드 원천 미확보로 `-`이며, 예측강우 격자 overlay는 이번 합리식 결과 범위에 포함하지 않는다.
|
||||
|
||||
## 5. 2026-10-20 위험(매핑관로없음)·분석시각 컬럼·분석시각 기간 검색
|
||||
|
||||
- 근거: `COMMON_DOCS_ROOT/작업계획/SFR-011-007-통수위험분석/004-de-...-매핑관로없음위험-분석시각-작업계획.md` 1~8 승인. 원본 화면설계서 19·21쪽(SN_MN010201·010202), 요구사항 SFR-011-007.
|
||||
- `components/water-risk/water-risk-types.ts`: `WaterRiskStatus`에 `DANGER_UNMAPPED` 추가, 라벨 `위험(매핑관로없음)`, 위험 색 판별 `isWaterRiskDanger()`.
|
||||
- `components/water-risk/water-risk-popup-page.tsx`
|
||||
- 목록: `중점관리지역` 다음 `분석시각` 컬럼(정렬 가능, 컬럼 id = BE 정렬 키 `analysisBaseAt`), 컬럼 폭 재배분, 최소 폭 124rem → 136rem, 위험 배지 줄바꿈 방지.
|
||||
- 과거이력 검색패널: `분석시각` 기간(시작 날짜·시 ~ 종료 날짜·시, 선택). 기본·초기화는 빈 값이며 적용 시 `validateWaterRiskAnalysisPeriod`로 검증한다. 위험상태 필터에 `위험(매핑관로없음)` 추가. 하단 판정 안내 문구 갱신.
|
||||
- 현재 탭은 기존처럼 현재 정시 `analysisBaseAt`만 보낸다.
|
||||
- `components/water-risk/water-risk-client.ts`: `analysisBaseFrom`·`analysisBaseTo`를 둘 다 있을 때만 쿼리스트링에 넣는다.
|
||||
- `components/water-risk/water-risk-date-range.ts`: `buildWaterRiskAnalysisDateTime`, `validateWaterRiskAnalysisPeriod`(짝 입력, 시작 ≤ 종료, 종료 ≤ 현재 정시, 366일) 추가.
|
||||
- `lib/water-risk-styles.ts`, `ui/map-page-client.tsx`: 지도 구역 색·정보카드 배지를 `isWaterRiskDanger()`로 판정해 위험(매핑관로없음)도 빨강으로 표시한다.
|
||||
- 검증: `git diff --check` 통과, `tsc --noEmit`(sn 전체) 통과, 변경 6개 파일 ESLint 문제 수가 수정 전(HEAD)과 동일(기존 `map-page-client.tsx` 46 errors 등은 baseline). 개발 서버가 꺼져 있어 화면 확인은 미실행, `next build` 미실행.
|
||||
- 롤백: 위 6개 파일을 되돌린다.
|
||||
- 2026-10-20 추가: BE mapView 위험상태가 목록과 같은 대표결과 판정값으로 바뀌어, `lib/use-water-risk-layer.ts`의 정보카드 상태 우선순위 주석만 갱신했다(동작 변경 없음).
|
||||
|
||||
## 6. 2026-10-21 지도 조회 키를 SWMM 실행 ID로 변경
|
||||
|
||||
- 근거: `COMMON_DOCS_ROOT/작업계획/SFR-011-007-통수위험분석/004-de-...-작업계획.md` 15절 9~13 승인.
|
||||
- `components/water-risk/water-risk-types.ts`: `WaterRiskResultRow.executionId`, `WaterRiskMapView.executionId·analysisBaseAt·rainStartedAt`, `WaterRiskSessionSnapshot.selectedExecutionId` 추가.
|
||||
- `components/water-risk/water-risk-client.ts`: 지도 조회를 `v1/waterRisk/executions/{executionId}/mapView`로 변경(날짜·지역 경로 미사용).
|
||||
- `components/water-risk/water-risk-popup-page.tsx`: 행 식별 키를 `executionId`로 변경(없으면 기존 조합), 부모 동기화 메시지에 `selectedExecutionId`가 있으면 그 행만 선택 표시.
|
||||
- `lib/use-water-risk-layer.ts`: 지도 응답과 행의 실행 ID가 다르면 무시, 세션에 `selectedExecutionId` 저장.
|
||||
- 검증: `tsc --noEmit` 통과, 변경 4개 파일 ESLint 문제 수 수정 전과 동일, `git diff --check` 통과. 화면 확인은 BE 재기동 후 필요(현재 BE는 이전 코드라 새 지도 API 없음).
|
||||
- 롤백: 위 4개 파일 복원. BE의 구 지도 API가 유지되는 동안은 FE만 되돌려도 기존 동작으로 복귀한다.
|
||||
|
||||
## 7. 2026-10-21 과거이력 검색조건 순서·명칭·시간 단위 입력
|
||||
|
||||
- 근거: `COMMON_DOCS_ROOT/작업계획/SFR-011-007-통수위험분석/004-de-...-작업계획.md` 16절 결정 14~17 승인. 원본 화면설계서 19쪽 SN_MN010201, 요구사항 SFR-011-007.
|
||||
- `components/water-risk/water-risk-popup-page.tsx`
|
||||
- 과거이력 검색조건 순서를 시도 → 시군구 → 중점관리지역 → 분석시각 → 강우시각 → 위험상태로 바꿨다(현재 탭은 중점관리지역만).
|
||||
- 명칭: 검색란 `강우 발생기간` → `강우시각`, 목록 컬럼 `강우시작시간` → `강우시작시각`(컬럼 id·정렬 키 `rainStartedAt` 유지).
|
||||
- 분석시각·강우시각 입력을 침수예측현황과 같은 `datetime-local` 시작 `~` 종료(`w-[17rem]`)로 바꾸고 `step={3600}`을 줬다. 분은 00으로 고정되고, 분이 들어오면 저장 전에 00으로 내린다. 현재 정시 이후는 `max`로 막는다.
|
||||
- 검색조건 상태를 `rainStartedFrom`·`rainStartedTo`(필수, 기본 365일 전 00시 ~ 현재 정시), `analysisFrom`·`analysisTo`(선택)로 정리했다. 조회 시 `yyyy-MM-ddTHH:00:00`으로 보내고, `startDate`·`endDate`는 강우시각 날짜로 맞춘다. 현재 탭은 기존처럼 최근 1년 날짜와 현재 정시 `analysisBaseAt`만 보낸다.
|
||||
- `components/water-risk/water-risk-date-range.ts`: 시간 단위 값 변환(`toWaterRiskHourValue`·`formatWaterRiskHourValue`·`toWaterRiskApiDateTime`)과 기간 검증(`validateWaterRiskRainStartedPeriod` 필수, `validateWaterRiskAnalysisPeriod` 선택: 짝 입력·정시·시작 ≤ 종료·종료 ≤ 현재 정시·366일)으로 정리. 오류 문구는 `강우시각…`/`분석시각…`.
|
||||
- `components/water-risk/water-risk-client.ts`: `rainStartedFrom`·`rainStartedTo`를 둘 다 있을 때만 전달.
|
||||
- 검증: `tsc --noEmit`(sn 전체) 통과, 변경 3개 파일 ESLint 0건(수정 전과 같음), `git diff --check` 통과. `step=3600` 입력의 분 고정은 앱 내장 브라우저(Chromium 152)에서 따로 확인했다. 로그인이 필요해 실제 팝업 화면은 미확인, `next build` 미실행.
|
||||
- 롤백: 위 3개 파일을 되돌린다. BE의 새 파라미터는 선택값이라 FE만 되돌려도 동작한다.
|
||||
|
||||
@@ -77,3 +77,53 @@
|
||||
- `fldng-predc-prst-excel.ts`가 화면 열 순서의 단일 시트 `침수예측현황.xlsx`를 브라우저에서 생성한다.
|
||||
- 서버 `/prst/excel` Blob 호출과 `getBlobData`, `fileDownload` 사용을 제거했다.
|
||||
- 독립 TypeScript `--noEmit` 검증을 통과했다.
|
||||
|
||||
## 2026-10-21 강우 명칭·시간 단위 입력 통일 계획 (2026-10-21 승인·구현)
|
||||
|
||||
### 배경
|
||||
|
||||
- 사용자 지시(2026-10-21)
|
||||
- 강우 관련 명칭: 검색란 `강우시각`, 목록 `강우시작시각`. 침수예측현황도 바꿔야 하면 이 문서에 추가한 뒤 승인 후 수정한다.
|
||||
- 검색란의 분석시각·강우시각은 `datetime-local`(`2026-10-21 오후 03:00` 형태)로 두되 **시간까지만 선택**한다. 분은 `00`으로 고정하고 수정할 수 없다. 침수예측현황도 같이 수정한다.
|
||||
- 통수위험 쪽 계획: `COMMON_DOCS_ROOT/작업계획/SFR-011-007-통수위험분석/004-de-SFR-011-007-통수위험분석-매핑관로없음위험-분석시각-작업계획.md` 16절.
|
||||
|
||||
### 현재 명칭
|
||||
|
||||
| 위치 | 현재 | 기준과 일치 |
|
||||
| --- | --- | --- |
|
||||
| 검색란 라벨 (`ui/fldng-predc-prst-popup-content.tsx`) | `강우시각` | 예 |
|
||||
| 목록 컬럼 (`ui/fldng-predc-prst-popup-content.tsx`) | `강우시작시각` | 예 |
|
||||
| 지도 정보 (`lib/use-flood-prediction-map.ts`) | `강우시작시각` | 예 |
|
||||
| 검색 오류 문구 (`ui/fldng-predc-prst-popup-content.tsx` `validateFilters`) | `강우 시작시각의 시작은 종료보다 늦을 수 없습니다.` | 아니오(검색란 이름은 `강우시각`) |
|
||||
| Excel 머리글 (`lib/fldng-predc-prst-excel.ts` `HEADERS.rainBeginningDateTime`) | `강우 시작시각` | 아니오(목록은 붙여 쓴 `강우시작시각`) |
|
||||
|
||||
- 화면에 보이는 라벨과 목록 컬럼 이름은 이미 기준과 같다. 이름은 문구 2곳만 맞춘다.
|
||||
|
||||
### 시간 단위 입력 결론
|
||||
|
||||
- 현재 분석시각·강우시각 입력(`renderDateTimeRange`)은 `datetime-local`이라 분까지 고를 수 있다.
|
||||
- `step={3600}`(1시간)을 주면 값의 분이 `00`일 때 브라우저가 분 칸을 고정해 바꿀 수 없다. 빈 값에서 처음 입력할 때처럼 분이 들어오는 경우는 `onChange`에서 분을 `00`으로 맞춘다. 입력 폭(`w-[17rem]`)과 표시 형태는 그대로다.
|
||||
- API는 지금처럼 `yyyyMMddHHmm`을 보내며 분만 항상 `00`이 된다. BE는 문자열 시작 이상·종료 이하로 비교하므로(`FldngPredcPrstRepository.addRange`) **BE 변경은 없다.**
|
||||
- 개발 DB `tb_sn_fldng_predc_anls` 208건의 분석시각(`obsrvn_tm`)·강우시작시각(`rain_bgng_tm`)이 모두 정시(분 `00`)라서 시간 단위 조건으로 빠지는 자료는 없다.
|
||||
|
||||
### 수정 계획
|
||||
|
||||
| 번호 | 파일 | 변경 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx` | 오류 문구 `강우 시작시각의 시작은 종료보다 늦을 수 없습니다.` → `강우시각의 시작은 종료보다 늦을 수 없습니다.` |
|
||||
| 2 | `src/features/fldng-predc-prst/lib/fldng-predc-prst-excel.ts` | Excel 머리글 `강우 시작시각` → `강우시작시각` |
|
||||
| 3 | `src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx` | `renderDateTimeRange`의 시작·종료 입력 4개(분석시각·강우시각)에 `step={3600}` 추가, `onChange`에서 분을 `00`으로 맞춰 저장 |
|
||||
|
||||
- 바꾸지 않는 것: API 파라미터·정렬 키, 기본값(빈 값), 분석도구 메뉴의 침수예측 건수 조회 범위(`model/analysis-time-range.ts`, 입력란과 무관).
|
||||
|
||||
### 검증·영향·롤백
|
||||
|
||||
- 검증: 분석시각·강우시각 입력에서 분 칸이 `00`으로 고정되는지, 빈 값에서 새로 입력해도 분이 `00`이 되는지 확인. 시작 > 종료로 강우시각을 넣어 바뀐 오류 문구 확인, 조회 요청의 시각 파라미터 끝 두 자리가 `00`인지 확인, Excel 다운로드 머리글 확인. `tsc --noEmit`·`git diff --check`.
|
||||
- 영향: 침수예측현황 팝업의 검색 입력 4개, 오류 문구, Excel 머리글 1칸. API·DB·지도 영향 없음.
|
||||
- 롤백: 위 2개 파일을 되돌린다.
|
||||
|
||||
### 구현 결과 (2026-10-21)
|
||||
|
||||
- 계획 1~3을 그대로 반영했다. `ui/fldng-predc-prst-popup-content.tsx`: 오류 문구 `강우시각의 시작은 종료보다 늦을 수 없습니다.`, 분석시각·강우시각 입력 4개에 `step={3600}`과 분 00 고정(`toHourInputValue`). `lib/fldng-predc-prst-excel.ts`: Excel 머리글 `강우시작시각`.
|
||||
- 검증: `tsc --noEmit`(sn 전체) 통과, `git diff --check` 통과. ESLint는 수정 전과 같은 1건(기존 미사용 import `createPredictionAnalysisTimeRange`, 이번 범위 밖). 로그인이 필요해 실제 팝업 화면과 Excel 다운로드는 미확인.
|
||||
- BE·API·DB 변경 없음.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# SN 인수테스트 팝업 자료 출처 표시 FE 작업 결과
|
||||
|
||||
## 1. 작업 목적
|
||||
|
||||
- 인수테스트(2026-10-16) 고객 요구: "팝업화면 자료의 원천 표시 및 자료 업데이트 주기 표시".
|
||||
- 대상 팝업 7개(침수예측, 침수흔적도, 지반침하정보, 통수위험, 침수위험지도, 도시침수지도, 관로분석)의 목록 아래에 자료 출처·분석 주기 문구를 넣는다.
|
||||
- 기존 하단 설명이 있는 팝업은 그 설명을 유지하고 글머리표로 문구를 더한다(사용자 지시 2026-10-21).
|
||||
|
||||
## 2. 근거
|
||||
|
||||
- `인수테스트_요구사항_1016.pptx` 1쪽(공통 요청), 2~8쪽(화면별 문구). 원본 파일은 사용자 공유 드라이브에 있으며 저장소에는 넣지 않았다.
|
||||
- 문구는 슬라이드 내용을 그대로 옮겼다. 줄 끝 쉼표 제거, `출처 :` 띄어쓰기 통일, 문장 끝 마침표 정도만 맞췄다.
|
||||
|
||||
## 3. 화면별 반영 내용
|
||||
|
||||
| 슬라이드 | 팝업 | 수정 파일 | 하단 글머리표 |
|
||||
| --- | --- | --- | --- |
|
||||
| 2 | 침수예측현황 | `src/features/fldng-predc-prst/ui/fldng-predc-prst-popup-content.tsx` | (기존) 빈도별 위험·경계·주의 기준, (기존) 행 선택 안내, 분석 실행 시기, 분석 방법(확률강우량 출처 map.wamis.go.kr, 도시침수지도 출처 한강홍수통제소) |
|
||||
| 3 | 통수위험 | `app/(map-layout)/map/components/water-risk/water-risk-popup-page.tsx` | 판정 기준(위험·위험(매핑관로없음)·정상), 지도에 빨간 관로가 안 보일 수 있는 경우, 분석 실행 시기, 분석 방법(SWMM), 최대 침수예상시점·최대침수유량 설명 |
|
||||
| 4 | 침수발생이력(침수흔적도) | `app/(map-layout)/map/components/fldng-hstry/fldng-hstry-popup-page.tsx` | (기존) 행 클릭 안내, 출처 : 행정안전부 재난보험과(2002년~2025년) |
|
||||
| 5 | 도시침수지도 목록 | `app/(map-layout)/map/components/flood-dngr-map/flood-dngr-map-popup-page.tsx` | 출처 : 한강홍수통제소, 행정구역별 도시침수지도(2025.09) |
|
||||
| 6 | 침수위험지도 목록 | `app/(map-layout)/map/components/sbmrg-dngr-map/sbmrg-dngr-map-popup-page.tsx` | 출처 : 하수도정비 중점관리지역 20개소 과업 산출물 |
|
||||
| 7 | 지반침하 목록 | `app/(map-layout)/map/components/sinkhole/sinkhole-popup-page.tsx` | 출처 : 지하안전정보시스템 지반침하사고 정보 연계 표출(연계 주기 : 매 시간 연계) |
|
||||
| 8 | 관로분석 | `app/(map-layout)/map/components/pipe-analysis/pipe-analysis-popup-page.tsx` | 국토교통부로부터 제공받은 GIS 관로정보(2025년)를 기반으로 분석하고 있음 |
|
||||
|
||||
- 공통 표시 컴포넌트 `src/features/window-popup/ui/popup-footer-note.tsx`(신규): 침수예측현황 하단 안내와 같은 글꼴(`text-xl text-muted-foreground`)의 글머리표 목록. 7개 팝업이 같은 모양을 쓴다.
|
||||
- 통수위험: 판정 기준 문구를 목록 머리글 오른쪽에서 하단으로 옮겼다(슬라이드 3 "침수예측과 동일한 위치에 표시할것").
|
||||
- 침수발생이력: 기존 회색 상자 안내를 공통 글머리표 모양으로 바꿨다(문구 유지).
|
||||
|
||||
## 4. 검증
|
||||
|
||||
- `tsc --noEmit`(sn 전체) 통과.
|
||||
- ESLint: 변경 8개 파일 모두 수정 전과 같은 결과(침수예측현황의 기존 미사용 import `createPredictionAnalysisTimeRange` 1건 외 0건).
|
||||
- `git diff --check` 통과.
|
||||
- 미실행: 로그인이 필요해 실제 팝업 화면 확인 못 함. `next build` 미실행.
|
||||
|
||||
## 5. 영향·롤백·남은 위험
|
||||
|
||||
- 영향: 7개 팝업의 목록 아래 문구 표시만 바뀐다. API·상태·데이터 변경 없음.
|
||||
- 롤백: 위 7개 파일을 되돌리고 `popup-footer-note.tsx`를 삭제한다.
|
||||
- 남은 위험
|
||||
- 팝업 높이가 낮으면 하단 문구가 길어(통수위험 5줄) 목록 영역이 그만큼 줄어든다. 화면 확인 후 필요하면 글꼴·간격을 조정한다.
|
||||
- 슬라이드 문구 중 "관측강우가 0mm이상으로 강우가 발생시"는 고객 문구 그대로 두었다. 의미상 "0mm 초과"인지 고객 확인이 필요할 수 있다.
|
||||
- 슬라이드 4의 "25년까지 데이터가 있는데 자동으로 26년으로 설정되는 문제"(지도 레이어 침수흔적도 연도 기본값)는 이번 범위(하단 문구)가 아니라 수정하지 않았다.
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FLDNG_HSTRY_LAYER_NAME,
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
FLDNG_HSTRY_MIN_YEAR,
|
||||
FLDNG_HSTRY_MAX_YEAR,
|
||||
FLDNG_HSTRY_PREVIEW_LAYER_TYPE,
|
||||
FLDNG_HSTRY_ROOT_NAME,
|
||||
type FldngHstryApiItem,
|
||||
@@ -50,11 +51,10 @@ export const formatFldngHstryDateTime = (
|
||||
return [normalizedDate, normalizedTime].filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
export const buildFldngHstryDefaultYearRange = (
|
||||
currentYear = new Date().getFullYear(),
|
||||
): FldngHstryYearRange => ({
|
||||
/** 침수흔적도 초기 조회 범위를 제공 자료의 연도 범위로 설정한다. */
|
||||
export const buildFldngHstryDefaultYearRange = (): FldngHstryYearRange => ({
|
||||
startYear: String(FLDNG_HSTRY_MIN_YEAR),
|
||||
endYear: String(currentYear),
|
||||
endYear: String(FLDNG_HSTRY_MAX_YEAR),
|
||||
});
|
||||
|
||||
export const normalizeFldngHstryYearRange = (
|
||||
|
||||
@@ -14,6 +14,8 @@ export const FLDNG_HSTRY_DEPTH_LAYER_ID = "SN_FLOOD_HISTORY_DEPTH";
|
||||
export const FLDNG_HSTRY_PAGE_SIZE = 10;
|
||||
export const FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE = 2000;
|
||||
export const FLDNG_HSTRY_MIN_YEAR = 2002;
|
||||
/** 침수흔적도 연도 선택 목록과 초기 조회 범위의 최대 연도다. */
|
||||
export const FLDNG_HSTRY_MAX_YEAR = 2025;
|
||||
|
||||
export type FldngHstryDetailCacheValue = {
|
||||
rainAnalysis: Record<string, unknown>;
|
||||
|
||||
@@ -17,7 +17,7 @@ const HEADERS: Record<string, string> = {
|
||||
ctpvNm: "시도",
|
||||
sggNm: "시군구",
|
||||
analysisDateTime: "분석시각",
|
||||
rainBeginningDateTime: "강우 시작시각",
|
||||
rainBeginningDateTime: "강우시작시각",
|
||||
observedAccumulatedRainfall: "관측 누적강수량(mm)",
|
||||
predictionHour: "예측시간",
|
||||
predictedAccumulatedRainfall: "예측 누적강수량(mm)",
|
||||
|
||||
@@ -14,6 +14,7 @@ import LoadingOverlay from "@/components/ui/loading-bar";
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/window-popup/model/types";
|
||||
import MapPopupDataTable from "@/features/window-popup/ui/map-popup-data-table";
|
||||
import PopupFooterNote from "@/features/window-popup/ui/popup-footer-note";
|
||||
import PopupSearchPanel from "@/features/window-popup/ui/popup-search-panel";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import useStore from "@/entities/reference-data/model/reference-data-store";
|
||||
@@ -135,6 +136,16 @@ const buildInitialFilters = (): Filters => ({
|
||||
|
||||
const apiDate = (value: string) => value.replace(/[^0-9]/g, "").slice(0, 12);
|
||||
|
||||
/**
|
||||
* 검색 시각 입력(`datetime-local`, `step=3600`)의 분을 00으로 맞춘다.
|
||||
* 브라우저는 값의 분이 00이면 분 칸을 고정하지만, 빈 값에서 처음 입력하거나 직접 입력해 분이 들어온 경우는
|
||||
* 스스로 고치지 않으므로 저장 전에 시간 단위로 내린다. 형식이 아니면(빈 값 포함) 그대로 둔다.
|
||||
*/
|
||||
const toHourInputValue = (value: string) =>
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)
|
||||
? `${value.slice(0, 13)}:00`
|
||||
: value;
|
||||
|
||||
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)}`
|
||||
@@ -184,7 +195,7 @@ const validateFilters = (filters: Filters) => {
|
||||
filters.rainBgngTo &&
|
||||
filters.rainBgngFrom > filters.rainBgngTo
|
||||
) {
|
||||
return "강우 시작시각의 시작은 종료보다 늦을 수 없습니다.";
|
||||
return "강우시각의 시작은 종료보다 늦을 수 없습니다.";
|
||||
}
|
||||
if (
|
||||
filters.analysisFrom &&
|
||||
@@ -477,6 +488,11 @@ export default function FldngPredcPrstPopupContent() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 분석시각·강우시각 범위를 시간 단위로만 고르게 그린다.
|
||||
* `step=3600`이라 값의 분이 00이면 브라우저가 분 칸을 고정해 바꿀 수 없고, 분이 들어온 경우는 저장 전에 00으로 내린다.
|
||||
* 침수예측 자료의 분석·강우시작 시각은 정시 단위이므로 시간 단위 조건으로 빠지는 자료는 없다.
|
||||
*/
|
||||
const renderDateTimeRange = (
|
||||
label: string,
|
||||
fromKey: "rainBgngFrom" | "analysisFrom",
|
||||
@@ -489,27 +505,31 @@ export default function FldngPredcPrstPopupContent() {
|
||||
<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"
|
||||
step={3600}
|
||||
value={filters[fromKey]}
|
||||
aria-label={`${label} 시작`}
|
||||
onChange={(event) =>
|
||||
onChange={(event) => {
|
||||
const nextValue = toHourInputValue(event.target.value);
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
[fromKey]: event.target.value,
|
||||
}))
|
||||
}
|
||||
[fromKey]: nextValue,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<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"
|
||||
step={3600}
|
||||
value={filters[toKey]}
|
||||
aria-label={`${label} 종료`}
|
||||
onChange={(event) =>
|
||||
onChange={(event) => {
|
||||
const nextValue = toHourInputValue(event.target.value);
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
[toKey]: event.target.value,
|
||||
}))
|
||||
}
|
||||
[toKey]: nextValue,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
@@ -667,17 +687,22 @@ export default function FldngPredcPrstPopupContent() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-1 px-1 text-xl text-muted-foreground">
|
||||
<p>
|
||||
{/* 판정 기준·행 선택 안내에 인수테스트 요구(자료 원천·분석 주기 표시) 문구를 글머리표로 더한다. */}
|
||||
<PopupFooterNote
|
||||
items={[
|
||||
<>
|
||||
30년빈도 이상인 경우{" "}
|
||||
<span className="font-semibold text-[#dc2626]">위험</span>, 10년빈도
|
||||
이상인 경우{" "}
|
||||
<span className="font-semibold text-[#f97316]">경계</span>, 2년빈도
|
||||
이상인 경우{" "}
|
||||
<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>
|
||||
</>,
|
||||
"행을 선택하면 해당 시군구·빈도의 도시침수지도로 이동합니다.",
|
||||
"분석 실행 시기 : 침수예측은 관측강우가 10mm이상으로 강우가 발생시 1시간 주기로 분석 자동실행",
|
||||
"분석 방법 : 기상청 관측강우와 향후 6시간 예측강우를 누적하여 지속시간별 빈도별 확률강우량(출처 : map.wamis.go.kr)과 근접한 강우량을 찾아 해당되는 빈도의 도시침수지도(출처 : 한강홍수통제소)를 표출",
|
||||
]}
|
||||
/>
|
||||
<PrbbtRnflDialog
|
||||
open={probabilityRow !== null}
|
||||
onOpenChange={handleProbabilityOpenChange}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { cn } from "@common/components";
|
||||
|
||||
type PopupFooterNoteProps = {
|
||||
/** 글머리표 한 줄씩 표시할 안내·자료 출처 문구. 강조가 필요한 줄은 JSX로 넘긴다. */
|
||||
items: ReactNode[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* WindowPopup 목록 아래에 판정 기준·자료 출처·분석 주기 안내를 글머리표 목록으로 표시한다.
|
||||
* 인수테스트 요구(2026-10-16 "팝업화면 자료의 원천 표시 및 자료 업데이트 주기 표시")에 따라
|
||||
* 침수예측현황 하단 안내와 같은 위치·글꼴로 모든 대상 팝업이 이 컴포넌트를 쓴다.
|
||||
*/
|
||||
export default function PopupFooterNote({ items, className }: PopupFooterNoteProps) {
|
||||
return (
|
||||
<ul
|
||||
data-popup-footer-note
|
||||
className={cn(
|
||||
"shrink-0 list-disc space-y-1 pl-[2.4rem] pr-1 text-xl text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
// 문구 목록은 화면마다 고정 순서라 순번을 키로 쓴다.
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export type { PopupFooterNoteProps };
|
||||
Reference in New Issue
Block a user