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

335 lines
10 KiB
TypeScript

/**
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창
*
* 📄 Search Modal
* Next.js App Router (app/) 기반 검색 모달 컴포넌트
*
* - 탭 기반 검색 인터페이스 (기본정보, 유지관리 정보, 보고서)
* - 테이블 데이터 표시 및 페이지네이션 기능
* - 시설물 조회 결과 및 유지관리 사업 현황 표시
* - 엑셀 다운로드 및 지도에서 보기 기능
* - Zustand를 통한 검색 모달 상태 관리
* - 로딩 오버레이 및 선택 초기화 지원
*/
"use client";
import LoadingOverlay from "@/components/ui/LoadingBar";
import { UITab } from "@common/business_components/ui/tab";
import { StandardPopupLayout } from "@common/business_components";
import { Button } from "@common/components/ui";
import React, { useEffect, useMemo, useRef } from "react";
import useSearchModalStore from "@/store/useSearchModalStore";
import { useAlertStore } from "@/shared/lib";
import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions";
import {
ExclnSewagePumpCol2,
FldngWtlvMapCol2,
MsSwageMhlCol2,
MsSwagePplCol2,
stofcbrCol2,
SwtrmtFcltCol2,
SwtrmtPrcsZoneCol2,
} from "../components/common/table";
import BasicTab, { BASIC_TYPES } from "./basicTab";
import OperationTab from "./operationTab";
import ReportTab from "./reportTab";
import useSearchModal from "./useSearchModal";
import useBasicTab from "./useBasicTab";
import useOperationTab from "./useOperationTab";
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
import type { UiHeadProps } from "@common/business_components/ui/table";
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
type SearchModalProps = {
renderMode?: "launcher" | "popup";
autoSearchReady?: boolean;
};
const SearchModal: React.FC<SearchModalProps> = ({
renderMode = "launcher",
autoSearchReady = false,
}) => {
const alert = useAlertStore((state) => state.setMessage);
const {
showTable,
isSearchModalOpen,
setIsSearchModalOpen,
activeTab,
basicType,
workType,
tableData,
detailTableData,
tableCount,
detailTableCount,
curPage,
detailCurPage,
detailBizId,
isLoading,
isDetailLoading,
setActiveTab,
setCurPage,
setDetailCurPage,
setIsResetPage,
setIsResetDetailPage,
setDetailBizId,
setIsResetDetailSelection,
resetFilters,
} = useSearchModalStore();
const { TABS, viewMultipleFeature, fetchFilteredData } = useSearchModal();
const { handleBasicCellClick, basicExcelDownload } = useBasicTab();
const {
WORK_TYPES,
OPERATION_TABLE_SIZE,
OPERATION_TABLE_HEADLIST,
MAINTENANACE_TABLE_HEADLIST,
CLEANING_TABLE_HEADLIST,
INSPECTION_TABLE_HEADLIST,
handleOperationCellClick,
handleOperationResultCellClick,
operationExcelDownload,
operationResultExcelDownload,
handleOperationPageNation,
} = useOperationTab();
const hasAutoSearchedRef = useRef(false);
const hasInitializedTabAutoSearchRef = useRef(false);
const dynamicHeadlist: UiHeadProps[] = useMemo(() => {
if (activeTab === "basic") {
const headlist =
basicType === "tb_ms_swage_ppl"
? MsSwagePplCol2
: basicType === "tb_il_swage_prcs_fclt"
? SwtrmtFcltCol2
: basicType === "tb_ms_swage_prcs_zone"
? SwtrmtPrcsZoneCol2
: basicType === "tb_ms_fldng_wtlv_map"
? FldngWtlvMapCol2
: basicType === "tb_ms_swage_mhl"
? MsSwageMhlCol2
: basicType === "tb_ms_swage_pmstn"
? ExclnSewagePumpCol2
: basicType === "tb_ms_stofcbr"
? stofcbrCol2
: [];
return headlist.map((head) => ({
...head,
title: head.id === "rowNumber" ? "순번" : head.title,
}));
}
if (activeTab === "operation") {
return OPERATION_TABLE_HEADLIST.map((head) => ({
...head,
title: head.id === "rowNumber" ? "순번" : head.title,
}));
}
return [];
}, [activeTab, basicType, workType, OPERATION_TABLE_HEADLIST]);
const dynamicHeadlist2: UiHeadProps[] = useMemo(() => {
if (activeTab === "operation") {
const headlist =
workType === WORK_TYPES[0]?.val
? MAINTENANACE_TABLE_HEADLIST
: workType === WORK_TYPES[1]?.val
? CLEANING_TABLE_HEADLIST
: workType === WORK_TYPES[2]?.val
? INSPECTION_TABLE_HEADLIST
: [];
return headlist.map((head) => ({
...head,
title: head.id === "rowNumber" ? "순번" : head.title,
}));
}
return [];
}, [
activeTab,
workType,
WORK_TYPES,
MAINTENANACE_TABLE_HEADLIST,
CLEANING_TABLE_HEADLIST,
INSPECTION_TABLE_HEADLIST,
]);
useEffect(() => {
if (renderMode !== "launcher" || !isSearchModalOpen) return;
const popupWindow = openWindowPopup("mapSearch", {
sourcePath: window.location.pathname,
});
if (!popupWindow) {
alert("팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.");
}
setIsSearchModalOpen(false);
}, [alert, isSearchModalOpen, renderMode, setIsSearchModalOpen]);
useEffect(() => {
resetFilters();
}, [activeTab, basicType, workType, resetFilters]);
useEffect(() => {
if (
renderMode !== "popup" ||
!autoSearchReady ||
hasAutoSearchedRef.current
)
return;
hasAutoSearchedRef.current = true;
hasInitializedTabAutoSearchRef.current = true;
void fetchFilteredData();
}, [autoSearchReady, fetchFilteredData, renderMode]);
useEffect(() => {
if (renderMode !== "popup" || !autoSearchReady) return;
if (!hasInitializedTabAutoSearchRef.current) return;
if (activeTab === "report") return;
setCurPage(1);
setIsResetPage(true);
setIsResetDetailPage(true);
void fetchFilteredData();
}, [activeTab, basicType, workType]);
useEffect(() => {
setIsResetPage(false);
setIsResetDetailPage(true);
setDetailBizId(0);
if (showTable) {
void fetchFilteredData();
}
}, [curPage]);
useEffect(() => {
setIsResetDetailSelection(false);
if (showTable) {
void handleOperationPageNation(Number(detailBizId));
}
}, [detailCurPage]);
const renderBody = () => (
<>
<UITab
defaultValue={activeTab}
variant="underline"
className="shrink-0"
tabList={TABS.map((tab: any) => ({
value: tab.val,
label: tab.text,
}))}
onValueChange={(value: string) => setActiveTab(value)}
/>
<LoadingOverlay isLoading={isLoading || isDetailLoading} />
{activeTab === "basic" && <BasicTab />}
{activeTab === "operation" && <OperationTab />}
{activeTab === "report" && <ReportTab open={activeTab === "report"} />}
{activeTab === "basic" && (
<div className="w-full">
<MapPopupDataTable
rows={tableData as Array<Record<string, unknown>>}
title={(() => {
const label = BASIC_TYPES.find(
(type) => type.val === basicType,
)?.text;
return <>{label} 목록</>;
})()}
headlist={dynamicHeadlist}
pageSize={10}
total={tableCount}
onCellClick={(_row, index, headId) =>
void handleBasicCellClick(index, headId)
}
onPageChange={setCurPage}
page={curPage}
scrollMode="horizontal"
minTableWidth="160rem"
actions={
<div className="flex gap-2">
<Button onClick={basicExcelDownload}>엑셀다운로드</Button>
<Button onClick={viewMultipleFeature}>지도에서 보기</Button>
</div>
}
/>
</div>
)}
{activeTab === "operation" && (
<>
<div className="w-full">
<MapPopupDataTable
rows={tableData as Array<Record<string, unknown>>}
title={(() => {
const label = WORK_TYPES.find(
(type) => type.val === workType,
)?.text;
return <>{label} 사업 목록</>;
})()}
headlist={dynamicHeadlist}
pageSize={OPERATION_TABLE_SIZE}
total={tableCount}
onCellClick={(_row, index, headId) =>
void handleOperationCellClick(index, headId)
}
onPageChange={setCurPage}
page={curPage}
scrollMode="horizontal"
minTableWidth="140rem"
actions={
<Button onClick={operationExcelDownload}>엑셀다운로드</Button>
}
/>
</div>
<div className="w-full">
<MapPopupDataTable
rows={detailTableData as Array<Record<string, unknown>>}
title={(() => {
const label = WORK_TYPES.find(
(type) => type.val === workType,
)?.text;
return <>{label} 결과 목록</>;
})()}
headlist={dynamicHeadlist2}
pageSize={OPERATION_TABLE_SIZE}
total={detailTableCount}
onCellClick={(_row, index, headId) =>
void handleOperationResultCellClick(index, headId)
}
onPageChange={setDetailCurPage}
page={detailCurPage}
scrollMode="horizontal"
minTableWidth="160rem"
actions={
<div className="flex gap-2">
<Button onClick={operationResultExcelDownload}>
엑셀다운로드
</Button>
<Button onClick={viewMultipleFeature}>지도에서 보기</Button>
</div>
}
/>
</div>
</>
)}
</>
);
if (renderMode === "launcher") {
return null;
}
return (
<StandardPopupLayout
title={windowPopupDefinitions.mapSearch.title}
bodyClassName="overflow-x-hidden"
>
<div className="flex min-w-0 flex-col gap-3">{renderBody()}</div>
</StandardPopupLayout>
);
};
export default React.memo(SearchModal);