조회조건 레이아웃 수정

엑셀다운버튼 수정
progress 추가
front pagenation 추가
This commit is contained in:
2025-04-09 15:44:51 +09:00
parent 80a3c0ab8a
commit a74376108a
11 changed files with 962 additions and 193 deletions

View File

@@ -1,7 +1,34 @@
import * as XLSX from 'xlsx-js-style';
import { ExcelDownButton } from '../../../styles/ModuleComponents';
import { useCallback, useEffect, useState } from 'react';
const ExcelDownloadButton = ({ tableRef, data, fileName = 'download.xlsx', sheetName = 'Sheet1', onLoadingChange }) => {
const [isDownloading, setIsDownloading] = useState(false);
const [lastProgress, setLastProgress] = useState(0);
// 타임아웃 감지 및 처리
useEffect(() => {
let timeoutTimer;
if (isDownloading && lastProgress >= 95) {
// 최종 단계에서 타임아웃 감지 타이머 설정
timeoutTimer = setTimeout(() => {
// 진행 상태가 여전히 변하지 않았다면 타임아웃으로 간주
if (isDownloading && lastProgress >= 95) {
console.log("Excel download timeout detected, completing process");
setIsDownloading(false);
if (onLoadingChange) {
onLoadingChange({ loading: false, progress: 100 });
}
}
}, 15000); // 15초 타임아웃
}
return () => {
if (timeoutTimer) clearTimeout(timeoutTimer);
};
}, [isDownloading, lastProgress, onLoadingChange]);
const ExcelDownloadButton = ({ tableRef, data, fileName = 'download.xlsx', sheetName = 'Sheet1' }) => {
const isNumeric = (value) => {
// 숫자 또는 숫자 문자열인지 확인
return !isNaN(value) && !isNaN(parseFloat(value));
@@ -71,192 +98,409 @@ const ExcelDownloadButton = ({ tableRef, data, fileName = 'download.xlsx', sheet
}, {});
};
const downloadTableExcel = () => {
try {
if (!tableRef || !tableRef.current) {
alert('테이블 참조가 없습니다.');
return;
}
const tableElement = tableRef.current;
const headerRows = tableElement.getElementsByTagName('thead')[0].getElementsByTagName('tr');
const bodyRows = tableElement.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
// 일반 행만 포함 (상세 행 제외)
const normalBodyRows = Array.from(bodyRows).filter(row => {
// 상세 행은 colspan 속성이 있는 td를 포함
const hasTdWithColspan = Array.from(row.cells).some(cell => cell.hasAttribute('colspan'));
return !hasTdWithColspan;
});
// 헤더 데이터 추출
const headers = Array.from(headerRows[0].cells).map(cell => cell.textContent);
// 바디 데이터 추출 및 숫자 타입 처리
const bodyData = normalBodyRows.map(row =>
Array.from(row.cells).map(cell => {
const value = cell.textContent;
return isNumeric(value) ? parseFloat(value) : value;
})
);
// 워크북 생성
const wb = XLSX.utils.book_new();
// 데이터에 스타일 적용
const wsData = [
// 헤더 행
headers.map(h => ({
v: h,
s: headerStyle
})),
// 데이터 행들
...bodyData.map(row =>
row.map(cell => ({
v: cell,
s: dataStyle
}))
)
];
// 워크시트 생성
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 열 너비 설정 (최소 8, 최대 50)
ws['!cols'] = headers.map((_, index) => {
const maxLength = Math.max(
headers[index].length * 2,
...bodyData.map(row => String(row[index] || '').length * 1.2)
);
return { wch: Math.max(8, Math.min(50, maxLength)) };
});
// 워크시트를 워크북에 추가
XLSX.utils.book_append_sheet(wb, ws, sheetName);
// 엑셀 파일 다운로드
XLSX.writeFile(wb, fileName);
} catch (error) {
console.error('Excel download failed:', error);
alert('엑셀 다운로드 중 오류가 발생했습니다.');
const updateLoadingState = (newProgress) => {
setLastProgress(newProgress);
if (onLoadingChange && typeof onLoadingChange === 'function') {
onLoadingChange({loading: true, progress: newProgress});
}
};
const downloadDataExcel = () => {
try {
if (!data || !data || data.length === 0) {
alert('다운로드할 데이터가 없습니다.');
return;
const downloadTableExcel = async () => {
return new Promise((resolve, reject) => {
try {
if (!tableRef || !tableRef.current) {
reject(new Error('테이블 참조가 없습니다.'));
return;
}
// Worker에 전달할 데이터 추출
updateLoadingState(10);
// 메인 스레드에서 데이터 추출
const tableElement = tableRef.current;
const headerRows = tableElement.getElementsByTagName('thead')[0].getElementsByTagName('tr');
const bodyRows = tableElement.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
// 일반 행만 포함 (상세 행 제외)
const normalBodyRows = Array.from(bodyRows).filter(row => {
const hasTdWithColspan = Array.from(row.cells).some(cell => cell.hasAttribute('colspan'));
return !hasTdWithColspan;
});
// 헤더 데이터 추출
const headers = Array.from(headerRows[0].cells).map(cell => cell.textContent);
// 바디 데이터 추출 및 숫자 타입 처리
const bodyData = normalBodyRows.map(row =>
Array.from(row.cells).map(cell => {
const value = cell.textContent;
return isNumeric(value) ? parseFloat(value) : value;
})
);
updateLoadingState(30);
// 큰 데이터셋 처리를 위해 setTimeout으로 이벤트 루프 차단 방지
setTimeout(() => {
try {
// 워크북 생성
const wb = XLSX.utils.book_new();
updateLoadingState(50);
// 처리는 여러 단계로 나누어 이벤트 루프 차단 최소화
setTimeout(() => {
try {
// 데이터에 스타일 적용
const wsData = [
// 헤더 행
headers.map(h => ({
v: h,
s: headerStyle
}))
];
// 데이터 행 추가 (메모리 사용량 최소화를 위해 별도 처리)
const chunkSize = 1000; // 한 번에 처리할 행 수
let currentIndex = 0;
function processDataChunk() {
updateLoadingState(50 + Math.floor((currentIndex / bodyData.length) * 30));
const end = Math.min(currentIndex + chunkSize, bodyData.length);
for (let i = currentIndex; i < end; i++) {
wsData.push(
bodyData[i].map(cell => ({
v: cell,
s: dataStyle
}))
);
}
currentIndex = end;
if (currentIndex < bodyData.length) {
// 아직 처리할 데이터가 남아있으면 다음 청크 처리 예약
setTimeout(processDataChunk, 0);
} else {
// 모든 데이터 처리 완료 후 워크시트 생성
finishExcelCreation(wsData, headers, wb);
}
}
// 첫 번째 청크 처리 시작
processDataChunk();
} catch (error) {
reject(error);
}
}, 0);
function finishExcelCreation(wsData, headers, wb) {
try {
updateLoadingState(80);
// 워크시트 생성
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 열 너비 설정 (최소 8, 최대 50)
ws['!cols'] = headers.map((_, index) => {
// 데이터의 일부만 샘플링하여 열 너비 계산 (전체 계산 시 성능 문제)
const samplingSize = Math.min(bodyData.length, 500);
const sampledRows = [];
for (let i = 0; i < samplingSize; i++) {
const randomIndex = Math.floor(Math.random() * bodyData.length);
sampledRows.push(bodyData[randomIndex]);
}
const maxLength = Math.max(
headers[index].length * 2,
...sampledRows.map(row => {
if (row[index] === undefined) return 0;
return String(row[index] || '').length * 1.2;
})
);
return { wch: Math.max(8, Math.min(50, maxLength)) };
});
updateLoadingState(90);
// 최종 다운로드는 별도 타임아웃에서 수행하여 UI 업데이트 가능하게 함
setTimeout(() => {
try {
// 워크시트를 워크북에 추가
XLSX.utils.book_append_sheet(wb, ws, sheetName);
updateLoadingState(95);
// 파일 다운로드 전 마지막 UI 업데이트를 위한 지연
setTimeout(() => {
try {
// 엑셀 파일 다운로드
XLSX.writeFile(wb, fileName);
updateLoadingState(100);
resolve();
} catch (error) {
reject(error);
}
}, 100);
} catch (error) {
reject(error);
}
}, 0);
} catch (error) {
reject(error);
}
}
} catch (error) {
reject(error);
}
}, 0);
} catch (error) {
reject(error);
}
});
};
// 모든 로그 항목을 플랫한 구조로 변환
const flattenedData = data.map(item => {
// 기본 필드
const baseData = {
'logTime': item.logTime,
'GUID': item.userGuid === 'None' ? '' : item.userGuid,
'Nickname': item.userNickname === 'None' ? '' : item.userNickname,
'Account ID': item.accountId === 'None' ? '' : item.accountId,
'Action': item.action,
'Domain' : item.domain === 'None' ? '' : item.domain,
'Tran ID': item.tranId
};
const chunkArray = (array, chunkSize) => {
const chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
};
// Actor 데이터 플랫하게 추가
const actorData = item.header && item.header.Actor ?
flattenObject(item.header.Actor, 'Actor') : {};
// Infos 데이터 플랫하게 추가
let infosData = {};
if (item.body && item.body.Infos && Array.isArray(item.body.Infos)) {
item.body.Infos.forEach((info, index) => {
infosData = {
...infosData,
...flattenObject(info, `Info`)
const processDataChunk = async (chunk, headers, allDataRows, processedCount, totalCount) => {
return new Promise((resolve) => {
setTimeout(() => {
// 각 청크의 데이터를 처리
const rowsData = chunk.map(item => {
return headers.map(header => {
const value = item[header] !== undefined ? item[header] : '';
return {
v: value,
s: dataStyle
};
});
});
// 진행률 계산 및 콜백 호출
const newProgress = Math.min(95, Math.round(((processedCount + chunk.length) / totalCount) * 80) + 15);
updateLoadingState(newProgress);
// 처리된 데이터 행들을 전체 데이터 배열에 추가
allDataRows.push(...rowsData);
resolve();
}, 0);
});
};
const downloadDataExcel = async () => {
return new Promise(async (resolve, reject) => {
try {
if (!data || data.length === 0) {
reject(new Error('다운로드할 데이터가 없습니다.'));
return;
}
updateLoadingState(5);
// 데이터 플랫 변환 과정을 더 작은 청크로 나누기
const dataChunkSize = 2000; // 한 번에 처리할 데이터 아이템 수
const dataChunks = chunkArray(data, dataChunkSize);
let flattenedData = [];
for (let i = 0; i < dataChunks.length; i++) {
await new Promise(resolve => {
setTimeout(() => {
// 청크 내 아이템들을 플랫하게 변환
const chunkData = dataChunks[i].map(item => {
// 기본 필드
const baseData = {
'logTime': item.logTime,
'GUID': item.userGuid === 'None' ? '' : item.userGuid,
'Nickname': item.userNickname === 'None' ? '' : item.userNickname,
'Account ID': item.accountId === 'None' ? '' : item.accountId,
'Action': item.action,
'Domain': item.domain === 'None' ? '' : item.domain,
'Tran ID': item.tranId
};
// Actor 데이터 플랫하게 추가
const actorData = item.header && item.header.Actor ?
flattenObject(item.header.Actor, 'Actor') : {};
// Infos 데이터 플랫하게 추가
let infosData = {};
if (item.body && item.body.Infos && Array.isArray(item.body.Infos)) {
item.body.Infos.forEach((info) => {
infosData = {
...infosData,
...flattenObject(info, `Info`)
};
});
}
return {
...baseData,
...actorData,
...infosData
};
});
flattenedData = [...flattenedData, ...chunkData];
const progress = 5 + Math.floor((i + 1) / dataChunks.length * 10);
updateLoadingState(progress);
resolve();
}, 0);
});
}
return {
...baseData,
...actorData,
...infosData
};
});
// 모든 항목의 모든 키 수집하여 헤더 생성
const allKeys = new Set();
// 모든 항목의 모든 키 수집하여 헤더 생성
const allKeys = new Set();
flattenedData.forEach(item => {
Object.keys(item).forEach(key => allKeys.add(key));
});
const headers = Array.from(allKeys);
// 헤더 수집도 청크로 나누기
for (let i = 0; i < flattenedData.length; i += dataChunkSize) {
await new Promise(resolve => {
setTimeout(() => {
const end = Math.min(i + dataChunkSize, flattenedData.length);
for (let j = i; j < end; j++) {
Object.keys(flattenedData[j]).forEach(key => allKeys.add(key));
}
const progress = 15 + Math.floor((i + dataChunkSize) / flattenedData.length * 5);
updateLoadingState(progress);
resolve();
}, 0);
});
}
// 워크북 생성
const wb = XLSX.utils.book_new();
const headers = Array.from(allKeys);
updateLoadingState(20);
// 데이터 행들 생성
const dataRows = flattenedData.map(item => {
return headers.map(header => {
const value = item[header] !== undefined ? item[header] : '';
return {
v: value,
s: dataStyle
};
});
});
// 워크북 생성
const wb = XLSX.utils.book_new();
// 워크시트 데이터 구
const wsData = [
// 헤더 행
headers.map(h => ({
// 헤더 행 생
const headerRow = headers.map(h => ({
v: h,
s: headerStyle
})),
// 데이터 행들
...dataRows
];
}));
// 워크시트 생성
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 청크로 데이터 나누기
const chunkSize = 500; // 한 번에 처리할 행의 수 (더 작게 조정)
const rowChunks = chunkArray(flattenedData, chunkSize);
const allDataRows = [];
// 열 너비 설정 (최소 10, 최대 50)
ws['!cols'] = headers.map((header) => {
// 헤더 길이와 데이터 길이 중 최대값으로 열 너비 결정
const maxLength = Math.max(
header.length * 1.5,
...flattenedData.map(item => {
const value = item[header];
return value !== undefined ? String(value).length * 1.2 : 0;
})
);
return { wch: Math.max(10, Math.min(50, maxLength)) };
});
// 각 청크 처리
let processedCount = 0;
for (const chunk of rowChunks) {
await processDataChunk(chunk, headers, allDataRows, processedCount, flattenedData.length);
processedCount += chunk.length;
// 워크시트를 워크북에 추가
XLSX.utils.book_append_sheet(wb, ws, sheetName);
// 메모리 정리를 위한 가비지 컬렉션 힌트
if (processedCount % (chunkSize * 10) === 0) {
// 5000행마다 짧은 지연을 두어 가비지 컬렉션 기회 제공
await new Promise(resolve => setTimeout(resolve, 10));
}
}
// 엑셀 파일 다운로드
XLSX.writeFile(wb, fileName);
updateLoadingState(95);
// 워크시트 데이터 구성 및 파일 다운로드를 메인 로직과 분리
setTimeout(() => {
try {
// 워크시트 데이터 구성
const wsData = [
// 헤더 행
headerRow,
// 데이터 행들
...allDataRows
];
// 워크시트 생성 (메모리 최적화)
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 열 너비 설정 (성능 최적화를 위해 샘플링)
ws['!cols'] = headers.map((header) => {
// 헤더 길이와 샘플 데이터 길이를 기준으로 열 너비 결정
const sampleSize = Math.min(flattenedData.length, 500);
const samples = [];
for (let i = 0; i < sampleSize; i++) {
const randomIndex = Math.floor(Math.random() * flattenedData.length);
const item = flattenedData[randomIndex];
if (item[header] !== undefined) {
samples.push(String(item[header]).length * 1.2);
}
}
const maxLength = Math.max(
header.length * 1.5,
...samples
);
return { wch: Math.max(10, Math.min(50, maxLength)) };
});
// 최종 단계 분리
setTimeout(() => {
try {
// 워크시트를 워크북에 추가
XLSX.utils.book_append_sheet(wb, ws, sheetName);
// 엑셀 파일 다운로드
XLSX.writeFile(wb, fileName);
updateLoadingState(100);
resolve();
} catch (error) {
reject(error);
}
}, 100);
} catch (error) {
reject(error);
}
}, 0);
} catch (error) {
reject(error);
}
});
};
const handleDownload = useCallback(async () => {
if (isDownloading) return; // 이미 다운로드 중이면 중복 실행 방지
setIsDownloading(true);
setLastProgress(0);
if (onLoadingChange) onLoadingChange({loading: true, progress: 0});
try {
if (tableRef) {
await downloadTableExcel();
} else if (data) {
await downloadDataExcel();
} else {
alert('유효한 데이터 소스가 없습니다.');
}
} catch (error) {
console.error('Excel download failed:', error);
alert('엑셀 다운로드 중 오류가 발생했습니다.');
} finally {
// 다운로드 완료 후 짧은 지연 시간을 두어 100% 상태를 잠시 보여줌
setTimeout(() => {
setIsDownloading(false);
if (onLoadingChange) onLoadingChange({loading: false, progress: 100});
}, 500);
}
};
const handleDownload = () => {
if (tableRef) {
downloadTableExcel();
} else if (data) {
downloadDataExcel();
} else {
alert('유효한 데이터 소스가 없습니다.');
}
};
}, [tableRef, data, fileName, sheetName, isDownloading, onLoadingChange]);
return (
<ExcelDownButton onClick={handleDownload}>
엑셀 다운로드
<ExcelDownButton onClick={handleDownload} disabled={isDownloading}>
{isDownloading ? '다운로드 중...' : '엑셀 다운로드'}
</ExcelDownButton>
);
};

View File

@@ -2,29 +2,26 @@ import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
const Button = styled.button`
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #666666;
color: white;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: ${props => (props.visible ? '1' : '0')};
visibility: ${props => (props.visible ? 'visible' : 'hidden')};
transition: opacity 0.3s, visibility 0.3s;
border: none;
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2);
z-index: 999;
&:hover {
background-color: #333333;
}
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #666666;
color: white;
font-size: 16px;
display: ${props => (props.$show ? 'flex' : 'none')};
align-items: center;
justify-content: center;
cursor: pointer;
border: none;
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2);
z-index: 999;
&:hover {
background-color: #333333;
}
`;
const TopButton = () => {
@@ -53,7 +50,7 @@ const TopButton = () => {
return (
<Button
visible={visible}
$show={visible}
onClick={scrollToTop}
title="맨 위로 이동"
>