383 lines
15 KiB
TypeScript
383 lines
15 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, 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, UiTableSort } from '@common/business_components/ui';
|
|
|
|
type SearchModalProps = {
|
|
renderMode?: 'launcher' | 'popup';
|
|
autoSearchReady?: boolean;
|
|
};
|
|
|
|
// 기본정보 표시 필드 중 GeoServer 원천 속성으로 정확히 정렬할 수 있는 필드만 매핑한다.
|
|
const BASIC_SORT_KEYS: Record<string, Record<string, string>> = {
|
|
tb_ms_swage_ppl: {
|
|
rowNumber: 'rowNumber',
|
|
fcltMngNo: 'fclt_mng_no', instMngNo: 'inst_mng_no', instlYmd: 'instl_ymd',
|
|
ctpvNm: 'pbadms_emd_cd',
|
|
sggNm: 'pbadms_emd_cd', swtrmt_zone_nm: 'swtrmt_zone_nm', usgCd: 'usg_cd',
|
|
pdmtVl: 'pdmt_vl', prlgVl: 'prlg_vl', tmptDpth: 'tmpt_dpth',
|
|
tmptMhlMngNo: 'tmpt_mhl_mng_no', scendDpth: 'scend_dpth',
|
|
scendMhlMngNo: 'scend_mhl_mng_no', vrtcLen: 'vrtc_len', wdthLen: 'wdth_len',
|
|
fcltyShpCd: 'fclty_shp_cd', mqltCd: 'mqlt_cd',
|
|
},
|
|
tb_il_swage_prcs_fclt: {
|
|
rowNumber: 'rowNumber',
|
|
swtrmtFcltNm: 'swtrmt_fclt_nm', fcltCpct: 'fclt_cpct', rgnExpln: 'rgn_expln',
|
|
oprtngStrtYmd: 'oprtng_strt_ymd', laeNm: 'lae_nm', sareaNm: 'sarea_nm',
|
|
rvrNm: 'rvr_nm', dschgWtstNm: 'dschg_wtst_nm', swtrmtPpltnCnt: 'swtrmt_ppltn_cnt',
|
|
rprsMsrcCn: 'rprs_msrc_cn',
|
|
},
|
|
tb_ms_swage_prcs_zone: {
|
|
rowNumber: 'rowNumber',
|
|
swtrmtZoneNm: 'swtrmt_zone_nm', ctpvNm: 'ctpv_nm', sggNm: 'sgg_nm',
|
|
swtrmtFcltNm: 'swtrmt_fclt_nm',
|
|
},
|
|
tb_ms_fldng_wtlv_map: {
|
|
rowNumber: 'rowNumber',
|
|
fldngYr: 'fldng_yr', fldngDstrNm: 'fldng_dstr_nm', ctpvNm: 'stdg_cd', sggNm: 'stdg_cd',
|
|
bgngYmd: 'bgng_ymd', fldngBgngHm: 'fldng_bgng_hm', endYmd: 'end_ymd', fldngEndHm: 'fldng_end_hm',
|
|
fldngHr: 'fldng_hr', fldngDstrctNm: 'fldng_dstrct_nm', fldngCsDtlCd: 'fldng_cs_dtl_cd',
|
|
avgFldngWtlv: 'avg_fldng_wtlv', fldngArea: 'fldng_area', exmnYr: 'exmn_yr', damAmt: 'dam_amt',
|
|
},
|
|
tb_ms_swage_mhl: {
|
|
rowNumber: 'rowNumber',
|
|
fcltMngNo: 'fclt_mng_no', instlYmd: 'instl_ymd', swageMhlDiamVl: 'swage_mhl_diam_vl',
|
|
ctpvNm: 'pbadms_emd_cd', sggNm: 'pbadms_emd_cd', usgCd: 'usg_cd',
|
|
wdthLen: 'wdth_len', vrtcLen: 'vrtc_len', altdVl: 'altd_vl', dpthlVl: 'dpthl_vl',
|
|
},
|
|
tb_ms_swage_pmstn: {
|
|
rowNumber: 'rowNumber',
|
|
fcltMngNo: 'fclt_mng_no', swagePmstnNm: 'swage_pmstn_nm', instlYmd: 'instl_ymd',
|
|
ctpvNm: 'pbadms_emd_cd', sggNm: 'pbadms_emd_cd',
|
|
dailyPrcsCpct: 'daily_prcs_cpct', maxImpndCpct: 'max_impnd_cpct', altdVl: 'altd_vl',
|
|
wtlvVl: 'wtlv_vl', exclnWpmpAbltVl: 'excln_wpmp_ablt_vl',
|
|
},
|
|
tb_ms_stofcbr: {
|
|
rowNumber: 'rowNumber',
|
|
fcltMngNo: 'fclt_mng_no', mapshtNo: 'mapsht_no', instlYmd: 'instl_ymd',
|
|
ctpvNm: 'pbadms_emd_cd', sggNm: 'pbadms_emd_cd',
|
|
worflwWallHgt: 'worflw_wall_hgt', swageMhlMngNo: 'swage_mhl_mng_no', drtnAngl: 'drtn_angl',
|
|
},
|
|
};
|
|
|
|
const OPERATION_LIST_SORT_KEYS: Record<string, string> = {
|
|
rowNumber: 'rowNumber', bizInfoNm: 'bizInfoNm', flfmtBgngYmd: 'flfmtBgngYmd',
|
|
flfmtEndYmd: 'flfmtEndYmd', flfmtYr: 'flfmtYr', bizTypeCd: 'bizTypeCd', bizAmt: 'bizAmt',
|
|
};
|
|
|
|
const OPERATION_DETAIL_SORT_KEYS: Record<string, string> = {
|
|
rowNumber: 'rowNumber',
|
|
fcltMngNo: 'fclt_mng_no',
|
|
swtrmt_zone_nm: 'swtrmt_zone_nm',
|
|
usgCd: 'usg_cd',
|
|
mqltCd: 'mqlt_cd',
|
|
pdmtVl: 'pdmt_vl',
|
|
prlgVl: 'prlg_vl',
|
|
instlYmd: 'instl_ymd',
|
|
strc_stts_grd: 'strc_stts_grd',
|
|
oper_stts_grd: 'oper_stts_grd',
|
|
};
|
|
|
|
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,
|
|
setIsResetSelection,
|
|
setDetailBizId,
|
|
setIsResetDetailSelection,
|
|
sort, detailSort,
|
|
setSort, setDetailSort,
|
|
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 : [];
|
|
const sortKeys = BASIC_SORT_KEYS[basicType] ?? {};
|
|
return headlist.map((head) => sortKeys[head.id]
|
|
? { ...head, sortable: true, sortKey: sortKeys[head.id] }
|
|
: head);
|
|
}
|
|
if (activeTab === 'operation') {
|
|
return OPERATION_TABLE_HEADLIST.map((head) => OPERATION_LIST_SORT_KEYS[head.id]
|
|
? { ...head, sortable: true, sortKey: OPERATION_LIST_SORT_KEYS[head.id] }
|
|
: head);
|
|
}
|
|
|
|
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) => OPERATION_DETAIL_SORT_KEYS[head.id]
|
|
? { ...head, sortable: true, sortKey: OPERATION_DETAIL_SORT_KEYS[head.id] }
|
|
: head);
|
|
}
|
|
|
|
return [];
|
|
}, [activeTab, workType, WORK_TYPES, MAINTENANACE_TABLE_HEADLIST, CLEANING_TABLE_HEADLIST, INSPECTION_TABLE_HEADLIST]);
|
|
|
|
// 목록 정렬 변경 시 첫 페이지에서 같은 검색조건으로 다시 조회한다.
|
|
const handleSortChange = (nextSort?: UiTableSort) => {
|
|
setSort(nextSort);
|
|
setIsResetPage(true);
|
|
setIsResetSelection(true);
|
|
setIsResetDetailPage(true);
|
|
setDetailSort(undefined);
|
|
setDetailBizId(0);
|
|
if (curPage === 1) {
|
|
void fetchFilteredData(nextSort);
|
|
return;
|
|
}
|
|
setCurPage(1);
|
|
};
|
|
|
|
// 유지관리 상세 정렬은 상단 사업 목록과 분리하고 선택된 사업 범위만 재조회한다.
|
|
const handleDetailSortChange = (nextSort?: UiTableSort) => {
|
|
setDetailSort(nextSort);
|
|
setIsResetDetailPage(true);
|
|
setIsResetDetailSelection(true);
|
|
if (!detailBizId) {
|
|
return;
|
|
}
|
|
if (detailCurPage === 1) {
|
|
void handleOperationPageNation(Number(detailBizId), nextSort);
|
|
return;
|
|
}
|
|
setDetailCurPage(1);
|
|
};
|
|
|
|
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}
|
|
sort={sort}
|
|
onSortChange={handleSortChange}
|
|
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}
|
|
sort={sort}
|
|
onSortChange={handleSortChange}
|
|
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}
|
|
sort={detailSort}
|
|
onSortChange={handleDetailSortChange}
|
|
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);
|
|
|