Files
frontend/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx
T
2026-07-09 15:57:30 +09:00

262 lines
9.8 KiB
TypeScript

/**
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창
*
* 📄 Search Modal
* Next.js App Router (app/) 기반 검색 모달 컴포넌트
*
* - 탭 기반 검색 인터페이스 (기본정보, 유지관리 정보, 보고서)
* - 테이블 데이터 표시 및 페이지네이션 기능
* - 시설물 조회 결과 및 유지관리 사업 현황 표시
* - 엑셀 다운로드 및 지도에서 보기 기능
* - Zustand를 통한 검색 모달 상태 관리
* - 로딩 오버레이 및 선택 초기화 지원
*/
'use client';
import LoadingOverlay from '@/components/ui/LoadingBar';
import { UITab, UiTable } from '@common/business_components/ui';
import { Button } from '@common/components/ui';
import React, { useEffect, useMemo, useRef } from 'react';
import { Toaster } from 'react-hot-toast';
import useSearchModalStore from '@/store/useSearchModalStore';
import { ExclnSewagePumpCol2, 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';
type SearchModalProps = {
renderMode?: 'launcher' | 'popup';
autoSearchReady?: boolean;
};
const SearchModal: React.FC<SearchModalProps> = ({ renderMode = 'launcher', autoSearchReady = false }) => {
const {
showTable,
isSearchModalOpen, setIsSearchModalOpen,
activeTab, basicType, workType,
tableData, detailTableData,
tableCount, detailTableCount,
curPage, detailCurPage,
detailBizId,
isLoading, isDetailLoading,
isResetPage, isResetDetailPage,
isResetSelection, isResetDetailSelection,
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: { 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 OPERATION_TABLE_HEADLIST;
return [];
}, [activeTab, basicType, workType, OPERATION_TABLE_HEADLIST]);
const dynamicHeadlist2: { id: string, title: string }[] = useMemo(() => {
if (activeTab === 'operation') {
if (workType === WORK_TYPES[0]?.val) return MAINTENANACE_TABLE_HEADLIST;
if (workType === WORK_TYPES[1]?.val) return CLEANING_TABLE_HEADLIST;
if (workType === WORK_TYPES[2]?.val) return INSPECTION_TABLE_HEADLIST;
}
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);
}, [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}
tabList={TABS.map((tab: any) => ({
value: tab.val,
label: tab.text
}))}
onValueChange={(value: string) => setActiveTab(value)}
/>
{activeTab === 'basic' && (<BasicTab />)}
{activeTab === 'operation' && (<OperationTab />)}
{activeTab === 'report' && (<ReportTab open={activeTab === 'report'} />)}
{activeTab === 'basic' && (
<div className="mt-4 h-[79dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border">
<LoadingOverlay isLoading={isLoading} />
<UiTable
className='overflow-x-auto [&_table]:min-w-full [&_table]:w-max'
headName={(() => {
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text;
return (<>{label} 조회 결과</>);
})()}
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>
)}
{activeTab === 'operation' && (
<>
<div className="mt-4 h-[39dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border">
<LoadingOverlay isLoading={isLoading} />
<UiTable
headName={(() => {
const label = WORK_TYPES.find((type) => type.val === workType)?.text;
return (<>{label} 사업 현황</>);
})()}
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 className="h-[39dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border">
<LoadingOverlay isLoading={isDetailLoading} />
<UiTable
headName={(() => {
const label = WORK_TYPES.find((type) => type.val === workType)?.text;
return (<>{label} 결과표</>);
})()}
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>
</>
)}
</>
);
if (renderMode === 'launcher') {
return null;
}
return (
<div className='h-dvh overflow-x-auto overflow-y-hidden bg-container p-4 text-foreground'>
<h1 className="mb-3 text-2xl font-semibold text-foreground">관망도 검색</h1>
<div className="h-[calc(100dvh-5.6rem)] w-full overflow-y-hidden rounded-lg border border-border bg-background p-4">
{renderBody()}
</div>
<Toaster position="top-center" />
</div>
);
};
export default React.memo(SearchModal);