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

@@ -483,11 +483,63 @@ export function AttendanceManagement() {
searchPlaceholder: '이름, 부서 검색...',
// 엑셀 다운로드 설정 (클라이언트 사이드 필터링이므로 filteredData 사용)
// 엑셀 다운로드 설정 (fetchAllUrl로 전체 데이터 조회)
excelDownload: {
columns: excelColumns,
filename: '근태현황',
sheetName: '근태',
fetchAllUrl: '/api/proxy/attendances',
fetchAllParams: () => {
const params: Record<string, string> = {};
if (startDate) params.date_from = startDate;
if (endDate) params.date_to = endDate;
return params;
},
mapResponse: (result: unknown) => {
const res = result as { data?: { data?: Record<string, unknown>[] } };
const items = res.data?.data ?? [];
return items.map((item) => {
const user = item.user as Record<string, unknown> | undefined;
const profiles = (user?.tenant_profiles ?? []) as Record<string, unknown>[];
const profile = profiles[0] as Record<string, unknown> | undefined;
const dept = profile?.department as Record<string, unknown> | undefined;
const legacyProfile = user?.tenant_profile as Record<string, unknown> | undefined;
const legacyDept = legacyProfile?.department as Record<string, unknown> | undefined;
const jsonDetails = (item.json_details ?? {}) as Record<string, unknown>;
const breakMins = item.break_minutes as number | null;
const overtimeMins = jsonDetails.overtime_minutes as number | undefined;
return {
id: String(item.id),
employeeId: String(item.user_id),
employeeName: (user?.name ?? '') as string,
department: (dept?.name ?? legacyDept?.name ?? '') as string,
position: (profile?.position_key ?? '') as string,
rank: ((legacyProfile?.rank ?? '') as string),
baseDate: item.base_date as string,
checkIn: (item.check_in ?? jsonDetails.check_in ?? null) as string | null,
checkOut: (item.check_out ?? jsonDetails.check_out ?? null) as string | null,
breakTime: breakMins != null
? `${Math.floor(breakMins / 60)}:${(breakMins % 60).toString().padStart(2, '0')}`
: (jsonDetails.break_time as string || null),
overtimeHours: overtimeMins
? (() => {
const h = Math.floor(overtimeMins / 60);
const m = overtimeMins % 60;
if (h > 0 && m > 0) return `${h}시간 ${m}`;
if (h > 0) return `${h}시간`;
return `${m}`;
})()
: null,
workMinutes: (jsonDetails.work_minutes || null) as number | null,
reason: (jsonDetails.reason || null) as AttendanceRecord['reason'],
status: item.status as string,
remarks: (item.remarks ?? null) as string | null,
createdAt: item.created_at as string,
updatedAt: item.updated_at as string,
} as AttendanceRecord;
});
},
},
itemsPerPage: itemsPerPage,