312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
/**
|
|
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭
|
|
*
|
|
* 📄 Report Tab
|
|
* Next.js App Router (app/) 기반 검색 모달 보고서 탭 컴포넌트
|
|
*
|
|
* - 하수도정비 기본계획 보고서 목록 표시 및 필터링
|
|
* - 시도/시군구/구분/보고서 구분/승인년도 기반 검색 기능
|
|
* - 테이블 데이터 표시 및 페이지네이션 지원
|
|
* - 파일명 클릭 시 대표 파일 바로 다운로드
|
|
* - 탭 진입 시 기본 조건 자동 조회 지원
|
|
* - Zustand를 통한 모달 상태 및 데이터 관리
|
|
* - 로딩 오버레이 및 선택 초기화 기능
|
|
*/
|
|
'use client';
|
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { Button, Label } from '@common/components';
|
|
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
|
import { SelectBox, UiTable } from '@common/business_components/ui';
|
|
import LoadingOverlay from '@/components/ui/LoadingBar';
|
|
import useReportTab from './useReportTab';
|
|
import useStore from '@/store/useStore';
|
|
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
|
import useSearchModalStore from '@/store/useSearchModalStore';
|
|
import { useReportTabStore } from '@/store/useReportTabStore';
|
|
|
|
// 하수도정비 기본계획 보고서
|
|
export const reportTableHeadlist = [
|
|
{ id: 'no', title: 'NO' },
|
|
{ id: 'sido', title: '시도' },
|
|
{ id: 'sigungu', title: '시군구' },
|
|
{ id: 'approvalYear', title: '승인년도' },
|
|
{ id: 'plantZoneGubun', title: '구분' },
|
|
{ id: 'reportGubun', title: '보고서구분' },
|
|
{ id: 'fileName', title: '파일명' },
|
|
{ id: 'regDate', title: '등록일자' },
|
|
];
|
|
interface ReportTabProps {
|
|
open?: boolean;
|
|
isSearch?: boolean;
|
|
ctpvNm?: string;
|
|
sggNm?: string;
|
|
}
|
|
|
|
const normalizeYear = (value: string) => {
|
|
if (!value || value === '0' || value === '9999') return '';
|
|
return value;
|
|
};
|
|
|
|
const getApprovalYearNumber = (value: string | number | undefined) => {
|
|
const normalized = String(value ?? '').replace('년', '').trim();
|
|
const parsed = Number(normalized);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
};
|
|
|
|
const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sggNm }) => {
|
|
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
|
const [isResetSelection, setIsResetSelection] = useState<boolean>(false);
|
|
const [showTable, setShowTable] = useState(false);
|
|
const [ctpvCd, setCtpvCd] = useState('0');
|
|
const [sggCd, setSggCd] = useState('0');
|
|
const [plantZoneGubun, setPlantZoneGubun] = useState('0');
|
|
const [reportGubun, setReportGubun] = useState('0');
|
|
const hasAutoFetchedRef = useRef(false);
|
|
|
|
const {
|
|
tableData,
|
|
tableCount,
|
|
isLoading,
|
|
yearFilter,
|
|
curPage,
|
|
setTableData,
|
|
setTableCount,
|
|
setIsLoading,
|
|
setYearFilter,
|
|
setCurPage
|
|
} = useSearchModalStore();
|
|
const { ctpv, sgg } = useStore();
|
|
const {
|
|
dcTree: reportOriginData,
|
|
fetchDcTree
|
|
} = useReportTabStore();
|
|
const { REPORT_TYPES, flattenFolders, addFileName, downloadReportFile } = useReportTab();
|
|
|
|
const flattenedReportData = useMemo(() => flattenFolders(reportOriginData), [reportOriginData]);
|
|
const plantZoneOptions = useMemo(() => {
|
|
const uniqueOptions = Array.from(
|
|
new Set(
|
|
flattenedReportData
|
|
.map((item) => String(item.plantZoneGubun ?? '').trim())
|
|
.filter((item) => item.length > 0)
|
|
)
|
|
).sort((a, b) => a.localeCompare(b, 'ko'));
|
|
|
|
return [
|
|
{ val: '0', text: '전체' },
|
|
...uniqueOptions.map((item) => ({ val: item, text: item })),
|
|
];
|
|
}, [flattenedReportData]);
|
|
|
|
useEffect(() => {
|
|
void fetchDcTree('기본계획');
|
|
}, [fetchDcTree]);
|
|
|
|
useEffect(() => {
|
|
hasAutoFetchedRef.current = false;
|
|
setCtpvCd('0');
|
|
setSggCd('0');
|
|
setPlantZoneGubun('0');
|
|
setReportGubun('0');
|
|
setYearFilter({ start: '', end: '' });
|
|
setShowTable(false);
|
|
setTableData([]);
|
|
setTableCount(0);
|
|
setCurPage(1);
|
|
setIsResetPage(true);
|
|
setIsResetSelection(true);
|
|
}, [open, setCurPage, setTableCount, setTableData, setYearFilter]);
|
|
|
|
useEffect(() => {
|
|
const newCtpvCd = ctpv.find((n) => n.text === ctpvNm)?.val || '0';
|
|
const newSggCd = sgg.find((n) => n.text === sggNm)?.val || '0';
|
|
|
|
setCtpvCd(newCtpvCd);
|
|
setSggCd(newSggCd);
|
|
}, [ctpv, ctpvNm, sgg, sggNm]);
|
|
|
|
useEffect(() => {
|
|
if (!open || flattenedReportData.length === 0 || hasAutoFetchedRef.current) return;
|
|
|
|
hasAutoFetchedRef.current = true;
|
|
void fetchFilteredData(1);
|
|
}, [flattenedReportData.length, open]);
|
|
|
|
useEffect(() => {
|
|
setIsResetPage(false);
|
|
|
|
if (showTable) {
|
|
void fetchFilteredData();
|
|
}
|
|
}, [curPage]);
|
|
|
|
const handleCtpvCdChange = (sidoId: string) => {
|
|
setCtpvCd(sidoId);
|
|
setSggCd('0');
|
|
};
|
|
|
|
const handleSearch = () => {
|
|
hasAutoFetchedRef.current = true;
|
|
setCurPage(1);
|
|
void fetchFilteredData(1);
|
|
setIsResetPage(true);
|
|
setIsResetSelection(false);
|
|
setShowTable(true);
|
|
};
|
|
|
|
const handleReportCellClick = (index: number | undefined, headId: string) => {
|
|
if (headId !== 'fileName') return;
|
|
if (typeof index !== 'number' || !tableData[index]) return;
|
|
|
|
const reportData = tableData[index];
|
|
if (!reportData.fileSn) return;
|
|
|
|
void (async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
await downloadReportFile(reportData);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
})();
|
|
};
|
|
|
|
const fetchFilteredData = async (page = curPage) => {
|
|
setIsLoading(true);
|
|
setIsResetSelection(true);
|
|
|
|
try {
|
|
let filterReportData = flattenedReportData;
|
|
|
|
if (ctpvCd !== '0') {
|
|
const sidoOption = ctpv.find((n) => String(n.val).trim() === ctpvCd.trim());
|
|
filterReportData = filterReportData.filter((item) => item.sido === sidoOption?.text);
|
|
}
|
|
if (sggCd !== '0') {
|
|
const sigunguOption = sgg.find((n) => String(n.val) === String(sggCd));
|
|
filterReportData = filterReportData.filter((item) => item.sigungu === sigunguOption?.text);
|
|
}
|
|
if (plantZoneGubun !== '0') {
|
|
filterReportData = filterReportData.filter((item) => String(item.plantZoneGubun ?? '') === plantZoneGubun);
|
|
}
|
|
if (reportGubun !== '0') {
|
|
const reportGubunOption = REPORT_TYPES.find((n) => n.val === reportGubun);
|
|
filterReportData = filterReportData.filter((item) => item.reportGubun === reportGubunOption?.text);
|
|
}
|
|
|
|
const normalizedStartYear = normalizeYear(yearFilter.start);
|
|
const normalizedEndYear = normalizeYear(yearFilter.end);
|
|
if (normalizedStartYear || normalizedEndYear) {
|
|
filterReportData = filterReportData.filter((item) => {
|
|
const approvalYear = getApprovalYearNumber(item.approvalYear);
|
|
if (approvalYear === null) return false;
|
|
if (normalizedStartYear && approvalYear < Number(normalizedStartYear)) return false;
|
|
if (normalizedEndYear && approvalYear > Number(normalizedEndYear)) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
setTableCount(filterReportData.length || 0);
|
|
|
|
const pagedReportData = filterReportData
|
|
.slice((page - 1) * 10, page * 10)
|
|
.map((item, index) => ({ ...item, no: (page - 1) * 10 + index + 1 }));
|
|
|
|
const reportRows = await addFileName(pagedReportData);
|
|
setTableData(reportRows || []);
|
|
setShowTable(true);
|
|
setIsResetSelection(false);
|
|
} catch (err) {
|
|
console.error('API ERROR:', err);
|
|
setTableData([]);
|
|
setIsResetSelection(false);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="relative mt-4 rounded-lg border px-5 pt-5 pb-3">
|
|
<div className="overflow-x-auto">
|
|
<div className="flex min-w-max items-center gap-4 whitespace-nowrap">
|
|
{isSearch && (
|
|
<CtpvSggPrcsFcltSelectBox
|
|
className="shrink-0"
|
|
sido={ctpvCd}
|
|
sigungu={sggCd}
|
|
onSidoChange={(sidoId: string) => { handleCtpvCdChange(sidoId); }}
|
|
onSigunguChange={setSggCd}
|
|
setIsLoading={setIsLoading}
|
|
/>
|
|
)}
|
|
<YearRangeSelectBox
|
|
className="shrink-0 !mr-1"
|
|
selectClass="rounded-lg bg-white flex-shrink-0 w-[78px]"
|
|
start={yearFilter.start}
|
|
end={yearFilter.end}
|
|
onChange={(range) => setYearFilter({
|
|
start: normalizeYear(range.start),
|
|
end: normalizeYear(range.end),
|
|
})}
|
|
startLabel="승인년도"
|
|
endLabel=""
|
|
/>
|
|
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">구분</Label>
|
|
<SelectBox
|
|
className="w-36 shrink-0"
|
|
selectClass="rounded-lg bg-white"
|
|
label=""
|
|
selectArray={plantZoneOptions}
|
|
value={plantZoneGubun}
|
|
onSelectValue={(val) => setPlantZoneGubun(String(val))}
|
|
selectContentClass="z-[101]"
|
|
/>
|
|
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">보고서 구분</Label>
|
|
<SelectBox
|
|
className="w-36 shrink-0"
|
|
selectClass="rounded-lg bg-white"
|
|
label=""
|
|
selectArray={[...REPORT_TYPES]}
|
|
value={reportGubun}
|
|
onSelectValue={(val) => setReportGubun(String(val))}
|
|
selectContentClass="z-[101]"
|
|
/>
|
|
<div className="ml-auto shrink-0 pl-4">
|
|
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3">
|
|
<LoadingOverlay isLoading={isLoading} />
|
|
<UiTable
|
|
className="!m-0"
|
|
clickableHeadIds={["fileName"]}
|
|
headName={<>조회 결과</>}
|
|
tableData={tableData}
|
|
headlist={reportTableHeadlist}
|
|
pageSize={10}
|
|
total={tableCount}
|
|
cellClick={handleReportCellClick}
|
|
cellClassName={(row, headId) => {
|
|
if (headId !== 'fileName') return '';
|
|
if (!row.fileSn) return '!text-gray-400';
|
|
return '!text-left !text-blue-600 underline underline-offset-4 hover:!text-blue-800';
|
|
}}
|
|
onPageChange={setCurPage}
|
|
resetPageNo={isResetPage}
|
|
resetSelection={isResetSelection}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default React.memo(ReportTab);
|
|
|
|
|
|
|
|
|