Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev
This commit is contained in:
@@ -174,9 +174,16 @@ const postMessageToOpener = (
|
||||
const getRiskBadgeClassName = (riskStatus: "DANGER" | "NORMAL") =>
|
||||
riskStatus === "DANGER" ? "sn-popup-badge-danger" : "sn-popup-badge-normal";
|
||||
|
||||
/** 날짜까지 포함해 과거 이력에서도 결과 행 선택을 유일하게 식별한다. */
|
||||
/** 동일 날짜·지역에 여러 강우이벤트가 있어도 실행별 행 키가 겹치지 않게 식별한다. */
|
||||
const buildResultRowKey = (row: WaterRiskResultRow) =>
|
||||
`${row.analysisDate ?? ""}:${row.ctpvCd ?? ""}:${row.sggCd ?? ""}:${row.areaId}`;
|
||||
[
|
||||
row.analysisDate ?? "",
|
||||
row.rainStartedAt ?? "",
|
||||
row.analysisBaseAt ?? "",
|
||||
row.ctpvCd ?? "",
|
||||
row.sggCd ?? "",
|
||||
row.areaId,
|
||||
].join(":");
|
||||
|
||||
/** 실제 통수위험 API를 조회하고 선택 결과를 부모 지도에 전달하는 팝업이다. */
|
||||
export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
@@ -433,6 +440,7 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
const handleChangeResultMode = (mode: WaterRiskResultMode) => {
|
||||
setResultMode(mode);
|
||||
setResultCurrentPage(1);
|
||||
setResultSort(undefined);
|
||||
setResultPage(EMPTY_RESULT_PAGE);
|
||||
setSelectedResultKey("");
|
||||
};
|
||||
|
||||
@@ -434,6 +434,7 @@ const DEFAULT_FEATURE_FIT_PADDING: [number, number, number, number] = [
|
||||
50, 50, 50, 50,
|
||||
];
|
||||
const SWAGE_PRCS_FCLT_LAYER_TYPE = "tb_il_swage_prcs_fclt";
|
||||
const RAIN_OBSERVATION_LAYER_TYPE = "tb_ms_rain_obsvtr_pstn_info";
|
||||
|
||||
type SwagePrcsFcltLayerMode = "all" | "gte500" | "lt500" | "none";
|
||||
|
||||
@@ -546,6 +547,21 @@ const normalizeFlowDirectionLayerInfos = (data: any[]): any[] => {
|
||||
});
|
||||
};
|
||||
|
||||
// 강우관측소는 위치와 라벨만 제공하며, 과거 DB 설정값이 Y여도 일반 속성 패널 선택 대상에서는 제외한다.
|
||||
const normalizeRainObservationLayerInteraction = (data: any[]): any[] => {
|
||||
if (!Array.isArray(data)) return data;
|
||||
|
||||
return data.map((item) => {
|
||||
const children = Array.isArray(item.children)
|
||||
? normalizeRainObservationLayerInteraction(item.children)
|
||||
: item.children;
|
||||
|
||||
return getLayerTypeName(item) === RAIN_OBSERVATION_LAYER_TYPE
|
||||
? { ...item, children, clickPsblYn: "N" }
|
||||
: { ...item, children };
|
||||
});
|
||||
};
|
||||
|
||||
// 레이어 설정의 최소 표출 줌을 읽되, 값이 없으면 유하방향 기본 줌 기준을 적용한다.
|
||||
const getFiniteNumber = (value: unknown) => {
|
||||
if (value === null || value === undefined || String(value).trim() === "")
|
||||
@@ -3416,6 +3432,17 @@ const Map = () => {
|
||||
[selectedWaterRiskApiResult],
|
||||
);
|
||||
|
||||
// 지도카드는 실제 GIS geometry까지 결합된 mapView 위험상태를 목록 상태보다 우선한다.
|
||||
const selectedWaterRiskDisplayStatus = useMemo<
|
||||
WaterRiskResultRow["riskStatus"]
|
||||
>(
|
||||
() =>
|
||||
selectedWaterRiskMapView?.riskStatus ??
|
||||
selectedWaterRiskResultRow?.riskStatus ??
|
||||
"NORMAL",
|
||||
[selectedWaterRiskMapView?.riskStatus, selectedWaterRiskResultRow?.riskStatus],
|
||||
);
|
||||
|
||||
/** 선택 영역 오른쪽에 중점관리지역 강우·침수 요약카드를 배치한다. */
|
||||
const syncWaterRiskInfoCardPosition = useCallback(() => {
|
||||
const map = gisMaps.current;
|
||||
@@ -3590,6 +3617,7 @@ const Map = () => {
|
||||
|
||||
await select.resetSelectedFeature?.();
|
||||
const selectedFeatureIds = new Set<string>();
|
||||
let floodLayerVisible = false;
|
||||
const selectLoadedGroupFeatures = (source: any) => {
|
||||
const features = source?.getFeatures?.() ?? [];
|
||||
features.forEach((feature: any) => {
|
||||
@@ -3617,6 +3645,7 @@ const Map = () => {
|
||||
FLOOD_DNGR_MAP_LAYER_TYPE
|
||||
)
|
||||
return;
|
||||
if (layer.getVisible?.() !== false) floodLayerVisible = true;
|
||||
const source = layer.getSource?.();
|
||||
if (!source?.getFeatures) return;
|
||||
selectLoadedGroupFeatures(source);
|
||||
@@ -3624,6 +3653,13 @@ const Map = () => {
|
||||
selectLoadedGroupFeatures(source),
|
||||
);
|
||||
});
|
||||
if (!floodLayerVisible) {
|
||||
event.source?.postMessage?.(
|
||||
{ type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_FLOOD_LAYER_REQUIRED },
|
||||
event.origin,
|
||||
);
|
||||
return;
|
||||
}
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
@@ -3793,7 +3829,11 @@ const Map = () => {
|
||||
appendExmnResultControlNodes(
|
||||
appendAgedPipeControlNode(
|
||||
appendSinkholeControlNode(
|
||||
normalizeTreeRowSpacing(normalizeFlowDirectionLayerInfos(data)),
|
||||
normalizeTreeRowSpacing(
|
||||
normalizeRainObservationLayerInteraction(
|
||||
normalizeFlowDirectionLayerInfos(data),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
exmnMapEntryContext.jobType,
|
||||
@@ -8765,20 +8805,24 @@ const Map = () => {
|
||||
</div>
|
||||
<span
|
||||
className={`inline-flex min-w-[48px] items-center justify-center rounded-full px-[7px] py-[5px] text-[10px] font-bold ${
|
||||
selectedWaterRiskResultRow.riskStatus === "DANGER"
|
||||
selectedWaterRiskDisplayStatus === "DANGER"
|
||||
? "bg-[#ff1c15] text-white"
|
||||
: "bg-[#e8f6f4] text-[#0f766e]"
|
||||
}`}
|
||||
>
|
||||
{buildWaterRiskStatusLabel(
|
||||
selectedWaterRiskResultRow.riskStatus,
|
||||
selectedWaterRiskDisplayStatus,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-background px-[9px] py-[6px] text-[9px] text-muted-foreground">
|
||||
capacity 0.8 초과 관로 존재 시{" "}
|
||||
<span className="font-bold text-[#ff1c15]">위험</span>, 없으면{" "}
|
||||
<span className="font-bold text-[#1677ff]">정상</span>
|
||||
<div>
|
||||
GIS 매핑 관로 중 Capacity 0.8 이상이 1개라도 있으면
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-[#ff1c15]">위험</span>, 없으면{" "}
|
||||
<span className="font-bold text-[#1677ff]">정상</span>
|
||||
</div>
|
||||
</div>
|
||||
<table className="w-full table-fixed border-collapse border-t border-border">
|
||||
<colgroup>
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
- 분석기준시간 라벨·값이 현재 탭 행 오른쪽 끝에 표시되고 검색패널에서 제거된 것을 정적 확인했다.
|
||||
- 행 클릭 성공 안내문 제거와 오류 메시지 조건부 렌더링을 확인했다.
|
||||
- 보령 동대궁촌의 목록 최대 침수유량과 지도카드 값이 모두 `121.48m³/s`로 일치함을 실 API로 확인했다.
|
||||
- 지도카드 위험/정상 판정 설명과 `capacity > 0.8` 서비스 기준이 일치한다.
|
||||
- 지도카드 안내는 GIS 매핑 관로의 `Capacity >= 0.8` 지역 위험 기준만 표시한다.
|
||||
- 페이지별 데이터 길이·스크롤바에 따른 전체표 확대축소를 제거하고 `1320px` 고정폭·10개 열 비율·긴 행정명 말줄임 처리를 확인했다.
|
||||
- 팝업 컴포넌트에 `window.resizeTo` 호출이 없고 창 크기는 `popupDefinitions`의 `1380×900`으로만 결정되는 것을 정적 확인했다. 검색·결과영역의 overflow 숨김과 결과영역 좌우 여백 보정은 유지한다.
|
||||
|
||||
@@ -192,3 +192,8 @@
|
||||
- 과거 종료결과에서 실제 `2026-07-01 01:00`, `03:00`, `05:00` 등의 분석기준시간이 반환됐다.
|
||||
- 과거 종료결과 전체의 상단 최신 분석기준시간은 `2026-07-06 08:00`으로 반환됐다.
|
||||
- Java Service 테스트와 FE 대상 ESLint가 통과했다.
|
||||
|
||||
## 14. 2026-08-26 과거분석이력 최신순·중복 키 검증
|
||||
|
||||
- 정렬 파라미터가 없는 기본 요청도 백엔드 `RAIN_BGNG_DT DESC`에 따라 2026-08-26 07:00, 2026-08-25 13:00, 10:00, 06:00 순으로 반환된다.
|
||||
- 2026-08-25 경기 가평 지역 2건은 기존 키가 1개로 충돌했지만 강우시작시간·분석기준시간을 포함한 신규 키는 2개 모두 고유하다.
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
- 행 선택 시 기존 도시침수지도 group extent API를 호출한다.
|
||||
- 부모 창에 기존 `MAP_SEARCH_SHOW_FLOOD_GROUP` 메시지를 보내 범위 이동·레이어 표시를 요청한다.
|
||||
- 도시침수지도에 없는 빈도는 자동 변환하지 않고 화면 안내를 표시한다.
|
||||
- 도시침수지도 빈도는 30년 이하를 `030`, 100년 초과를 `100`으로 보정하고 그 외 미지원 빈도는 안내한다.
|
||||
|
||||
## 검증
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
- 분석기준시간은 DB 결과와 독립된 현재 정시이므로 진행중 데이터가 없어도 사라지지 않는다. 입력창으로 오해하지 않도록 테두리·배경 없이 글씨만 표시한다.
|
||||
- `분석기준시간` 라벨과 시각은 현재 탭 행의 오른쪽 끝으로 이동하고 검색패널에는 중점관리지역 선택과 우측 조회 버튼만 유지한다.
|
||||
- 행 선택 성공 후 왼쪽 아래 안내문은 제거하고 오류 메시지만 조건부 표시한다.
|
||||
- 지도 요약카드에는 capacity 0.8 초과 관로 존재 여부가 위험/정상을 결정한다는 설명을 추가했다.
|
||||
- 지도 요약카드에는 GIS 매핑 관로 중 Capacity 0.8 이상이 1개라도 있으면 위험이라는 기준만 간결하게 표시한다.
|
||||
- 결과표 `No.`를 `No`로 바꾸고 표 전체를 `1320px` 고정폭으로 지정한 뒤 `table-fixed`·`colgroup` 비율을 적용해 1·2페이지의 절대 열 너비를 동일하게 고정했다.
|
||||
- 공통 그리드 교체 후 검색영역과 결과영역의 가로·세로 overflow를 숨기고 결과영역 좌우 여백을 보정해 팝업 내부 스크롤바가 나타나지 않게 했다.
|
||||
|
||||
@@ -245,3 +245,9 @@
|
||||
- 중점관리지역 아래 capacity 설명 자체에는 구분선을 두지 않고 문장 안의 `위험`은 빨간색, `정상`은 파란색 텍스트로 강조한다. 설명과 수치표의 경계인 표 상단선은 유지한다.
|
||||
- 기존 위험상태 배지와 카드 `290px` 폭을 유지한다.
|
||||
- 팝업은 `pagehide`·`beforeunload`에서 `WATER_RISK_CLOSED` 메시지를 부모로 전달한다. 부모는 메시지 수신 즉시 중점관리지역·관로·맨홀 레이어, capacity/Flooding 범례, 정보카드와 선택상태를 모두 초기화하며 300ms close watcher는 예비 감시로 유지한다.
|
||||
|
||||
## 17. 2026-08-26 과거분석이력 최신순 기본 정렬·행 키 보완
|
||||
|
||||
- 팝업 최초 조회와 현재·과거 탭 전환 시 정렬 파라미터를 비워 백엔드 기본 강우시작시간 최신순을 적용한다.
|
||||
- 사용자가 다른 정렬 열을 선택하면 기존 서버 정렬을 유지하고, 탭을 전환하면 다시 강우시작시간 최신순으로 돌아간다.
|
||||
- 동일 날짜·지역에 여러 강우이벤트가 존재해도 React 행 키가 겹치지 않도록 분석일·강우시작시간·분석기준시간·행정코드·지역번호를 조합한다.
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@common/business_components";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { getBlobData, getRawJson } from "@/components/lib/api";
|
||||
import { getBlobData, getData, getRawJson } from "@/components/lib/api";
|
||||
import { fileDownload } from "@/components/lib/utils/fileDownload";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
|
||||
@@ -57,18 +57,26 @@ const PAGE_SIZE = 10;
|
||||
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: "rainBeginningDateTimeDisplay",
|
||||
title: "강우시각",
|
||||
id: "analysisDateTimeDisplay",
|
||||
title: "분석시각",
|
||||
widthPercent: 13,
|
||||
},
|
||||
{
|
||||
id: "analysisDateTimeDisplay",
|
||||
title: "분석시각",
|
||||
id: "rainBeginningDateTimeDisplay",
|
||||
title: "강우시각",
|
||||
widthPercent: 13,
|
||||
},
|
||||
{
|
||||
@@ -208,6 +216,21 @@ export default function FldngPredcPrstPopupContent() {
|
||||
// 개발 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();
|
||||
@@ -297,7 +320,8 @@ export default function FldngPredcPrstPopupContent() {
|
||||
/** 선택 행과 동일한 시군구·빈도의 도시침수지도 범위를 부모 지도에 전달한다. */
|
||||
const handleRowClick = async (row: TableRow) => {
|
||||
const frequencyYear = Number(row.exceededFrequencyYear ?? 0);
|
||||
if (!MAP_FREQUENCIES.has(frequencyYear)) {
|
||||
const sbmrgFrqExpln = toFloodMapFrequencyCode(frequencyYear);
|
||||
if (!sbmrgFrqExpln) {
|
||||
alert(
|
||||
`${frequencyYear || "해당"}년 빈도 도시침수지도는 제공되지 않습니다. (30·50·80·100년 빈도만 제공)`,
|
||||
);
|
||||
@@ -315,12 +339,12 @@ export default function FldngPredcPrstPopupContent() {
|
||||
setSelectedRowId(buildRowId(row));
|
||||
setLoading(true);
|
||||
try {
|
||||
const sbmrgFrqExpln = `${frequencyYear}년`;
|
||||
const params = new URLSearchParams({
|
||||
sggCd: row.sggCd,
|
||||
sbmrgFrqExpln,
|
||||
});
|
||||
const extentData = await getRawJson(
|
||||
// 백엔드 공통 응답({ data: ... })의 data 영역을 반환하는 API 헬퍼를 사용한다.
|
||||
const extentData = await getData(
|
||||
`/sewerage-service/api/v1/fldng/flood-dngr-map/group/extent?${params.toString()}`,
|
||||
);
|
||||
const extent = [
|
||||
@@ -345,7 +369,7 @@ export default function FldngPredcPrstPopupContent() {
|
||||
alert(
|
||||
error instanceof Error && error.message.includes("위치 정보")
|
||||
? error.message
|
||||
: "도시침수지도 이동에 실패했습니다.",
|
||||
: "도시침수지도 이동에 실패하였습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -471,8 +495,8 @@ export default function FldngPredcPrstPopupContent() {
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{renderDateTimeRange("강우시각", "rainBgngFrom", "rainBgngTo")}
|
||||
{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">
|
||||
|
||||
@@ -51,6 +51,7 @@ export const WINDOW_POPUP_MESSAGE_TYPE = {
|
||||
MAP_SEARCH_SHOW_MULTIPLE: "sn:popup:mapSearchShowMultiple",
|
||||
MAP_SEARCH_FIT_EXTENT: "sn:popup:mapSearchFitExtent",
|
||||
MAP_SEARCH_SHOW_FLOOD_GROUP: "sn:popup:mapSearchShowFloodGroup",
|
||||
MAP_SEARCH_FLOOD_LAYER_REQUIRED: "sn:popup:mapSearchFloodLayerRequired",
|
||||
GIS_EDIT_SESSION_READY: "sn:popup:gisEditSessionReady",
|
||||
GIS_EDIT_SESSION_SYNC: "sn:popup:gisEditSessionSync",
|
||||
GIS_EDIT_SESSION_SHOW_INLINE: "sn:popup:gisEditSessionShowInline",
|
||||
|
||||
Reference in New Issue
Block a user