Files
frontend/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx
T

1230 lines
43 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.
/**
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창
*
* 📄 FeatureClickEventPopup
* 시설물(피처) 클릭시 오른쪽 팝업창 컴포넌트
*
* - 지도 시설물(피처) 클릭 시 팝업 표시 및 데이터 로딩
* - 레이어 유형에 따른 동적 테이블 및 헤드리스트 설정
* - 자체입력정보 조회 및 저장 기능 (하수관로, 우수토실, 하수펌프장, 하수맨홀)
* - 코드 매핑 및 데이터 포맷팅 처리
* - 상류관로 검색, 침입수/RDII 분석 차트 표시
* - 모달을 통한 저장 확인 및 API 연동
*
* 🔗 사용 API
* - 하수관로 자체입력정보 조회: GET /api/v1/fcltppl/{fcltMngNo}/{mngInstCd} (sewerage-service)
* - 우수토실 자체입력정보 조회: GET /api/v1/fcltstofcbr/{fcltMngNo}/{mngInstCd} (sewerage-service)
* - 하수펌프장 자체입력정보 조회: GET /api/v1/fcltpump/{fcltMngNo}/{mngInstCd} (sewerage-service)
* - 하수맨홀 자체입력정보 조회: GET /api/v1/fcltmhl/{fcltMngNo}/{mngInstCd} (sewerage-service)
* - 하수관로 자체입력정보 저장: POST /api/v1/fcltppl (sewerage-service)
* - 우수토실 자체입력정보 저장: POST /api/v1/fcltstofcbr (sewerage-service)
* - 하수펌프장 자체입력정보 저장: POST /api/v1/fcltpump (sewerage-service)
* - 하수맨홀 자체입력정보 저장: POST /api/v1/fcltmhl (sewerage-service)
*/
"use client";
import { getData, postData } from "@/components/lib/api";
import { VerticalTable } from "@/components/ui/verticalTable";
import { useGisContext } from "@common/business_components/ui/map/Gis";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { Button } from "@common/components/ui";
import { useEffect, useRef, useState, useMemo } from "react";
import Style from "ol/style/Style.js";
import Fill from "ol/style/Fill.js";
import Stroke from "ol/style/Stroke.js";
import CircleStyle from "ol/style/Circle.js";
import { getLayerContentComponent } from "../common";
import {
FldngWtlvMapCol,
FldngWtlvMapCol2,
FldngDpthMapCol,
SwtrmtFcltCol,
SwtrmtFcltCol2,
SwtrmtPrcsZoneCol,
SwtrmtPrcsZoneCol2,
untrtSwgRainCol1,
untrtSwgRainCol2,
MsSwagePplCol,
MsSwagePplCol2,
ExclnSewagePumpCol,
ExclnSewagePumpCol2,
DrtwSewagePumpCol,
DrtwSewagePumpCol2,
MsSwageMhlCol,
MsSwageMhlCol2,
stofcbrCol,
stofcbrCol2,
MS_SWAGE_MHL_CODES,
DRTW_SEWAGE_PUMP_USE_CODES,
} from "../common/table";
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
import { codeMapper } from "../common/codeMapper";
import LoadingOverlay from "@/components/ui/LoadingBar";
import { searchUpperPpls } from "./smh";
import {
getElapsedYearsDisplayText,
isInvalidInstallYmd,
} from "../agedPipe/agedPipeUtils";
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
import { useAlertStore } from "@/shared/lib";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import useStore from "@/store/useStore";
import ExmnSurveyResultSection from "../exmn/ExmnSurveyResultSection";
const linkedFacilityHighlightStyle = new Style({
image: new CircleStyle({
radius: 18,
fill: new Fill({
color: "rgba(255, 235, 59, 0.35)",
}),
stroke: new Stroke({
color: "rgba(255, 193, 7, 0.45)",
width: 2,
}),
}),
fill: new Fill({
color: "rgba(255, 235, 59, 0.2)",
}),
stroke: new Stroke({
color: "rgba(255, 193, 7, 0.35)",
width: 2.5,
}),
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 normalizeRainAnalysisPayload = (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,
};
};
// 침수흔적도 침수시작일시, 침수종료일시 한줄로 보이도록
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;
};
const FeatureClickEventPopup = ({
onClose,
featureData,
surveyMode = false,
}: {
onClose: () => void;
featureData: any;
surveyMode?: boolean;
}) => {
const alert = useAlertStore((state) => state.setMessage);
const { confirm } = useConfirm();
const { gisMap } = useGisContext() as unknown as {
checkedLayerIds: React.MutableRefObject<number[]>;
treeData: { lyrSn: number }[] | null;
gisCore: any;
gisMap: any;
};
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
const [layerSpecificNm, setLayerSpecificNm] = useState<string>(""); // 레이어 이름 상태
const [tableData, setTableData] = useState<any>(null); // 피처 데이터
const [headlist, setHeadlist] = useState<{ id: string; title: string }[]>([]); // 컬럼리스트 변경상태
const [headlist2, setHeadlist2] = useState<{ id: string; title: string }[]>(
[],
); // 컬럼리스트 변경상태
const [buttonChk, setButtonChk] = useState(true);
const [fetchData, setFetchData] = useState<any>(); // api 호출여부
const contentRef = useRef<HTMLDivElement>(null);
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
const [isClicked, setIsClicked] = useState(true); // 클릭 여부 상태
const [isLoading, setIsLoading] = useState(false); // 로딩 상태
const [chmbrXcnts, setChmbrXcnts] = useState<string>(""); // 로딩 상태
const [chmbrYdnts, setChmbrYdnts] = useState<string>(""); // 로딩 상태
const linkedFacilityFeatureRef = useRef<any>(null);
const linkedFacilityOriginalStyleRef = useRef<any>(null);
const flowDirectionBasisRequestCacheRef = useRef(
new Map<string, Promise<any | null>>(),
);
const [mngInstOptions, setMngInstOptions] = useState<MngInstOption[]>([]);
const [floodDepthRows, setFloodDepthRows] = useState<
Record<string, unknown>[]
>([]);
const [selectedFloodDepthYear, setSelectedFloodDepthYear] = useState<
string | null
>(null);
const floodDepthYearListRef = useRef<HTMLDivElement>(null);
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
// 🎯 관로관리번호 추출
const pplMngNo =
featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || "";
const featureId =
typeof featureData.getId === "function"
? featureData.getId()
: featureData.fid;
const selectedLayerTypeName = featureData?.values_
? featureData?.id_?.split(".")?.[0]
: featureData?.lyrTypeNm;
const isPipeFeature = selectedLayerTypeName === "tb_ms_swage_ppl";
// 🎯 데이터 전처리 메모이제이션
const processedData = useMemo(() => {
if (!featureData?.values_) return null;
return {
...featureData.values_,
discharge_pipe_spec: `직경: ${featureData.values_.pipe_diameter || "-"} / 높이: ${featureData.values_.pipe_hg || "-"} / 길이: ${featureData.values_.pipe_lt || "-"}`,
};
}, [featureData?.values_]);
useEffect(() => {
let isMounted = true;
if (isPipeFeature) {
setMngInstOptions([]);
return () => {
isMounted = false;
};
}
const fetchMngInstOptions = async () => {
try {
const result = await getData("v1/fcltppl/mngInsts");
if (!isMounted) return;
const sortedOptions = Array.isArray(result)
? [...result].sort((a, b) =>
String(a?.lwrInstNm ?? a?.mngInstCd ?? "").localeCompare(
String(b?.lwrInstNm ?? b?.mngInstCd ?? ""),
"ko",
),
)
: [];
setMngInstOptions(sortedOptions);
} catch (error) {
console.error("관리기관 목록 조회 실패:", error);
if (isMounted) setMngInstOptions([]);
}
};
fetchMngInstOptions();
return () => {
isMounted = false;
};
}, [isPipeFeature]);
useEffect(() => {
if (ctpv.length === 0) void fetchCtpv();
if (sgg.length === 0) void fetchSgg();
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
const mngInstNameMap = useMemo(() => {
const map = new Map<string, string>();
mngInstOptions.forEach((option) => {
const code = String(option.mngInstCd ?? "").trim();
if (!code) return;
map.set(code, option.lwrInstNm || code);
});
return map;
}, [mngInstOptions]);
const withMngInstName = (data: any) => {
if (!data || typeof data !== "object") return data;
const mngInstCd = String(data.mng_inst_cd ?? data.mngInstCd ?? "").trim();
if (!mngInstCd) return data;
return {
...data,
mngInstNm:
mngInstNameMap.get(mngInstCd) ??
data.mngInstNm ??
data.mng_inst_nm ??
mngInstCd,
};
};
const withResolvedRegionNames = (data: any) => {
if (!data || typeof data !== "object") return data;
const stdgCd = String(data.stdg_cd ?? data.stdgCd ?? "").trim();
const ctpvCd = String(
data.ctpv_cd ?? data.ctpvCd ?? stdgCd.slice(0, 2),
).trim();
const sggCd = String(
data.sgg_cd ?? data.sggCd ?? stdgCd.slice(0, 5),
).trim();
const resolvedCtpvNm =
ctpv.find((item) => String(item.val).trim() === ctpvCd)?.text ?? "";
const resolvedSggNm =
sgg.find((item) => String(item.val).trim() === sggCd)?.text ?? "";
return {
...data,
ctpv_nm: data.ctpv_nm ?? data.ctpvNm ?? resolvedCtpvNm,
ctpvNm: data.ctpvNm ?? data.ctpv_nm ?? resolvedCtpvNm,
sgg_nm: data.sgg_nm ?? data.sggNm ?? resolvedSggNm,
sggNm: data.sggNm ?? data.sgg_nm ?? resolvedSggNm,
};
};
const hasDetailValue = (value: unknown) => {
if (value === null || value === undefined) return false;
if (typeof value === "string") return value.trim() !== "";
return true;
};
const isValidDetailIdentifier = (value: unknown) => {
if (typeof value !== "string") return false;
const normalizedValue = value.trim();
if (!normalizedValue) return false;
if (normalizedValue.toLowerCase() === "null") return false;
if (normalizedValue.toLowerCase() === "undefined") return false;
return true;
};
const mergeDetailData = (baseData: any, detailData: any) => {
if (!detailData || typeof detailData !== "object") return baseData;
const meaningfulDetailData = Object.entries(detailData).reduce<
Record<string, unknown>
>((acc, [key, value]) => {
if (hasDetailValue(value)) acc[key] = value;
return acc;
}, {});
return {
...(baseData ?? {}),
...meaningfulDetailData,
};
};
const clearLinkedFacilityHighlight = () => {
if (linkedFacilityFeatureRef.current) {
linkedFacilityFeatureRef.current.setStyle(
linkedFacilityOriginalStyleRef.current ?? undefined,
);
linkedFacilityFeatureRef.current.changed?.();
}
linkedFacilityFeatureRef.current = null;
linkedFacilityOriginalStyleRef.current = null;
};
// 하수처리구역에 연결된 하수처리시설 피처를 찾아 기존 아이콘 위에 노랑 하이라이트를 덧씌운다.
const highlightLinkedFacility = (facilityCode: string | null | undefined) => {
clearLinkedFacilityHighlight();
if (!gisMap || !facilityCode) return;
const normalizedFacilityCode = String(facilityCode).trim();
const layers = gisMap.getLayers().getArray();
for (const layer of layers) {
const source = layer?.getSource?.();
if (!source?.getFeatures) continue;
const features = source.getFeatures();
const targetFeature = features.find((feature: any) => {
const featureId = feature?.getId?.();
const isFacilityLayer =
typeof featureId === "string" &&
featureId.startsWith("tb_il_swage_prcs_fclt.");
const featureFacilityCode = feature?.get?.("swtrmt_fclt_cd");
return (
isFacilityLayer &&
String(featureFacilityCode ?? "").trim() === normalizedFacilityCode
);
});
if (targetFeature) {
const originalFeatureStyle = targetFeature.getStyle?.();
const featureLayer = targetFeature.getLayer?.();
const layerStyleFn = featureLayer?.getStyleFunction?.() ?? null;
linkedFacilityOriginalStyleRef.current = originalFeatureStyle;
linkedFacilityFeatureRef.current = targetFeature;
targetFeature.setStyle((feature: any, resolution: number) => {
let styles: any[] = [];
if (typeof originalFeatureStyle === "function") {
const originalStyles = originalFeatureStyle(feature, resolution);
if (Array.isArray(originalStyles)) styles.push(...originalStyles);
else if (originalStyles) styles.push(originalStyles);
} else if (originalFeatureStyle) {
if (Array.isArray(originalFeatureStyle))
styles.push(...originalFeatureStyle);
else styles.push(originalFeatureStyle);
} else if (layerStyleFn) {
const layerStyles = layerStyleFn(feature, resolution);
if (Array.isArray(layerStyles)) styles.push(...layerStyles);
else if (layerStyles) styles.push(layerStyles);
}
styles.push(linkedFacilityHighlightStyle);
return styles;
});
targetFeature.changed?.();
return;
}
}
};
// 피쳐데이터의 현재 레이어 이름 가져오는 함수
const getSelectedLayers = () => {
let result = null;
const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기
let featureLayer: any = null; // 피처가 속한 레이어를 찾기 위한 변수
layers.some((layer: any) => {
const source = layer.getSource();
// if (source && featureData && featureData.fid) {
// featureLayer = layer; // 피처가 속한 레이어 찾기
// }
if (
source &&
source.hasFeature &&
featureData &&
typeof featureData.getId === "function" &&
source.hasFeature(featureData)
) {
featureLayer = layer; // 피처가 속한 레이어 찾기
return true; // 반복 종료
}
return false;
});
if (featureData["ol_uid"]) {
// Gis 레이어 클릭 이벤트
setIsClicked(true);
const lyrTyNm = featureData.id_.split(".")[0];
switch (lyrTyNm) {
case "tb_ms_swage_ppl": {
result = "하수관로";
setHeadlist(MsSwagePplCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
break;
}
case "tb_ms_swage_mhl": {
result = "하수맨홀";
setHeadlist(MsSwageMhlCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
break;
}
case "tb_ms_swage_pmstn": {
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(
(code) => code.text === "오수펌프장",
);
if (
drtwPumpCode &&
featureData.values_.usg_cd.trim() === drtwPumpCode.val
) {
setIsDrtwSewagePump(true);
setHeadlist(DrtwSewagePumpCol);
} else {
setIsDrtwSewagePump(false);
setHeadlist(ExclnSewagePumpCol);
}
setLayerSpecificNm(featureData.values_.fclt_mng_no);
result = "하수펌프장";
break;
}
case "tb_il_swage_prcs_fclt": {
result = "하수처리시설";
setHeadlist(SwtrmtFcltCol);
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
break;
}
case "tb_ms_swage_prcs_zone": {
result = "하수처리구역";
setHeadlist(SwtrmtPrcsZoneCol);
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
break;
}
case "tb_ms_fldng_wtlv_map": {
result = "침수흔적위선";
setHeadlist(FldngWtlvMapCol);
setLayerSpecificNm(
normalizeFldngWtlvMapDisplayId(
featureData.values_.fid ?? featureId,
),
);
break;
}
case "tb_ms_fldng_dpth_map": {
result = "침수흔적심선";
setHeadlist(FldngDpthMapCol);
setLayerSpecificNm(
String(featureId ?? "").replace(/^tb_ms_fldng_dpth_map\./, ""),
);
break;
}
case "tb_ms_stofcbr": {
result = "우수토실";
setHeadlist(stofcbrCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
break;
}
case "om_dt_untrt_swg_rain": {
result = "중점우수토실";
setHeadlist(untrtSwgRainCol1);
setHeadlist2(untrtSwgRainCol2);
setLayerSpecificNm(featureData.values_.manage_no);
break;
}
default: {
result = featureLayer ? featureLayer.get("lyrNm") : null;
break;
}
}
} else {
// SearchModal 클릭 이벤트
setIsClicked(false);
const lyrTyNm = featureData.lyrTypeNm;
switch (lyrTyNm) {
case "tb_ms_swage_ppl": {
result = "하수관로";
setHeadlist(MsSwagePplCol2);
setLayerSpecificNm(featureData.fcltMngNo ?? featureData.swtrmtFcltCd);
break;
}
case "tb_ms_swage_mhl": {
result = "하수맨홀";
setHeadlist(MsSwageMhlCol2);
setLayerSpecificNm(featureData.fcltMngNo);
break;
}
case "tb_ms_swage_pmstn": {
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(
(code) => code.text === "오수펌프장",
);
if (drtwPumpCode && featureData.usgCd.trim() === drtwPumpCode.val) {
setIsDrtwSewagePump(false);
setHeadlist(ExclnSewagePumpCol2);
} else {
setIsDrtwSewagePump(true);
setHeadlist(DrtwSewagePumpCol2);
}
setLayerSpecificNm(featureData.fcltMngNo);
result = "하수펌프장";
break;
}
case "tb_il_swage_prcs_fclt": {
result = "하수처리시설";
setHeadlist(SwtrmtFcltCol2);
setLayerSpecificNm(featureData.swtrmtFcltCd);
break;
}
case "tb_ms_swage_prcs_zone": {
result = "하수처리구역";
setHeadlist(SwtrmtPrcsZoneCol2);
setLayerSpecificNm(featureData.swtrmtFcltCd);
break;
}
case "tb_ms_fldng_wtlv_map": {
result = "침수흔적위선";
setHeadlist(FldngWtlvMapCol2);
setLayerSpecificNm(normalizeFldngWtlvMapDisplayId(featureData.fid));
break;
}
case "tb_ms_fldng_dpth_map": {
result = "침수흔적심선";
setHeadlist(FldngDpthMapCol);
setLayerSpecificNm(
String(featureData.fid ?? featureId ?? "").replace(
/^tb_ms_fldng_dpth_map\./,
"",
),
);
break;
}
case "tb_ms_stofcbr": {
result = "우수토실";
setHeadlist(stofcbrCol2);
setLayerSpecificNm(featureData.fcltMngNo);
break;
}
}
}
return result;
};
useEffect(() => {
const fetchData = async () => {
if (!featureId) return;
try {
const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기
setLayerNm(selectedLayerName); // 상태 업데이트
let data;
if (featureData["ol_uid"])
data = featureData.values_; // Gis 레이어 클릭 이벤트일 때
else data = featureData; // SearchModal 클릭 이벤트일 때
// 하수맨홀인 경우 용도코드를 텍스트로 변환
if (selectedLayerName === "하수맨홀" && data) {
// mqlt_cd 값 변환 (재질코드)
if (data.mqlt_cd) {
const materialCodeItem = MS_SWAGE_MHL_CODES.find(
(code) => code.val === data.mqlt_cd.trim(),
);
if (materialCodeItem) data.mqlt_cd = `${materialCodeItem.text}`;
}
} else {
// 중점우수토실인 경우 구분 값
if (selectedLayerName === "중점우수토실" && data) {
if (data.mesure_spot_se) {
const mesureSpotSe = [
{ id: "0", text: "합류식" },
{ id: "1", text: "분류식" },
];
const mesureSpotSeItem = mesureSpotSe.find(
(code) => code.id === data.mesure_spot_se.trim(),
);
if (mesureSpotSeItem) data.mesure_spot_se = mesureSpotSeItem.text;
}
}
}
// 🎯 기본 데이터 포맷팅
const normalizedRegionData =
selectedLayerName === "침수흔적위선" ||
selectedLayerName === "침수흔적심선"
? withResolvedRegionNames(data)
: data;
const formattedData = withMngInstName(
formatDisplayData(normalizedRegionData),
);
if (selectedLayerName === "침수흔적위선") {
const fldngBgngDtm = formatFldngWtlvDateTime(
formattedData.bgngYmd ??
formattedData.bgng_ymd ??
data.bgngYmd ??
data.bgng_ymd,
formattedData.fldngBgngHm ??
formattedData.fldng_bgng_hm ??
data.fldngBgngHm ??
data.fldng_bgng_hm,
);
const fldngEndDtm = formatFldngWtlvDateTime(
formattedData.endYmd ??
formattedData.end_ymd ??
data.endYmd ??
data.end_ymd,
formattedData.fldngEndHm ??
formattedData.fldng_end_hm ??
data.fldngEndHm ??
data.fldng_end_hm,
);
formattedData.fldng_bgng_dtm = fldngBgngDtm;
formattedData.fldngBgngDtm = fldngBgngDtm;
formattedData.fldng_end_dtm = fldngEndDtm;
formattedData.fldngEndDtm = fldngEndDtm;
}
if (selectedLayerName === "하수관로") {
const installYmd = data.instl_ymd ?? data.instlYmd;
formattedData.elapsedYears = getElapsedYearsDisplayText(installYmd);
if (isInvalidInstallYmd(installYmd)) {
formattedData.instl_ymd = "확인불가";
formattedData.instlYmd = "확인불가";
}
}
let mergedDetailData = formattedData;
setFetchData(formattedData);
setTableData(formattedData);
switch (selectedLayerName) {
case "하수관로":
setButtonChk(true);
// ✅ codeMapper 사용하여 API 결과도 변환
{
const initialPipeData = withMngInstName(
codeMapper({
layerName: selectedLayerName,
data: { ...formattedData },
}),
);
setFetchData(initialPipeData);
setTableData(initialPipeData);
}
break;
case "하수맨홀":
setButtonChk(true);
// ✅ codeMapper 사용하여 API 결과도 변환
setTableData(
codeMapper({
layerName: selectedLayerName,
data: { ...formattedData },
}),
);
break;
case "하수펌프장":
setButtonChk(true);
setTableData(
codeMapper({
layerName: selectedLayerName,
data: { ...formattedData },
}),
);
break;
case "하수처리시설":
setButtonChk(false);
break;
case "하수처리구역":
setButtonChk(false);
break;
case "침수흔적위선":
setButtonChk(false);
break;
case "우수토실":
setButtonChk(true);
setIsMainSsc(false);
break;
case "중점우수토실":
setButtonChk(false);
setIsMainSsc(true);
// 🎯 processedData도 포맷팅하여 설정
const processedFormattedData = formatDisplayData(processedData);
setFetchData([processedFormattedData]);
setTableData(processedFormattedData);
// setIsLoading(false); // 기본 렌더링 완료
setTableData(
codeMapper({
layerName: selectedLayerName,
data: { ...processedFormattedData },
}),
);
setChmbrXcnts(data.chmbr_xcnts);
setChmbrYdnts(data.chmbr_ydnts);
break;
default:
break;
}
// 자체입력정보 API 엔드포인트 선택
let apiEndpoint = "";
const fcltMngNo = String(
featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || "",
).trim();
const mngInstCd = String(
featureData?.values_?.mng_inst_cd || featureData?.mngInstCd || "",
).trim();
const canRequestDetail =
isValidDetailIdentifier(fcltMngNo) &&
isValidDetailIdentifier(mngInstCd);
if (selectedLayerName === "하수관로")
apiEndpoint = `v1/fcltppl/${fcltMngNo}/${mngInstCd}`;
else if (selectedLayerName === "우수토실")
apiEndpoint = `v1/fcltstofcbr/${fcltMngNo}/${mngInstCd}`;
else if (selectedLayerName === "하수펌프장")
apiEndpoint = `v1/fcltpump/${fcltMngNo}/${mngInstCd}`;
else if (selectedLayerName === "하수맨홀")
apiEndpoint = `v1/fcltmhl/${fcltMngNo}/${mngInstCd}`;
// API에서 상세 데이터 가져오기
if (apiEndpoint && canRequestDetail) {
try {
const result = await getData(apiEndpoint);
if (result) {
mergedDetailData = mergeDetailData(mergedDetailData, result);
const processedApiData = withMngInstName(
codeMapper({
layerName: selectedLayerName,
data: mergedDetailData,
}),
);
setFetchData(processedApiData);
setTableData(processedApiData);
}
} catch (apiError) {
void apiError;
console.warn("Pipe detail fallback kept initial feature data.");
}
}
if (selectedLayerName === "하수관로" && canRequestDetail) {
try {
const escapeCqlLiteral = (value: string) =>
value.replace(/'/g, "''");
const requestCacheKey = `${fcltMngNo}::${mngInstCd}`;
const params = new URLSearchParams({
lyrTypeNm: "tb_ms_swage_ppl",
includeFlowDirectionBasis: "true",
cqlFiter:
`fclt_mng_no='${escapeCqlLiteral(fcltMngNo)}' and ` +
`mng_inst_cd='${escapeCqlLiteral(mngInstCd)}'`,
page: "0",
size: "1",
});
let flowDirectionRequest =
flowDirectionBasisRequestCacheRef.current.get(requestCacheKey);
if (!flowDirectionRequest) {
flowDirectionRequest = getData(`v1/fclts?${params.toString()}`)
.then((response) =>
Array.isArray(response?.data) ? response.data[0] : null,
)
.catch((error) => {
flowDirectionBasisRequestCacheRef.current.delete(
requestCacheKey,
);
throw error;
});
flowDirectionBasisRequestCacheRef.current.set(
requestCacheKey,
flowDirectionRequest,
);
}
const enrichedPipe = await flowDirectionRequest;
if (enrichedPipe) {
mergedDetailData = mergeDetailData(
mergedDetailData,
enrichedPipe,
);
const processedPipeData = withMngInstName(
codeMapper({
layerName: selectedLayerName,
data: mergedDetailData,
}),
);
setFetchData(processedPipeData);
setTableData(processedPipeData);
}
} catch (flowDirectionError) {
console.warn(
"관로 유하방향 근거 조회 실패, 기존 상세정보를 유지합니다.",
flowDirectionError,
);
}
}
if (selectedLayerName === "침수흔적위선" && featureId) {
const depthLookupKey = normalizeFldngWtlvMapApiFid(featureId);
setFloodDepthRows([]);
setSelectedFloodDepthYear(null);
try {
const normalizedFloodMapFid =
normalizeFldngWtlvMapApiFid(featureId);
if (!normalizedFloodMapFid) {
throw new Error("침수흔적도 fid가 비어 있습니다.");
}
const rainAnalysis = await getData(
`v1/fldng/hstry/rain-anls/${encodeURIComponent(normalizedFloodMapFid)}`,
);
if (rainAnalysis) {
const mergedData = {
...formattedData,
...normalizeRainAnalysisPayload(rainAnalysis),
};
setFetchData(mergedData);
setTableData(mergedData);
}
} catch (rainError) {
console.warn("침수강우분석 조회 실패:", rainError);
}
try {
if (depthLookupKey) {
const depthRows = await getData(
`v1/fldng/hstry/depth-map/${encodeURIComponent(depthLookupKey)}`,
);
const depthPayload = depthRows?.data ?? depthRows;
const normalizedDepthRows = Array.isArray(depthPayload)
? depthPayload.filter((row) => row && typeof row === "object")
: [];
setFloodDepthRows(normalizedDepthRows);
const years = normalizedDepthRows
.map((row) =>
String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim(),
)
.filter(Boolean);
console.log("depth years:", years);
// 최초에는 연도를 선택하지 않고 목록만 표시한다.
setSelectedFloodDepthYear(null);
}
} catch (depthError) {
console.warn("침수깊이지도 조회 실패:", depthError);
}
}
} catch (error) {
console.error("데이터 로딩 중 오류:", error);
}
};
fetchData();
}, [featureId, processedData, mngInstNameMap]);
useEffect(() => {
const layerTypeNm = featureData?.values_
? featureData?.id_?.split(".")?.[0]
: featureData?.lyrTypeNm;
if (layerTypeNm === "tb_ms_swage_prcs_zone") {
const facilityCode =
featureData?.values_?.swtrmt_fclt_cd ?? featureData?.swtrmtFcltCd;
highlightLinkedFacility(facilityCode);
return () => {
clearLinkedFacilityHighlight();
};
}
clearLinkedFacilityHighlight();
return () => {
clearLinkedFacilityHighlight();
};
}, [featureData, gisMap]);
// 저장 시 모달 띄우기
const handleSaveClick = async () => {
const shouldSave = await confirm("정보를 저장하시겠습니까?");
if (!shouldSave) return;
btnClickEvent();
};
// 저장 버튼 이벤트
const btnClickEvent = async () => {
const bodyData: Record<string, any> = {};
if (buttonChk && contentRef.current) {
const inputElements = contentRef.current.querySelectorAll("input");
const selectElements = contentRef.current.querySelectorAll("select");
// 카멜케이스 변환 함수
const toCamelCase = (str: string) =>
str.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
// select 태그의 값 가져오기
selectElements.forEach((select) => {
const camelCaseId = toCamelCase(select.id);
bodyData[camelCaseId] = select.value;
});
// // input 태그의 값 가져오기
inputElements.forEach((input) => {
const camelCaseId = toCamelCase(input.id);
bodyData[camelCaseId] = input.value;
});
// 레이어명에 따라 API 엔드포인트 분기처리
let apiEndpoint = "";
let result = null;
if (layerNm === "하수관로") apiEndpoint = "v1/fcltppl";
else if (layerNm === "우수토실") apiEndpoint = "v1/fcltstofcbr";
else if (layerNm === "하수펌프장") apiEndpoint = "v1/fcltpump";
else if (layerNm === "하수맨홀") apiEndpoint = "v1/fcltmhl";
bodyData["fcltMngNo"] =
featureData.values_.fclt_mng_no.toString() || null;
bodyData["mngInstCd"] =
featureData.values_.mng_inst_cd.toString() || null;
// 빈 키 제거
if (bodyData[""] !== undefined) delete bodyData[""];
// 🎯 우수토실일 경우 prrtymngYn 값 변환
if (layerNm === "우수토실" && bodyData.prrtymngYn) {
if (bodyData.prrtymngYn === "중점우수토실") {
bodyData.prrtymngYn = "Y";
} else if (bodyData.prrtymngYn === "우수토실") {
bodyData.prrtymngYn = "N";
}
}
try {
result = await postData(apiEndpoint, bodyData);
if (result) {
alert("저장되었습니다");
setFetchData(result.data || result);
} else {
alert("저장에 실패했습니다");
console.error("API 응답 오류:", result);
}
} catch (error) {
alert("저장 중 오류가 발생했습니다");
console.error("API 요청 오류:", error);
}
}
};
// 레이어명에 따라 상세페이지 렌더링
const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null;
return (
<div
id="popup"
className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[350px] flex-col overflow-hidden border-r border-border bg-background transition-all duration-300"
>
<LoadingOverlay isLoading={isLoading} />
<div className="card-header relative flex items-center justify-between bg-primary p-4 pr-12">
<div className="flex flex-1 items-center space-x-4">
{surveyMode && (
<h3 className="whitespace-nowrap text-2xl font-medium text-primary-foreground">
조사관리 <br />
관로번호 :{" "}
{featureData?.get?.("gisManagementNo") ??
featureData?.values_?.gisManagementNo ??
featureData?.gisManagementNo ??
layerSpecificNm}
</h3>
)}
<h3
className={`${surveyMode ? "hidden" : ""} whitespace-nowrap text-2xl font-medium text-primary-foreground`}
>
{layerNm}
{tableData?.usg_cd && <>({tableData.usg_cd})</>} 정보 <br />
관리번호 : {layerSpecificNm}
</h3>
</div>
<div
className={`flex shrink-0 items-center space-x-3 ${layerNm === "하수관로" ? "mr-8" : ""}`}
>
{/* 하수관로일 때만 Infil/RDII 버튼 표시 */}
{layerNm === "하수관로" && (
<>
<Button
className="px-2"
onClick={() => {
const popupWindow = openWindowPopup("infilChart", {
pplMngNo,
});
if (!popupWindow) {
alert(
"팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.",
);
}
}}
color="primary"
>
침입수분석
</Button>
<Button
className="px-2"
onClick={() => {
const popupWindow = openWindowPopup("rdiiChart", {
pplMngNo,
});
if (!popupWindow) {
alert(
"팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.",
);
}
}}
color="primary"
>
RDII분석
</Button>
</>
)}
{layerNm === "하수맨홀" && (
<>
<Button
color="white"
onClick={() => {
setIsLoading(true);
searchUpperPpls(gisMap, layerSpecificNm, alert).finally(() =>
setIsLoading(false),
);
}}
>
상류관로 보기
</Button>
</>
)}
</div>
<Button
onClick={onClose}
className="text-primary-foreground/80 absolute right-2 top-1/2 z-10 h-8 w-8 -translate-y-1/2 p-0 hover:text-primary-foreground focus:outline-none"
size="xs"
aria-label="시설 상세정보 닫기"
title="닫기"
>
<span aria-hidden="true">✕</span>
</Button>
</div>
<div
className="flex flex-1 flex-col overflow-y-auto overflow-x-hidden"
ref={contentRef}
>
<VerticalTable
headName={
layerNm === "침수흔적위선" && floodDepthRows.length > 0 ? (
<div className="flex flex-col items-start gap-2">
<span>기본정보</span>
<div className="flex w-full items-center gap-1">
<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"
>
{Array.from(
new Set(
floodDepthRows
.map((row) =>
String(
row.fldng_yr ?? row.fldngYr ?? row.yr ?? "",
).trim(),
)
.filter(Boolean),
),
).map((year) => (
<button
key={year}
type="button"
onClick={async () => {
setSelectedFloodDepthYear(year);
const selectedDepthRow = floodDepthRows.find(
(row) =>
String(
row.fldng_yr ?? row.fldngYr ?? row.yr ?? "",
).trim() === year,
);
const depthFid = String(
selectedDepthRow?.fldng_dpth_map_sn ??
selectedDepthRow?.fid ??
"",
).trim();
if (depthFid) {
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON?.(
"tb_ms_fldng_dpth_map",
`tb_ms_fldng_dpth_map.${depthFid.replace(/^tb_ms_fldng_dpth_map\./, "")}`,
);
}
}}
className={`rounded border px-3 py-1 text-[1.2rem] font-medium leading-5 ${selectedFloodDepthYear === year ? "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>
</div>
) : layerNm === "하수관로" && tableData?.elapsedYears ? (
<>
기본정보{" "}
<span className="ml-2 rounded-sm bg-red-50 px-2 py-0.5 text-[1.1rem] font-semibold text-red-700">
경과년도 {tableData.elapsedYears}
</span>
</>
) : (
"기본정보"
)
}
tableName1={layerNm === "중점우수토실" ? "측정지점" : undefined}
tableData={[tableData]}
headlist={headlist}
pageSize={20}
total={0}
cellClick={undefined}
onPageChange={function (): void {
throw new Error("Function not implemented.");
}}
// 두 번째 테이블 조건부 props
showSecondTable={layerNm === "중점우수토실" ? true : false}
tableName2={layerNm === "중점우수토실" ? "시설현황" : undefined}
tableData2={
layerNm === "중점우수토실" && tableData ? [tableData] : []
}
headlist2={layerNm === "중점우수토실" ? headlist2 : undefined}
pageSize2={20}
total2={0}
isEditable2={buttonChk}
/>
{ContentComponent && (
<ContentComponent
featureData={fetchData}
isMainSsc={isMainSsc}
isDrtwSewagePump={isDrtwSewagePump}
onSaveClick={handleSaveClick}
chmbrXcnts={chmbrXcnts}
chmbrYdnts={chmbrYdnts}
gisObject={featureData?.values_ ?? featureData}
rawFeatureData={featureData}
surveyResultContent={
surveyMode ? (
<ExmnSurveyResultSection featureData={featureData} />
) : undefined
}
/>
)}
</div>
</div>
);
};
export default FeatureClickEventPopup;