feat: 엑셀 다운로드 전체 데이터 페이지별 순차 조회 방식으로 개선

- UniversalListPage: size=10000 단건 호출을 1000건씩 페이지 순회 방식으로 변경
- fetchAllUrl 우선순위를 clientSideFiltering보다 상위로 조정
- 첫 페이지에서 total 확인 후 나머지 페이지 병렬(Promise.all) 호출
- 10000건 초과 데이터도 누락 없이 다운로드 가능
- 근태현황: fetchAllUrl, fetchAllParams, mapResponse 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-29 16:48:35 +09:00
parent a5578bf669
commit 4014b3fb84
2 changed files with 114 additions and 22 deletions

View File

@@ -545,18 +545,12 @@ export function UniversalListPage<T>({
console.log('[Excel] filteredData.length:', filteredData.length);
console.log('[Excel] fetchAllUrl:', fetchAllUrl);
// 클라이언트 사이드 필터링: 현재 필터링된 전체 데이터 사용
if (config.clientSideFiltering) {
dataToDownload = filteredData;
}
// 서버 사이드: fetchAllUrl로 전체 데이터 조회
else if (fetchAllUrl) {
const params = new URLSearchParams();
// 전체 데이터 조회 - API마다 다른 파라미터명 사용 가능하므로 둘 다 설정
params.append('size', '10000');
params.append('per_page', '10000');
// fetchAllUrl이 있으면 서버에서 전체 데이터 페이지별 순차 조회 (clientSideFiltering 여부 무관)
if (fetchAllUrl) {
const PAGE_SIZE = 1000;
// 동적 파라미터 추가
// 동적 파라미터 구성
const baseParams = new URLSearchParams();
if (fetchAllParams) {
const additionalParams = fetchAllParams({
activeTab,
@@ -564,23 +558,69 @@ export function UniversalListPage<T>({
searchValue: debouncedSearchValue,
});
Object.entries(additionalParams).forEach(([key, value]) => {
if (value) params.append(key, value);
if (value) baseParams.append(key, value);
});
}
const response = await fetch(`${fetchAllUrl}?${params.toString()}`);
const result = await response.json();
// 1) 첫 페이지 호출로 total 확인
const firstParams = new URLSearchParams(baseParams);
firstParams.set('size', String(PAGE_SIZE));
firstParams.set('per_page', String(PAGE_SIZE));
firstParams.set('page', '1');
if (!result.success) {
throw new Error(result.message || '데이터 조회에 실패했습니다.');
const firstResponse = await fetch(`${fetchAllUrl}?${firstParams.toString()}`);
const firstResult = await firstResponse.json();
if (!firstResult.success) {
throw new Error(firstResult.message || '데이터 조회에 실패했습니다.');
}
// 응답 매핑
const rawData = mapResponse
? mapResponse(result)
: (result.data?.data ?? result.data ?? []);
const firstPageData = mapResponse
? mapResponse(firstResult)
: (firstResult.data?.data ?? firstResult.data ?? []);
dataToDownload = rawData as T[];
const total = firstResult.data?.total ?? firstPageData.length;
const totalPages = Math.ceil(total / PAGE_SIZE);
console.log(`[Excel] 전체 ${total}건, ${totalPages}페이지 조회 시작`);
// 2) 나머지 페이지 병렬 호출
const allData: T[] = [...(firstPageData as T[])];
if (totalPages > 1) {
const remainingPages = Array.from(
{ length: totalPages - 1 },
(_, i) => i + 2
);
const pageResults = await Promise.all(
remainingPages.map(async (page) => {
const pageParams = new URLSearchParams(baseParams);
pageParams.set('size', String(PAGE_SIZE));
pageParams.set('per_page', String(PAGE_SIZE));
pageParams.set('page', String(page));
const res = await fetch(`${fetchAllUrl}?${pageParams.toString()}`);
const result = await res.json();
if (!result.success) {
console.warn(`[Excel] 페이지 ${page} 조회 실패:`, result.message);
return [];
}
return mapResponse
? mapResponse(result)
: (result.data?.data ?? result.data ?? []);
})
);
for (const pageData of pageResults) {
allData.push(...(pageData as T[]));
}
}
console.log(`[Excel] 총 ${allData.length}건 조회 완료`);
dataToDownload = allData;
}
// fetchAllUrl 없으면 현재 로드된 데이터 사용
else {