Files
frontend/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx
T

1210 lines
47 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import LoadingOverlay from '@/components/ui/LoadingBar';
import { UiModal, UITab, UiTable, downloadExcelWithKoreanHeaders } from '@common/business_components/ui';
import { Button } from '@common/components/ui';
import axios from 'axios';
import {
ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2,
MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES,
MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol
} from '../components/common/table';
import { getData } from "@/components/lib/api";
import BasicTab, { BASIC_TYPES } from './basicTab';
import OperationTab, { operationCleaningTableHeadlist, operationInspectionTableHeadlist, operationMaintenanceTableHeadlist, operationTableHeadlist, WORK_TYPES } from './operationTab';
import ReportTab from './reportTab';
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import useStore from '../../../store/useStore';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { confirmToast } from '@/components/ui/toast'
import { toast, Toaster } from "react-hot-toast";
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`;
const TABS = [
{ val: "basic", text: "기본정보" },
{ val: "operation", text: "운영정보" },
{ val: "report", text: "하수도정비기본계획보고서" },
];
const OPERATION_TABLE_SIZE = 5;
// 날짜 조합 함수
const getDateString = (
year: string,
month: string,
day: string,
defaultDate: string,
) => {
if (!year && !month && !day) return defaultDate;
const y = year || defaultDate.slice(0, 4);
const m = month || defaultDate.slice(4, 6);
const d = day || defaultDate.slice(6, 8);
return `${y}${m}${d}`;
};
interface SearchModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onFeatureSelect: (row: any) => void;
}
const SearchModal: React.FC<SearchModalProps> = ({
open,
onOpenChange,
onFeatureSelect,
}) => {
const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "basic");
const [basicType, setBasicType] = useState(BASIC_TYPES[0]?.val ?? "");
const [workType, setWorkType] = useState(WORK_TYPES[0]?.val ?? "");
const [showTable, setShowTable] = useState(false);
const [tableData, setTableData] = useState<any[]>([]);
const [detailTableData, setDetailTableData] = useState<any[]>([]);
const [tableCount, setTableCount] = useState(0);
const [detailTableCount, setDetailTableCount] = useState(0);
const [curPage, setCurPage] = useState(1);
const [detailCurPage, setDetailCurPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isDetailLoading, setIsDetailLoading] = useState(false);
const [isResetPage, setIsResetPage] = useState<boolean>(false);
const [isResetDetailPage, setIsResetDetailPage] = useState<boolean>(false);
const [isResetSelection, setIsResetSelection] = useState<boolean>(false);
const [isResetDetailSelection, setIsResetDetailSelection] = useState<boolean>(false);
const [detailBizId, setDetailBizId] = useState(0);
const [ctpvCd, setCtpvCd] = useState<string>("0");
const [ctpvName, setCtpvName] = useState<string>('');
const [sggCd, setSggCd] = useState<string>("0");
const [sggName, setSggName] = useState<string>('');
const [start1, setStart1] = useState("");
const [start2, setStart2] = useState("");
const [end1, setEnd1] = useState("");
const [end2, setEnd2] = useState("");
const [var1, setVar1] = useState("");
const [var2, setVar2] = useState("");
const [yearFilter, setYearFilter] = useState({ start: "", end: "" });
const { ctpv, sgg, BIZ_TY_CD } = useStore();
// 오늘 날짜 구하기
const today = new Date();
const todayYear = today.getFullYear().toString();
const todayMonth = (today.getMonth() + 1).toString().padStart(2, "0");
const todayDate = today.getDate().toString().padStart(2, "0");
const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => {
if (activeTab === 'basic') {
if (basicType === 'tb_ms_swage_ppl') return MsSwagePplCol2; // 하수관로
if (basicType === 'tb_il_swage_prcs_fclt') return SwtrmtFcltCol2; // 하수처리시설
if (basicType === 'tb_ms_swage_prcs_zone') return SwtrmtPrcsZoneCol2; // 하수처리구역
if (basicType === 'tb_ms_swage_mhl') return MsSwageMhlCol2; // 하수맨홀
if (basicType === 'tb_ms_swage_pmstn') return ExclnSewagePumpCol2; // 하수펌프장
if (basicType === 'tb_ms_stofcbr') return stofcbrCol2; // 우수토실
return [];
}
if (activeTab === 'operation') return operationTableHeadlist;
return [];
}, [activeTab, basicType, workType]);
const dynamicHeadlist2: { id: string, title: string }[] = useMemo(() => {
if (activeTab === 'operation') {
if (workType === WORK_TYPES[0]?.val) return operationMaintenanceTableHeadlist;
if (workType === WORK_TYPES[1]?.val) return operationInspectionTableHeadlist;
if (workType === WORK_TYPES[2]?.val) return operationCleaningTableHeadlist;
}
return [];
}, [activeTab, workType]);
useEffect(() => {
// filterParams 초기화
setStart1("");
setStart2("");
setEnd1("");
setEnd2("");
setVar1("");
setVar2("");
setCtpvCd("0");
setSggCd("0");
setYearFilter({
start: "",
end: "",
});
setShowTable(false);
setTableData([]);
setDetailTableData([]);
setTableCount(0);
setDetailTableCount(0);
setCurPage(1); // 페이지를 1로 초기화
setDetailCurPage(1); // 페이지를 1로 초기화
setIsResetPage(true); // 페이지를 초기화
setIsResetDetailPage(true); // 디테일 페이지를 초기화
setIsResetSelection(true); // 선택 초기화
setIsResetDetailSelection(true); // 선택 초기화
setDetailBizId(0);
// queryParams는 fetchFilteredData 내에서 매번 새로 생성되므로 별도 상태 초기화는 필요 없음
}, [activeTab, basicType, workType]);
// 페이지 변경 시마다 데이터 fetch
useEffect(() => {
// showTable이 true일 때만 fetch (최초 렌더 방지)
setIsResetPage(false);
setIsResetDetailPage(true);
setDetailBizId(0);
if (showTable) fetchFilteredData();
}, [curPage]);
// 페이지 변경 시마다 데이터 fetch
useEffect(() => {
setIsResetDetailSelection(false);
// showTable이 true일 때만 fetch (최초 렌더 방지)
if (showTable) {
handleOperationPageNation(Number(detailBizId));
}
}, [detailCurPage]);
// 검색 API 호출 함수
const fetchFilteredData = async () => {
setIsLoading(true); // 로딩 시작
setIsResetSelection(true); // 선택 초기화
setIsResetDetailSelection(true); // 선택 초기화
try {
let cqlFilters: string[] = [];
if (activeTab === 'basic') {
const queryParams = new URLSearchParams({
lyrTypeNm: basicType,
page: `${curPage - 1}`,
size: '10',
sort: 'string',
});
if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = zoneCqlFilters(); // 하수처리구역
else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = facilityCqlFilters(); // 하수처리시설
else if (basicType === 'tb_ms_swage_ppl') cqlFilters = pplCqlFilters(); // 하수관로
else if (basicType === 'tb_ms_swage_mhl') cqlFilters = mhCqlFilters(); // 하수맨홀
else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = psmtnCqlFilters(); // 하수펌프장
else if (basicType === 'tb_ms_stofcbr') cqlFilters = otflCqlFilters(); // 우수토실
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
console.log('api:', `${API_URL}/v1/fclts?${queryParams.toString()}`);
// 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가
const dataWithRowNumber = (res.data.data || []).map((item: any, index: number) => {
// 🎯 먼저 각 아이템을 포맷팅
const formattedItem = formatDisplayData(item);
let ctpvCd = item?.ctpvNm ?? formattedItem?.ctpvNm ?? '';
let sggCd = item?.sggNm ?? formattedItem?.sggNm ?? '';
if (!ctpvCd || !sggCd) {
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
if (!sggCd && code) sggCd = code.slice(0, 5);
}
const matchedMhlCode = MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim());
const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd);
const ctpvNm = ctpv.find(code => code.val.trim() === ctpvCd)?.text || ctpvCd;
const sggNm = sgg.find(code => code.val.trim() === sggCd)?.text || '-';
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
// 추가 처리 및 일련번호 추가
return {
...formattedItem,
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item?.scsYn, // scsYn이 null이면 'N'으로 변경
mqltCd: matchedMhlCode ? matchedMhlCode.text : item?.mqltCd,
usgCd: matchedMhlUsgCode ? matchedMhlUsgCode.text : (usgCdObj ? usgCdObj.text : item?.usgCd),
ctpvNm,
sggNm,
swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || '-',
};
});
console.log("dataWithRowNumber: ", dataWithRowNumber)
setTableData(dataWithRowNumber);
setTableCount(res.data.count || []);
} else if (activeTab === 'operation') {
const RequestDTO: Record<string, string> = {
page: `${curPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
};
if (var1) RequestDTO.bizInfoNm = var1;
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
// 수행기간 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
RequestDTO.fromflfmtEndYmd = startDate;
RequestDTO.toflfmtEndYmd = endDate;
// JSON.parse 사용으로 문자열 -> 배열 변환
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const params = new URLSearchParams(RequestDTO).toString();
const res = await getData(`v1/bizInfo?${params}`);
console.log('운영정보 사업 데이터:', res.content);
const mappedContent = (res.content || []).map((item: any, index: number) => {
const formattedItem = formatDisplayData(item);
const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd);
return {
...formattedItem,
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
};
});
setTableData(mappedContent || []);
setTableCount(res.totalElements || 0);
setDetailTableData([]);
setDetailTableCount(0);
}
setShowTable(true);
setIsResetSelection(false);
} catch (err) {
console.error('API ERROR:', err);
setTableData([]);
setIsResetSelection(false);
} finally {
setIsLoading(false); // 로딩 종료
setIsResetSelection(false);
// setIsResetDetailSelection(false);
}
};
// 하수관로용 CQL 필터 생성
const pplCqlFilters = () => {
const cqlFilters: string[] = [];
// 추후 작성 예정
if (var1) cqlFilters.push(`usg_cd='${var1}'`);
if (var2) cqlFilters.push(`mqlt_cd='${var2}'`);
if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`);
else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`);
// 설치년도 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
cqlFilters.push(
`instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`,
);
// 관경(mm) 범위
if (start1 && end1) cqlFilters.push(`pdmt_vl BETWEEN ${start1} AND ${end1}`);
else if (start1) cqlFilters.push(`pdmt_vl >= ${start1}`);
else if (end1) cqlFilters.push(`pdmt_vl <= ${end1}`);
// 연장(m) 범위
if (start2 && end2) cqlFilters.push(`prlg_vl BETWEEN ${start2} AND ${end2}`);
else if (start2) cqlFilters.push(`prlg_vl >= ${start2}`);
else if (end2) cqlFilters.push(`prlg_vl <= ${end2}`);
return cqlFilters;
};
// 하수처리시설용 CQL 필터 생성
const facilityCqlFilters = () => {
const cqlFilters: string[] = [];
// 가동개시일자 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
cqlFilters.push(
`oprtng_strt_ymd >= '${startDate}' and oprtng_strt_ymd <= '${endDate}'`,
);
// 시설용량 범위
if (start1 && end1)
cqlFilters.push(`fclt_cpct BETWEEN ${start1} AND ${end1}`);
else if (start1) cqlFilters.push(`fclt_cpct >= ${start1}`);
else if (end1) cqlFilters.push(`fclt_cpct <= ${end1}`);
// 시도
if (ctpvName) cqlFilters.push(`ctpv_nm ILIKE '%${ctpvName}%'`);
if (sggName) cqlFilters.push(`sgg_nm ILIKE '%${sggName}%'`);
// // 처리구역 면적
// if (start2 && end2)
// cqlFilters.push(`prcsZoneAreaSum BETWEEN ${start2} AND ${end2}`);
// else if (start2) cqlFilters.push(`prcsZoneAreaSum >= ${start2}`);
// else if (end2) cqlFilters.push(`prcsZoneAreaSum <= ${end2}`);
// // 하수처리시설명
// if (var1) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var1}%'`);
return cqlFilters;
};
// 하수처리구역용 CQL 필터 생성
const zoneCqlFilters = () => {
const cqlFilters: string[] = [];
if (ctpvCd !== '' && ctpvCd !== '0') cqlFilters.push(`ctpv_cd='${ctpvCd?.trim()}'`);
if (sggCd !== '' && sggCd !== '0') cqlFilters.push(`sgg_cd='${sggCd}'`);
if (var1) cqlFilters.push(`swtrmt_zone_nm ILIKE '%${var1}%'`);
if (var2) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var2}%'`);
return cqlFilters;
};
// 하수맨홀용 CQL 필터 생성
const mhCqlFilters = () => {
const cqlFilters: string[] = [];
// 시도, 시군구
if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`);
else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`);
// // 설치년도 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
cqlFilters.push(
`instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`,
);
if (var1) cqlFilters.push(`usg_cd='${var1}'`);
// 구경(mm) 범위
if (start1 && end1) cqlFilters.push(`swage_mhL_diam_vl BETWEEN ${start1} AND ${end1}`);
else if (start1) cqlFilters.push(`swage_mhL_diam_vl >= ${start1}`);
else if (end1) cqlFilters.push(`swage_mhL_diam_vl <= ${end1}`);
return cqlFilters;
};
// 하수펌프장용 CQL 필터 생성
const psmtnCqlFilters = () => {
const cqlFilters: string[] = [];
// 시도, 시군구
if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`);
else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`);
// 설치년도 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
cqlFilters.push(
`instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`,
);
// 일일처리용량 범위
if (start1 && end1)
cqlFilters.push(`daily_prcs_cpct BETWEEN ${start1} AND ${end1}`);
else if (start1) cqlFilters.push(`daily_prcs_cpct >= ${start1}`);
else if (end1) cqlFilters.push(`daily_prcs_cpct <= ${end1}`);
// 양수능력 범위
if (start2 && end2)
cqlFilters.push(`excln_wpmp_ablt_vl BETWEEN ${start2} AND ${end2}`);
else if (start2) cqlFilters.push(`excln_wpmp_ablt_vl >= ${start2}`);
else if (end2) cqlFilters.push(`excln_wpmp_ablt_vl <= ${end2}`);
return cqlFilters;
};
// 우수토실용 CQL 필터 생성
const otflCqlFilters = () => {
const cqlFilters: string[] = [];
// 시도, 시군구
if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`);
else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`);
// 설치년도 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
cqlFilters.push(
`instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`,
);
return cqlFilters;
};
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
const handleSearch = () => {
setCurPage(1); // 페이지를 1로 초기화
fetchFilteredData();
setIsResetPage(true); // 페이지를 초기화
setIsResetDetailPage(true); // 디테일 페이지를 초기화
setIsResetSelection(false); // 선택 초기화
setIsResetDetailSelection(false); // 선택 초기화
setShowTable(true);
};
// 기본정보 테이블 cellClick 핸들러
const handleBasicCellClick = async (index?: number | undefined, headId?: String) => {
if (typeof index === "number") {
setIsLoading(true);
const row = tableData[index];
const layerNm = row.lyrTypeNm;
const fid = row.fid;
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON(layerNm, fid);
setIsLoading(false);
onOpenChange(false); // SearchModal 닫기
}
};
// 운영정보 테이블 cellClick 핸들러
const handleOperationCellClick = async (index?: number | undefined, headId?: String) => {
setIsResetDetailPage(true); // 페이지를 1로 초기화
setIsResetDetailSelection(true); // 선택 초기화
setIsResetDetailPage(true); // 디테일 페이지를 초기화
if (typeof index === "number") {
setIsDetailLoading(true);
setDetailBizId(tableData[index].bizId);
setDetailCurPage(1); // 상세 페이지 초기화
try {
const RequestDTO: Record<string, string> = {
page: `${detailCurPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
bizId: tableData[index % OPERATION_TABLE_SIZE].bizId,
};
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
console.log('운영정보 cellClick 데이터:', res.data);
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
const formattedItem = formatDisplayData(item);
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim());
return {
...formattedItem,
rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1,
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
};
});
console.log('운영정보 cellClick 데이터:', mappedData);
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
} catch (error) {
console.error('운영정보 cellClick 에러:', error);
} finally {
setIsDetailLoading(false);
setIsResetDetailPage(false);
setIsResetDetailSelection(false);
}
}
};
// 운영정보 디테일 테이블 페이지네이션 핸들러
const handleOperationPageNation = async (index?: number | undefined) => {
if (typeof index === "number") {
setIsDetailLoading(true);
try {
const RequestDTO: Record<string, string> = {
page: `${detailCurPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
bizId: String(detailBizId),
};
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
const formattedItem = formatDisplayData(item);
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim());
return {
...formattedItem,
rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1,
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
};
});
console.log('운영정보 PageNation 데이터:', mappedData);
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
} catch (error) {
console.error('운영정보 페이지네이션 에러:', error);
} finally {
setIsDetailLoading(false);
}
}
};
const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => {
if (typeof index === "number") {
setIsLoading(true);
const row = detailTableData[index];
const layerNm = row.lyrTypeNm;
const fid = row.fid;
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON(layerNm, fid);
setIsLoading(false);
onOpenChange(false); // SearchModal 닫기
}
};
// 기본정보 Excel 다운로드 핸들러
const basicExcelDownload = async () => {
if (tableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onAnalysis() {
setIsLoading(true);
try {
const queryParams = new URLSearchParams({
lyrTypeNm: basicType,
size: '10000',
});
let cqlFilters: string[] = [];
let headerMapping: { [key: string]: string } = {};
let headerRange: string = "A1:F1";
switch (basicType) {
case 'tb_ms_swage_ppl': {
// 하수관로
cqlFilters = pplCqlFilters()
headerMapping = MsSwagePplExcelCol;
headerRange = "A1:M1";
break;
}
case 'tb_il_swage_prcs_fclt': {
// 하수처리시설
cqlFilters = facilityCqlFilters();
headerMapping = SwtrmtFcltExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_swage_prcs_zone': {
// 하수처리구역
cqlFilters = zoneCqlFilters();
headerMapping = SwtrmtPrcsZoneExcelCol;
headerRange = "A1:D1";
break;
}
case 'tb_ms_swage_mhl': {
// 하수맨홀
cqlFilters = mhCqlFilters();
headerMapping = MsSwageMhlExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_swage_pmstn': {
// 하수펌프장
cqlFilters = psmtnCqlFilters();
headerMapping = ExclnSewagePumpExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_stofcbr': {
// 우수토실
cqlFilters = otflCqlFilters();
headerMapping = stofcbrExcelCol;
headerRange = "A1:H1";
break;
}
}
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
console.log('res.data.data', res.data.data);
// const mhlCdMap = new Map(MS_SWAGE_MHL_CODES.map(code => [code.val?.trim(), code.text]));
const mhlUsgCdMap = new Map(MS_SWAGE_MHL_USE_CODES.map(code => [code.val?.trim(), code.text]));
const pplUsgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val?.trim(), code.text]));
const ctpvMap = new Map(ctpv.map(code => [code.val?.trim(), code.text]));
const sggMap = new Map(sgg.map(code => [code.val?.trim(), code.text]));
const mappedContent = (res.data.data || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
let ctpvCd = item?.ctpvCd ?? formattedItem?.ctpvCd ?? '';
let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? '';
if (!ctpvCd || !sggCd) {
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
if (!sggCd && code) sggCd = code.slice(0, 5);
}
// trim 한 번만 수행
const trimmedUsgCd = item?.usgCd?.trim();
// const trimmedMqltCd = item?.mqltCd?.trim();
switch (basicType) {
case 'tb_ms_swage_ppl': {
// 하수관로
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
instMngNo: formattedItem.instMngNo ?? item.instMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? item.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? item.sggNm ?? '-',
swtrmt_zone_nm: formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
usgCd: pplUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-',
pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '-',
prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '-',
tmptDpth: formattedItem.tmptDpth ?? item.tmptDpth ?? '-',
tmptMhlMngNo: formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? '-',
scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? '-',
scendMhlMngNo: formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? '-',
};
}
case 'tb_il_swage_prcs_fclt': {
// 하수처리시설
return {
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-',
fcltCpct: formattedItem.fcltCpct ?? item.fcltCpct ?? '-',
rgnExpln: formattedItem.rgnExpln ?? item.rgnExpln ?? '-',
oprtngStrtYmd: formattedItem.oprtngStrtYmd ?? item.oprtngStrtYmd ?? '-',
laeNm: formattedItem.laeNm ?? item.laeNm ?? '-',
sareaNm: formattedItem.sareaNm ?? item.sareaNm ?? '-',
rvrNm: formattedItem.rvrNm ?? item.rvrNm ?? '-',
dschgWtstNm: formattedItem.dschgWtstNm ?? item.dschgWtstNm ?? '-',
swtrmtPpltnCnt: formattedItem.swtrmtPpltnCnt ?? item.swtrmtPpltnCnt ?? '-',
rprsMsrcCn: formattedItem.rprsMsrcCn ?? item.rprsMsrcCn ?? '-'
};
}
case 'tb_ms_swage_prcs_zone': {
// 하수처리구역
return {
swtrmtZoneNm: formattedItem.swtrmtZoneNm ?? item.swtrmtZoneNm ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-'
};
}
case 'tb_ms_swage_mhl': {
// 하수맨홀
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
usgCd: mhlUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-',
swageMhlDiamVl: formattedItem.swageMhlDiamVl ?? item.swageMhlDiamVl ?? '-',
wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-',
vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-',
altdVl: formattedItem.altdVl ?? item.altdVl ?? '-',
dpthlVl: formattedItem.dpthlVl ?? item.dpthlVl ?? '-'
};
}
case 'tb_ms_swage_pmstn': {
// 하수펌프장
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
swagePmstnNm: formattedItem.swagePmstnNm ?? item.swagePmstnNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
dailyPrcsCpct: formattedItem.dailyPrcsCpct ?? item.dailyPrcsCpct ?? '-',
maxImpndCpct: formattedItem.maxImpndCpct ?? item.maxImpndCpct ?? '-',
altdVl: formattedItem.altdVl ?? item.altdVl ?? '-',
wtlvVl: formattedItem.wtlvVl ?? item.wtlvVl ?? '-',
exclnWpmpAbltVl: formattedItem.exclnWpmpAbltVl ?? item.exclnWpmpAbltVl ?? '-'
};
}
case 'tb_ms_stofcbr': {
// 우수토실
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
mapshtNo: formattedItem.mapshtNo ?? item.mapshtNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
worflwWallHgt: formattedItem.worflwWallHgt ?? item.worflwWallHgt ?? '-',
swageMhlMngNo: formattedItem.swageMhlMngNo ?? item.swageMhlMngNo ?? '-',
drtnAngl: formattedItem.drtnAngl ?? item.drtnAngl ?? '-'
};
}
}
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedContent; // 배열로 감싸서 전달
console.log('excelData', excelData);
// 파일명 생성 (분석번호 기반)
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
const fileName = `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: headerRange,
headerMapping: headerMapping
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
// 운영정보 Excel 다운로드 핸들러 추가
const operationExcelDownload = async () => {
if (tableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onAnalysis() {
setIsLoading(true);
try {
const RequestDTO: Record<string, string> = {
size: '10000',
};
if (var1) RequestDTO.bizInfoNm = var1;
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
// 수행기간 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
RequestDTO.fromflfmtEndYmd = startDate;
RequestDTO.toflfmtEndYmd = endDate;
// JSON.parse 사용으로 문자열 -> 배열 변환
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const params = new URLSearchParams(RequestDTO).toString();
const res = await getData(`v1/bizInfo?${params}`);
// 헤더 매핑 정의
const headerMapping: { [key: string]: string } = {
'bizInfoNm': '사업명',
'flfmtBgngYmd': '수행시작일자',
'flfmtEndYmd': '수행종료일자',
'flfmtYr': '수행년도',
'bizTypeCd': '사업유형',
'bizAmt': '사업금액(원)',
};
const bizTyCdMap = new Map(BIZ_TY_CD.map(code => [code.lwpoCd, code.lwpoCdNm]));
const mappedContent = (res.content || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
return {
bizInfoNm: formattedItem.bizInfoNm ?? item.bizInfoNm ?? '',
flfmtBgngYmd: formattedItem.flfmtBgngYmd ?? item.flfmtBgngYmd ?? '',
flfmtEndYmd: formattedItem.flfmtEndYmd ?? item.flfmtEndYmd ?? '',
flfmtYr: formattedItem.flfmtYr ?? item.flfmtYr ?? '',
bizTypeCd: bizTyCdMap.get(item.bizTypeCd) || item.bizTypeCd || formattedItem.bizTypeCd || '',
bizAmt: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.bizAmt ?? item.bizAmt),
};
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedContent; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const label = WORK_TYPES.find((type) => type.val === workType)?.text
const fileName = `${label} 사업 현황_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: "A1:F1",
headerMapping: headerMapping
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
// 운영정보 Excel 다운로드 핸들러 추가
const operationResultExcelDownload = async () => {
if (detailTableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onAnalysis() {
setIsLoading(true);
try {
const RequestDTO: Record<string, string> = {
size: '10000',
bizId: String(detailBizId),
};
const apiStartTime = performance.now();
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const apiEndTime = performance.now();
console.log(`🚀 1. API 호출 시간: ${(apiEndTime - apiStartTime).toFixed(2)}ms`);
console.log('운영정보 결과 다운로드.data:', res.data);
const mappingStartTime = performance.now();
// 헤더 매핑 정의
const headerMapping: { [key: string]: string } = {
'fcltMngNo': '관리번호',
'swtrmt_zone_nm': '처리구역',
'usgCd': '용도',
'mqltCd': '재질',
'pdmtVl': '관경(mm)',
'prlgVl': '연장(m)',
'instlYmd': '설치일자',
'strc_stts_grd': '구조적 상태등급',
'oper_stts_grd': '운영적 상태등급',
};
const usgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val, code.text]));
const mqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val, code.text]));
// 🎯 단일 반복문으로 최적화 - 각 아이템마다 필요한 필드만 직접 매핑
const mappedData = (res.data || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
// trim 한 번만 수행
const trimmedUsgCd = item?.usgCd?.trim();
const trimmedMqltCd = item?.mqltCd?.trim();
// zone 값 한 번만 계산
const zone = item.swtrmtZoneNm ?? formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '';
// 직접 매핑 - 반복문 없이 한 번에 객체 생성
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '',
swtrmt_zone_nm: (zone === null || zone === '') ? '-' : zone,
usgCd: usgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '',
mqltCd: mqltCdMap.get(trimmedMqltCd) || item.mqltCd || formattedItem.mqltCd || '',
pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '',
prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '',
strc_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.strc_stts_grd ?? item.strc_stts_grd),
oper_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.oper_stts_grd ?? item.oper_stts_grd),
};
});
const mappingEndTime = performance.now();
console.log(`🔄 2. 데이터 매핑 시간: ${(mappingEndTime - mappingStartTime).toFixed(2)}ms`);
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedData; // 배열로 감싸서 전달
console.log('excelData', excelData);
const excelStartTime = performance.now();
// 파일명 생성 (분석번호 기반)
const label = WORK_TYPES.find((type) => type.val === workType)?.text
const fileName = `${label} 결과표_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: "A1:I1",
headerMapping: headerMapping
});
const excelEndTime = performance.now();
console.log(`📊 3. Excel 다운로드 시간: ${(excelEndTime - excelStartTime).toFixed(2)}ms`);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
const viewMultipleFeature = async () => {
if (activeTab === 'basic') {
if (!(tableData.length === 0)) {
setIsLoading(true);
const row = tableData[0];
const layerNm = row.lyrTypeNm;
const fids = tableData.map((item) => item.fid);
const core = GisApp.Module.layer as any;
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
setIsLoading(false);
onOpenChange(false);
}
} else if (activeTab === 'operation') {
if (!(detailTableData.length === 0)) {
setIsLoading(true);
const row = detailTableData[0];
console.log('row', row);
const layerNm = row.lyrTypeNm;
const fids = detailTableData.map((item) => item.fid);
const core = GisApp.Module.layer as any;
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
setIsLoading(false);
onOpenChange(false);
}
}
}
const renderBody = () => (
<>
{/* 탭 영역 */}
<UITab
defaultValue={activeTab}
tabList={TABS.map((tab) => ({
value: tab.val,
label: tab.text
}))}
onValueChange={(value: string) => setActiveTab(value)}
/>
{/* 기본정보 탭 */}
{activeTab === 'basic' && (
<BasicTab
basicType={basicType}
city={ctpvCd}
sgg={sggCd}
start1={start1}
start2={start2}
end1={end1}
end2={end2}
var1={var1}
var2={var2}
yearFilter={yearFilter}
handleSearch={handleSearch}
setBasicType={setBasicType}
setCity={setCtpvCd}
setCtpvName={setCtpvName}
setSgg={setSggCd}
setSggName={setSggName}
setYearFilter={setYearFilter}
setStart1={setStart1}
setStart2={setStart2}
setEnd1={setEnd1}
setEnd2={setEnd2}
setVar1={setVar1}
setVar2={setVar2}
/>
)}
{/* 운영정보 탭 */}
{activeTab === 'operation' && (
<OperationTab
workType={workType}
var1={var1}
var2={var2}
yearFilter={yearFilter}
handleSearch={handleSearch}
setWorkType={setWorkType}
setVar1={setVar1}
setVar2={setVar2}
setYearFilter={setYearFilter}
/>
)}
{/* 하수도정비기본계획보고서 탭 */}
{activeTab === 'report' && (
<ReportTab
open={open}
/>
)}
{/* 📋 결과 리스트 */}
{/* 기본정보 탭 */}
{activeTab === 'basic' && (
<div className="h-[79vh] w-full overflow-hidden rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<UiTable
headName={(() => {
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
return (<>{label} 조회 결과 ({tableCount.toLocaleString('ko-KR')})건</>);
})()
}
tableData={tableData}
headlist={dynamicHeadlist}
pageSize={10}
total={tableCount}
cellClick={handleBasicCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
curPage={curPage}
headButton={
<div className='flex'>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={basicExcelDownload}>파일 다운로드</Button>
</div>
}
/>
</div>
</div>
)}
{/* 운영정보 탭 */}
{activeTab === 'operation' && (
<>
<div className="h-[39vh] w-full overflow-hidden rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<UiTable
headName={
(() => {
const label = WORK_TYPES.find((type) => type.val === workType)?.text
return (<>{label} 사업 현황 ({tableCount.toLocaleString('ko-KR')})건</>);
})()
}
tableData={tableData}
headlist={dynamicHeadlist}
pageSize={OPERATION_TABLE_SIZE}
total={tableCount}
cellClick={handleOperationCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
curPage={curPage}
headButton={
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-sm font-medium green" size="sm" onClick={operationExcelDownload}>파일 다운로드</Button>
}
/>
</div>
</div>
<div className="h-[39vh] w-full overflow-hidden rounded-lg border">
{/* 테이블2 영역 */}
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isDetailLoading} />
<UiTable
headName={
(() => {
const label = WORK_TYPES.find((type) => type.val === workType)?.text
return (<>{label} 결과표 ({detailTableCount.toLocaleString('ko-KR')})건</>);
})()
}
tableData={detailTableData}
headlist={dynamicHeadlist2}
pageSize={OPERATION_TABLE_SIZE}
total={detailTableCount}
cellClick={handleOperationResultCellClick}
onPageChange={setDetailCurPage}
resetPageNo={isResetDetailPage}
resetSelection={isResetDetailSelection}
curPage={detailCurPage}
headButton={
<div className='flex'>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={operationResultExcelDownload}>파일 다운로드</Button>
</div>
}
/>
</div>
</div>
</>
)}
</>
);
return (
<>
<UiModal
open={open}
onOpenChange={onOpenChange}
bodyClass={`
h-[100dvh]
w-[60dvw]
max-w-[100dvw]
max-h-[100dvh]
overflow-hidden
z-[101]
`}
body={renderBody()}
useDirectRender={true}
/>
<Toaster position="top-center"></Toaster>
</>
);
};
export default React.memo(SearchModal);