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

560 lines
19 KiB
TypeScript

/**
* 📄 useOperationTab Hook
* 검색 모달 유지관리 정보 탭 훅
*
* - 필터링된 사업 현황 데이터 조회 및 포맷팅
* - 사업 현황 테이블 셀 클릭 시 상세 결과표 조회
* - 결과표 셀 클릭 시 지도 피처 선택 및 줌 기능
* - 엑셀 다운로드 기능 (사업 현황 및 결과표)
* - CQL 필터 생성을 통한 쿼리 조건 설정
* - Zustand를 통한 상태 관리 및 로딩 처리
* - 데이터 매핑 및 코드 변환 (사업 유형, 용도, 재질 등)
*
* 🔗 사용 API
* - 유지관리 정보 목록 조회: GET /api/v1/bizInfo (sewerage-service)
* - 유지관리 정보(사업정보) 관련 시설 목록 조회: GET /api/v1/bizInfo/fclts (sewerage-service)
*/
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import {
MS_SWAGE_PPL_CODES,
MS_SWAGE_PPL_USE_CODES,
} from "../components/common/table";
import { getData } from "@/components/lib/api";
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
import useSearchModalStore from "@/store/useSearchModalStore";
import useStore from "@/store/useStore";
import { postPopupMessageAndClose } from "@/features/windowPopup/lib/popupMessage";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
const isPopupWindow = () =>
typeof window !== "undefined" &&
!!window.opener &&
window.location.pathname.startsWith("/popup/");
export default function useOperationTab() {
const OPERATION_TABLE_SIZE = 5;
const alert = useAlertStore((state) => state.setMessage);
const { confirm } = useConfirm();
const {
workType,
tableData,
detailTableData,
curPage,
detailCurPage,
detailBizId,
var1,
var2,
yearFilter,
todayYear,
todayMonth,
todayDate,
setIsSearchModalOpen,
setTableData,
setDetailTableData,
setTableCount,
setDetailTableCount,
setDetailCurPage,
setIsLoading,
setIsDetailLoading,
setIsResetDetailPage,
setIsResetDetailSelection,
setDetailBizId,
getDateString,
} = useSearchModalStore();
const { BIZ_TY_CD } = useStore();
const WORK_TYPES = [
// 유지관리 정보 '구분' 선택 목록
{ val: "['200', '201', '204']", text: "보수 및 갱신" },
{ val: "['100']", text: "점검 및 진단" },
{ val: "['203']", text: "청소 및 준설" },
];
const OPERATION_TABLE_HEADLIST = [
// 유지관리 정보 - 사업 현황
{ id: "rowNumber", title: "순번" },
{ id: "bizInfoNm", title: "사업명" },
{ id: "flfmtBgngYmd", title: "수행시작일자" },
{ id: "flfmtEndYmd", title: "수행종료일자" },
{ id: "flfmtYr", title: "수행년도" },
{ id: "bizTypeCd", title: "사업유형" },
{ id: "bizAmt", title: "사업금액" },
];
const MAINTENANACE_TABLE_HEADLIST = [
// 유지관리 정보 - 보수 및 갱신 결과표
{ id: "rowNumber", title: "순번" },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "acqsPrmpc", title: "취득금액" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
// { id: "instlYmd", title: "사업구분" },
];
const CLEANING_TABLE_HEADLIST = [
// 유지관리 정보 - 청소 및 준설 결과표
{ id: "rowNumber", title: "순번" },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "acqsPrmpc", title: "취득금액" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
];
const INSPECTION_TABLE_HEADLIST = [
// 유지관리 정보 - 점검 및 진단 결과표
{ id: "rowNumber", title: "순번" },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "riskEval", title: "위험평가" },
// { id: "acqsPrmpc", title: "취득금액" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "remLife", title: "잔존수명" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
{ id: "strc_stts_grd", title: "구조적 상태등급" },
{ id: "oper_stts_grd", title: "운영적 상태등급" },
];
const fetchOperationFilteredData = async () => {
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}`);
const totalElements = Number(res.totalElements ?? 0);
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:
totalElements - ((curPage - 1) * OPERATION_TABLE_SIZE + index),
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
};
},
);
setTableData(mappedContent || []);
setTableCount(totalElements);
setDetailTableData([]);
setDetailTableCount(0);
};
// 유지관리 정보 테이블 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: "0",
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()}`,
);
const totalDetailElements = Number(res.count ?? 0);
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: totalDetailElements - detailIndex,
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : "-",
};
},
);
setDetailTableData(mappedData);
setDetailTableCount(totalDetailElements);
} catch (error) {
console.error("유지관리 정보 cellClick 에러:", error);
setDetailTableData([]);
setDetailTableCount(0);
alert("선택한 사업의 결과 목록을 불러오지 못했습니다.");
} finally {
setIsDetailLoading(false);
setIsResetDetailPage(false);
setIsResetDetailSelection(false);
}
}
};
// 유지관리 정보 디테일 테이블 cellClick 핸들러
const handleOperationResultCellClick = async (
index?: number | undefined,
headId?: String,
) => {
if (typeof index === "number") {
const row = detailTableData[index];
if (isPopupWindow()) {
postPopupMessageAndClose(
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
{
layerNm: row.lyrTypeNm,
fid: String(row.fid),
},
);
return;
}
setIsLoading(true);
const layerNm = row.lyrTypeNm;
const fid = row.fid;
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON(layerNm, fid);
setIsLoading(false);
setIsSearchModalOpen(false); // SearchModal 닫기
}
};
// 유지관리 정보 디테일 테이블 페이지네이션 핸들러
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 totalElements = Number(res.count ?? 0);
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:
totalElements -
((detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex),
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : "-",
};
},
);
setDetailTableData(mappedData);
setDetailTableCount(totalElements);
} catch (error) {
console.error("유지관리 정보 페이지네이션 에러:", error);
alert("선택한 사업의 결과 목록을 불러오지 못했습니다.");
} finally {
setIsDetailLoading(false);
}
}
};
// 유지관리 정보 Excel 다운로드 핸들러 추가
const operationExcelDownload = async () => {
if (tableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
if (
!(await confirm(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
))
)
return;
await onAnalysis();
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;
}
if (
!(await confirm(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
))
)
return;
await onAnalysis();
async function onAnalysis() {
setIsLoading(true);
try {
const RequestDTO: Record<string, string> = {
size: "100",
bizId: String(detailBizId),
};
const res = await getData(
`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`,
);
// 헤더 매핑 정의
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,
),
};
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedData; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
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,
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
const emitMultipleFeaturesToMap = () => {
if (detailTableData.length === 0 || !isPopupWindow()) return false;
const row = detailTableData[0];
postPopupMessageAndClose(
WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_MULTIPLE,
{
layerNm: row.lyrTypeNm,
fids: JSON.stringify(detailTableData.map((item) => item.fid)),
},
);
return true;
};
return {
WORK_TYPES,
OPERATION_TABLE_HEADLIST,
MAINTENANACE_TABLE_HEADLIST,
CLEANING_TABLE_HEADLIST,
INSPECTION_TABLE_HEADLIST,
OPERATION_TABLE_SIZE,
fetchOperationFilteredData,
handleOperationCellClick,
handleOperationResultCellClick,
handleOperationPageNation,
emitMultipleFeaturesToMap,
operationExcelDownload,
operationResultExcelDownload,
};
}