Compare commits
2
Commits
4ff80dd257
...
735bb397ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
735bb397ec | ||
|
|
8b5a2b713c |
@@ -194,6 +194,7 @@ import {
|
||||
isSbmrgDngrMapRootNode,
|
||||
} from "./components/sbmrgDngrMap/sbmrgDngrMapTypes";
|
||||
import {
|
||||
FLOOD_DNGR_MAP_LAYER_NAME,
|
||||
FLOOD_DNGR_MAP_LAYER_TYPE,
|
||||
FLOOD_DNGR_MAP_POPUP_KEY,
|
||||
isFloodDngrMapRootNode,
|
||||
@@ -1176,11 +1177,24 @@ const Map = () => {
|
||||
const sinkholeScope = useMemo(() => buildSinkholeScope(userInfo), [userInfo]);
|
||||
|
||||
const [treeData, setTreeData] = useState<any[]>([]); // 초기값을 객체 또는 null로 설정
|
||||
// 통수위험 결과 행을 선택할 때 기존 체크를 유지하면서 강우 격자 레이어를 켜기 위한 요청이다.
|
||||
// 지도 연계 팝업의 행을 선택할 때 기존 체크를 유지하면서 필요한 업무 레이어를 켜기 위한 요청이다.
|
||||
const [checkedLayerRequest, setCheckedLayerRequest] = useState<{
|
||||
requestKey: number;
|
||||
layerNames: string[];
|
||||
} | null>(null);
|
||||
// 침수예측현황에서 선택한 행은 도시침수지도 레이어가 켜진 뒤 지도 이동에 사용한다.
|
||||
const [pendingFloodPredictionSelection, setPendingFloodPredictionSelection] =
|
||||
useState<{
|
||||
extent: [number, number, number, number];
|
||||
sggCd: string;
|
||||
frequency: string;
|
||||
analysisDateTime: string;
|
||||
} | null>(null);
|
||||
// 팝업 Window 객체는 렌더링 대상이 아니므로 자동 레이어 선택 실패 안내용 ref로 보관한다.
|
||||
const pendingFloodPredictionPopupRef = useRef<{
|
||||
source: Window | null;
|
||||
origin: string;
|
||||
} | null>(null);
|
||||
const [layerLoadError, setLayerLoadError] = useState<string | null>(null);
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||
@@ -1856,6 +1870,74 @@ const Map = () => {
|
||||
[removeFloodPredictionGridLayer],
|
||||
);
|
||||
|
||||
/** 활성화된 도시침수지도에서 선택 행의 시군구·빈도 피처를 선택하고 지도와 오버레이를 이동한다. */
|
||||
const applyFloodPredictionSelection = useCallback(
|
||||
async (selection: {
|
||||
extent: [number, number, number, number];
|
||||
sggCd: string;
|
||||
frequency: string;
|
||||
analysisDateTime: string;
|
||||
}) => {
|
||||
const map = gisMaps.current;
|
||||
const select = GisApp.Module.select as any;
|
||||
if (!map?.getView || !select?.selectFocusedBoundaryFeature) return false;
|
||||
|
||||
const floodLayers = map
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.filter(
|
||||
(layer: any) =>
|
||||
String(layer.get?.("lyrTyNm") ?? "").trim() ===
|
||||
FLOOD_DNGR_MAP_LAYER_TYPE && layer.getVisible?.() !== false,
|
||||
);
|
||||
if (floodLayers.length === 0) return false;
|
||||
|
||||
await select.resetSelectedFeature?.();
|
||||
const selectedFeatureIds = new Set<string>();
|
||||
const selectLoadedGroupFeatures = (source: any) => {
|
||||
const features = source?.getFeatures?.() ?? [];
|
||||
features.forEach((feature: any) => {
|
||||
const featureSggCd = String(feature.get?.("sgg_cd") ?? "").trim();
|
||||
const featureFrequency = String(
|
||||
feature.get?.("sbmrg_frq_expln") ?? "",
|
||||
).trim();
|
||||
if (
|
||||
featureSggCd !== selection.sggCd ||
|
||||
featureFrequency !== selection.frequency
|
||||
)
|
||||
return;
|
||||
const featureId = String(
|
||||
feature.getId?.() ?? feature.get?.("fid") ?? "",
|
||||
);
|
||||
if (featureId && selectedFeatureIds.has(featureId)) return;
|
||||
if (featureId) selectedFeatureIds.add(featureId);
|
||||
select.selectFocusedBoundaryFeature(feature, true);
|
||||
});
|
||||
};
|
||||
|
||||
floodLayers.forEach((layer: any) => {
|
||||
const source = layer.getSource?.();
|
||||
if (!source?.getFeatures) return;
|
||||
selectLoadedGroupFeatures(source);
|
||||
source.once?.("featuresloadend", () =>
|
||||
selectLoadedGroupFeatures(source),
|
||||
);
|
||||
});
|
||||
map.getView().fit(selection.extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
});
|
||||
void loadFloodPredictionGridLayer(
|
||||
selection.sggCd,
|
||||
selection.analysisDateTime,
|
||||
selection.extent,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
[loadFloodPredictionGridLayer],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
@@ -2008,58 +2090,23 @@ const Map = () => {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await select.resetSelectedFeature?.();
|
||||
const selectedFeatureIds = new Set<string>();
|
||||
let floodLayerVisible = false;
|
||||
const selectLoadedGroupFeatures = (source: any) => {
|
||||
const features = source?.getFeatures?.() ?? [];
|
||||
features.forEach((feature: any) => {
|
||||
const featureSggCd = String(feature.get?.("sgg_cd") ?? "").trim();
|
||||
const featureFrequency = String(
|
||||
feature.get?.("sbmrg_frq_expln") ?? "",
|
||||
).trim();
|
||||
if (featureSggCd !== sggCd || featureFrequency !== frequency)
|
||||
return;
|
||||
const featureId = String(
|
||||
feature.getId?.() ?? feature.get?.("fid") ?? "",
|
||||
);
|
||||
if (featureId && selectedFeatureIds.has(featureId)) return;
|
||||
if (featureId) selectedFeatureIds.add(featureId);
|
||||
select.selectFocusedBoundaryFeature(feature, true);
|
||||
});
|
||||
const selection = {
|
||||
extent: extent as [number, number, number, number],
|
||||
sggCd,
|
||||
frequency,
|
||||
analysisDateTime,
|
||||
};
|
||||
if (await applyFloodPredictionSelection(selection)) return;
|
||||
|
||||
map
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.forEach((layer: any) => {
|
||||
if (
|
||||
String(layer.get?.("lyrTyNm") ?? "").trim() !==
|
||||
FLOOD_DNGR_MAP_LAYER_TYPE
|
||||
)
|
||||
return;
|
||||
if (layer.getVisible?.() !== false) floodLayerVisible = true;
|
||||
const source = layer.getSource?.();
|
||||
if (!source?.getFeatures) return;
|
||||
selectLoadedGroupFeatures(source);
|
||||
source.once?.("featuresloadend", () =>
|
||||
selectLoadedGroupFeatures(source),
|
||||
);
|
||||
});
|
||||
if (!floodLayerVisible) {
|
||||
(event.source as Window | null)?.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],
|
||||
maxZoom: 16,
|
||||
});
|
||||
void loadFloodPredictionGridLayer(sggCd, analysisDateTime, extent);
|
||||
pendingFloodPredictionPopupRef.current = {
|
||||
source: event.source as Window | null,
|
||||
origin: event.origin,
|
||||
};
|
||||
setPendingFloodPredictionSelection(selection);
|
||||
setCheckedLayerRequest((current) => ({
|
||||
requestKey: (current?.requestKey ?? 0) + 1,
|
||||
layerNames: [FLOOD_DNGR_MAP_LAYER_NAME],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2098,7 +2145,50 @@ const Map = () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener("message", handlePopupMessage);
|
||||
};
|
||||
}, [loadFloodPredictionGridLayer]);
|
||||
}, [applyFloodPredictionSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingFloodPredictionSelection) return;
|
||||
|
||||
let retryTimer: number | null = null;
|
||||
let retryCount = 0;
|
||||
const maxRetryCount = 20;
|
||||
|
||||
/** 자동 체크 요청으로 도시침수지도 레이어가 생성될 때까지 선택 행 적용을 제한적으로 재시도한다. */
|
||||
const applyPendingSelection = async () => {
|
||||
if (
|
||||
await applyFloodPredictionSelection(pendingFloodPredictionSelection)
|
||||
) {
|
||||
setPendingFloodPredictionSelection((current) =>
|
||||
current === pendingFloodPredictionSelection ? null : current,
|
||||
);
|
||||
pendingFloodPredictionPopupRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
retryCount += 1;
|
||||
if (retryCount >= maxRetryCount) {
|
||||
const popup = pendingFloodPredictionPopupRef.current;
|
||||
popup?.source?.postMessage(
|
||||
{ type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_FLOOD_LAYER_REQUIRED },
|
||||
popup.origin,
|
||||
);
|
||||
pendingFloodPredictionPopupRef.current = null;
|
||||
setPendingFloodPredictionSelection((current) =>
|
||||
current === pendingFloodPredictionSelection ? null : current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
retryTimer = window.setTimeout(() => {
|
||||
void applyPendingSelection();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
void applyPendingSelection();
|
||||
return () => {
|
||||
if (retryTimer !== null) window.clearTimeout(retryTimer);
|
||||
};
|
||||
}, [applyFloodPredictionSelection, pendingFloodPredictionSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShowGisEditSession) return;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 침수예측현황 분석시각 초기화 구현기록
|
||||
|
||||
## 작업 목적
|
||||
|
||||
- 침수예측현황 팝업을 처음 열거나 검색조건을 초기화할 때 분석시각 범위를 자동 입력하지 않는다.
|
||||
- 분석시각 제한 없이 조회한 결과는 최근 분석 데이터부터 표시한다.
|
||||
|
||||
## 원본 및 변경 근거
|
||||
|
||||
- 요구사항정의서 `SFR-011-008 위험도 산정`
|
||||
- 화면설계서 슬라이드 24, `SN_MN010205 침수예측현황`
|
||||
- 사용자 승인사항: 분석시각 기본 검색조건 제거 및 최근 데이터 우선 표시
|
||||
|
||||
## 변경 내용
|
||||
|
||||
- 최초·초기화 검색조건의 `analysisFrom`, `analysisTo`를 빈 값으로 변경했다.
|
||||
- 목록과 Excel 전체 조회에 `analysisDateTime,desc` 정렬을 명시했다.
|
||||
- 사용자가 필요할 때 직접 분석시각 범위를 입력할 수 있도록 검색 입력란은 유지했다.
|
||||
|
||||
## 영향 범위
|
||||
|
||||
- `apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx`
|
||||
- 백엔드 API와 응답 구조는 변경하지 않는다.
|
||||
- 시도, 시군구, 강우시각, 초과 빈도 검색조건은 기존 동작을 유지한다.
|
||||
|
||||
## 검증
|
||||
|
||||
- `pnpm --filter sn run typecheck` 통과
|
||||
- `git diff --check` 통과
|
||||
- 실행 중인 개발 서버를 유지하기 위해 별도 빌드는 수행하지 않는다.
|
||||
|
||||
## 롤백
|
||||
|
||||
- `buildInitialFilters`에서 분석시각의 금일 기본 범위를 다시 생성하고 명시적 최신순 정렬 파라미터를 제거한다.
|
||||
|
||||
## 남은 확인
|
||||
|
||||
- 팝업 최초 진입 시 분석시각 입력란이 비어 있는지 확인한다.
|
||||
- 초기화 버튼을 누르면 분석시각이 비워지고 첫 페이지에 최신 분석 데이터가 표시되는지 확인한다.
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# 침수예측현황 도시침수지도 자동 선택 구현기록
|
||||
|
||||
## 작업 목적
|
||||
|
||||
- 침수예측현황 팝업에서 행을 선택하면 사용자가 레이어 트리에서 직접 조작하지 않아도 도시침수지도 레이어를 자동으로 체크한다.
|
||||
- 레이어 활성화 후 선택한 시군구와 빈도에 맞는 위치 이동, 피처 선택, 침수예측 오버레이 표시를 이어서 실행한다.
|
||||
|
||||
## 원본 근거
|
||||
|
||||
- 요구사항정의서 `3. (기능)요구사항정의 및 추적표` 94행, `SFR-011-008 위험도 산정`
|
||||
- 화면설계서 슬라이드 24, `SN_MN010205 침수예측현황`
|
||||
- 화면설계서 슬라이드 28, `SN_MN010206 침수예측지도`
|
||||
- 자동 레이어 선택 비교 화면: 슬라이드 47, `SN_MN040106 조사결과 지도보기`
|
||||
|
||||
## 변경 내용
|
||||
|
||||
- 하수관망도에 이미 연결된 `checkedLayerRequest`를 사용하여 `도시침수지도(정보제공포털)` 레이어를 기존 체크 상태에 추가한다.
|
||||
- 도시침수지도 레이어가 활성화되기 전에 도착한 행 선택 정보는 임시 상태에 보관한다.
|
||||
- 레이어가 준비되면 기존 시군구·빈도 피처 선택, 지도 범위 이동, 침수예측 오버레이 표시 로직을 실행한다.
|
||||
- 제한 시간 안에 레이어가 준비되지 않으면 기존 수동 선택 안내 메시지를 팝업에 전달한다.
|
||||
|
||||
## 영향 범위
|
||||
|
||||
- `apps/sn/app/(map-layout)/map/page.tsx`
|
||||
- 백엔드 API와 메시지 payload 계약은 변경하지 않는다.
|
||||
- 기존에 사용자가 체크한 다른 지도 레이어는 유지한다.
|
||||
- 도시침수지도 미제공 빈도에 대한 기존 안내는 유지한다.
|
||||
|
||||
## 검증
|
||||
|
||||
- `pnpm --filter sn exec tsc --noEmit` 통과
|
||||
- `git diff --check` 통과
|
||||
- 개발 서버가 3002 포트에서 실행 중이므로 별도 빌드는 수행하지 않는다.
|
||||
|
||||
## 롤백
|
||||
|
||||
- `map/page.tsx`의 도시침수지도 자동 체크 요청, 보류 상태, 재시도 effect를 제거하고 기존 레이어 가시성 검사와 수동 선택 안내 흐름으로 복원한다.
|
||||
|
||||
## 남은 확인
|
||||
|
||||
- 로그인된 브라우저에서 도시침수지도가 꺼진 상태와 이미 켜진 상태를 각각 확인한다.
|
||||
- 행을 연속 선택했을 때 마지막 선택 행 기준으로 지도와 오버레이가 갱신되는지 확인한다.
|
||||
@@ -17,7 +17,6 @@ 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";
|
||||
|
||||
@@ -111,21 +110,16 @@ const HEADS: UiHeadProps[] = [
|
||||
{ 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 buildInitialFilters = (): Filters => ({
|
||||
ctpvCd: "0",
|
||||
sggCd: "0",
|
||||
rainBgngFrom: "",
|
||||
rainBgngTo: "",
|
||||
analysisFrom: "",
|
||||
analysisTo: "",
|
||||
frequencyYear: "0",
|
||||
});
|
||||
|
||||
const apiDate = (value: string) => value.replace(/[^0-9]/g, "").slice(0, 12);
|
||||
|
||||
@@ -151,6 +145,7 @@ const buildQuery = (filters: Filters, page?: number, size = PAGE_SIZE) => {
|
||||
if (page !== undefined) {
|
||||
params.set("page", String(page - 1));
|
||||
params.set("size", String(size));
|
||||
params.set("sort", "analysisDateTime,desc");
|
||||
}
|
||||
appendApiSearchParams(params, {
|
||||
ctpvCd: filters.ctpvCd === "0" ? "0" : filters.ctpvCd.slice(0, 2),
|
||||
|
||||
Reference in New Issue
Block a user