331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
/**
|
||
* 화면명: 침수흔적위선 연계정보
|
||
* 우측 정보 화면의 심선·위선 목록, 선택 상태와 화면 생명주기 캐시를 관리한다.
|
||
*/
|
||
"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,
|
||
};
|
||
};
|