Files
frontend/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx
T
2025-07-05 22:37:03 +09:00

350 lines
15 KiB
TypeScript

'use client'
import { getData, putData } from "@/components/lib/api";
import Modal from "@/components/ui/modal";
import { VerticalTable } from "@/components/ui/verticalTable";
import { useGisContext } from "@common/business_components/ui/map/Gis";
import { Button } from "@common/components/ui";
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
import { getLayerContentComponent } from "./components/common";
import { SwtrmtFcltCol, SwtrmtPrcsZoneCol, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, SewagePump, MsSwageMhlCol, stofcbrCol } from "./components/common/table";
import InfilChart from './components/popupComponent/chart/infilChart';
import RdiiChart from './components/popupComponent/chart/rdiiChart';
import { useInfilChartModalStore, useRdiiChartModalStore } from "./components/popupComponent/chartModalStore";
// TODO:
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
const [tableData, setTableData] = useState<any>(null); // 피처 데이터
const [mntrngsData, setMntrngsData] = useState(false); // 수량/수질 측정 정보
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 [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
const [chkMainSsc, setChkMainSsc] = useState(false); // 중점우수토실 여부
const [isLoading, setIsLoading] = useState(false);
// 🎯 추가 데이터 로딩 상태 분리
const [isAdditionalDataLoading, setIsAdditionalDataLoading] = useState(false);
const [additionalDataError, setAdditionalDataError] = useState<string | null>(null);
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
const { setIsOpen:setIsModalInfil } = useInfilChartModalStore();
const { setIsOpen:setIsModalRdii } = useRdiiChartModalStore();
// 🎯 관로관리번호 추출
const pplMngNo = featureData?.values_?.fclt_mng_no || "";
// const featureId = featureData.getId(); // 피처의 id 가져오기
const featureId = typeof featureData.getId === 'function'
? featureData.getId()
: featureData.fid;
console.log('featureId', featureId);
// 🎯 추가 데이터 API 호출 함수 (독립적으로 실행)
const fetchAdditionalData = useCallback(async (adstDstrCd: string, manageNo: string) => {
if (!adstDstrCd || !manageNo) {
console.warn('추가 데이터 호출 실패: 필수 매개변수 누락', { adstDstrCd, manageNo });
return [];
}
setIsAdditionalDataLoading(true);
setAdditionalDataError(null);
try {
console.log(`추가 데이터 API 호출 시작: v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs`);
// 🎯 타임아웃 설정 (10초)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('API 타임아웃')), 10000)
);
const apiPromise = getData(`v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs`);
const response = await Promise.race([apiPromise, timeoutPromise]) as any;
console.log('추가 데이터 API 호출 성공:', response);
return response?.content || [];
} catch (error) {
console.error('추가 데이터 로딩 실패:', error);
setAdditionalDataError(error instanceof Error ? error.message : '알 수 없는 오류');
return [];
} finally {
setIsAdditionalDataLoading(false);
}
}, []);
// 🎯 데이터 전처리 메모이제이션
const processedData = useMemo(() => {
if (!featureData?.values_) return null;
const data = featureData.values_;
return {
...data,
discharge_pipe_spec: `직경: ${data.pipe_diameter || '-'} / 높이: ${data.pipe_hg || '-'} / 길이: ${data.pipe_lt || '-'}`,
flwmtr_combined: `${data.flwmtr_mesure_tp_etc || '-'} / ${data.flwmtr_knd || '-'}`
};
}, [featureData?.values_]);
// 피쳐데이터의 현재 레이어 이름 가져오는 함수
const getSelectedLayers = () => {
let result = null;
const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기
let featureLayer: { get: (key: string) => any } = { get: (key: string) => null }; // 피처가 속한 레이어를 찾기 위한 변수
layers.forEach((layer: any) => {
const source = layer.getSource();
if (source && source.hasFeature && featureData
&& typeof featureData.getId === 'function' && source.hasFeature(featureData)) {
featureLayer = layer; // 피처가 속한 레이어 찾기
}
});
// console.log("featureLayer.get('lyrTyNm');", featureLayer.get('lyrTyNm'));
// SearchModal 클릭 이벤트와 Gis 클릭 이벤트 구분하기
if (featureLayer.get('lyrNm') === null) {
if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설';
else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역';
else if (featureData.lyrTypeNm === 'tb_ms_stofcbr') result = '우수토실';
else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
else if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') result = '하수관로';
else if (featureData.lyrTypeNm === 'tb_ms_swage_pmstn') result = '하수펌프장';
else if (featureData.lyrTypeNm === 'tb_ms_swage_mhl') result = '하수맨홀';
}
else {
const lyrTyNm = featureLayer.get('lyrTyNm');
if (lyrTyNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설';
else if (lyrTyNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역';
else if (lyrTyNm === 'tb_ms_stofcbr') result = '우수토실';
else if (lyrTyNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
else if (lyrTyNm === 'tb_ms_swage_ppl') result = '하수관로';
else if (lyrTyNm === 'tb_ms_swage_pmstn') result = '하수펌프장';
else if (lyrTyNm === 'tb_ms_swage_mhl') result = '하수맨홀';
else result = featureLayer.get('lyrNm');
}
return result;
// return featureLayer.get('lyrNm'); // 레이어 이름 출력
};
useEffect(() => {
const fetchData = async () => {
if (!featureId) return;
try {
const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기
setLayerNm(selectedLayerName); // 상태 업데이트
console.log('gis data', featureData.values_);
let data = featureData.values_;
setfetchData(data)
setTableData(data);
switch (selectedLayerName) {
case '하수처리시설':
setHeadlist(SwtrmtFcltCol);
setButtonChk(false)
break;
case '하수처리구역':
setHeadlist(SwtrmtPrcsZoneCol)
setButtonChk(false)
break;
case '중점우수토실':
setHeadlist(untrtSwgRainCol1)
setHeadlist2(untrtSwgRainCol2)
setButtonChk(false)
setChkMainSsc(true)
setfetchData([processedData])
setTableData(processedData);
setIsLoading(false); // 기본 렌더링 완료
// 수량/수질측정 정보 데이터 호출
if (data?.adst_dstr_cd && data?.manage_no) {
fetchAdditionalData(data.adst_dstr_cd, data.manage_no)
.then(response => {
console.log('additionalResponse', response);
setMntrngsData(response || []);
})
.catch(error => {
console.error('추가 데이터 로딩 실패:', error);
setMntrngsData(false);
});
}
return;
case '우수토실':
setHeadlist(stofcbrCol)
setButtonChk(false)
break;
case '하수관로':
setHeadlist(MsSwagePplCol)
setButtonChk(true)
break;
// case '오수펌프':
// setButtonChk(true)
// break;
// case '빗물펌프':
// setButtonChk(true)
// break;
case '하수펌프장':
setHeadlist(SewagePump)
setButtonChk(true)
break;
case '하수맨홀':
setHeadlist(MsSwageMhlCol)
setButtonChk(true)
break;
default:
break;
}
} catch (error) {
console.error('데이터 로딩 중 오류:', error);
}
};
fetchData();
}, [featureId, processedData]);
// 🎯 추가 데이터는 별도 useEffect로 비동기 로딩
useEffect(() => {
if (layerNm === '중점우수토실' && featureData?.values_) {
const data = featureData.values_;
if (data?.adst_dstr_cd && data?.manage_no) {
// 🎯 추가 데이터를 비동기로 로딩 (UI 블로킹 없음)
fetchAdditionalData(data.adst_dstr_cd, data.manage_no)
.then(response => {
console.log('추가 데이터 로딩 완료:', response);
setMntrngsData(response || []);
})
.catch(error => {
console.error('추가 데이터 최종 로딩 실패:', error);
setMntrngsData(false);
});
}
}
}, [layerNm, featureData?.values_, fetchAdditionalData]);
// 저장 시 모달 띄우기
const handleSaveClick = () => {
setIsModalOpen(true); // 모달 열기
};
const handleModalSave = () => {
setIsModalOpen(false); // 모달 닫기
btnClickEvent(); // 저장 이벤트 실행
};
const handleModalClose = () => {
setIsModalOpen(false); // 모달 닫기
};
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;
});
if (layerNm === '하수관로') bodyData['lyrTypeNm'] = 'tb_ms_swage_ppl'
else if (layerNm === '우수토실') bodyData['lyrTypeNm'] = 'tb_ms_stofcbr'
else if (layerNm === '중점우수토실') bodyData['lyrTypeNm'] = 'om_dt_untrt_swg_rain'
else if (layerNm === '오수펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn'
else if (layerNm === '빗물펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn'
else if (layerNm === '하수맨홀') bodyData['lyrTypeNm'] = 'tb_ms_swage_mnhl'
bodyData['fid'] = featureId
const data = await putData(`v1/fclts/${featureId}`, bodyData);
if (data.status == 200) {
alert('저장되었습니다')
setTableData(data)
setfetchData(data)
}
}
}
// 레이어명에 따라 상세페이지 렌더링
const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null;
return (
<div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[50]">
<div className="card-header bg-primary flex items-center justify-between p-4">
<div className="flex items-center">
<h3 className="text-2xl font-medium text-white text-label whitespace-nowrap">{layerNm} 정보</h3>
{/* <div className="font-medium text-white text-label">(00구 00리 하수관로)</div> */}
</div>
<div className="flex items-center space-x-3">
{/* 🎯 하수관로일 때만 Infil/RDII 버튼 표시 */}
{layerNm === '하수관로' && (
<>
<Button onClick={() => setIsModalInfil(true)}
className="px-2 py-1 text-xl text-blue-600 border border-blue-600 rounded hover:bg-gray-700"
color="white"
>
침입수분석
</Button>
<Button onClick={() => setIsModalRdii(true)}
className="px-2 py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700"
color="white"
>
RDII분석
</Button>
</>
)}
<Button
onClick={onClose}
className="text-white hover:text-gray-300 focus:outline-none ml-2"
>
✕
</Button>
</div>
</div>
<div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}>
<VerticalTable
headName='기본정보'
tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined}
tableData={[tableData]}
headlist={headlist}
pageSize={20}
total={0}
children={undefined}
cellClick={undefined}
onPageChange={function (page: number): 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}
/>
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
{ContentComponent &&
<ContentComponent
featureData={fetchData}
chkMainSsc={chkMainSsc}
additionalData={mntrngsData}
onSaveClick={handleSaveClick}
/>
}
</div>
{/* 🎯 InfilChart 및 RdiiChart 컴포넌트 추가 */}
<InfilChart pplMngNo={pplMngNo} />
<RdiiChart pplMngNo={pplMngNo} />
</div>
)
}
export default FeatureClickEventPopup;