그리드 공통 정렬 기능 확대 적용
This commit is contained in:
@@ -23,15 +23,17 @@ import useStore from '@/store/useStore';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import type { UiTableSort } from '@common/business_components/ui';
|
||||
import type { FlattenedFolder, ReportDataFile } from './useReportTab';
|
||||
|
||||
// 하수도정비 기본계획 보고서
|
||||
export const reportTableHeadlist = [
|
||||
{ id: 'no', title: 'NO' },
|
||||
{ id: 'sido', title: '시도' },
|
||||
{ id: 'sigungu', title: '시군구' },
|
||||
{ id: 'approvalYear', title: '승인년도' },
|
||||
{ id: 'plantZoneGubun', title: '구분' },
|
||||
{ id: 'reportGubun', title: '보고서구분' },
|
||||
{ id: 'no', title: 'NO', sortable: true },
|
||||
{ id: 'sido', title: '시도', sortable: true },
|
||||
{ id: 'sigungu', title: '시군구', sortable: true },
|
||||
{ id: 'approvalYear', title: '승인년도', sortable: true },
|
||||
{ id: 'plantZoneGubun', title: '구분', sortable: true },
|
||||
{ id: 'reportGubun', title: '보고서구분', sortable: true },
|
||||
{ id: 'fileName', title: '파일명' },
|
||||
{ id: 'regDate', title: '등록일자' },
|
||||
];
|
||||
@@ -53,6 +55,51 @@ const getApprovalYearNumber = (value: string | number | undefined) => {
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const REPORT_COLLATOR = new Intl.Collator('ko', { numeric: true, sensitivity: 'base' });
|
||||
const REPORT_PAGE_SIZE = 10;
|
||||
const REPORT_METADATA_CONCURRENCY = 8;
|
||||
const REPORT_METADATA_TIMEOUT_MS = 8000;
|
||||
const DISABLED_REPORT_SORT_KEYS = new Set(['fileName', 'regDate']);
|
||||
|
||||
type ReportFileMetadata = Pick<FlattenedFolder,
|
||||
'fileName' | 'regDate' | 'fileSn' | 'filePath' | 'fileCount' | 'downloadFileTitle'>;
|
||||
|
||||
const EMPTY_REPORT_METADATA: ReportFileMetadata = {
|
||||
fileName: '-',
|
||||
regDate: '',
|
||||
fileSn: '',
|
||||
filePath: '',
|
||||
fileCount: 0,
|
||||
downloadFileTitle: '',
|
||||
};
|
||||
|
||||
// 파일 정보 추가 조회 전에도 존재하는 분류 트리 필드만 전체 데이터 기준으로 정렬한다.
|
||||
const compareReportRows = (left: FlattenedFolder, right: FlattenedFolder, sort: UiTableSort) => {
|
||||
const leftValue = left[sort.key];
|
||||
const rightValue = right[sort.key];
|
||||
const leftEmpty = leftValue === null || leftValue === undefined || String(leftValue).trim() === '';
|
||||
const rightEmpty = rightValue === null || rightValue === undefined || String(rightValue).trim() === '';
|
||||
|
||||
if (leftEmpty || rightEmpty) {
|
||||
if (leftEmpty && rightEmpty) return Number(left.no ?? 0) - Number(right.no ?? 0);
|
||||
return leftEmpty ? 1 : -1;
|
||||
}
|
||||
|
||||
const comparison = sort.key === 'approvalYear'
|
||||
? Number(getApprovalYearNumber(leftValue) ?? 0) - Number(getApprovalYearNumber(rightValue) ?? 0)
|
||||
: REPORT_COLLATOR.compare(String(leftValue), String(rightValue));
|
||||
if (comparison === 0) return Number(left.no ?? 0) - Number(right.no ?? 0);
|
||||
return sort.direction === 'asc' ? comparison : -comparison;
|
||||
};
|
||||
|
||||
const resolveReportSort = (sort?: UiTableSort) => {
|
||||
if (!sort) return undefined;
|
||||
if (DISABLED_REPORT_SORT_KEYS.has(sort.key)) {
|
||||
return undefined;
|
||||
}
|
||||
return sort;
|
||||
};
|
||||
|
||||
const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sggNm }) => {
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
const [isResetSelection, setIsResetSelection] = useState<boolean>(false);
|
||||
@@ -62,6 +109,8 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
const [plantZoneGubun, setPlantZoneGubun] = useState('0');
|
||||
const [reportGubun, setReportGubun] = useState('0');
|
||||
const hasAutoFetchedRef = useRef(false);
|
||||
const fileMetadataCacheRef = useRef<Map<string, ReportFileMetadata>>(new Map());
|
||||
const fileMetadataPromiseCacheRef = useRef<Map<string, Promise<ReportFileMetadata>>>(new Map());
|
||||
|
||||
const {
|
||||
tableData,
|
||||
@@ -73,16 +122,23 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
setTableCount,
|
||||
setIsLoading,
|
||||
setYearFilter,
|
||||
setCurPage
|
||||
setCurPage,
|
||||
sort,
|
||||
setSort,
|
||||
} = useSearchModalStore();
|
||||
const { ctpv, sgg } = useStore();
|
||||
const {
|
||||
dcTree: reportOriginData,
|
||||
fetchDcTree
|
||||
} = useReportTabStore();
|
||||
const { REPORT_TYPES, flattenFolders, addFileName, downloadReportFile } = useReportTab();
|
||||
const { REPORT_TYPES, flattenFolders, getDcFiles, downloadReportFile } = useReportTab();
|
||||
|
||||
const flattenedReportData = useMemo(() => flattenFolders(reportOriginData), [reportOriginData]);
|
||||
|
||||
useEffect(() => {
|
||||
fileMetadataCacheRef.current.clear();
|
||||
fileMetadataPromiseCacheRef.current.clear();
|
||||
}, [reportOriginData]);
|
||||
const plantZoneOptions = useMemo(() => {
|
||||
const uniqueOptions = Array.from(
|
||||
new Set(
|
||||
@@ -113,9 +169,10 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
setTableData([]);
|
||||
setTableCount(0);
|
||||
setCurPage(1);
|
||||
setSort(undefined);
|
||||
setIsResetPage(true);
|
||||
setIsResetSelection(true);
|
||||
}, [open, setCurPage, setTableCount, setTableData, setYearFilter]);
|
||||
}, [open, setCurPage, setSort, setTableCount, setTableData, setYearFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
const newCtpvCd = ctpv.find((n) => n.text === ctpvNm)?.val || '0';
|
||||
@@ -171,11 +228,94 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
})();
|
||||
};
|
||||
|
||||
const fetchFilteredData = async (page = curPage) => {
|
||||
const buildReportMetadata = (fileArr: ReportDataFile[]): ReportFileMetadata => {
|
||||
if (fileArr.length === 0) {
|
||||
return EMPTY_REPORT_METADATA;
|
||||
}
|
||||
|
||||
const primaryFile = fileArr[0];
|
||||
return {
|
||||
fileName: fileArr.length === 1
|
||||
? primaryFile?.fileTitle ?? '-'
|
||||
: `${primaryFile?.fileTitle ?? '대표 파일'} 외 ${fileArr.length - 1}건`,
|
||||
regDate: primaryFile?.regDt ?? '',
|
||||
fileSn: primaryFile?.fileSn ?? '',
|
||||
filePath: primaryFile?.filePath ?? '',
|
||||
fileCount: fileArr.length,
|
||||
downloadFileTitle: primaryFile?.fileTitle ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
const resolveReportMetadata = (item: FlattenedFolder) => {
|
||||
const key = String(item.clsfSystmSn ?? '');
|
||||
if (!key) {
|
||||
return Promise.resolve(EMPTY_REPORT_METADATA);
|
||||
}
|
||||
|
||||
const cached = fileMetadataCacheRef.current.get(key);
|
||||
if (cached) {
|
||||
return Promise.resolve(cached);
|
||||
}
|
||||
|
||||
const pending = fileMetadataPromiseCacheRef.current.get(key);
|
||||
if (pending) {
|
||||
return pending;
|
||||
}
|
||||
|
||||
const promise = Promise.race<ReportDataFile[] | never>([
|
||||
getDcFiles(key),
|
||||
new Promise<never>((_, reject) => {
|
||||
window.setTimeout(() => reject(new Error(`Report metadata timeout: ${key}`)), REPORT_METADATA_TIMEOUT_MS);
|
||||
}),
|
||||
])
|
||||
.then((fileArr) => buildReportMetadata(fileArr))
|
||||
.catch((error) => {
|
||||
console.warn('보고서 파일 메타데이터 조회 실패:', key, error);
|
||||
return EMPTY_REPORT_METADATA;
|
||||
})
|
||||
.then((metadata) => {
|
||||
fileMetadataCacheRef.current.set(key, metadata);
|
||||
fileMetadataPromiseCacheRef.current.delete(key);
|
||||
return metadata;
|
||||
});
|
||||
|
||||
fileMetadataPromiseCacheRef.current.set(key, promise);
|
||||
return promise;
|
||||
};
|
||||
|
||||
const enrichReportRows = async (data: FlattenedFolder[], concurrency = REPORT_METADATA_CONCURRENCY) => {
|
||||
const missingRows = data.filter((item) => {
|
||||
const key = String(item.clsfSystmSn ?? '');
|
||||
return key && !fileMetadataCacheRef.current.has(key);
|
||||
});
|
||||
|
||||
if (missingRows.length > 0) {
|
||||
let cursor = 0;
|
||||
const workerCount = Math.min(concurrency, missingRows.length);
|
||||
await Promise.all(Array.from({ length: workerCount }, async () => {
|
||||
while (cursor < missingRows.length) {
|
||||
const currentIndex = cursor;
|
||||
cursor += 1;
|
||||
const currentRow = missingRows[currentIndex];
|
||||
if (currentRow) {
|
||||
await resolveReportMetadata(currentRow);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return data.map((item) => ({
|
||||
...item,
|
||||
...(fileMetadataCacheRef.current.get(String(item.clsfSystmSn ?? '')) ?? EMPTY_REPORT_METADATA),
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilteredData = async (page = curPage, requestedSort = sort) => {
|
||||
setIsLoading(true);
|
||||
setIsResetSelection(true);
|
||||
|
||||
try {
|
||||
const activeSort = resolveReportSort(requestedSort);
|
||||
let filterReportData = flattenedReportData;
|
||||
|
||||
if (ctpvCd !== '0') {
|
||||
@@ -206,13 +346,29 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
});
|
||||
}
|
||||
|
||||
setTableCount(filterReportData.length || 0);
|
||||
const totalCount = filterReportData.length || 0;
|
||||
setTableCount(totalCount);
|
||||
|
||||
const pagedReportData = filterReportData
|
||||
.slice((page - 1) * 10, page * 10)
|
||||
.map((item, index) => ({ ...item, no: (page - 1) * 10 + index + 1 }));
|
||||
const pageOffset = (page - 1) * REPORT_PAGE_SIZE;
|
||||
const pageStart = (page - 1) * REPORT_PAGE_SIZE;
|
||||
const pageEnd = page * REPORT_PAGE_SIZE;
|
||||
|
||||
let reportRows: FlattenedFolder[];
|
||||
const sortedReportData = activeSort
|
||||
? [...filterReportData].sort((left, right) => compareReportRows(left, right, activeSort))
|
||||
: filterReportData;
|
||||
|
||||
const pagedReportData = sortedReportData
|
||||
.slice(pageStart, pageEnd)
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
no: activeSort?.key === 'no' && activeSort.direction === 'desc'
|
||||
? sortedReportData.length - (pageOffset + index)
|
||||
: pageOffset + index + 1,
|
||||
}));
|
||||
|
||||
reportRows = await enrichReportRows(pagedReportData);
|
||||
|
||||
const reportRows = await addFileName(pagedReportData);
|
||||
setTableData(reportRows || []);
|
||||
setShowTable(true);
|
||||
setIsResetSelection(false);
|
||||
@@ -225,6 +381,18 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
}
|
||||
};
|
||||
|
||||
// 보고서 분류 원본 전체를 다시 정렬한 뒤 첫 페이지를 표시한다.
|
||||
const handleSortChange = (nextSort?: UiTableSort) => {
|
||||
setSort(nextSort);
|
||||
setIsResetPage(true);
|
||||
setIsResetSelection(true);
|
||||
if (curPage === 1) {
|
||||
void fetchFilteredData(1, nextSort);
|
||||
return;
|
||||
}
|
||||
setCurPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative mt-4 rounded-lg border px-5 pt-5 pb-3">
|
||||
@@ -298,6 +466,9 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
curPage={curPage}
|
||||
sort={sort}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user