Files
frontend/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts
T
2026-09-15 10:00:42 +09:00

524 lines
20 KiB
TypeScript

/**
* 📄 useBasicTab Hook
* 검색 모달 기본정보 탭 훅
*
* - 필터링된 시설물 데이터 조회 및 포맷팅
* - 테이블 셀 클릭 시 지도 피처 선택 및 줌 기능
* - 엑셀 다운로드 기능 (한글 헤더 지원)
* - CQL 필터 생성을 통한 쿼리 조건 설정
* - Zustand를 통한 상태 관리 및 로딩 처리
* - 데이터 매핑 및 코드 변환 (시도/시군구, 용도 등)
*
* 🔗 사용 API
* - 관망도 시설물 목록 조회: GET /api/v1/fclts (sewerage-service)
*/
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
import { fetchAllPages } from "@/shared/lib/fetch-all-pages";
import {
MS_SWAGE_PPL_USE_CODES,
MS_SWAGE_PPL_CODES,
MS_SWAGE_PPL_SHAPE,
MS_SWAGE_MHL_CODES,
MS_SWAGE_MHL_USE_CODES,
FldngWtlvMapExcelCol,
MsSwagePplExcelCol,
SwtrmtFcltExcelCol,
SwtrmtPrcsZoneExcelCol,
MsSwageMhlExcelCol,
ExclnSewagePumpExcelCol,
stofcbrExcelCol,
} from "../components/common/table";
import { getData } from "@/components/lib/api";
import { BASIC_TYPES } from "./basicTab";
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
import useStore from "@/store/useStore";
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 { swagePplCqlFilters } from "./swagePpl";
import { swagePrcsFcltCqlFilters } from "./swagePrcsFclt";
import { swagePrcsZoneCqlFilters } from "./swagePrcsZone";
import { fldngWtlvMapCqlFilters } from "./fldngWtlvMap";
import { swageMhlCqlFilters } from "./swageMhl";
import { swagePmstnCqlFilters } from "./swagePmstn";
import { stofcbrCqlFilters } from "./stofcbr";
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/");
// Excel 열에 포함되지 않는 화면용 유하방향 보강은 제외하고 서버가 일괄 병합할 수 있는 크기로 순회한다.
const BASIC_EXCEL_BATCH_SIZE = 2_000;
export default function useBasicTab() {
const alert = useAlertStore((state) => state.setMessage);
const { confirm } = useConfirm();
const {
basicType,
tableData,
curPage,
todayYear,
todayMonth,
todayDate,
setIsSearchModalOpen,
setTableData,
setTableCount,
setIsLoading,
setAppliedBasicQuery,
} = useSearchModalStore();
const { ctpv, sgg } = useStore();
const fetchBasicFilteredData = async () => {
// 초기화 직후에도 이전 렌더의 조건을 사용하지 않도록 최신 스토어 값을 읽는다.
const { basicType, curPage } = useSearchModalStore.getState();
let cqlFilters: string[] = [];
const queryParams = new URLSearchParams({
lyrTypeNm: basicType,
page: `${curPage - 1}`,
size: "10",
});
// 현재 서버의 유하방향 배치 보강 옵션은 실패 시 목록 전체를 500으로 중단한다.
// 상세 유하방향 조회와 분리해 기본 정보검색 목록을 우선 정상 제공한다.
if (basicType === "tb_ms_swage_ppl")
cqlFilters = swagePplCqlFilters(); // 하수관로
else if (basicType === "tb_il_swage_prcs_fclt")
cqlFilters = swagePrcsFcltCqlFilters(); // 하수처리시설
else if (basicType === "tb_ms_swage_prcs_zone")
cqlFilters = swagePrcsZoneCqlFilters(); // 하수처리구역
else if (basicType === "tb_ms_fldng_wtlv_map")
cqlFilters = fldngWtlvMapCqlFilters(); // 침수흔적도
else if (basicType === "tb_ms_swage_mhl")
cqlFilters = swageMhlCqlFilters(); // 하수맨홀
else if (basicType === "tb_ms_swage_pmstn")
cqlFilters = swagePmstnCqlFilters(); // 하수펌프장
else if (basicType === "tb_ms_stofcbr") cqlFilters = stofcbrCqlFilters(); // 우수토실
if (cqlFilters.length > 0)
queryParams.append("cqlFiter", cqlFilters.join(" and "));
const res = await getData(`v1/fclts?${queryParams.toString()}`);
const totalCount = Number(res.count ?? 0);
// 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가
const dataWithRowNumber = (res.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 ??
item?.stdgCd ??
formattedItem?.stdgCd ??
"",
)?.trim();
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
if (!sggCd && code) sggCd = code.slice(0, 5);
}
const matchedMqltCode =
basicType === "tb_ms_swage_ppl"
? MS_SWAGE_PPL_CODES.find(
(code) => code.val === item?.mqltCd?.trim(),
) // 하수관로 재질
: basicType === "tb_ms_swage_mhl"
? MS_SWAGE_MHL_CODES.find(
(code) => code.val === item?.mqltCd?.trim(),
) // 맨홀 재질
: undefined;
const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(
(code) => code.val === item?.usgCd,
); // 맨홀 용도
const matchedPplShpCode = MS_SWAGE_PPL_SHAPE.find(
(code) => code.val === item?.fcltyShpCd,
); // 하수관로 시설물형태코드
const ctpvNm =
ctpv.find((code) => code.val.trim() === ctpvCd)?.text || item?.ctpvNm;
const sggNm =
sgg.find((code) => code.val.trim() === sggCd)?.text || item?.sggNm;
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(
(code) => code.val === item?.usgCd?.trim(),
);
// 추가 처리 및 일련번호 추가
return {
...formattedItem,
rowNumber: totalCount - ((curPage - 1) * 10 + index),
scsYn:
item.scsYn === null || item.scsYn === undefined ? "N" : item?.scsYn, // scsYn이 null이면 'N'으로 변경
mqltCd: matchedMqltCode ? matchedMqltCode.text : item?.mqltCd,
usgCd: matchedMhlUsgCode
? matchedMhlUsgCode.text
: usgCdObj
? usgCdObj.text
: item?.usgCd,
ctpvNm: ctpvNm ?? "-",
sggNm: sggNm ?? "-",
swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || "-",
fcltyShpCd: matchedPplShpCode
? matchedPplShpCode.text
: item?.fcltyShpCd,
};
},
);
setTableData(dataWithRowNumber);
setTableCount(totalCount);
queryParams.delete("page");
queryParams.delete("size");
setAppliedBasicQuery({
basicType,
query: queryParams.toString(),
});
};
// 기본정보 테이블 cellClick 핸들러
const handleBasicCellClick = async (
index?: number | undefined,
headId?: String,
) => {
if (typeof index === "number") {
const row = tableData[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 닫기
}
};
// 기본정보 Excel 다운로드 핸들러
const basicExcelDownload = async () => {
if (tableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
if (
!(await confirm(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
))
)
return;
await onDownload();
async function onDownload() {
setIsLoading(true);
try {
const appliedQuery = useSearchModalStore.getState().appliedBasicQuery;
if (!appliedQuery) {
throw new Error(
"화면 조회조건을 확인할 수 없습니다. 다시 조회해주세요.",
);
}
const appliedBasicType = appliedQuery.basicType;
const queryParams = new URLSearchParams(appliedQuery.query);
if (appliedBasicType === "tb_ms_swage_ppl") {
queryParams.delete("includeFlowDirectionBasis");
}
let headerMapping: { [key: string]: string } = {};
let headerRange: string = "A1:F1";
switch (appliedBasicType) {
case "tb_ms_swage_ppl": {
// 하수관로
headerMapping = MsSwagePplExcelCol;
headerRange = "A1:Q1";
break;
}
case "tb_il_swage_prcs_fclt": {
// 하수처리시설
headerMapping = SwtrmtFcltExcelCol;
headerRange = "A1:J1";
break;
}
case "tb_ms_swage_prcs_zone": {
// 하수처리구역
headerMapping = SwtrmtPrcsZoneExcelCol;
headerRange = "A1:D1";
break;
}
case "tb_ms_fldng_wtlv_map": {
// 침수흔적도
headerMapping = FldngWtlvMapExcelCol;
headerRange = "A1:O1";
break;
}
case "tb_ms_swage_mhl": {
// 하수맨홀
headerMapping = MsSwageMhlExcelCol;
headerRange = "A1:J1";
break;
}
case "tb_ms_swage_pmstn": {
// 하수펌프장
headerMapping = ExclnSewagePumpExcelCol;
headerRange = "A1:J1";
break;
}
case "tb_ms_stofcbr": {
// 우수토실
headerMapping = stofcbrExcelCol;
headerRange = "A1:H1";
break;
}
}
const res = await fetchAllPages<any, any>({
batchSize: BASIC_EXCEL_BATCH_SIZE,
fetchPage: (page, size) => {
const pageParams = new URLSearchParams(queryParams);
pageParams.set("page", String(page));
pageParams.set("size", String(size));
return getData(`v1/fclts?${pageParams.toString()}`);
},
getItems: (page) => (Array.isArray(page?.data) ? page.data : []),
getTotal: (page) => Number(page?.count ?? 0),
});
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 pplMqltCdMap = new Map(
MS_SWAGE_PPL_CODES.map((code) => [code.val?.trim(), code.text]),
);
const pplShpCdMap = new Map(
MS_SWAGE_PPL_SHAPE.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]),
);
let mappedContent = res.items.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 ??
item?.stdgCd ??
formattedItem?.stdgCd ??
"",
)?.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 (appliedBasicType) {
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 ?? "-",
vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? "-",
wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? "-",
fcltyShpCd:
pplShpCdMap.get(item?.fcltyShpCd?.trim()) ||
item.fcltyShpCd ||
formattedItem.fcltyShpCd ||
"-",
mqltCd:
pplMqltCdMap.get(item?.mqltCd?.trim()) ||
item.mqltCd ||
formattedItem.mqltCd ||
"-",
};
}
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_fldng_wtlv_map": {
// 침수흔적도
return {
fldngYr: formattedItem.fldngYr ?? item.fldngYr ?? "-",
fldngDstrNm:
formattedItem.fldngDstrNm ?? item.fldngDstrNm ?? "-",
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? "-",
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? "-",
bgngYmd: formattedItem.bgngYmd ?? item.bgngYmd ?? "-",
fldngBgngHm:
formattedItem.fldngBgngHm ?? item.fldngBgngHm ?? "-",
endYmd: formattedItem.endYmd ?? item.endYmd ?? "-",
fldngEndHm: formattedItem.fldngEndHm ?? item.fldngEndHm ?? "-",
fldngHr: formattedItem.fldngHr ?? item.fldngHr ?? "-",
fldngDstrctNm:
formattedItem.fldngDstrctNm ?? item.fldngDstrctNm ?? "-",
fldngCsDtlCd:
formattedItem.fldngCsDtlCd ?? item.fldngCsDtlCd ?? "-",
avgFldngWtlv:
formattedItem.avgFldngWtlv ?? item.avgFldngWtlv ?? "-",
fldngArea: formattedItem.fldngArea ?? item.fldngArea ?? "-",
exmnYr: formattedItem.exmnYr ?? item.exmnYr ?? "-",
damAmt: formattedItem.damAmt ?? item.damAmt ?? "-",
};
}
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; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const label = BASIC_TYPES.find(
(type) => type.val === appliedBasicType,
)?.text;
const fileName = `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as unknown as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: headerRange,
headerMapping: headerMapping,
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
return { fetchBasicFilteredData, handleBasicCellClick, basicExcelDownload };
}