Files
frontend/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryFeatureClick.tsx
T

331 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 화면명: 침수흔적위선 연계정보
* 우측 정보 화면의 심선·위선 목록, 선택 상태와 화면 생명주기 캐시를 관리한다.
*/
"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,
};
};