sn: featureClickEventPopup에서 침수흔적도 관련 분리
This commit is contained in:
@@ -5,11 +5,8 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
formatFloodArea,
|
||||
formatFloodDepth,
|
||||
type FldngHstryItem,
|
||||
} from "./fldngHstryTypes";
|
||||
import type { FldngHstryItem } from "./fldngHstryTypes";
|
||||
import { formatFldngHstryArea, formatFldngHstryDepth } from "./fldngHstryUtils";
|
||||
|
||||
type Props = {
|
||||
detail: FldngHstryItem;
|
||||
@@ -54,8 +51,8 @@ export default function FldngHstryDetailPanel({
|
||||
const yearRows = useMemo(
|
||||
() =>
|
||||
[
|
||||
["침수심", `${detail.floodDepthM.toFixed(2)} m`],
|
||||
["침수면적", formatFloodArea(detail.floodAreaHa)],
|
||||
["침수심", `${formatFldngHstryDepth(detail.floodDepthM)} m`],
|
||||
["침수면적", formatFldngHstryArea(detail.floodAreaHa)],
|
||||
["침수년도", `${detail.floodYear}년`],
|
||||
["등급", detail.gradeLabel],
|
||||
["침수구역", detail.districtName],
|
||||
@@ -67,7 +64,7 @@ export default function FldngHstryDetailPanel({
|
||||
() =>
|
||||
[
|
||||
["침수재해명", detail.disasterName],
|
||||
["침수면적", formatFloodArea(detail.floodAreaHa)],
|
||||
["침수면적", formatFldngHstryArea(detail.floodAreaHa)],
|
||||
["침수원인상세명", detail.causeDetailName],
|
||||
["침수시작년월일", detail.startDate],
|
||||
["침수종료년월일", detail.endDate],
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 화면명: 침수흔적위선 연계정보
|
||||
* 우측 정보 화면의 심선·위선 목록, 선택 상태와 화면 생명주기 캐시를 관리한다.
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import type Feature from "ol/Feature.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
|
||||
import { loadFldngHstryDetail } from "./fldngHstryClient";
|
||||
import {
|
||||
FLDNG_HSTRY_DEPTH_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
type FldngHstryDetailCacheValue,
|
||||
} from "./fldngHstryTypes";
|
||||
|
||||
type FldngHstryGisLayer = {
|
||||
searchLayer?: { getSource?: () => VectorSource | null } | null;
|
||||
clearSearchLayer: (option: unknown) => void;
|
||||
ensureSearchLayer: (option: unknown) => {
|
||||
set: (key: string, value: unknown) => void;
|
||||
setSource: (source: VectorSource) => void;
|
||||
};
|
||||
showSearchResultsFromGeoJSON: (
|
||||
layerTypeName: string,
|
||||
featureId: string,
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
|
||||
const cloneFldngHstryFeature = (feature: Feature) => {
|
||||
const clonedFeature = feature.clone();
|
||||
const featureId = feature.getId();
|
||||
if (featureId !== undefined) clonedFeature.setId(featureId);
|
||||
return clonedFeature;
|
||||
};
|
||||
|
||||
const displayCachedFldngHstryFeatures = (
|
||||
gisLayer: FldngHstryGisLayer,
|
||||
layerTypeName: string,
|
||||
features: Feature[],
|
||||
) => {
|
||||
const matched = Object.values(GisApp.layerCode ?? {}).find(
|
||||
(item: any) => item?.lyrTyNm === layerTypeName,
|
||||
) as any;
|
||||
if (!matched || features.length === 0) return;
|
||||
|
||||
gisLayer.clearSearchLayer(matched);
|
||||
const layer = gisLayer.ensureSearchLayer(matched);
|
||||
layer.set("gobjTy", matched.gobjTy);
|
||||
const clonedFeatures = features.map(cloneFldngHstryFeature);
|
||||
layer.setSource(new VectorSource({ features: clonedFeatures }));
|
||||
(GisApp.Module.select as any)?.selectFeature(clonedFeatures[0], false);
|
||||
};
|
||||
|
||||
const showFldngHstryFeature = async (
|
||||
cache: Map<string, Promise<Feature[]>>,
|
||||
gisLayer: FldngHstryGisLayer,
|
||||
layerTypeName: string,
|
||||
featureId: string,
|
||||
) => {
|
||||
const cacheKey = `${layerTypeName}:${featureId}`;
|
||||
const cachedRequest = cache.get(cacheKey);
|
||||
if (cachedRequest) {
|
||||
displayCachedFldngHstryFeatures(
|
||||
gisLayer,
|
||||
layerTypeName,
|
||||
await cachedRequest,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await gisLayer.showSearchResultsFromGeoJSON(layerTypeName, featureId);
|
||||
const source = gisLayer.searchLayer?.getSource?.();
|
||||
if (!source) return;
|
||||
|
||||
const featureRequest = new Promise<Feature[]>((resolve, reject) => {
|
||||
const resolveFeatures = () => {
|
||||
const features = source.getFeatures();
|
||||
if (features.length > 0) {
|
||||
resolve(features.map(cloneFldngHstryFeature));
|
||||
} else {
|
||||
reject(new Error("침수흔적 위치 정보가 없습니다."));
|
||||
}
|
||||
};
|
||||
const rejectFeatures = () =>
|
||||
reject(new Error("침수흔적 위치 조회에 실패했습니다."));
|
||||
|
||||
if (source.getFeatures().length > 0) {
|
||||
resolveFeatures();
|
||||
return;
|
||||
}
|
||||
source.once("featuresloadend", resolveFeatures);
|
||||
source.once("featuresloaderror", rejectFeatures);
|
||||
});
|
||||
|
||||
cache.set(cacheKey, featureRequest);
|
||||
try {
|
||||
await featureRequest;
|
||||
} catch (error) {
|
||||
cache.delete(cacheKey);
|
||||
console.warn(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const useFldngHstryFeatureClick = () => {
|
||||
const fldngHstryDetailCacheRef = useRef(
|
||||
new Map<string, Promise<FldngHstryDetailCacheValue>>(),
|
||||
);
|
||||
const fldngHstryFeatureCacheRef = useRef(
|
||||
new Map<string, Promise<Feature[]>>(),
|
||||
);
|
||||
const [fldngHstryDepthRows, setFldngHstryDepthRows] = useState<
|
||||
Record<string, unknown>[]
|
||||
>([]);
|
||||
const [fldngHstryWtlvRows, setFldngHstryWtlvRows] = useState<
|
||||
Record<string, unknown>[]
|
||||
>([]);
|
||||
const [selectedFldngHstryDepthFid, setSelectedFldngHstryDepthFid] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [selectedFldngHstryWtlvFid, setSelectedFldngHstryWtlvFid] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const fldngHstryDepthYearListRef = useRef<HTMLDivElement>(null);
|
||||
const fldngHstryWtlvListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const detailCache = fldngHstryDetailCacheRef.current;
|
||||
const featureCache = fldngHstryFeatureCacheRef.current;
|
||||
return () => {
|
||||
detailCache.clear();
|
||||
featureCache.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const resetFldngHstryFeatureClick = useCallback(() => {
|
||||
setFldngHstryDepthRows([]);
|
||||
setFldngHstryWtlvRows([]);
|
||||
setSelectedFldngHstryDepthFid(null);
|
||||
setSelectedFldngHstryWtlvFid(null);
|
||||
}, []);
|
||||
|
||||
const loadFldngHstryFeatureClickDetail = useCallback(
|
||||
async (fldngHstryFid: string) => {
|
||||
const cacheKey = `${FLDNG_HSTRY_LAYER_TYPE}:${fldngHstryFid}`;
|
||||
let request = fldngHstryDetailCacheRef.current.get(cacheKey);
|
||||
if (!request) {
|
||||
request = loadFldngHstryDetail(fldngHstryFid);
|
||||
fldngHstryDetailCacheRef.current.set(cacheKey, request);
|
||||
}
|
||||
|
||||
const detail = await request;
|
||||
if (!detail.complete) fldngHstryDetailCacheRef.current.delete(cacheKey);
|
||||
setFldngHstryDepthRows(detail.fldngHstryDepthRows);
|
||||
setFldngHstryWtlvRows(detail.fldngHstryWtlvRows);
|
||||
setSelectedFldngHstryDepthFid(null);
|
||||
setSelectedFldngHstryWtlvFid(null);
|
||||
return detail;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const headName = useMemo(
|
||||
() => (
|
||||
<div className="flex w-full min-w-0 flex-col items-start gap-2">
|
||||
<span>기본정보</span>
|
||||
{fldngHstryDepthRows.length > 0 && (
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<span className="w-9 shrink-0 text-[1.1rem] font-semibold">
|
||||
심선
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="이전 연도"
|
||||
className="shrink-0 px-1 text-lg leading-none text-foreground"
|
||||
onClick={() =>
|
||||
fldngHstryDepthYearListRef.current?.scrollBy({
|
||||
left: -120,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<div
|
||||
ref={fldngHstryDepthYearListRef}
|
||||
className="flex min-w-0 flex-1 gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{[...fldngHstryDepthRows]
|
||||
.map((row, index) => ({
|
||||
row,
|
||||
index,
|
||||
year: String(
|
||||
row.fldng_yr ?? row.fldngYr ?? row.yr ?? "",
|
||||
).trim(),
|
||||
}))
|
||||
.filter(({ year }) => Boolean(year))
|
||||
.sort((left, right) => {
|
||||
const yearOrder = left.year.localeCompare(
|
||||
right.year,
|
||||
undefined,
|
||||
{ numeric: true },
|
||||
);
|
||||
return yearOrder !== 0 ? yearOrder : left.index - right.index;
|
||||
})
|
||||
.map(({ row, index, year }) => {
|
||||
const depthFid = String(
|
||||
row.fldng_dpth_map_sn ?? row.fid ?? "",
|
||||
).trim();
|
||||
return (
|
||||
<button
|
||||
key={`${depthFid || "row"}-${index}`}
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!depthFid) return;
|
||||
setSelectedFldngHstryDepthFid(depthFid);
|
||||
setSelectedFldngHstryWtlvFid(null);
|
||||
await showFldngHstryFeature(
|
||||
fldngHstryFeatureCacheRef.current,
|
||||
GisApp.Module.layer as any,
|
||||
FLDNG_HSTRY_DEPTH_LAYER_TYPE,
|
||||
`${FLDNG_HSTRY_DEPTH_LAYER_TYPE}.${depthFid.replace(new RegExp(`^${FLDNG_HSTRY_DEPTH_LAYER_TYPE}\\.`), "")}`,
|
||||
);
|
||||
}}
|
||||
className={`shrink-0 rounded border px-3 py-1 text-[1.2rem] font-medium leading-5 ${selectedFldngHstryDepthFid === depthFid ? "border-blue-600 bg-white text-blue-700" : "border-border bg-muted text-muted-foreground"}`}
|
||||
>
|
||||
{year}년
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="다음 연도"
|
||||
className="shrink-0 px-1 text-lg leading-none text-foreground"
|
||||
onClick={() =>
|
||||
fldngHstryDepthYearListRef.current?.scrollBy({
|
||||
left: 120,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{fldngHstryWtlvRows.length > 0 && (
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<span className="w-9 shrink-0 text-[1.1rem] font-semibold">
|
||||
위선
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="이전 위선"
|
||||
className="shrink-0 px-1 text-lg leading-none text-foreground"
|
||||
onClick={() =>
|
||||
fldngHstryWtlvListRef.current?.scrollBy({
|
||||
left: -120,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<div
|
||||
ref={fldngHstryWtlvListRef}
|
||||
className="flex min-w-0 flex-1 gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{fldngHstryWtlvRows.map((row, index) => {
|
||||
const waterLevelFid = String(row.fid ?? "").trim();
|
||||
const disasterName = String(
|
||||
row.fldng_dstr_nm ?? row.fldngDstrNm ?? "",
|
||||
).trim();
|
||||
return (
|
||||
<button
|
||||
key={`${waterLevelFid || "wtlv"}-${index}`}
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!waterLevelFid) return;
|
||||
setSelectedFldngHstryWtlvFid(waterLevelFid);
|
||||
setSelectedFldngHstryDepthFid(null);
|
||||
await showFldngHstryFeature(
|
||||
fldngHstryFeatureCacheRef.current,
|
||||
GisApp.Module.layer as any,
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
`${FLDNG_HSTRY_LAYER_TYPE}.${waterLevelFid.replace(new RegExp(`^${FLDNG_HSTRY_LAYER_TYPE}\\.`), "")}`,
|
||||
);
|
||||
}}
|
||||
className={`shrink-0 rounded border px-3 py-1 text-[1.2rem] font-medium leading-5 ${selectedFldngHstryWtlvFid === waterLevelFid ? "border-blue-600 bg-white text-blue-700" : "border-border bg-muted text-muted-foreground"}`}
|
||||
>
|
||||
{disasterName || "재해명 없음"}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="다음 위선"
|
||||
className="shrink-0 px-1 text-lg leading-none text-foreground"
|
||||
onClick={() =>
|
||||
fldngHstryWtlvListRef.current?.scrollBy({
|
||||
left: 120,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
[
|
||||
fldngHstryDepthRows,
|
||||
fldngHstryWtlvRows,
|
||||
selectedFldngHstryDepthFid,
|
||||
selectedFldngHstryWtlvFid,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
hasFldngHstryLinkedFeatures:
|
||||
fldngHstryDepthRows.length > 0 || fldngHstryWtlvRows.length > 0,
|
||||
fldngHstryFeatureClickHeadName: headName,
|
||||
loadFldngHstryFeatureClickDetail,
|
||||
resetFldngHstryFeatureClick,
|
||||
};
|
||||
};
|
||||
@@ -6,16 +6,16 @@
|
||||
|
||||
import { SelectBox } from "@common/business_components/ui";
|
||||
import {
|
||||
FLOOD_HISTORY_MIN_YEAR,
|
||||
normalizeFloodHistoryYearRange,
|
||||
type FloodHistoryYearRange,
|
||||
FLDNG_HSTRY_MIN_YEAR,
|
||||
type FldngHstryYearRange,
|
||||
} from "./fldngHstryTypes";
|
||||
import { normalizeFldngHstryYearRange } from "./fldngHstryUtils";
|
||||
import LayerSettingCard from "../layerSetting/LayerSettingCard";
|
||||
|
||||
type Props = {
|
||||
yearRange: FloodHistoryYearRange;
|
||||
yearRange: FldngHstryYearRange;
|
||||
enabled: boolean;
|
||||
onYearRangeChange: (yearRange: FloodHistoryYearRange) => void;
|
||||
onYearRangeChange: (yearRange: FldngHstryYearRange) => void;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -28,10 +28,10 @@ export default function FldngHstryLayerSettingPanel({
|
||||
onEnabledChange,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const normalizedRange = normalizeFloodHistoryYearRange(yearRange);
|
||||
const normalizedRange = normalizeFldngHstryYearRange(yearRange);
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearOptions = Array.from(
|
||||
{ length: currentYear - FLOOD_HISTORY_MIN_YEAR + 1 },
|
||||
{ length: currentYear - FLDNG_HSTRY_MIN_YEAR + 1 },
|
||||
(_, index) => String(currentYear - index),
|
||||
);
|
||||
const startYearOptions = yearOptions
|
||||
@@ -43,9 +43,9 @@ export default function FldngHstryLayerSettingPanel({
|
||||
const yearSelectClass =
|
||||
"!h-[26px] w-full !rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground";
|
||||
|
||||
const changeYear = (key: keyof FloodHistoryYearRange, value: string) => {
|
||||
const changeYear = (key: keyof FldngHstryYearRange, value: string) => {
|
||||
onYearRangeChange(
|
||||
normalizeFloodHistoryYearRange({
|
||||
normalizeFldngHstryYearRange({
|
||||
...normalizedRange,
|
||||
[key]: value,
|
||||
}),
|
||||
|
||||
@@ -17,22 +17,24 @@ import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import {
|
||||
FLOOD_HISTORY_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
FLDNG_HSTRY_DEFAULT_FILTERS,
|
||||
FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE,
|
||||
FLDNG_HSTRY_DOWNLOAD_HEADERS,
|
||||
FLDNG_HSTRY_LIST_HEADLIST,
|
||||
FLDNG_HSTRY_PAGE_SIZE,
|
||||
FLDNG_HSTRY_ROW_SELECT_MESSAGE,
|
||||
type FldngHstryApiItem,
|
||||
type FldngHstryFilters,
|
||||
type FldngHstryTableRow,
|
||||
} from "./fldngHstryTypes";
|
||||
import {
|
||||
attachFldngHstryGeometry,
|
||||
buildFldngHstryListQueryString,
|
||||
buildFldngHstryYearOptions,
|
||||
normalizeFldngHstryFilters,
|
||||
toFldngHstryTableRows,
|
||||
type FldngHstryApiItem,
|
||||
type FldngHstryFilters,
|
||||
type FldngHstryTableRow,
|
||||
} from "./fldngHstryTypes";
|
||||
} from "./fldngHstryUtils";
|
||||
|
||||
const unwrapPayload = <T,>(response: any): T =>
|
||||
(response?.data ?? response) as T;
|
||||
@@ -50,8 +52,8 @@ const resolveOptionText = (
|
||||
|
||||
const resolveFldngHstryMapFeatureFid = (row: FldngHstryTableRow) => {
|
||||
const fid = String(row.fid ?? "").trim();
|
||||
if (fid.startsWith(`${FLOOD_HISTORY_LAYER_TYPE}.`)) return fid;
|
||||
if (fid) return `${FLOOD_HISTORY_LAYER_TYPE}.${fid}`;
|
||||
if (fid.startsWith(`${FLDNG_HSTRY_LAYER_TYPE}.`)) return fid;
|
||||
if (fid) return `${FLDNG_HSTRY_LAYER_TYPE}.${fid}`;
|
||||
|
||||
return "";
|
||||
};
|
||||
@@ -123,7 +125,6 @@ export default function FldngHstryPopupPage() {
|
||||
|
||||
const fldngYearOptions = useMemo(() => buildFldngHstryYearOptions(), []);
|
||||
|
||||
|
||||
const sggOptions = useMemo(
|
||||
() =>
|
||||
filters.ctpvCd === "0"
|
||||
@@ -157,11 +158,15 @@ export default function FldngHstryPopupPage() {
|
||||
...hydrated,
|
||||
ctpvNm:
|
||||
resolveOptionText(hydrated.ctpvCd ?? "", ctpv as any, 2) ||
|
||||
String((hydrated as any).ctpvNm ?? (hydrated as any).ctpv_nm ?? "").trim() ||
|
||||
String(
|
||||
(hydrated as any).ctpvNm ?? (hydrated as any).ctpv_nm ?? "",
|
||||
).trim() ||
|
||||
String(hydrated.ctpvCd ?? "").trim(),
|
||||
sggNm:
|
||||
resolveOptionText(hydrated.sggCd ?? "", sgg as any, 5) ||
|
||||
String((hydrated as any).sggNm ?? (hydrated as any).sgg_nm ?? "").trim() ||
|
||||
String(
|
||||
(hydrated as any).sggNm ?? (hydrated as any).sgg_nm ?? "",
|
||||
).trim() ||
|
||||
String(hydrated.sggCd ?? "").trim(),
|
||||
};
|
||||
});
|
||||
@@ -233,7 +238,7 @@ export default function FldngHstryPopupPage() {
|
||||
{
|
||||
type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
|
||||
payload: {
|
||||
layerNm: FLOOD_HISTORY_LAYER_TYPE,
|
||||
layerNm: FLDNG_HSTRY_LAYER_TYPE,
|
||||
fid: mapFeatureFid,
|
||||
},
|
||||
},
|
||||
@@ -262,8 +267,7 @@ export default function FldngHstryPopupPage() {
|
||||
try {
|
||||
const response = await fetchAllPages({
|
||||
batchSize: FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE,
|
||||
fetchPage: (page, size) =>
|
||||
fetchRows(page + 1, appliedFilters, size),
|
||||
fetchPage: (page, size) => fetchRows(page + 1, appliedFilters, size),
|
||||
getItems: (page) => page.rows,
|
||||
getTotal: (page) => page.total,
|
||||
});
|
||||
@@ -362,7 +366,9 @@ export default function FldngHstryPopupPage() {
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2">
|
||||
<span className="whitespace-nowrap text-[1.3rem] text-label">강우시작일</span>
|
||||
<span className="whitespace-nowrap text-[1.3rem] text-label">
|
||||
강우시작일
|
||||
</span>
|
||||
<Input
|
||||
className="w-[11rem]"
|
||||
type="date"
|
||||
@@ -370,13 +376,32 @@ export default function FldngHstryPopupPage() {
|
||||
min={filters.floodYear ? `${filters.floodYear}-01-01` : undefined}
|
||||
max={filters.floodYear ? `${filters.floodYear}-12-31` : undefined}
|
||||
disabled={!filters.floodYear}
|
||||
onChange={(event) => setFilters((current) => ({ ...current, rainStartDate: event.target.value }))}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
rainStartDate: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2">
|
||||
<span className="whitespace-nowrap text-[1.3rem] text-label">침수지구명</span>
|
||||
<Input type="text" value={filters.districtName} onChange={(event) => setFilters((current) => ({ ...current, districtName: event.target.value }))} onKeyDown={(event) => { if (event.key === "Enter") void handleSearch(1); }} />
|
||||
<span className="whitespace-nowrap text-[1.3rem] text-label">
|
||||
침수지구명
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={filters.districtName}
|
||||
onChange={(event) =>
|
||||
setFilters((current) => ({
|
||||
...current,
|
||||
districtName: event.target.value,
|
||||
}))
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") void handleSearch(1);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2">
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/** 침수흔적 강우분석과 연결된 심선·위선 목록 조회를 담당한다. */
|
||||
|
||||
import { getData } from "@/components/lib/api";
|
||||
|
||||
import type { FldngHstryDetailCacheValue } from "./fldngHstryTypes";
|
||||
|
||||
const normalizeFldngHstryRainAnalysisPayload = (data: any) => {
|
||||
if (!data || typeof data !== "object") return {};
|
||||
return {
|
||||
rain_bgng_tm: data.rainBgngTm ?? data.rain_bgng_tm ?? null,
|
||||
rainBgngTm: data.rainBgngTm ?? data.rain_bgng_tm ?? null,
|
||||
rain_end_tm: data.rainEndTm ?? data.rain_end_tm ?? null,
|
||||
rainEndTm: data.rainEndTm ?? data.rain_end_tm ?? null,
|
||||
rain_tot: data.rainTot ?? data.rain_tot ?? null,
|
||||
rainTot: data.rainTot ?? data.rain_tot ?? null,
|
||||
rain_str: data.rainStr ?? data.rain_str ?? null,
|
||||
rainStr: data.rainStr ?? data.rain_str ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
export const loadFldngHstryDetail = async (
|
||||
fldngHstryFid: string,
|
||||
): Promise<FldngHstryDetailCacheValue> => {
|
||||
const [rainResult, depthResult] = await Promise.allSettled([
|
||||
getData(`v1/fldng/hstry/rain-anls/${encodeURIComponent(fldngHstryFid)}`),
|
||||
getData(`v1/fldng/hstry/depth-map/${encodeURIComponent(fldngHstryFid)}`),
|
||||
]);
|
||||
|
||||
if (rainResult.status === "rejected") {
|
||||
console.warn("침수강우분석 조회 실패:", rainResult.reason);
|
||||
}
|
||||
if (depthResult.status === "rejected") {
|
||||
console.warn("침수깊이지도 조회 실패:", depthResult.reason);
|
||||
}
|
||||
|
||||
const depthPayload =
|
||||
depthResult.status === "fulfilled"
|
||||
? (depthResult.value?.data ?? depthResult.value)
|
||||
: null;
|
||||
const depthItems = Array.isArray(depthPayload)
|
||||
? depthPayload
|
||||
: depthPayload?.depthMaps;
|
||||
const waterLevelItems = Array.isArray(depthPayload?.waterLevelMaps)
|
||||
? depthPayload.waterLevelMaps
|
||||
: [];
|
||||
|
||||
return {
|
||||
rainAnalysis:
|
||||
rainResult.status === "fulfilled"
|
||||
? normalizeFldngHstryRainAnalysisPayload(rainResult.value)
|
||||
: {},
|
||||
fldngHstryDepthRows: Array.isArray(depthItems)
|
||||
? depthItems.filter((row) => row && typeof row === "object")
|
||||
: [],
|
||||
fldngHstryWtlvRows: waterLevelItems.filter(
|
||||
(row: unknown) => row && typeof row === "object",
|
||||
),
|
||||
complete:
|
||||
rainResult.status === "fulfilled" && depthResult.status === "fulfilled",
|
||||
};
|
||||
};
|
||||
@@ -1,238 +1,27 @@
|
||||
/**
|
||||
* 침수흔적 화면에서 사용하는 타입·상수·조회 조건과 feature 캐시 유틸을 제공한다.
|
||||
*/
|
||||
"use client";
|
||||
|
||||
/** 침수흔적 화면에서 사용하는 타입·상수와 화면 설정값을 정의한다. */
|
||||
import type { UiHeadProps } from "@common/business_components/ui";
|
||||
import { appendApiSearchParams } from "@/shared/util/util";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import type Feature from "ol/Feature.js";
|
||||
import VectorSource from "ol/source/Vector.js";
|
||||
|
||||
export const FLOOD_HISTORY_POPUP_KEY = "fldngHstryPopup";
|
||||
export const FLDNG_HSTRY_POPUP_KEY = "fldngHstryPopup";
|
||||
export const FLDNG_HSTRY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT";
|
||||
export const FLOOD_HISTORY_LAYER_TYPE = "tb_ms_fldng_wtlv_map";
|
||||
export const FLOOD_HISTORY_DEPTH_LAYER_TYPE = "tb_ms_fldng_dpth_map";
|
||||
export const FLOOD_HISTORY_PREVIEW_LAYER_TYPE = "tb_sn_flood_trace_mock";
|
||||
export const FLOOD_HISTORY_LAYER_NAME = "침수흔적위선";
|
||||
export const FLOOD_HISTORY_DEPTH_LAYER_NAME = "침수흔적심선";
|
||||
export const FLOOD_HISTORY_ROOT_NAME = "침수흔적도";
|
||||
export const FLOOD_HISTORY_AREA_LAYER_ID = "SN_FLOOD_HISTORY_AREA";
|
||||
export const FLOOD_HISTORY_DEPTH_LAYER_ID = "SN_FLOOD_HISTORY_DEPTH";
|
||||
export const FLDNG_HSTRY_LAYER_TYPE = "tb_ms_fldng_wtlv_map";
|
||||
export const FLDNG_HSTRY_DEPTH_LAYER_TYPE = "tb_ms_fldng_dpth_map";
|
||||
export const FLDNG_HSTRY_PREVIEW_LAYER_TYPE = "tb_sn_flood_trace_mock";
|
||||
export const FLDNG_HSTRY_LAYER_NAME = "침수흔적위선";
|
||||
export const FLDNG_HSTRY_DEPTH_LAYER_NAME = "침수흔적심선";
|
||||
export const FLDNG_HSTRY_ROOT_NAME = "침수흔적도";
|
||||
export const FLDNG_HSTRY_AREA_LAYER_ID = "SN_FLOOD_HISTORY_AREA";
|
||||
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 FLOOD_HISTORY_MIN_YEAR = 2002;
|
||||
export const FLDNG_HSTRY_MIN_YEAR = 2002;
|
||||
|
||||
export type FldngHstryDetailCacheValue = {
|
||||
rainAnalysis: Record<string, unknown>;
|
||||
depthRows: Record<string, unknown>[];
|
||||
waterLevelRows: Record<string, unknown>[];
|
||||
fldngHstryDepthRows: Record<string, unknown>[];
|
||||
fldngHstryWtlvRows: Record<string, unknown>[];
|
||||
complete: boolean;
|
||||
};
|
||||
|
||||
const normalizeFldngHstryRainAnalysisPayload = (data: any) => {
|
||||
if (!data || typeof data !== "object") return {};
|
||||
return {
|
||||
rain_bgng_tm: data.rainBgngTm ?? data.rain_bgng_tm ?? null,
|
||||
rainBgngTm: data.rainBgngTm ?? data.rain_bgng_tm ?? null,
|
||||
rain_end_tm: data.rainEndTm ?? data.rain_end_tm ?? null,
|
||||
rainEndTm: data.rainEndTm ?? data.rain_end_tm ?? null,
|
||||
rain_tot: data.rainTot ?? data.rain_tot ?? null,
|
||||
rainTot: data.rainTot ?? data.rain_tot ?? null,
|
||||
rain_str: data.rainStr ?? data.rain_str ?? null,
|
||||
rainStr: data.rainStr ?? data.rain_str ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
/** 침수흔적 강우분석과 연결된 심선·위선 목록을 함께 조회한다. */
|
||||
export const loadFldngHstryDetail = async (
|
||||
fldngHstryFid: string,
|
||||
): Promise<FldngHstryDetailCacheValue> => {
|
||||
const [rainResult, depthResult] = await Promise.allSettled([
|
||||
getData(
|
||||
`v1/fldng/hstry/rain-anls/${encodeURIComponent(fldngHstryFid)}`,
|
||||
),
|
||||
getData(
|
||||
`v1/fldng/hstry/depth-map/${encodeURIComponent(fldngHstryFid)}`,
|
||||
),
|
||||
]);
|
||||
|
||||
if (rainResult.status === "rejected") {
|
||||
console.warn("침수강우분석 조회 실패:", rainResult.reason);
|
||||
}
|
||||
if (depthResult.status === "rejected") {
|
||||
console.warn("침수깊이지도 조회 실패:", depthResult.reason);
|
||||
}
|
||||
|
||||
const depthPayload =
|
||||
depthResult.status === "fulfilled"
|
||||
? (depthResult.value?.data ?? depthResult.value)
|
||||
: null;
|
||||
const depthItems = Array.isArray(depthPayload)
|
||||
? depthPayload
|
||||
: depthPayload?.depthMaps;
|
||||
const waterLevelItems = Array.isArray(depthPayload?.waterLevelMaps)
|
||||
? depthPayload.waterLevelMaps
|
||||
: [];
|
||||
|
||||
return {
|
||||
rainAnalysis:
|
||||
rainResult.status === "fulfilled"
|
||||
? normalizeFldngHstryRainAnalysisPayload(rainResult.value)
|
||||
: {},
|
||||
depthRows: Array.isArray(depthItems)
|
||||
? depthItems.filter((row) => row && typeof row === "object")
|
||||
: [],
|
||||
waterLevelRows: waterLevelItems.filter(
|
||||
(row: unknown) => row && typeof row === "object",
|
||||
),
|
||||
complete:
|
||||
rainResult.status === "fulfilled" && depthResult.status === "fulfilled",
|
||||
};
|
||||
};
|
||||
|
||||
type FldngHstryGisLayer = {
|
||||
searchLayer?: {
|
||||
getSource?: () => VectorSource | null;
|
||||
} | null;
|
||||
clearSearchLayer: (option: unknown) => void;
|
||||
ensureSearchLayer: (option: unknown) => {
|
||||
set: (key: string, value: unknown) => void;
|
||||
setSource: (source: VectorSource) => void;
|
||||
};
|
||||
showSearchResultsFromGeoJSON: (
|
||||
layerTypeName: string,
|
||||
featureId: string,
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
|
||||
/** 침수흔적 심선·위선의 GeoServer 조회 결과를 화면 생명주기와 분리해 보관한다. */
|
||||
export const fldngHstryFeatureCache = new Map<
|
||||
string,
|
||||
Promise<Feature[]>
|
||||
>();
|
||||
|
||||
/** OpenLayers Feature.clone()이 복사하지 않는 feature ID까지 보존한다. */
|
||||
const cloneFldngHstryFeature = (feature: Feature) => {
|
||||
const clonedFeature = feature.clone();
|
||||
const featureId = feature.getId();
|
||||
if (featureId !== undefined) {
|
||||
clonedFeature.setId(featureId);
|
||||
}
|
||||
return clonedFeature;
|
||||
};
|
||||
|
||||
const displayCachedFldngHstryFeatures = (
|
||||
gisLayer: FldngHstryGisLayer,
|
||||
layerTypeName: string,
|
||||
features: Feature[],
|
||||
) => {
|
||||
const matched = Object.values(GisApp.layerCode ?? {}).find(
|
||||
(item: any) => item?.lyrTyNm === layerTypeName,
|
||||
) as any;
|
||||
if (!matched || features.length === 0) return;
|
||||
|
||||
gisLayer.clearSearchLayer(matched);
|
||||
const layer = gisLayer.ensureSearchLayer(matched);
|
||||
layer.set("gobjTy", matched.gobjTy);
|
||||
const clonedFeatures = features.map(cloneFldngHstryFeature);
|
||||
layer.setSource(new VectorSource({ features: clonedFeatures }));
|
||||
(GisApp.Module.select as any)?.selectFeature(clonedFeatures[0], false);
|
||||
};
|
||||
|
||||
/** 침수흔적 심선·위선을 최초 한 번만 조회하고 이후에는 캐시로 highlight한다. */
|
||||
export const showFldngHstryFeature = async (
|
||||
gisLayer: FldngHstryGisLayer,
|
||||
layerTypeName: string,
|
||||
featureId: string,
|
||||
) => {
|
||||
const cacheKey = `${layerTypeName}:${featureId}`;
|
||||
const cachedRequest = fldngHstryFeatureCache.get(cacheKey);
|
||||
if (cachedRequest) {
|
||||
const cachedFeatures = await cachedRequest;
|
||||
displayCachedFldngHstryFeatures(
|
||||
gisLayer,
|
||||
layerTypeName,
|
||||
cachedFeatures,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await gisLayer.showSearchResultsFromGeoJSON(layerTypeName, featureId);
|
||||
const source = gisLayer.searchLayer?.getSource?.();
|
||||
if (!source) return;
|
||||
|
||||
const featureRequest = new Promise<Feature[]>((resolve, reject) => {
|
||||
const resolveFeatures = () => {
|
||||
const features = source.getFeatures();
|
||||
if (features.length > 0) {
|
||||
resolve(features.map(cloneFldngHstryFeature));
|
||||
} else {
|
||||
reject(new Error("침수흔적 위치 정보가 없습니다."));
|
||||
}
|
||||
};
|
||||
const rejectFeatures = () =>
|
||||
reject(new Error("침수흔적 위치 조회에 실패했습니다."));
|
||||
|
||||
if (source.getFeatures().length > 0) {
|
||||
resolveFeatures();
|
||||
return;
|
||||
}
|
||||
source.once("featuresloadend", resolveFeatures);
|
||||
source.once("featuresloaderror", rejectFeatures);
|
||||
});
|
||||
|
||||
fldngHstryFeatureCache.set(cacheKey, featureRequest);
|
||||
try {
|
||||
await featureRequest;
|
||||
} catch (error) {
|
||||
fldngHstryFeatureCache.delete(cacheKey);
|
||||
console.warn(error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 지도 침수흔적위선·심선이 공통으로 사용하는 침수년도 범위다. */
|
||||
export type FloodHistoryYearRange = {
|
||||
startYear: string;
|
||||
endYear: string;
|
||||
};
|
||||
|
||||
/** 침수년도 설정의 초기 범위는 데이터 최소연도부터 현재년도까지다. */
|
||||
export const buildFloodHistoryDefaultYearRange = (
|
||||
currentYear = new Date().getFullYear(),
|
||||
): FloodHistoryYearRange => ({
|
||||
startYear: String(FLOOD_HISTORY_MIN_YEAR),
|
||||
endYear: String(currentYear),
|
||||
});
|
||||
|
||||
/** 연도 설정값을 4자리 숫자로 정규화하고 유효하지 않으면 기본 범위를 사용한다. */
|
||||
export const normalizeFloodHistoryYearRange = (
|
||||
range?: Partial<FloodHistoryYearRange> | null,
|
||||
fallback: FloodHistoryYearRange = buildFloodHistoryDefaultYearRange(),
|
||||
): FloodHistoryYearRange => {
|
||||
const normalizeYear = (value: unknown) =>
|
||||
String(value ?? "")
|
||||
.replace(/[^0-9]/g, "")
|
||||
.slice(0, 4);
|
||||
const startYear = normalizeYear(range?.startYear);
|
||||
const endYear = normalizeYear(range?.endYear);
|
||||
return {
|
||||
startYear: /^\d{4}$/.test(startYear) ? startYear : fallback.startYear,
|
||||
endYear: /^\d{4}$/.test(endYear) ? endYear : fallback.endYear,
|
||||
};
|
||||
};
|
||||
|
||||
/** 위선·심선 WFS에 동일하게 결합할 침수년도 CQL 조건을 생성한다. */
|
||||
export const buildFloodHistoryYearCql = (
|
||||
range: FloodHistoryYearRange,
|
||||
enabled: boolean,
|
||||
) => {
|
||||
if (!enabled) return "";
|
||||
const normalized = normalizeFloodHistoryYearRange(range);
|
||||
return `(fldng_yr >= '${normalized.startYear}' AND fldng_yr <= '${normalized.endYear}')`;
|
||||
};
|
||||
|
||||
export type FldngHstryYearRange = { startYear: string; endYear: string };
|
||||
export type FldngHstryFilters = {
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
@@ -241,47 +30,44 @@ export type FldngHstryFilters = {
|
||||
rainStartDate: string;
|
||||
districtName: string;
|
||||
};
|
||||
|
||||
export type FldngHstryDepthZone = {
|
||||
zoneId: string;
|
||||
depthLabel: string;
|
||||
color: string;
|
||||
ring: [number, number][];
|
||||
};
|
||||
|
||||
export type FldngHstryApiItem = {
|
||||
fid: string; // FID
|
||||
fldngMapFid?: string; // 침수흔적도지도 FID
|
||||
fldngWtlvMapSn?: number | null; // 침수흔적도지도 일련번호
|
||||
taskClCd?: string; // 업무 구분 코드
|
||||
fldngDstrNm?: string; // 침수재해명
|
||||
exmnYr?: string; // 조사년도
|
||||
avgFldngWtlv?: number | null; // 평균침수위
|
||||
fldngArea?: number | null; // 침수면적
|
||||
dtlOcrnCsCn?: string; // 상세발생원인내용
|
||||
stdgCd?: string; // 표준지역코드
|
||||
ctpvCd?: string; // 시도코드
|
||||
sggCd?: string; // 시군구코드
|
||||
dataSn?: number | null; // 데이터 일련번호
|
||||
bgngYmd?: string; // 시작일자
|
||||
endYmd?: string; // 종료일자
|
||||
fldngBgngHm?: string; // 침수시작시분
|
||||
fldngEndHm?: string; // 침수종료시분
|
||||
fldngCsDtlCd?: string; // 침수원인상세코드
|
||||
fldngDstrctNm?: string; // 침수지구명
|
||||
damAmt?: number | null; // 피해금액
|
||||
fldngHr?: number | null; // 침수시간
|
||||
fldngYr?: string; // 침수년도
|
||||
cstrnFbctnYn?: string; // 공사여부
|
||||
atchFileSn?: number | null; // 첨부파일 일련번호
|
||||
confArea?: number | null; // 확정면적
|
||||
confLen?: number | null; // 확정연장
|
||||
rainBgngTm?: string | null; // 강우시작일시
|
||||
rainEndTm?: string | null; // 강우종료일시
|
||||
rainTot?: number | null; // 총강우량
|
||||
rainStr?: number | null; // 강우강도
|
||||
fid: string;
|
||||
fldngMapFid?: string;
|
||||
fldngWtlvMapSn?: number | null;
|
||||
taskClCd?: string;
|
||||
fldngDstrNm?: string;
|
||||
exmnYr?: string;
|
||||
avgFldngWtlv?: number | null;
|
||||
fldngArea?: number | null;
|
||||
dtlOcrnCsCn?: string;
|
||||
stdgCd?: string;
|
||||
ctpvCd?: string;
|
||||
sggCd?: string;
|
||||
dataSn?: number | null;
|
||||
bgngYmd?: string;
|
||||
endYmd?: string;
|
||||
fldngBgngHm?: string;
|
||||
fldngEndHm?: string;
|
||||
fldngCsDtlCd?: string;
|
||||
fldngDstrctNm?: string;
|
||||
damAmt?: number | null;
|
||||
fldngHr?: number | null;
|
||||
fldngYr?: string;
|
||||
cstrnFbctnYn?: string;
|
||||
atchFileSn?: number | null;
|
||||
confArea?: number | null;
|
||||
confLen?: number | null;
|
||||
rainBgngTm?: string | null;
|
||||
rainEndTm?: string | null;
|
||||
rainTot?: number | null;
|
||||
rainStr?: number | null;
|
||||
};
|
||||
|
||||
export type FldngHstryItem = FldngHstryApiItem & {
|
||||
historyId: string;
|
||||
ctpvNm: string;
|
||||
@@ -311,7 +97,6 @@ export type FldngHstryItem = FldngHstryApiItem & {
|
||||
areaRing: [number, number][];
|
||||
depthZones: FldngHstryDepthZone[];
|
||||
};
|
||||
|
||||
export type FldngHstryTableRow = FldngHstryItem & {
|
||||
rowNo: number;
|
||||
floodYearDisplay: string;
|
||||
@@ -334,7 +119,6 @@ export const FLDNG_HSTRY_DEFAULT_FILTERS: FldngHstryFilters = {
|
||||
rainStartDate: "",
|
||||
districtName: "",
|
||||
};
|
||||
|
||||
export const FLDNG_HSTRY_DEPTH_LEGEND_ITEMS = [
|
||||
{ color: "#fff27a", label: "0.0~0.5m" },
|
||||
{ color: "#c2f13d", label: "0.5~1.0m" },
|
||||
@@ -342,7 +126,6 @@ export const FLDNG_HSTRY_DEPTH_LEGEND_ITEMS = [
|
||||
{ color: "#8f8bff", label: "2.0~5.0m" },
|
||||
{ color: "#f2528b", label: "5.0m 이상" },
|
||||
] as const;
|
||||
|
||||
export const FLDNG_HSTRY_LIST_HEADLIST: UiHeadProps[] = [
|
||||
{ id: "rowNo", title: "순번", widthPercent: 3 },
|
||||
{ id: "floodYearDisplay", title: "침수\n년도", widthPercent: 4 },
|
||||
@@ -356,11 +139,10 @@ export const FLDNG_HSTRY_LIST_HEADLIST: UiHeadProps[] = [
|
||||
{ id: "floodHourDisplay", title: "침수시간", widthPercent: 7 },
|
||||
{ id: "districtName", title: "침수지구명", widthPercent: 10 },
|
||||
{ id: "avgFloodDepthDisplay", title: "평균\n침수위", widthPercent: 7 },
|
||||
{ id: "floodAreaDisplay", title: "침수\n면적\(ha)", widthPercent: 7 },
|
||||
{ id: "floodAreaDisplay", title: "침수\n면적(ha)", widthPercent: 7 },
|
||||
{ id: "exmnYearDisplay", title: "조사년도", widthPercent: 6 },
|
||||
{ id: "damageAmountDisplay", title: "피해금액", widthPercent: 9 },
|
||||
];
|
||||
|
||||
export const FLDNG_HSTRY_DOWNLOAD_HEADERS: Record<string, string> = {
|
||||
rowNo: "순번",
|
||||
floodYearDisplay: "침수년도",
|
||||
@@ -380,370 +162,3 @@ export const FLDNG_HSTRY_DOWNLOAD_HEADERS: Record<string, string> = {
|
||||
exmnYearDisplay: "조사년도",
|
||||
damageAmountDisplay: "피해금액",
|
||||
};
|
||||
|
||||
const toRadian = (degree: number) => (degree * Math.PI) / 180;
|
||||
|
||||
const createIrregularRing = ({
|
||||
centerX,
|
||||
centerY,
|
||||
radiusX,
|
||||
radiusY,
|
||||
scales,
|
||||
rotationDeg = 0,
|
||||
}: {
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
radiusX: number;
|
||||
radiusY: number;
|
||||
scales: number[];
|
||||
rotationDeg?: number;
|
||||
}): [number, number][] => {
|
||||
const rotation = toRadian(rotationDeg);
|
||||
const points = scales.map((scale, index) => {
|
||||
const angle = rotation + (Math.PI * 2 * index) / scales.length;
|
||||
const jitterX = radiusX * scale * Math.cos(angle);
|
||||
const jitterY = radiusY * scale * Math.sin(angle);
|
||||
return [centerX + jitterX, centerY + jitterY] as [number, number];
|
||||
});
|
||||
|
||||
return points.length > 0 ? [...points, points[0]!] : [];
|
||||
};
|
||||
|
||||
const toNumericSeed = (value: string) =>
|
||||
Array.from(value).reduce(
|
||||
(sum, char, index) => sum + char.charCodeAt(0) * (index + 1),
|
||||
0,
|
||||
);
|
||||
|
||||
const buildGeometrySeed = (fid: string) => {
|
||||
const seed = toNumericSeed(fid);
|
||||
return {
|
||||
centerX: 1050000 + (seed % 120000),
|
||||
centerY: 1680000 + (seed % 150000),
|
||||
width: 14000 + (seed % 9000),
|
||||
height: 11000 + (seed % 7000),
|
||||
};
|
||||
};
|
||||
|
||||
const createFldngHstryDepthZones = (
|
||||
historyId: string,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
): FldngHstryDepthZone[] => {
|
||||
return [
|
||||
{
|
||||
zoneId: `${historyId}-depth-1`,
|
||||
depthLabel: "0.0~0.5m",
|
||||
color: "#fff27a",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX - 2600,
|
||||
centerY: centerY - 1200,
|
||||
radiusX: 2900,
|
||||
radiusY: 2300,
|
||||
rotationDeg: -18,
|
||||
scales: [1.02, 0.78, 0.9, 1.08, 0.86, 0.72, 0.94, 1.04],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-2`,
|
||||
depthLabel: "0.5~1.0m",
|
||||
color: "#c2f13d",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 1800,
|
||||
centerY: centerY - 600,
|
||||
radiusX: 3500,
|
||||
radiusY: 2800,
|
||||
rotationDeg: 12,
|
||||
scales: [1.04, 0.88, 1.02, 0.8, 0.92, 1.08, 0.84, 0.95],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-3`,
|
||||
depthLabel: "1.0~2.0m",
|
||||
color: "#67d8ff",
|
||||
ring: createIrregularRing({
|
||||
centerX,
|
||||
centerY: centerY + 1800,
|
||||
radiusX: 4700,
|
||||
radiusY: 3900,
|
||||
rotationDeg: 8,
|
||||
scales: [1.12, 0.86, 0.94, 1.06, 0.82, 0.97, 1.04, 0.9, 1.08, 0.84],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-4`,
|
||||
depthLabel: "2.0~5.0m",
|
||||
color: "#8f8bff",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 3200,
|
||||
centerY: centerY + 3200,
|
||||
radiusX: 2600,
|
||||
radiusY: 2000,
|
||||
rotationDeg: 26,
|
||||
scales: [1.05, 0.82, 1.08, 0.88, 0.96, 0.78, 1.02, 0.9],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-5`,
|
||||
depthLabel: "5.0m 이상",
|
||||
color: "#f2528b",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 400,
|
||||
centerY: centerY - 3600,
|
||||
radiusX: 1200,
|
||||
radiusY: 1050,
|
||||
rotationDeg: -10,
|
||||
scales: [1.0, 0.8, 1.06, 0.84, 0.94, 0.76, 0.98],
|
||||
}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const normalizeDate = (value?: string | null) => {
|
||||
const source = String(value ?? "").trim();
|
||||
if (!/^\d{8}$/.test(source)) return "";
|
||||
return `${source.slice(0, 4)}-${source.slice(4, 6)}-${source.slice(6, 8)}`;
|
||||
};
|
||||
|
||||
const normalizeHm = (value?: string | null) => {
|
||||
const digits = String(value ?? "").replace(/[^0-9]/g, "");
|
||||
if (!digits) return "";
|
||||
if (digits.length === 1) return `0${digits}:00`;
|
||||
if (digits.length === 2) return `${digits}:00`;
|
||||
if (digits.length === 3)
|
||||
return `0${digits.slice(0, 1)}:${digits.slice(1, 3)}`;
|
||||
return `${digits.slice(0, 2)}:${digits.slice(2, 4)}`;
|
||||
};
|
||||
|
||||
const normalizeDateTime = (value?: string | null) => {
|
||||
const source = String(value ?? "").trim();
|
||||
if (!source) return "";
|
||||
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(source)) return source;
|
||||
const date = new Date(source);
|
||||
if (Number.isNaN(date.getTime()))
|
||||
return source.slice(0, 16).replace("T", " ");
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = `${date.getMonth() + 1}`.padStart(2, "0");
|
||||
const dd = `${date.getDate()}`.padStart(2, "0");
|
||||
const hh = `${date.getHours()}`.padStart(2, "0");
|
||||
const mi = `${date.getMinutes()}`.padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
|
||||
};
|
||||
|
||||
const formatNumber = (value?: number | null, digits = 1, suffix = "") => {
|
||||
const numeric = Number(value ?? 0);
|
||||
if (!Number.isFinite(numeric)) return `0${suffix}`;
|
||||
return `${numeric.toFixed(digits)}${suffix}`;
|
||||
};
|
||||
|
||||
const formatInteger = (value?: number | null, suffix = "") => {
|
||||
const numeric = Number(value ?? 0);
|
||||
if (!Number.isFinite(numeric)) return `0${suffix}`;
|
||||
return `${numeric.toLocaleString("ko-KR")}${suffix}`;
|
||||
};
|
||||
|
||||
const formatOptionalNumber = (
|
||||
value?: number | null,
|
||||
digits = 1,
|
||||
suffix = "",
|
||||
) => {
|
||||
if (value === null || value === undefined) return "-";
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return "-";
|
||||
return `${numeric.toFixed(digits)}${suffix}`;
|
||||
};
|
||||
|
||||
const buildCauseDetailName = (item: FldngHstryApiItem) => {
|
||||
const direct = String(item.dtlOcrnCsCn ?? "").trim();
|
||||
if (direct) return direct;
|
||||
const code = String(item.fldngCsDtlCd ?? "").trim();
|
||||
return code ? `원인코드 ${code}` : "-";
|
||||
};
|
||||
|
||||
const buildGradeLabel = (avgFloodDepthM: number) => {
|
||||
if (avgFloodDepthM >= 2) return "3등급";
|
||||
if (avgFloodDepthM >= 1) return "2등급";
|
||||
return "1등급";
|
||||
};
|
||||
|
||||
export const normalizeFldngHstryFilters = (
|
||||
filters: FldngHstryFilters,
|
||||
): FldngHstryFilters => ({
|
||||
ctpvCd: String(filters.ctpvCd ?? "0") || "0",
|
||||
sggCd: String(filters.sggCd ?? "0") || "0",
|
||||
floodYear: String(filters.floodYear ?? "")
|
||||
.replace(/[^0-9]/g, "")
|
||||
.slice(0, 4),
|
||||
disasterName: String(filters.disasterName ?? "").trim(),
|
||||
rainStartDate: String(filters.rainStartDate ?? "").trim(),
|
||||
districtName: String(filters.districtName ?? "").trim(),
|
||||
});
|
||||
|
||||
export const buildFldngHstryYearOptions = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
return Array.from({ length: 30 }, (_, index) => String(currentYear - index));
|
||||
};
|
||||
|
||||
export const buildFldngHstryListQueryString = (
|
||||
filters: FldngHstryFilters,
|
||||
page: number,
|
||||
size: number,
|
||||
) => {
|
||||
const normalized = normalizeFldngHstryFilters(filters);
|
||||
const params = new URLSearchParams({
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
});
|
||||
|
||||
appendApiSearchParams(params, {
|
||||
ctpvCd: normalized.ctpvCd === "0" ? "0" : normalized.ctpvCd.slice(0, 2),
|
||||
sggCd: normalized.sggCd === "0" ? "0" : normalized.sggCd.slice(0, 5),
|
||||
fldngYr: normalized.floodYear,
|
||||
fldngDstrNm: normalized.disasterName,
|
||||
rainBgngTm: normalized.rainStartDate,
|
||||
fldngDstrctNm: normalized.districtName,
|
||||
});
|
||||
return params.toString();
|
||||
};
|
||||
|
||||
export const formatFloodArea = (value: number) =>
|
||||
Number(value ?? 0).toLocaleString("ko-KR", {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export const formatFloodDepth = (value: number) => value.toFixed(2);
|
||||
|
||||
export const attachFldngHstryGeometry = (
|
||||
item: FldngHstryApiItem,
|
||||
): FldngHstryItem => {
|
||||
const historyId = String(item.fid ?? "");
|
||||
const geometrySeed = buildGeometrySeed(historyId);
|
||||
const startDate = normalizeDate(item.bgngYmd);
|
||||
const endDate = normalizeDate(item.endYmd);
|
||||
const startTime = normalizeHm(item.fldngBgngHm);
|
||||
const endTime = normalizeHm(item.fldngEndHm);
|
||||
const avgFloodDepthM = Number(item.avgFldngWtlv ?? 0);
|
||||
const floodAreaHa = Number(item.fldngArea ?? 0);
|
||||
|
||||
return {
|
||||
...item,
|
||||
historyId,
|
||||
ctpvNm: "",
|
||||
sggNm: "",
|
||||
floodYear: String(item.fldngYr ?? "").trim(),
|
||||
exmnYear: String(item.exmnYr ?? "").trim(),
|
||||
disasterName: String(item.fldngDstrNm ?? "").trim(),
|
||||
districtName: String(item.fldngDstrctNm ?? "").trim(),
|
||||
floodAreaHa,
|
||||
avgFloodDepthM,
|
||||
floodDepthM: avgFloodDepthM,
|
||||
gradeLabel: buildGradeLabel(avgFloodDepthM),
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime,
|
||||
floodHour: formatNumber(Number(item.fldngHr ?? 0), 1),
|
||||
causeDetailCode: String(item.fldngCsDtlCd ?? "").trim(),
|
||||
causeDetailName: buildCauseDetailName(item),
|
||||
damageAmount: formatInteger(Number(item.damAmt ?? 0)),
|
||||
rainStartDateTime: normalizeDateTime(item.rainBgngTm),
|
||||
rainEndDateTime: normalizeDateTime(item.rainEndTm),
|
||||
rainStartTime: normalizeDateTime(item.rainBgngTm).slice(11, 16),
|
||||
rainEndTime: normalizeDateTime(item.rainEndTm).slice(11, 16),
|
||||
totalRainfallMm: Number(item.rainTot ?? 0),
|
||||
rainfallIntensityMmHr: Number(item.rainStr ?? 0),
|
||||
areaRing: createIrregularRing({
|
||||
centerX: geometrySeed.centerX,
|
||||
centerY: geometrySeed.centerY,
|
||||
radiusX: geometrySeed.width / 2,
|
||||
radiusY: geometrySeed.height / 2,
|
||||
rotationDeg: 9,
|
||||
scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86],
|
||||
}),
|
||||
depthZones: createFldngHstryDepthZones(
|
||||
historyId,
|
||||
geometrySeed.centerX,
|
||||
geometrySeed.centerY,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const toFldngHstryTableRows = (
|
||||
items: FldngHstryItem[],
|
||||
page: number,
|
||||
pageSize = FLDNG_HSTRY_PAGE_SIZE,
|
||||
): FldngHstryTableRow[] =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
rowNo: (page - 1) * pageSize + index + 1,
|
||||
floodYearDisplay: item.floodYear || "-",
|
||||
floodHourDisplay: item.floodHour || "-",
|
||||
avgFloodDepthDisplay: formatFloodDepth(item.avgFloodDepthM),
|
||||
floodAreaDisplay: formatFloodArea(item.floodAreaHa),
|
||||
exmnYearDisplay: item.exmnYear || "-",
|
||||
damageAmountDisplay: item.damageAmount || "-",
|
||||
rainBgngTmDisplay: item.rainStartDateTime || "-",
|
||||
rainEndTmDisplay: item.rainEndDateTime || "-",
|
||||
rainTotDisplay: formatOptionalNumber(item.rainTot, 1),
|
||||
rainStrDisplay: formatOptionalNumber(item.rainStr, 1),
|
||||
}));
|
||||
|
||||
const resolveFldngHstryNodeName = (node: any) =>
|
||||
String(
|
||||
node?.text ??
|
||||
node?.data?.lyrNm ??
|
||||
node?.data?.name ??
|
||||
node?.lyrNm ??
|
||||
node?.name ??
|
||||
"",
|
||||
).trim();
|
||||
|
||||
const resolveFldngHstryLayerType = (node: any) =>
|
||||
String(
|
||||
node?.data?.lyrTyNm ??
|
||||
node?.data?.lyrTypeNm ??
|
||||
node?.lyrTyNm ??
|
||||
node?.lyrTypeNm ??
|
||||
"",
|
||||
).trim();
|
||||
|
||||
export const isFldngHstryNode = (node: any) => {
|
||||
const name = resolveFldngHstryNodeName(node);
|
||||
const layerType = resolveFldngHstryLayerType(node);
|
||||
return (
|
||||
name === FLOOD_HISTORY_LAYER_NAME ||
|
||||
name === FLOOD_HISTORY_DEPTH_LAYER_NAME ||
|
||||
layerType === FLOOD_HISTORY_LAYER_TYPE ||
|
||||
layerType === FLOOD_HISTORY_DEPTH_LAYER_TYPE ||
|
||||
layerType === FLOOD_HISTORY_PREVIEW_LAYER_TYPE
|
||||
);
|
||||
};
|
||||
|
||||
/** 침수흔적위선 체크 상태를 심선과 분리해 판별한다. */
|
||||
export const isFloodHistoryWaterLevelNode = (node: any) => {
|
||||
const name = resolveFldngHstryNodeName(node);
|
||||
const layerType = resolveFldngHstryLayerType(node);
|
||||
return (
|
||||
name === FLOOD_HISTORY_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE
|
||||
);
|
||||
};
|
||||
|
||||
/** 침수흔적심선 체크 상태를 위선과 분리해 판별한다. */
|
||||
export const isFloodHistoryDepthNode = (node: any) => {
|
||||
const name = resolveFldngHstryNodeName(node);
|
||||
const layerType = resolveFldngHstryLayerType(node);
|
||||
return (
|
||||
name === FLOOD_HISTORY_DEPTH_LAYER_NAME ||
|
||||
layerType === FLOOD_HISTORY_DEPTH_LAYER_TYPE
|
||||
);
|
||||
};
|
||||
|
||||
export const isFldngHstryRootNode = (node: any) =>
|
||||
resolveFldngHstryNodeName(node) === FLOOD_HISTORY_ROOT_NAME;
|
||||
|
||||
export const isFloodHistoryNode = isFldngHstryNode;
|
||||
export const FLOOD_HISTORY_ROW_SELECT_MESSAGE = FLDNG_HSTRY_ROW_SELECT_MESSAGE;
|
||||
export const FLOOD_HISTORY_PAGE_SIZE = FLDNG_HSTRY_PAGE_SIZE;
|
||||
export const FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE =
|
||||
FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE;
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
/** 침수흔적 검색조건·표시값·geometry·레이어 판별 변환을 제공한다. */
|
||||
|
||||
import { appendApiSearchParams } from "@/shared/util/util";
|
||||
|
||||
import {
|
||||
FLDNG_HSTRY_PAGE_SIZE,
|
||||
FLDNG_HSTRY_DEPTH_LAYER_NAME,
|
||||
FLDNG_HSTRY_DEPTH_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_NAME,
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
FLDNG_HSTRY_MIN_YEAR,
|
||||
FLDNG_HSTRY_PREVIEW_LAYER_TYPE,
|
||||
FLDNG_HSTRY_ROOT_NAME,
|
||||
type FldngHstryApiItem,
|
||||
type FldngHstryDepthZone,
|
||||
type FldngHstryFilters,
|
||||
type FldngHstryItem,
|
||||
type FldngHstryTableRow,
|
||||
type FldngHstryYearRange,
|
||||
} from "./fldngHstryTypes";
|
||||
|
||||
export const normalizeFldngHstryWtlvDisplayId = (value: unknown) => {
|
||||
const normalized = String(value ?? "").trim();
|
||||
const prefix = `${FLDNG_HSTRY_LAYER_TYPE}.`;
|
||||
return normalized.startsWith(prefix)
|
||||
? normalized.slice(prefix.length)
|
||||
: normalized;
|
||||
};
|
||||
|
||||
export const normalizeFldngHstryWtlvApiFid = (value: unknown) => {
|
||||
const normalized = String(value ?? "").trim();
|
||||
return normalized ? normalizeFldngHstryWtlvDisplayId(normalized) : "";
|
||||
};
|
||||
|
||||
export const formatFldngHstryDateTime = (
|
||||
dateValue: unknown,
|
||||
timeValue: unknown,
|
||||
) => {
|
||||
const rawDate = String(dateValue ?? "").trim();
|
||||
const rawTime = String(timeValue ?? "").trim();
|
||||
const normalizedDate = /^\d{8}$/.test(rawDate)
|
||||
? `${rawDate.slice(0, 4)}-${rawDate.slice(4, 6)}-${rawDate.slice(6, 8)}`
|
||||
: rawDate;
|
||||
const timeDigits = rawTime.replace(/[^0-9]/g, "");
|
||||
const normalizedTime = !timeDigits
|
||||
? ""
|
||||
: timeDigits.length <= 2
|
||||
? `${timeDigits.padStart(2, "0")}:00`
|
||||
: `${timeDigits.padStart(4, "0").slice(0, 2)}:${timeDigits.padStart(4, "0").slice(2, 4)}`;
|
||||
return [normalizedDate, normalizedTime].filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
export const buildFldngHstryDefaultYearRange = (
|
||||
currentYear = new Date().getFullYear(),
|
||||
): FldngHstryYearRange => ({
|
||||
startYear: String(FLDNG_HSTRY_MIN_YEAR),
|
||||
endYear: String(currentYear),
|
||||
});
|
||||
|
||||
export const normalizeFldngHstryYearRange = (
|
||||
range?: Partial<FldngHstryYearRange> | null,
|
||||
fallback: FldngHstryYearRange = buildFldngHstryDefaultYearRange(),
|
||||
): FldngHstryYearRange => {
|
||||
const normalizeYear = (value: unknown) =>
|
||||
String(value ?? "")
|
||||
.replace(/[^0-9]/g, "")
|
||||
.slice(0, 4);
|
||||
const startYear = normalizeYear(range?.startYear);
|
||||
const endYear = normalizeYear(range?.endYear);
|
||||
return {
|
||||
startYear: /^\d{4}$/.test(startYear) ? startYear : fallback.startYear,
|
||||
endYear: /^\d{4}$/.test(endYear) ? endYear : fallback.endYear,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildFldngHstryYearCql = (
|
||||
range: FldngHstryYearRange,
|
||||
enabled: boolean,
|
||||
) => {
|
||||
if (!enabled) return "";
|
||||
const normalized = normalizeFldngHstryYearRange(range);
|
||||
return `(fldng_yr >= '${normalized.startYear}' AND fldng_yr <= '${normalized.endYear}')`;
|
||||
};
|
||||
|
||||
const toRadian = (degree: number) => (degree * Math.PI) / 180;
|
||||
|
||||
const createIrregularRing = ({
|
||||
centerX,
|
||||
centerY,
|
||||
radiusX,
|
||||
radiusY,
|
||||
scales,
|
||||
rotationDeg = 0,
|
||||
}: {
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
radiusX: number;
|
||||
radiusY: number;
|
||||
scales: number[];
|
||||
rotationDeg?: number;
|
||||
}): [number, number][] => {
|
||||
const rotation = toRadian(rotationDeg);
|
||||
const points = scales.map((scale, index) => {
|
||||
const angle = rotation + (Math.PI * 2 * index) / scales.length;
|
||||
return [
|
||||
centerX + radiusX * scale * Math.cos(angle),
|
||||
centerY + radiusY * scale * Math.sin(angle),
|
||||
] as [number, number];
|
||||
});
|
||||
return points.length > 0 ? [...points, points[0]!] : [];
|
||||
};
|
||||
|
||||
const toNumericSeed = (value: string) =>
|
||||
Array.from(value).reduce(
|
||||
(sum, char, index) => sum + char.charCodeAt(0) * (index + 1),
|
||||
0,
|
||||
);
|
||||
|
||||
const buildGeometrySeed = (fid: string) => {
|
||||
const seed = toNumericSeed(fid);
|
||||
return {
|
||||
centerX: 1050000 + (seed % 120000),
|
||||
centerY: 1680000 + (seed % 150000),
|
||||
width: 14000 + (seed % 9000),
|
||||
height: 11000 + (seed % 7000),
|
||||
};
|
||||
};
|
||||
|
||||
const createFldngHstryDepthZones = (
|
||||
historyId: string,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
): FldngHstryDepthZone[] => [
|
||||
{
|
||||
zoneId: `${historyId}-depth-1`,
|
||||
depthLabel: "0.0~0.5m",
|
||||
color: "#fff27a",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX - 2600,
|
||||
centerY: centerY - 1200,
|
||||
radiusX: 2900,
|
||||
radiusY: 2300,
|
||||
rotationDeg: -18,
|
||||
scales: [1.02, 0.78, 0.9, 1.08, 0.86, 0.72, 0.94, 1.04],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-2`,
|
||||
depthLabel: "0.5~1.0m",
|
||||
color: "#c2f13d",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 1800,
|
||||
centerY: centerY - 600,
|
||||
radiusX: 3500,
|
||||
radiusY: 2800,
|
||||
rotationDeg: 12,
|
||||
scales: [1.04, 0.88, 1.02, 0.8, 0.92, 1.08, 0.84, 0.95],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-3`,
|
||||
depthLabel: "1.0~2.0m",
|
||||
color: "#67d8ff",
|
||||
ring: createIrregularRing({
|
||||
centerX,
|
||||
centerY: centerY + 1800,
|
||||
radiusX: 4700,
|
||||
radiusY: 3900,
|
||||
rotationDeg: 8,
|
||||
scales: [1.12, 0.86, 0.94, 1.06, 0.82, 0.97, 1.04, 0.9, 1.08, 0.84],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-4`,
|
||||
depthLabel: "2.0~5.0m",
|
||||
color: "#8f8bff",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 3200,
|
||||
centerY: centerY + 3200,
|
||||
radiusX: 2600,
|
||||
radiusY: 2000,
|
||||
rotationDeg: 26,
|
||||
scales: [1.05, 0.82, 1.08, 0.88, 0.96, 0.78, 1.02, 0.9],
|
||||
}),
|
||||
},
|
||||
{
|
||||
zoneId: `${historyId}-depth-5`,
|
||||
depthLabel: "5.0m 이상",
|
||||
color: "#f2528b",
|
||||
ring: createIrregularRing({
|
||||
centerX: centerX + 400,
|
||||
centerY: centerY - 3600,
|
||||
radiusX: 1200,
|
||||
radiusY: 1050,
|
||||
rotationDeg: -10,
|
||||
scales: [1, 0.8, 1.06, 0.84, 0.94, 0.76, 0.98],
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const normalizeDate = (value?: string | null) => {
|
||||
const source = String(value ?? "").trim();
|
||||
return /^\d{8}$/.test(source)
|
||||
? `${source.slice(0, 4)}-${source.slice(4, 6)}-${source.slice(6, 8)}`
|
||||
: "";
|
||||
};
|
||||
|
||||
const normalizeHm = (value?: string | null) => {
|
||||
const digits = String(value ?? "").replace(/[^0-9]/g, "");
|
||||
if (!digits) return "";
|
||||
if (digits.length === 1) return `0${digits}:00`;
|
||||
if (digits.length === 2) return `${digits}:00`;
|
||||
if (digits.length === 3)
|
||||
return `0${digits.slice(0, 1)}:${digits.slice(1, 3)}`;
|
||||
return `${digits.slice(0, 2)}:${digits.slice(2, 4)}`;
|
||||
};
|
||||
|
||||
const normalizeDateTime = (value?: string | null) => {
|
||||
const source = String(value ?? "").trim();
|
||||
if (!source) return "";
|
||||
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(source)) return source;
|
||||
const date = new Date(source);
|
||||
if (Number.isNaN(date.getTime()))
|
||||
return source.slice(0, 16).replace("T", " ");
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = `${date.getMonth() + 1}`.padStart(2, "0");
|
||||
const dd = `${date.getDate()}`.padStart(2, "0");
|
||||
const hh = `${date.getHours()}`.padStart(2, "0");
|
||||
const mi = `${date.getMinutes()}`.padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
|
||||
};
|
||||
|
||||
const formatNumber = (value?: number | null, digits = 1, suffix = "") => {
|
||||
const numeric = Number(value ?? 0);
|
||||
return Number.isFinite(numeric)
|
||||
? `${numeric.toFixed(digits)}${suffix}`
|
||||
: `0${suffix}`;
|
||||
};
|
||||
|
||||
const formatInteger = (value?: number | null, suffix = "") => {
|
||||
const numeric = Number(value ?? 0);
|
||||
return Number.isFinite(numeric)
|
||||
? `${numeric.toLocaleString("ko-KR")}${suffix}`
|
||||
: `0${suffix}`;
|
||||
};
|
||||
|
||||
const formatOptionalNumber = (
|
||||
value?: number | null,
|
||||
digits = 1,
|
||||
suffix = "",
|
||||
) => {
|
||||
if (value === null || value === undefined) return "-";
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? `${numeric.toFixed(digits)}${suffix}` : "-";
|
||||
};
|
||||
|
||||
const buildCauseDetailName = (item: FldngHstryApiItem) => {
|
||||
const direct = String(item.dtlOcrnCsCn ?? "").trim();
|
||||
if (direct) return direct;
|
||||
const code = String(item.fldngCsDtlCd ?? "").trim();
|
||||
return code ? `원인코드 ${code}` : "-";
|
||||
};
|
||||
|
||||
const buildGradeLabel = (avgFloodDepthM: number) => {
|
||||
if (avgFloodDepthM >= 2) return "3등급";
|
||||
if (avgFloodDepthM >= 1) return "2등급";
|
||||
return "1등급";
|
||||
};
|
||||
|
||||
export const normalizeFldngHstryFilters = (
|
||||
filters: FldngHstryFilters,
|
||||
): FldngHstryFilters => ({
|
||||
ctpvCd: String(filters.ctpvCd ?? "0") || "0",
|
||||
sggCd: String(filters.sggCd ?? "0") || "0",
|
||||
floodYear: String(filters.floodYear ?? "")
|
||||
.replace(/[^0-9]/g, "")
|
||||
.slice(0, 4),
|
||||
disasterName: String(filters.disasterName ?? "").trim(),
|
||||
rainStartDate: String(filters.rainStartDate ?? "").trim(),
|
||||
districtName: String(filters.districtName ?? "").trim(),
|
||||
});
|
||||
|
||||
export const buildFldngHstryYearOptions = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
return Array.from({ length: 30 }, (_, index) => String(currentYear - index));
|
||||
};
|
||||
|
||||
export const buildFldngHstryListQueryString = (
|
||||
filters: FldngHstryFilters,
|
||||
page: number,
|
||||
size: number,
|
||||
) => {
|
||||
const normalized = normalizeFldngHstryFilters(filters);
|
||||
const params = new URLSearchParams({
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
});
|
||||
appendApiSearchParams(params, {
|
||||
ctpvCd: normalized.ctpvCd === "0" ? "0" : normalized.ctpvCd.slice(0, 2),
|
||||
sggCd: normalized.sggCd === "0" ? "0" : normalized.sggCd.slice(0, 5),
|
||||
fldngYr: normalized.floodYear,
|
||||
fldngDstrNm: normalized.disasterName,
|
||||
rainBgngTm: normalized.rainStartDate,
|
||||
fldngDstrctNm: normalized.districtName,
|
||||
});
|
||||
return params.toString();
|
||||
};
|
||||
|
||||
export const formatFldngHstryArea = (value: number) =>
|
||||
Number(value ?? 0).toLocaleString("ko-KR", {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export const formatFldngHstryDepth = (value: number) => value.toFixed(2);
|
||||
|
||||
export const attachFldngHstryGeometry = (
|
||||
item: FldngHstryApiItem,
|
||||
): FldngHstryItem => {
|
||||
const historyId = String(item.fid ?? "");
|
||||
const geometrySeed = buildGeometrySeed(historyId);
|
||||
const startDate = normalizeDate(item.bgngYmd);
|
||||
const endDate = normalizeDate(item.endYmd);
|
||||
const startTime = normalizeHm(item.fldngBgngHm);
|
||||
const endTime = normalizeHm(item.fldngEndHm);
|
||||
const avgFloodDepthM = Number(item.avgFldngWtlv ?? 0);
|
||||
const floodAreaHa = Number(item.fldngArea ?? 0);
|
||||
return {
|
||||
...item,
|
||||
historyId,
|
||||
ctpvNm: "",
|
||||
sggNm: "",
|
||||
floodYear: String(item.fldngYr ?? "").trim(),
|
||||
exmnYear: String(item.exmnYr ?? "").trim(),
|
||||
disasterName: String(item.fldngDstrNm ?? "").trim(),
|
||||
districtName: String(item.fldngDstrctNm ?? "").trim(),
|
||||
floodAreaHa,
|
||||
avgFloodDepthM,
|
||||
floodDepthM: avgFloodDepthM,
|
||||
gradeLabel: buildGradeLabel(avgFloodDepthM),
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime,
|
||||
floodHour: formatNumber(Number(item.fldngHr ?? 0), 1),
|
||||
causeDetailCode: String(item.fldngCsDtlCd ?? "").trim(),
|
||||
causeDetailName: buildCauseDetailName(item),
|
||||
damageAmount: formatInteger(Number(item.damAmt ?? 0)),
|
||||
rainStartDateTime: normalizeDateTime(item.rainBgngTm),
|
||||
rainEndDateTime: normalizeDateTime(item.rainEndTm),
|
||||
rainStartTime: normalizeDateTime(item.rainBgngTm).slice(11, 16),
|
||||
rainEndTime: normalizeDateTime(item.rainEndTm).slice(11, 16),
|
||||
totalRainfallMm: Number(item.rainTot ?? 0),
|
||||
rainfallIntensityMmHr: Number(item.rainStr ?? 0),
|
||||
areaRing: createIrregularRing({
|
||||
centerX: geometrySeed.centerX,
|
||||
centerY: geometrySeed.centerY,
|
||||
radiusX: geometrySeed.width / 2,
|
||||
radiusY: geometrySeed.height / 2,
|
||||
rotationDeg: 9,
|
||||
scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86],
|
||||
}),
|
||||
depthZones: createFldngHstryDepthZones(
|
||||
historyId,
|
||||
geometrySeed.centerX,
|
||||
geometrySeed.centerY,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const toFldngHstryTableRows = (
|
||||
items: FldngHstryItem[],
|
||||
page: number,
|
||||
pageSize = FLDNG_HSTRY_PAGE_SIZE,
|
||||
): FldngHstryTableRow[] =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
rowNo: (page - 1) * pageSize + index + 1,
|
||||
floodYearDisplay: item.floodYear || "-",
|
||||
floodHourDisplay: item.floodHour || "-",
|
||||
avgFloodDepthDisplay: formatFldngHstryDepth(item.avgFloodDepthM),
|
||||
floodAreaDisplay: formatFldngHstryArea(item.floodAreaHa),
|
||||
exmnYearDisplay: item.exmnYear || "-",
|
||||
damageAmountDisplay: item.damageAmount || "-",
|
||||
rainBgngTmDisplay: item.rainStartDateTime || "-",
|
||||
rainEndTmDisplay: item.rainEndDateTime || "-",
|
||||
rainTotDisplay: formatOptionalNumber(item.rainTot, 1),
|
||||
rainStrDisplay: formatOptionalNumber(item.rainStr, 1),
|
||||
}));
|
||||
|
||||
const resolveFldngHstryNodeName = (node: any) =>
|
||||
String(
|
||||
node?.text ??
|
||||
node?.data?.lyrNm ??
|
||||
node?.data?.name ??
|
||||
node?.lyrNm ??
|
||||
node?.name ??
|
||||
"",
|
||||
).trim();
|
||||
|
||||
const resolveFldngHstryLayerType = (node: any) =>
|
||||
String(
|
||||
node?.data?.lyrTyNm ??
|
||||
node?.data?.lyrTypeNm ??
|
||||
node?.lyrTyNm ??
|
||||
node?.lyrTypeNm ??
|
||||
"",
|
||||
).trim();
|
||||
|
||||
export const isFldngHstryNode = (node: any) => {
|
||||
const name = resolveFldngHstryNodeName(node);
|
||||
const layerType = resolveFldngHstryLayerType(node);
|
||||
return (
|
||||
name === FLDNG_HSTRY_LAYER_NAME ||
|
||||
name === FLDNG_HSTRY_DEPTH_LAYER_NAME ||
|
||||
layerType === FLDNG_HSTRY_LAYER_TYPE ||
|
||||
layerType === FLDNG_HSTRY_DEPTH_LAYER_TYPE ||
|
||||
layerType === FLDNG_HSTRY_PREVIEW_LAYER_TYPE
|
||||
);
|
||||
};
|
||||
|
||||
export const isFldngHstryWtlvNode = (node: any) => {
|
||||
const name = resolveFldngHstryNodeName(node);
|
||||
const layerType = resolveFldngHstryLayerType(node);
|
||||
return (
|
||||
name === FLDNG_HSTRY_LAYER_NAME || layerType === FLDNG_HSTRY_LAYER_TYPE
|
||||
);
|
||||
};
|
||||
|
||||
export const isFldngHstryDepthNode = (node: any) => {
|
||||
const name = resolveFldngHstryNodeName(node);
|
||||
const layerType = resolveFldngHstryLayerType(node);
|
||||
return (
|
||||
name === FLDNG_HSTRY_DEPTH_LAYER_NAME ||
|
||||
layerType === FLDNG_HSTRY_DEPTH_LAYER_TYPE
|
||||
);
|
||||
};
|
||||
|
||||
export const isFldngHstryRootNode = (node: any) =>
|
||||
resolveFldngHstryNodeName(node) === FLDNG_HSTRY_ROOT_NAME;
|
||||
+21
-233
@@ -71,10 +71,11 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import useStore from "@/store/useStore";
|
||||
import ExmnSurveyResultSection from "../exmn/ExmnSurveyResultSection";
|
||||
import {
|
||||
loadFldngHstryDetail,
|
||||
showFldngHstryFeature,
|
||||
type FldngHstryDetailCacheValue,
|
||||
} from "../fldngHstry/fldngHstryTypes";
|
||||
formatFldngHstryDateTime,
|
||||
normalizeFldngHstryWtlvApiFid,
|
||||
normalizeFldngHstryWtlvDisplayId,
|
||||
} from "../fldngHstry/fldngHstryUtils";
|
||||
import { useFldngHstryFeatureClick } from "../fldngHstry/FldngHstryFeatureClick";
|
||||
|
||||
const linkedFacilityHighlightStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
@@ -97,39 +98,6 @@ const linkedFacilityHighlightStyle = new Style({
|
||||
zIndex: 9999,
|
||||
});
|
||||
|
||||
const normalizeFldngWtlvMapDisplayId = (value: unknown) => {
|
||||
const normalized = String(value ?? "").trim();
|
||||
const prefix = "tb_ms_fldng_wtlv_map.";
|
||||
return normalized.startsWith(prefix)
|
||||
? normalized.slice(prefix.length)
|
||||
: normalized;
|
||||
};
|
||||
|
||||
const normalizeFldngWtlvMapApiFid = (value: unknown) => {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized) return "";
|
||||
return normalizeFldngWtlvMapDisplayId(normalized);
|
||||
};
|
||||
|
||||
// 침수흔적도 침수시작일시, 침수종료일시 한줄로 보이도록
|
||||
const formatFldngWtlvDateTime = (dateValue: unknown, timeValue: unknown) => {
|
||||
const rawDate = String(dateValue ?? "").trim();
|
||||
const rawTime = String(timeValue ?? "").trim();
|
||||
|
||||
const normalizedDate = /^\d{8}$/.test(rawDate)
|
||||
? `${rawDate.slice(0, 4)}-${rawDate.slice(4, 6)}-${rawDate.slice(6, 8)}`
|
||||
: rawDate;
|
||||
|
||||
const timeDigits = rawTime.replace(/[^0-9]/g, "");
|
||||
const normalizedTime = !timeDigits
|
||||
? ""
|
||||
: timeDigits.length <= 2
|
||||
? `${timeDigits.padStart(2, "0")}:00`
|
||||
: `${timeDigits.padStart(4, "0").slice(0, 2)}:${timeDigits.padStart(4, "0").slice(2, 4)}`;
|
||||
|
||||
return [normalizedDate, normalizedTime].filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
type MngInstOption = {
|
||||
mngInstCd: string;
|
||||
lwrInstNm: string;
|
||||
@@ -174,24 +142,13 @@ const FeatureClickEventPopup = ({
|
||||
const flowDirectionBasisRequestCacheRef = useRef(
|
||||
new Map<string, Promise<any | null>>(),
|
||||
);
|
||||
const fldngHstryDetailCacheRef = useRef(
|
||||
new Map<string, Promise<FldngHstryDetailCacheValue>>(),
|
||||
);
|
||||
const [mngInstOptions, setMngInstOptions] = useState<MngInstOption[]>([]);
|
||||
const [floodDepthRows, setFloodDepthRows] = useState<
|
||||
Record<string, unknown>[]
|
||||
>([]);
|
||||
const [floodWtlvRows, setFloodWtlvRows] = useState<Record<string, unknown>[]>(
|
||||
[],
|
||||
);
|
||||
const [selectedFloodDepthFid, setSelectedFloodDepthFid] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [selectedFloodWtlvFid, setSelectedFloodWtlvFid] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const floodDepthYearListRef = useRef<HTMLDivElement>(null);
|
||||
const floodWtlvListRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
hasFldngHstryLinkedFeatures,
|
||||
fldngHstryFeatureClickHeadName,
|
||||
loadFldngHstryFeatureClickDetail,
|
||||
resetFldngHstryFeatureClick,
|
||||
} = useFldngHstryFeatureClick();
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
|
||||
// 🎯 관로관리번호 추출
|
||||
@@ -483,7 +440,7 @@ const FeatureClickEventPopup = ({
|
||||
result = "침수흔적위선";
|
||||
setHeadlist(FldngWtlvMapCol);
|
||||
setLayerSpecificNm(
|
||||
normalizeFldngWtlvMapDisplayId(
|
||||
normalizeFldngHstryWtlvDisplayId(
|
||||
featureData.values_.fid ?? featureId,
|
||||
),
|
||||
);
|
||||
@@ -563,7 +520,7 @@ const FeatureClickEventPopup = ({
|
||||
case "tb_ms_fldng_wtlv_map": {
|
||||
result = "침수흔적위선";
|
||||
setHeadlist(FldngWtlvMapCol2);
|
||||
setLayerSpecificNm(normalizeFldngWtlvMapDisplayId(featureData.fid));
|
||||
setLayerSpecificNm(normalizeFldngHstryWtlvDisplayId(featureData.fid));
|
||||
break;
|
||||
}
|
||||
case "tb_ms_fldng_dpth_map": {
|
||||
@@ -637,7 +594,7 @@ const FeatureClickEventPopup = ({
|
||||
formatDisplayData(normalizedRegionData),
|
||||
);
|
||||
if (selectedLayerName === "침수흔적위선") {
|
||||
const fldngBgngDtm = formatFldngWtlvDateTime(
|
||||
const fldngBgngDtm = formatFldngHstryDateTime(
|
||||
formattedData.bgngYmd ??
|
||||
formattedData.bgng_ymd ??
|
||||
data.bgngYmd ??
|
||||
@@ -647,7 +604,7 @@ const FeatureClickEventPopup = ({
|
||||
data.fldngBgngHm ??
|
||||
data.fldng_bgng_hm,
|
||||
);
|
||||
const fldngEndDtm = formatFldngWtlvDateTime(
|
||||
const fldngEndDtm = formatFldngHstryDateTime(
|
||||
formattedData.endYmd ??
|
||||
formattedData.end_ymd ??
|
||||
data.endYmd ??
|
||||
@@ -850,41 +807,20 @@ const FeatureClickEventPopup = ({
|
||||
}
|
||||
|
||||
if (selectedLayerName === "침수흔적위선" && featureId) {
|
||||
setFloodDepthRows([]);
|
||||
setFloodWtlvRows([]);
|
||||
setSelectedFloodDepthFid(null);
|
||||
setSelectedFloodWtlvFid(null);
|
||||
const fldngHstryFid = normalizeFldngWtlvMapApiFid(featureId);
|
||||
resetFldngHstryFeatureClick();
|
||||
const fldngHstryFid = normalizeFldngHstryWtlvApiFid(featureId);
|
||||
if (!fldngHstryFid) {
|
||||
throw new Error("침수흔적도 fid가 비어 있습니다.");
|
||||
}
|
||||
|
||||
const fldngHstryCacheKey = `tb_ms_fldng_wtlv_map:${fldngHstryFid}`;
|
||||
let fldngHstryDetailRequest =
|
||||
fldngHstryDetailCacheRef.current.get(fldngHstryCacheKey);
|
||||
if (!fldngHstryDetailRequest) {
|
||||
fldngHstryDetailRequest = loadFldngHstryDetail(fldngHstryFid);
|
||||
fldngHstryDetailCacheRef.current.set(
|
||||
fldngHstryCacheKey,
|
||||
fldngHstryDetailRequest,
|
||||
);
|
||||
}
|
||||
|
||||
const fldngHstryDetail = await fldngHstryDetailRequest;
|
||||
if (!fldngHstryDetail.complete) {
|
||||
fldngHstryDetailCacheRef.current.delete(fldngHstryCacheKey);
|
||||
}
|
||||
const fldngHstryDetail =
|
||||
await loadFldngHstryFeatureClickDetail(fldngHstryFid);
|
||||
const mergedFldngHstryData = {
|
||||
...formattedData,
|
||||
...fldngHstryDetail.rainAnalysis,
|
||||
};
|
||||
setFetchData(mergedFldngHstryData);
|
||||
setTableData(mergedFldngHstryData);
|
||||
setFloodDepthRows(fldngHstryDetail.depthRows);
|
||||
setFloodWtlvRows(fldngHstryDetail.waterLevelRows);
|
||||
// 최초에는 연계 항목을 선택하지 않고 버튼 목록만 표시한다.
|
||||
setSelectedFloodDepthFid(null);
|
||||
setSelectedFloodWtlvFid(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("데이터 로딩 중 오류:", error);
|
||||
@@ -1089,156 +1025,8 @@ const FeatureClickEventPopup = ({
|
||||
>
|
||||
<VerticalTable
|
||||
headName={
|
||||
layerNm === "침수흔적위선" &&
|
||||
(floodDepthRows.length > 0 || floodWtlvRows.length > 0) ? (
|
||||
<div className="flex w-full min-w-0 flex-col items-start gap-2">
|
||||
<span>기본정보</span>
|
||||
{floodDepthRows.length > 0 && (
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<span className="w-9 shrink-0 text-[1.1rem] font-semibold">
|
||||
심선
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="이전 연도"
|
||||
className="shrink-0 px-1 text-lg leading-none text-foreground"
|
||||
onClick={() =>
|
||||
floodDepthYearListRef.current?.scrollBy({
|
||||
left: -120,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<div
|
||||
ref={floodDepthYearListRef}
|
||||
className="flex min-w-0 flex-1 gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{[...floodDepthRows]
|
||||
.map((row, index) => ({
|
||||
row,
|
||||
index,
|
||||
year: String(
|
||||
row.fldng_yr ?? row.fldngYr ?? row.yr ?? "",
|
||||
).trim(),
|
||||
}))
|
||||
.filter(({ year }) => Boolean(year))
|
||||
.sort((left, right) => {
|
||||
const yearOrder = left.year.localeCompare(
|
||||
right.year,
|
||||
undefined,
|
||||
{ numeric: true },
|
||||
);
|
||||
return yearOrder !== 0
|
||||
? yearOrder
|
||||
: left.index - right.index;
|
||||
})
|
||||
.map(({ row, index, year }) => (
|
||||
<button
|
||||
key={`${String(row.fldng_dpth_map_sn ?? row.fid ?? "row")}-${index}`}
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const depthFid = String(
|
||||
row.fldng_dpth_map_sn ?? row.fid ?? "",
|
||||
).trim();
|
||||
if (depthFid) {
|
||||
setSelectedFloodDepthFid(depthFid);
|
||||
setSelectedFloodWtlvFid(null);
|
||||
const core = GisApp.Module.layer as any;
|
||||
await showFldngHstryFeature(
|
||||
core,
|
||||
"tb_ms_fldng_dpth_map",
|
||||
`tb_ms_fldng_dpth_map.${depthFid.replace(/^tb_ms_fldng_dpth_map\./, "")}`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
className={`shrink-0 rounded border px-3 py-1 text-[1.2rem] font-medium leading-5 ${selectedFloodDepthFid === String(row.fldng_dpth_map_sn ?? row.fid ?? "").trim() ? "border-blue-600 bg-white text-blue-700" : "border-border bg-muted text-muted-foreground"}`}
|
||||
>
|
||||
{year}년
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="다음 연도"
|
||||
className="shrink-0 px-1 text-lg leading-none text-foreground"
|
||||
onClick={() =>
|
||||
floodDepthYearListRef.current?.scrollBy({
|
||||
left: 120,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{floodWtlvRows.length > 0 && (
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<span className="w-9 shrink-0 text-[1.1rem] font-semibold">
|
||||
위선
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="이전 위선"
|
||||
className="shrink-0 px-1 text-lg leading-none text-foreground"
|
||||
onClick={() =>
|
||||
floodWtlvListRef.current?.scrollBy({
|
||||
left: -120,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<div
|
||||
ref={floodWtlvListRef}
|
||||
className="flex min-w-0 flex-1 gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{floodWtlvRows.map((row, index) => {
|
||||
const waterLevelFid = String(row.fid ?? "").trim();
|
||||
const disasterName = String(
|
||||
row.fldng_dstr_nm ?? row.fldngDstrNm ?? "",
|
||||
).trim();
|
||||
return (
|
||||
<button
|
||||
key={`${waterLevelFid || "wtlv"}-${index}`}
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!waterLevelFid) return;
|
||||
setSelectedFloodWtlvFid(waterLevelFid);
|
||||
setSelectedFloodDepthFid(null);
|
||||
const core = GisApp.Module.layer as any;
|
||||
await showFldngHstryFeature(
|
||||
core,
|
||||
"tb_ms_fldng_wtlv_map",
|
||||
`tb_ms_fldng_wtlv_map.${waterLevelFid.replace(/^tb_ms_fldng_wtlv_map\./, "")}`,
|
||||
);
|
||||
}}
|
||||
className={`shrink-0 rounded border px-3 py-1 text-[1.2rem] font-medium leading-5 ${selectedFloodWtlvFid === waterLevelFid ? "border-blue-600 bg-white text-blue-700" : "border-border bg-muted text-muted-foreground"}`}
|
||||
>
|
||||
{disasterName || "재해명 없음"}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="다음 위선"
|
||||
className="shrink-0 px-1 text-lg leading-none text-foreground"
|
||||
onClick={() =>
|
||||
floodWtlvListRef.current?.scrollBy({
|
||||
left: 120,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
layerNm === "침수흔적위선" && hasFldngHstryLinkedFeatures ? (
|
||||
fldngHstryFeatureClickHeadName
|
||||
) : layerNm === "하수관로" && tableData?.elapsedYears ? (
|
||||
<>
|
||||
기본정보{" "}
|
||||
|
||||
@@ -145,23 +145,25 @@ import {
|
||||
type SinkholeYearRange,
|
||||
} from "./components/sinkhole/sinkholeTypes";
|
||||
import {
|
||||
FLOOD_HISTORY_AREA_LAYER_ID,
|
||||
FLOOD_HISTORY_DEPTH_LAYER_NAME,
|
||||
FLOOD_HISTORY_DEPTH_LAYER_TYPE,
|
||||
FLOOD_HISTORY_DEPTH_LAYER_ID,
|
||||
FLOOD_HISTORY_PREVIEW_LAYER_TYPE,
|
||||
FLOOD_HISTORY_LAYER_TYPE,
|
||||
FLOOD_HISTORY_LAYER_NAME,
|
||||
FLOOD_HISTORY_POPUP_KEY,
|
||||
FLDNG_HSTRY_AREA_LAYER_ID,
|
||||
FLDNG_HSTRY_DEPTH_LAYER_NAME,
|
||||
FLDNG_HSTRY_DEPTH_LAYER_TYPE,
|
||||
FLDNG_HSTRY_DEPTH_LAYER_ID,
|
||||
FLDNG_HSTRY_PREVIEW_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_TYPE,
|
||||
FLDNG_HSTRY_LAYER_NAME,
|
||||
FLDNG_HSTRY_POPUP_KEY,
|
||||
FLDNG_HSTRY_ROW_SELECT_MESSAGE,
|
||||
buildFloodHistoryDefaultYearRange,
|
||||
buildFloodHistoryYearCql,
|
||||
isFloodHistoryDepthNode,
|
||||
isFloodHistoryWaterLevelNode,
|
||||
isFldngHstryRootNode,
|
||||
type FloodHistoryYearRange,
|
||||
type FldngHstryYearRange,
|
||||
type FldngHstryItem,
|
||||
} from "./components/fldngHstry/fldngHstryTypes";
|
||||
import {
|
||||
buildFldngHstryDefaultYearRange,
|
||||
buildFldngHstryYearCql,
|
||||
isFldngHstryDepthNode,
|
||||
isFldngHstryWtlvNode,
|
||||
isFldngHstryRootNode,
|
||||
} from "./components/fldngHstry/fldngHstryUtils";
|
||||
import {
|
||||
SBMRG_DNGR_MAP_POPUP_KEY,
|
||||
isSbmrgDngrMapRootNode,
|
||||
@@ -268,7 +270,7 @@ const WATER_RISK_AREA_LAYER_ID = "SN_WATER_RISK_AREA";
|
||||
const WATER_RISK_PIPE_LAYER_ID = "SN_WATER_RISK_PIPE";
|
||||
const WATER_RISK_MANHOLE_LAYER_ID = "SN_WATER_RISK_MANHOLE";
|
||||
const SHORT_TERM_FORECAST_GRID_LAYER_NAME = "초단기예보격자망";
|
||||
const FLOOD_HISTORY_SELECTION_PADDING: [number, number, number, number] = [
|
||||
const FLDNG_HSTRY_SELECTION_PADDING: [number, number, number, number] = [
|
||||
100, 380, 120, 120,
|
||||
];
|
||||
const RIGHT_PANEL_TAB_LIMIT = 5;
|
||||
@@ -1090,7 +1092,7 @@ const getRightPanelFeatureTitle = (data: any) => {
|
||||
case "tb_ms_swage_prcs_zone":
|
||||
return "처리구역";
|
||||
case "tb_ms_fldng_wtlv_map":
|
||||
return FLOOD_HISTORY_LAYER_NAME;
|
||||
return FLDNG_HSTRY_LAYER_NAME;
|
||||
case "tb_ms_fldng_dpth_map":
|
||||
return "침수흔적심선";
|
||||
case "om_dt_untrt_swg_rain":
|
||||
@@ -3035,28 +3037,27 @@ const Map = () => {
|
||||
useState(true);
|
||||
const [isFldngHstryChecked, setIsFldngHstryChecked] = useState(false);
|
||||
// 침수흔적위선과 심선은 하나의 연도 범위를 공유하지만 체크 상태는 독립적으로 관리한다.
|
||||
const [isFloodWtlvChecked, setIsFloodWtlvChecked] = useState(false);
|
||||
const [isFloodDepthChecked, setIsFloodDepthChecked] = useState(false);
|
||||
const [isFldngHstryWtlvChecked, setIsFldngHstryWtlvChecked] = useState(false);
|
||||
const [isFldngHstryDepthChecked, setIsFldngHstryDepthChecked] =
|
||||
useState(false);
|
||||
// 심선은 6개 깊이 등급 중 사용자가 실제 체크한 lyrSn만 필터 레이어로 생성한다.
|
||||
const [checkedFloodDepthLayerKeys, setCheckedFloodDepthLayerKeys] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [floodYearRange, setFloodYearRange] = useState<FloodHistoryYearRange>(
|
||||
() => buildFloodHistoryDefaultYearRange(),
|
||||
);
|
||||
const [isFloodYearFilterEnabled, setIsFloodYearFilterEnabled] =
|
||||
const [checkedFldngHstryDepthLayerKeys, setCheckedFldngHstryDepthLayerKeys] =
|
||||
useState<string[]>([]);
|
||||
const [fldngHstryYearRange, setFldngHstryYearRange] =
|
||||
useState<FldngHstryYearRange>(() => buildFldngHstryDefaultYearRange());
|
||||
const [isFldngHstryYearFilterEnabled, setIsFldngHstryYearFilterEnabled] =
|
||||
useState(true);
|
||||
const [isFloodYearSettingVisible, setIsFloodYearSettingVisible] =
|
||||
const [isFldngHstryYearSettingVisible, setIsFldngHstryYearSettingVisible] =
|
||||
useState(true);
|
||||
// 닫힌 설정 카드는 레이어가 완전히 해제된 뒤 다시 체크될 때만 자동으로 연다.
|
||||
const floodLayerCheckedRef = useRef(false);
|
||||
const fldngHstryLayerCheckedRef = useRef(false);
|
||||
const sinkholeLayerCheckedRef = useRef(false);
|
||||
const agedPipeLayerCheckedRef = useRef(false);
|
||||
const exmnPplLayerCheckedRef = useRef(false);
|
||||
const exmnMnhlLayerCheckedRef = useRef(false);
|
||||
const waterDepthLayerCheckedRef = useRef(false);
|
||||
// 체크 등급 변경과 연도 필터 변경을 구분해 연도 변경 때만 기존 선택 상세를 정리한다.
|
||||
const floodYearFilterSignatureRef = useRef("");
|
||||
const fldngHstryYearFilterSignatureRef = useRef("");
|
||||
const [isExmnPplChecked, setIsExmnPplChecked] = useState(false);
|
||||
const [isExmnMnhlChecked, setIsExmnMnhlChecked] = useState(false);
|
||||
const [exmnPplFilters, setExmnPplFilters] = useState<ExmnMapFilters>(() => ({
|
||||
@@ -5344,7 +5345,7 @@ const Map = () => {
|
||||
// Select 컬렉션에 다시 넣어도 강조가 렌더링되지 않는다. 탭으로 돌아올
|
||||
// 때는 FID로 search-layer를 복원한 뒤 로드된 feature를 선택하게 한다.
|
||||
if (
|
||||
layerTypeName === FLOOD_HISTORY_LAYER_TYPE ||
|
||||
layerTypeName === FLDNG_HSTRY_LAYER_TYPE ||
|
||||
layerTypeName === "tb_ms_fldng_dpth_map"
|
||||
) {
|
||||
clearSinkholeFocusedPipeLayer();
|
||||
@@ -5404,9 +5405,9 @@ const Map = () => {
|
||||
source,
|
||||
zIndex: 9993,
|
||||
});
|
||||
layer.set("id", FLOOD_HISTORY_AREA_LAYER_ID);
|
||||
layer.set("lyrNm", FLOOD_HISTORY_LAYER_NAME);
|
||||
layer.set("lyrTyNm", FLOOD_HISTORY_PREVIEW_LAYER_TYPE);
|
||||
layer.set("id", FLDNG_HSTRY_AREA_LAYER_ID);
|
||||
layer.set("lyrNm", FLDNG_HSTRY_LAYER_NAME);
|
||||
layer.set("lyrTyNm", FLDNG_HSTRY_PREVIEW_LAYER_TYPE);
|
||||
layer.set("clickPsblYn", "N");
|
||||
gisMaps.current.addLayer(layer);
|
||||
fldngHstryAreaLayerRef.current = layer;
|
||||
@@ -5424,9 +5425,9 @@ const Map = () => {
|
||||
source,
|
||||
zIndex: 9992,
|
||||
});
|
||||
layer.set("id", FLOOD_HISTORY_DEPTH_LAYER_ID);
|
||||
layer.set("lyrNm", FLOOD_HISTORY_LAYER_NAME);
|
||||
layer.set("lyrTyNm", FLOOD_HISTORY_PREVIEW_LAYER_TYPE);
|
||||
layer.set("id", FLDNG_HSTRY_DEPTH_LAYER_ID);
|
||||
layer.set("lyrNm", FLDNG_HSTRY_LAYER_NAME);
|
||||
layer.set("lyrTyNm", FLDNG_HSTRY_PREVIEW_LAYER_TYPE);
|
||||
layer.set("clickPsblYn", "N");
|
||||
gisMaps.current.addLayer(layer);
|
||||
fldngHstryDepthLayerRef.current = layer;
|
||||
@@ -5480,7 +5481,7 @@ const Map = () => {
|
||||
const geometry = new Polygon([detail.areaRing]);
|
||||
gisMaps.current.getView?.().fit?.(geometry.getExtent(), {
|
||||
duration: 350,
|
||||
padding: FLOOD_HISTORY_SELECTION_PADDING,
|
||||
padding: FLDNG_HSTRY_SELECTION_PADDING,
|
||||
maxZoom: 15,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
@@ -5508,7 +5509,7 @@ const Map = () => {
|
||||
);
|
||||
|
||||
const openFldngHstryPopupWindow = useCallback(() => {
|
||||
const popup = openWindowPopup(FLOOD_HISTORY_POPUP_KEY as WindowPopupKey);
|
||||
const popup = openWindowPopup(FLDNG_HSTRY_POPUP_KEY as WindowPopupKey);
|
||||
if (!popup) {
|
||||
alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요.");
|
||||
return null;
|
||||
@@ -6053,7 +6054,7 @@ const Map = () => {
|
||||
// 제거되지 않는다. 마지막 탭을 닫는 경우와 다른 일반 feature 탭으로
|
||||
// 전환되는 경우 모두 잔상으로 남지 않도록 여기서 명시적으로 제거한다.
|
||||
if (
|
||||
closingLayerTypeName === FLOOD_HISTORY_LAYER_TYPE ||
|
||||
closingLayerTypeName === FLDNG_HSTRY_LAYER_TYPE ||
|
||||
closingLayerTypeName === "tb_ms_fldng_dpth_map"
|
||||
) {
|
||||
const layerModule = GisApp.Module.layer as any;
|
||||
@@ -6478,33 +6479,33 @@ const Map = () => {
|
||||
agedPipeLayerCheckedRef.current = agedPipeChecked;
|
||||
waterDepthLayerCheckedRef.current = waterDepthChecked;
|
||||
setIsAgedPipeChecked(agedPipeChecked);
|
||||
const floodWtlvChecked = checkedNodesData.some((node) =>
|
||||
isFloodHistoryWaterLevelNode(node),
|
||||
const fldngHstryWtlvChecked = checkedNodesData.some((node) =>
|
||||
isFldngHstryWtlvNode(node),
|
||||
);
|
||||
const checkedFloodDepthNodes = checkedNodesData.filter((node) =>
|
||||
isFloodHistoryDepthNode(node),
|
||||
const checkedFldngHstryDepthNodes = checkedNodesData.filter((node) =>
|
||||
isFldngHstryDepthNode(node),
|
||||
);
|
||||
const checkedDepthLayerKeys = Array.from(
|
||||
const checkedFldngHstryDepthLayerKeys = Array.from(
|
||||
new Set(
|
||||
checkedFloodDepthNodes
|
||||
checkedFldngHstryDepthNodes
|
||||
.map((node) => String(node?.data?.lyrSn ?? node?.id ?? "").trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
).sort((left, right) => Number(left) - Number(right));
|
||||
const floodDepthChecked = checkedDepthLayerKeys.length > 0;
|
||||
const fldngHstryChecked = floodWtlvChecked || floodDepthChecked;
|
||||
setIsFloodWtlvChecked(floodWtlvChecked);
|
||||
setIsFloodDepthChecked(floodDepthChecked);
|
||||
setCheckedFloodDepthLayerKeys((currentKeys) =>
|
||||
currentKeys.join(",") === checkedDepthLayerKeys.join(",")
|
||||
const fldngHstryDepthChecked = checkedFldngHstryDepthLayerKeys.length > 0;
|
||||
const fldngHstryChecked = fldngHstryWtlvChecked || fldngHstryDepthChecked;
|
||||
setIsFldngHstryWtlvChecked(fldngHstryWtlvChecked);
|
||||
setIsFldngHstryDepthChecked(fldngHstryDepthChecked);
|
||||
setCheckedFldngHstryDepthLayerKeys((currentKeys) =>
|
||||
currentKeys.join(",") === checkedFldngHstryDepthLayerKeys.join(",")
|
||||
? currentKeys
|
||||
: checkedDepthLayerKeys,
|
||||
: checkedFldngHstryDepthLayerKeys,
|
||||
);
|
||||
setIsFldngHstryChecked(fldngHstryChecked);
|
||||
if (fldngHstryChecked && !floodLayerCheckedRef.current) {
|
||||
setIsFloodYearSettingVisible(true);
|
||||
if (fldngHstryChecked && !fldngHstryLayerCheckedRef.current) {
|
||||
setIsFldngHstryYearSettingVisible(true);
|
||||
}
|
||||
floodLayerCheckedRef.current = fldngHstryChecked;
|
||||
fldngHstryLayerCheckedRef.current = fldngHstryChecked;
|
||||
if (!fldngHstryChecked) {
|
||||
clearFldngHstrySelection();
|
||||
}
|
||||
@@ -6693,18 +6694,18 @@ const Map = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isFldngHstryChecked && isFloodYearSettingVisible) {
|
||||
if (isFldngHstryChecked && isFldngHstryYearSettingVisible) {
|
||||
nextItems.push({
|
||||
key: "fldngHstry",
|
||||
label: "침수흔적도",
|
||||
order: resolveSettingOrder("fldngHstry", 9997),
|
||||
content: (
|
||||
<FldngHstryLayerSettingPanel
|
||||
yearRange={floodYearRange}
|
||||
enabled={isFloodYearFilterEnabled}
|
||||
onYearRangeChange={setFloodYearRange}
|
||||
onEnabledChange={setIsFloodYearFilterEnabled}
|
||||
onClose={() => setIsFloodYearSettingVisible(false)}
|
||||
yearRange={fldngHstryYearRange}
|
||||
enabled={isFldngHstryYearFilterEnabled}
|
||||
onYearRangeChange={setFldngHstryYearRange}
|
||||
onEnabledChange={setIsFldngHstryYearFilterEnabled}
|
||||
onClose={() => setIsFldngHstryYearSettingVisible(false)}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -6798,7 +6799,7 @@ const Map = () => {
|
||||
exmnMnhlFilters,
|
||||
exmnPplFilters,
|
||||
handleApplyAgedPipeFilters,
|
||||
floodYearRange,
|
||||
fldngHstryYearRange,
|
||||
isAgedPipeChecked,
|
||||
isAgedPipeSettingEnabled,
|
||||
isAgedPipeSettingVisible,
|
||||
@@ -6809,8 +6810,8 @@ const Map = () => {
|
||||
isExmnPplSettingEnabled,
|
||||
isExmnPplSettingVisible,
|
||||
isFldngHstryChecked,
|
||||
isFloodYearFilterEnabled,
|
||||
isFloodYearSettingVisible,
|
||||
isFldngHstryYearFilterEnabled,
|
||||
isFldngHstryYearSettingVisible,
|
||||
isSinkholeChecked,
|
||||
isSinkholeSettingEnabled,
|
||||
isSinkholeSettingVisible,
|
||||
@@ -6934,60 +6935,60 @@ const Map = () => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
const yearCql = buildFloodHistoryYearCql(
|
||||
floodYearRange,
|
||||
isFloodYearFilterEnabled,
|
||||
const yearCql = buildFldngHstryYearCql(
|
||||
fldngHstryYearRange,
|
||||
isFldngHstryYearFilterEnabled,
|
||||
);
|
||||
const yearFilterSignature = `${floodYearRange.startYear}:${floodYearRange.endYear}:${isFloodYearFilterEnabled}`;
|
||||
const yearFilterSignature = `${fldngHstryYearRange.startYear}:${fldngHstryYearRange.endYear}:${isFldngHstryYearFilterEnabled}`;
|
||||
if (
|
||||
floodYearFilterSignatureRef.current &&
|
||||
floodYearFilterSignatureRef.current !== yearFilterSignature
|
||||
fldngHstryYearFilterSignatureRef.current &&
|
||||
fldngHstryYearFilterSignatureRef.current !== yearFilterSignature
|
||||
) {
|
||||
clearFldngHstrySelection();
|
||||
}
|
||||
floodYearFilterSignatureRef.current = yearFilterSignature;
|
||||
fldngHstryYearFilterSignatureRef.current = yearFilterSignature;
|
||||
|
||||
/** 기준 레이어 필터를 보존하면서 선택한 침수년도 조건을 결합한다. */
|
||||
const resolveFloodFilter = (baseLayerCode: any) => {
|
||||
const resolveFldngHstryFilter = (baseLayerCode: any) => {
|
||||
const baseFilter = String(baseLayerCode?.flterSql ?? "").trim();
|
||||
return [baseFilter && `(${baseFilter})`, yearCql]
|
||||
.filter(Boolean)
|
||||
.join(" AND ");
|
||||
};
|
||||
|
||||
type FloodLayerSpec = {
|
||||
type FldngHstryLayerSpec = {
|
||||
id: string;
|
||||
layerType: string;
|
||||
layerName: string;
|
||||
baseLayerCode: any;
|
||||
filterSql: string;
|
||||
};
|
||||
const desiredLayers: FloodLayerSpec[] = [];
|
||||
const desiredLayers: FldngHstryLayerSpec[] = [];
|
||||
const layerCodes = Object.values(GisApp.layerCode ?? {}) as any[];
|
||||
|
||||
if (isFloodWtlvChecked) {
|
||||
if (isFldngHstryWtlvChecked) {
|
||||
const baseLayerCode = layerCodes.find(
|
||||
(item) =>
|
||||
item?.lyrTyNm === FLOOD_HISTORY_LAYER_TYPE && item?.actlLyrYn === "Y",
|
||||
item?.lyrTyNm === FLDNG_HSTRY_LAYER_TYPE && item?.actlLyrYn === "Y",
|
||||
);
|
||||
if (baseLayerCode) {
|
||||
desiredLayers.push({
|
||||
id: FLDNG_HSTRY_WFS_LAYER_ID,
|
||||
layerType: FLOOD_HISTORY_LAYER_TYPE,
|
||||
layerName: baseLayerCode?.lyrNm || FLOOD_HISTORY_LAYER_NAME,
|
||||
layerType: FLDNG_HSTRY_LAYER_TYPE,
|
||||
layerName: baseLayerCode?.lyrNm || FLDNG_HSTRY_LAYER_NAME,
|
||||
baseLayerCode,
|
||||
filterSql: resolveFloodFilter(baseLayerCode),
|
||||
filterSql: resolveFldngHstryFilter(baseLayerCode),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isFloodDepthChecked) {
|
||||
const selectedDepthLayerKeys = new Set(checkedFloodDepthLayerKeys);
|
||||
if (isFldngHstryDepthChecked) {
|
||||
const selectedDepthLayerKeys = new Set(checkedFldngHstryDepthLayerKeys);
|
||||
layerCodes
|
||||
.filter((item) => {
|
||||
const layerKey = String(item?.lyrSn ?? item?.id ?? "").trim();
|
||||
return (
|
||||
item?.lyrTyNm === FLOOD_HISTORY_DEPTH_LAYER_TYPE &&
|
||||
item?.lyrTyNm === FLDNG_HSTRY_DEPTH_LAYER_TYPE &&
|
||||
item?.actlLyrYn === "Y" &&
|
||||
selectedDepthLayerKeys.has(layerKey)
|
||||
);
|
||||
@@ -6998,26 +6999,27 @@ const Map = () => {
|
||||
).trim();
|
||||
desiredLayers.push({
|
||||
id: `${FLDNG_DEPTH_WFS_LAYER_ID_PREFIX}_${layerKey}`,
|
||||
layerType: FLOOD_HISTORY_DEPTH_LAYER_TYPE,
|
||||
layerName: baseLayerCode?.lyrNm || FLOOD_HISTORY_DEPTH_LAYER_NAME,
|
||||
layerType: FLDNG_HSTRY_DEPTH_LAYER_TYPE,
|
||||
layerName: baseLayerCode?.lyrNm || FLDNG_HSTRY_DEPTH_LAYER_NAME,
|
||||
baseLayerCode,
|
||||
filterSql: resolveFloodFilter(baseLayerCode),
|
||||
filterSql: resolveFldngHstryFilter(baseLayerCode),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const desiredLayerIds = new Set(desiredLayers.map((item) => item.id));
|
||||
const mapLayers = [
|
||||
...(gisMaps.current?.getLayers?.()?.getArray?.() ?? []),
|
||||
];
|
||||
const isManagedFloodLayerId = (id: string) =>
|
||||
const mapLayers = [...(gisMaps.current?.getLayers?.()?.getArray?.() ?? [])];
|
||||
const isManagedFldngHstryLayerId = (id: string) =>
|
||||
id === FLDNG_HSTRY_WFS_LAYER_ID ||
|
||||
id.startsWith(`${FLDNG_DEPTH_WFS_LAYER_ID_PREFIX}_`);
|
||||
|
||||
// 체크 해제된 등급만 삭제하고, 계속 체크된 등급은 지도에 그대로 유지한다.
|
||||
mapLayers.forEach((mapLayer: any) => {
|
||||
const layerId = String(mapLayer?.get?.("id") ?? "");
|
||||
if (isManagedFloodLayerId(layerId) && !desiredLayerIds.has(layerId)) {
|
||||
if (
|
||||
isManagedFldngHstryLayerId(layerId) &&
|
||||
!desiredLayerIds.has(layerId)
|
||||
) {
|
||||
gisMaps.current?.removeLayer?.(mapLayer);
|
||||
}
|
||||
});
|
||||
@@ -7032,7 +7034,9 @@ const Map = () => {
|
||||
?.find(
|
||||
(mapLayer: any) => String(mapLayer?.get?.("id") ?? "") === spec.id,
|
||||
);
|
||||
if (existingLayer?.get?.("floodFilterSignature") === filterSignature) {
|
||||
if (
|
||||
existingLayer?.get?.("fldngHstryFilterSignature") === filterSignature
|
||||
) {
|
||||
existingLayer?.setVisible?.(true);
|
||||
return;
|
||||
}
|
||||
@@ -7056,7 +7060,7 @@ const Map = () => {
|
||||
layer?.set?.("id", spec.id);
|
||||
layer?.set?.("lyrNm", spec.layerName);
|
||||
layer?.set?.("lyrTyNm", spec.layerType);
|
||||
layer?.set?.("floodFilterSignature", filterSignature);
|
||||
layer?.set?.("fldngHstryFilterSignature", filterSignature);
|
||||
});
|
||||
|
||||
// 전용 필터 레이어가 담당하는 유형의 원본 레이어만 숨겨 중복 표출을 막는다.
|
||||
@@ -7071,19 +7075,19 @@ const Map = () => {
|
||||
const layerType = String(mapLayer?.get?.("lyrTyNm") ?? "");
|
||||
if (
|
||||
activeFilteredTypes.has(layerType) &&
|
||||
!isManagedFloodLayerId(layerId)
|
||||
!isManagedFldngHstryLayerId(layerId)
|
||||
) {
|
||||
mapLayer?.setVisible?.(false);
|
||||
}
|
||||
});
|
||||
}, [
|
||||
checkedFloodDepthLayerKeys,
|
||||
checkedFldngHstryDepthLayerKeys,
|
||||
clearFldngHstrySelection,
|
||||
floodYearRange,
|
||||
fldngHstryYearRange,
|
||||
isFldngHstryChecked,
|
||||
isFloodDepthChecked,
|
||||
isFloodWtlvChecked,
|
||||
isFloodYearFilterEnabled,
|
||||
isFldngHstryDepthChecked,
|
||||
isFldngHstryWtlvChecked,
|
||||
isFldngHstryYearFilterEnabled,
|
||||
mapReady,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user