조회조건 레이아웃 수정
엑셀다운버튼 수정 progress 추가 front pagenation 추가
This commit is contained in:
75
src/components/common/CircularProgress.js
Normal file
75
src/components/common/CircularProgress.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// 원형 프로그레스 컴포넌트
|
||||
const CircularProgress = ({
|
||||
progress,
|
||||
size = 40,
|
||||
strokeWidth,
|
||||
backgroundColor = '#E0E0E0',
|
||||
progressColor = '#4A90E2',
|
||||
textColor = '#4A90E2',
|
||||
showText = true,
|
||||
textSize,
|
||||
className
|
||||
}) => {
|
||||
// 기본값 계산
|
||||
const actualStrokeWidth = strokeWidth || size * 0.1; // 프로그레스 바 두께
|
||||
const radius = (size - actualStrokeWidth) / 2; // 원의 반지름
|
||||
const circumference = 2 * Math.PI * radius; // 원의 둘레
|
||||
const strokeDashoffset = circumference - (progress / 100) * circumference; // 진행률에 따른 offset 계산
|
||||
const actualTextSize = textSize || Math.max(10, size * 0.3); // 텍스트 크기
|
||||
|
||||
return (
|
||||
<ProgressContainer size={size} className={className}>
|
||||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
||||
{/* 배경 원 */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={backgroundColor}
|
||||
strokeWidth={actualStrokeWidth}
|
||||
/>
|
||||
{/* 진행률 표시 원 */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={progressColor}
|
||||
strokeWidth={actualStrokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
/>
|
||||
</svg>
|
||||
{showText && (
|
||||
<ProgressText color={textColor} fontSize={actualTextSize}>
|
||||
{`${Math.round(progress)}%`}
|
||||
</ProgressText>
|
||||
)}
|
||||
</ProgressContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CircularProgress;
|
||||
|
||||
// 스타일 컴포넌트
|
||||
const ProgressContainer = styled.div`
|
||||
position: relative;
|
||||
width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ProgressText = styled.div`
|
||||
position: absolute;
|
||||
font-size: ${props => props.fontSize}px;
|
||||
font-weight: bold;
|
||||
color: ${props => props.color};
|
||||
`;
|
||||
42
src/components/common/DownloadProgress.js
Normal file
42
src/components/common/DownloadProgress.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const DownloadProgress = ({ progress }) => {
|
||||
return (
|
||||
<ProgressWrapper>
|
||||
<ProgressText>다운로드 중... {progress}%</ProgressText>
|
||||
<ProgressBarContainer>
|
||||
<ProgressBarFill style={{ width: `${progress}%` }} />
|
||||
</ProgressBarContainer>
|
||||
</ProgressWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadProgress;
|
||||
|
||||
const ProgressWrapper = styled.div`
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
`;
|
||||
|
||||
const ProgressText = styled.div`
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
`;
|
||||
|
||||
const ProgressBarContainer = styled.div`
|
||||
height: 20px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const ProgressBarFill = styled.div`
|
||||
height: 100%;
|
||||
background-color: #4caf50;
|
||||
transition: width 0.3s ease;
|
||||
`;
|
||||
151
src/components/common/Pagination/FrontPagination.js
Normal file
151
src/components/common/Pagination/FrontPagination.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import PaginationIcon from '../../../assets/img/icon/icon-pagination.png';
|
||||
|
||||
const FrontPagination = ({
|
||||
data, // 전체 데이터 배열
|
||||
itemsPerPage, // 페이지당 표시할 항목 수
|
||||
currentPage, // 현재 페이지
|
||||
setCurrentPage, // 현재 페이지 설정 함수
|
||||
pageLimit = 10, // 페이지 네비게이션에 표시할 페이지 수
|
||||
onPageChange // 페이지 변경 시 호출될 콜백 함수 (선택 사항)
|
||||
}) => {
|
||||
const [blockNum, setBlockNum] = useState(0);
|
||||
|
||||
// 전체 페이지 수 계산
|
||||
const totalItems = data?.length || 0;
|
||||
const maxPage = Math.ceil(totalItems / itemsPerPage);
|
||||
|
||||
// 페이지 번호 배열 생성
|
||||
const pageNumbers = [];
|
||||
for (let i = 1; i <= maxPage; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
|
||||
// 현재 블록에 표시할 페이지 번호
|
||||
const v = blockNum * pageLimit;
|
||||
const pArr = pageNumbers.slice(v, pageLimit + v);
|
||||
|
||||
const processPageData = useCallback(() => {
|
||||
if (!data || !Array.isArray(data) || data.length === 0) return [];
|
||||
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return data.slice(startIndex, endIndex);
|
||||
}, [data, currentPage, itemsPerPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onPageChange) {
|
||||
const pageData = processPageData();
|
||||
onPageChange(pageData);
|
||||
}
|
||||
}, [processPageData, onPageChange]);
|
||||
|
||||
// itemsPerPage나 데이터가 변경되면 블록 초기화
|
||||
useEffect(() => {
|
||||
setBlockNum(0);
|
||||
}, [itemsPerPage, totalItems]);
|
||||
|
||||
// 첫 페이지로 이동
|
||||
const firstPage = () => {
|
||||
setBlockNum(0);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// 마지막 페이지로 이동
|
||||
const lastPage = () => {
|
||||
setBlockNum(Math.ceil(maxPage / pageLimit) - 1);
|
||||
setCurrentPage(maxPage);
|
||||
};
|
||||
|
||||
// 이전 페이지로 이동
|
||||
const prePage = () => {
|
||||
if (currentPage <= 1) return;
|
||||
|
||||
if (currentPage - 1 <= pageLimit * blockNum) {
|
||||
setBlockNum(n => n - 1);
|
||||
}
|
||||
|
||||
setCurrentPage(n => n - 1);
|
||||
};
|
||||
|
||||
// 다음 페이지로 이동
|
||||
const nextPage = () => {
|
||||
if (currentPage >= maxPage) return;
|
||||
|
||||
if (pageLimit * (blockNum + 1) <= currentPage) {
|
||||
setBlockNum(n => n + 1);
|
||||
}
|
||||
|
||||
setCurrentPage(n => n + 1);
|
||||
};
|
||||
|
||||
// 특정 페이지로 이동
|
||||
const clickPage = number => {
|
||||
setCurrentPage(number);
|
||||
};
|
||||
|
||||
// 현재 표시 중인 항목 범위 계산 (예: "1-10 / 총 100개")
|
||||
const startItem = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
|
||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginationWrapper>
|
||||
<Button $position="0" onClick={firstPage} disabled={currentPage === 1} />
|
||||
<Button $position="-20px" onClick={prePage} disabled={currentPage === 1} />
|
||||
|
||||
{pArr.map(number => (
|
||||
<PageNum
|
||||
$state={currentPage === number ? 'on' : ''}
|
||||
key={number}
|
||||
onClick={() => clickPage(number)}
|
||||
>
|
||||
{number}
|
||||
</PageNum>
|
||||
))}
|
||||
|
||||
<Button $position="-40px" onClick={nextPage} disabled={currentPage === maxPage} />
|
||||
<Button $position="-60px" onClick={lastPage} disabled={currentPage === maxPage} />
|
||||
</PaginationWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FrontPagination;
|
||||
|
||||
const PaginationWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
background: url('${PaginationIcon}') no-repeat;
|
||||
background-position: ${props => props.$position} 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: ${props => props.disabled ? 0.5 : 1};
|
||||
cursor: ${props => props.disabled ? 'default' : 'pointer'};
|
||||
|
||||
&:hover {
|
||||
background-position: ${props => props.$position} ${props => props.disabled ? '0' : '-20px'};
|
||||
}
|
||||
`;
|
||||
|
||||
const PageNum = styled.div`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${props => (props.$state === 'on' ? '#2c2c2c' : '#CECECE')};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #2c2c2c;
|
||||
}
|
||||
`;
|
||||
@@ -2,7 +2,7 @@ import { styled } from 'styled-components';
|
||||
import { TextInput, SelectInput, SearchBarAlert, BtnWrapper } from '../../../styles/Components';
|
||||
import Button from '../button/Button';
|
||||
|
||||
const SearchBarLayout = ({ firstColumnData, secondColumnData, direction, onReset, handleSubmit }) => {
|
||||
const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction, onReset, handleSubmit }) => {
|
||||
return (
|
||||
<SearchbarStyle direction={direction}>
|
||||
<SearchRow>
|
||||
@@ -17,6 +17,11 @@ const SearchBarLayout = ({ firstColumnData, secondColumnData, direction, onReset
|
||||
))}
|
||||
</SearchRow>
|
||||
)}
|
||||
{filter && (
|
||||
<SearchRow>
|
||||
{filter}
|
||||
</SearchRow>
|
||||
)}
|
||||
<SearchRow>
|
||||
<BtnWrapper $gap="8px">
|
||||
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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="맨 위로 이동"
|
||||
>
|
||||
|
||||
@@ -12,8 +12,10 @@ import CustomConfirmModal from './modal/CustomConfirmModal';
|
||||
import Modal from './modal/Modal';
|
||||
import Pagination from './Pagination/Pagination';
|
||||
import DynamoPagination from './Pagination/DynamoPagination';
|
||||
import FrontPagination from './Pagination/FrontPagination';
|
||||
import ViewTableInfo from './Table/ViewTableInfo';
|
||||
import Loading from './Loading';
|
||||
import DownloadProgress from './DownloadProgress';
|
||||
import CDivider from './CDivider';
|
||||
import TopButton from './button/TopButton';
|
||||
export {
|
||||
@@ -42,5 +44,7 @@ export { DateTimeInput,
|
||||
Loading,
|
||||
CDivider,
|
||||
TopButton,
|
||||
DynamoPagination
|
||||
DynamoPagination,
|
||||
FrontPagination,
|
||||
DownloadProgress
|
||||
};
|
||||
Reference in New Issue
Block a user