조회조건 레이아웃 수정
엑셀다운버튼 수정 progress 추가 front pagenation 추가
This commit is contained in:
@@ -272,6 +272,12 @@ export const opMailType = [
|
|||||||
{ value: 'SEND', name: '보낸 우편' },
|
{ value: 'SEND', name: '보낸 우편' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const opInputType = [
|
||||||
|
{ value: 'String', name: '문자열' },
|
||||||
|
{ value: 'Number', name: '숫자' },
|
||||||
|
{ value: 'Boolean', name: '부울' },
|
||||||
|
];
|
||||||
|
|
||||||
// export const logAction = [
|
// export const logAction = [
|
||||||
// { value: "None", name: "ALL" },
|
// { value: "None", name: "ALL" },
|
||||||
// { value: "AIChatDeleteCharacter", name: "NPC 삭제" },
|
// { value: "AIChatDeleteCharacter", name: "NPC 삭제" },
|
||||||
@@ -567,7 +573,7 @@ export const opMailType = [
|
|||||||
// ];
|
// ];
|
||||||
|
|
||||||
export const logAction = [
|
export const logAction = [
|
||||||
{ value: "None", name: "ALL" },
|
{ value: "None", name: "전체" },
|
||||||
{ value: "AIChatDeleteCharacter", name: "AIChatDeleteCharacter" },
|
{ value: "AIChatDeleteCharacter", name: "AIChatDeleteCharacter" },
|
||||||
{ value: "AIChatDeleteUser", name: "AIChatDeleteUser" },
|
{ value: "AIChatDeleteUser", name: "AIChatDeleteUser" },
|
||||||
{ value: "AIChatGetCharacter", name: "AIChatGetCharacter" },
|
{ value: "AIChatGetCharacter", name: "AIChatGetCharacter" },
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ReportListDetailModal from './modal/ReportListDetailModal';
|
|||||||
import UserBlockDetailModal from './modal/UserBlockDetailModal';
|
import UserBlockDetailModal from './modal/UserBlockDetailModal';
|
||||||
import OwnerChangeModal from './modal/OwnerChangeModal';
|
import OwnerChangeModal from './modal/OwnerChangeModal';
|
||||||
//searchbar
|
//searchbar
|
||||||
|
import SearchFilter from './searchBar/SearchFilter';
|
||||||
import ReportListSearchBar from './searchBar/ReportListSearchBar';
|
import ReportListSearchBar from './searchBar/ReportListSearchBar';
|
||||||
import UserBlockSearchBar from './searchBar/UserBlockSearchBar';
|
import UserBlockSearchBar from './searchBar/UserBlockSearchBar';
|
||||||
import ItemsSearchBar from './searchBar/ItemsSearchBar';
|
import ItemsSearchBar from './searchBar/ItemsSearchBar';
|
||||||
@@ -29,6 +30,7 @@ export {
|
|||||||
BoardInfoModal,
|
BoardInfoModal,
|
||||||
BoardRegistModal,
|
BoardRegistModal,
|
||||||
MailDetailModal,
|
MailDetailModal,
|
||||||
|
SearchFilter,
|
||||||
MailListSearchBar,
|
MailListSearchBar,
|
||||||
LandInfoSearchBar,
|
LandInfoSearchBar,
|
||||||
BusinessLogSearchBar,
|
BusinessLogSearchBar,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
|
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
|
||||||
import Button from '../../common/button/Button';
|
|
||||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { LandAuctionView, LandInfoData } from '../../../apis';
|
import { logAction, logDomain, userSearchType2 } from '../../../assets/data/options';
|
||||||
import { landAuctionStatus, landSearchType, landSize, opLandCategoryType } from '../../../assets/data';
|
|
||||||
import { logAction, logDomain, opLandInfoStatusType, userSearchType2 } from '../../../assets/data/options';
|
|
||||||
import { BusinessLogList } from '../../../apis/Log';
|
import { BusinessLogList } from '../../../apis/Log';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {SearchFilter} from '../';
|
||||||
|
|
||||||
export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => {
|
export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -27,6 +25,7 @@ export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => {
|
|||||||
date.setDate(date.getDate() - 1);
|
date.setDate(date.getDate() - 1);
|
||||||
return date;
|
return date;
|
||||||
})(),
|
})(),
|
||||||
|
filters: [],
|
||||||
order_by: 'ASC',
|
order_by: 'ASC',
|
||||||
page_size: initialPageSize,
|
page_size: initialPageSize,
|
||||||
page_no: 1
|
page_no: 1
|
||||||
@@ -96,6 +95,7 @@ export const useBusinessLogSearch = (token, initialPageSize, setAlertMsg) => {
|
|||||||
tran_id: '',
|
tran_id: '',
|
||||||
start_dt: now,
|
start_dt: now,
|
||||||
end_dt: now,
|
end_dt: now,
|
||||||
|
filters: [],
|
||||||
order_by: 'ASC',
|
order_by: 'ASC',
|
||||||
page_size: initialPageSize,
|
page_size: initialPageSize,
|
||||||
page_no: 1
|
page_no: 1
|
||||||
@@ -198,7 +198,12 @@ const BusinessLogSearchBar = ({ searchParams, onSearch, onReset }) => {
|
|||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
];
|
];
|
||||||
return <SearchBarLayout firstColumnData={searchList} secondColumnData={optionList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
|
|
||||||
|
const filterComponent = (
|
||||||
|
<SearchFilter value={searchParams.filters} onChange={e => onSearch({filters: e.target.value })} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return <SearchBarLayout firstColumnData={searchList} secondColumnData={optionList} filter={filterComponent} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BusinessLogSearchBar;
|
export default BusinessLogSearchBar;
|
||||||
238
src/components/ServiceManage/searchBar/SearchFilter.js
Normal file
238
src/components/ServiceManage/searchBar/SearchFilter.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { styled } from 'styled-components';
|
||||||
|
import { TextInput, InputLabel, SelectInput } from '../../../styles/Components';
|
||||||
|
import { logDomain, opInputType } from '../../../assets/data/options';
|
||||||
|
|
||||||
|
const SearchFilter = ({ value = [], onChange }) => {
|
||||||
|
const [filters, setFilters] = useState(value || []);
|
||||||
|
const [filterSections, setFilterSections] = useState([{ field_name: '', field_type: 'String', value: '' }]);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilters(value || []);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleInputChange = (index, field, inputValue) => {
|
||||||
|
const updatedSections = [...filterSections];
|
||||||
|
updatedSections[index] = { ...updatedSections[index], [field]: inputValue };
|
||||||
|
setFilterSections(updatedSections);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddFilter = (index) => {
|
||||||
|
const filterToAdd = filterSections[index];
|
||||||
|
|
||||||
|
// Only add if both field_name and value are provided
|
||||||
|
if (filterToAdd.field_name && filterToAdd.value && filterToAdd.field_type) {
|
||||||
|
const updatedFilters = [...filters, { field_name: filterToAdd.field_name, field_type: filterToAdd.field_type, value: filterToAdd.value }];
|
||||||
|
setFilters(updatedFilters);
|
||||||
|
onChange({ target: { value: updatedFilters } });
|
||||||
|
|
||||||
|
// Reset this section
|
||||||
|
const updatedSections = [...filterSections];
|
||||||
|
updatedSections[index] = { field_name: '', field_type: 'String', value: '' };
|
||||||
|
setFilterSections(updatedSections);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddFilterSection = () => {
|
||||||
|
setFilterSections([...filterSections, { field_name: '', field_type: 'String', value: '' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFilterSection = (index) => {
|
||||||
|
if (filterSections.length > 1) {
|
||||||
|
const updatedSections = filterSections.filter((_, i) => i !== index);
|
||||||
|
setFilterSections(updatedSections);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFilter = (index) => {
|
||||||
|
const updatedFilters = filters.filter((_, i) => i !== index);
|
||||||
|
setFilters(updatedFilters);
|
||||||
|
onChange({ target: { value: updatedFilters } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFilters = () => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterWrapper>
|
||||||
|
<FilterToggle onClick={toggleFilters}>
|
||||||
|
필터 {isOpen ? '▲' : '▼'}
|
||||||
|
</FilterToggle>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
|
||||||
|
{filters.length > 0 && (
|
||||||
|
<FilterSection>
|
||||||
|
{filters.map((filter, index) => (
|
||||||
|
<FilterItem key={index}>
|
||||||
|
<FilterName>{filter.field_name}:</FilterName>
|
||||||
|
<FilterValue>{filter.value}</FilterValue>
|
||||||
|
<RemoveButton onClick={() => handleRemoveFilter(index)}>×</RemoveButton>
|
||||||
|
</FilterItem>
|
||||||
|
))}
|
||||||
|
</FilterSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filterSections.map((section, index) => (
|
||||||
|
<FilterInputSection key={index}>
|
||||||
|
<InputLabel>속성 이름</InputLabel>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
placeholder="속성 이름 입력"
|
||||||
|
value={section.field_name}
|
||||||
|
onChange={(e) => handleInputChange(index, 'field_name', e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputLabel>속성 유형</InputLabel>
|
||||||
|
<SelectInput
|
||||||
|
value={section.field_type}
|
||||||
|
onChange={(e) => handleInputChange(index, 'field_type', e.target.value)}
|
||||||
|
>
|
||||||
|
{opInputType.map((data, index) => (
|
||||||
|
<option key={index} value={data.value}>
|
||||||
|
{data.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
|
||||||
|
<InputLabel>속성 값</InputLabel>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
placeholder="속성 값 입력"
|
||||||
|
value={section.value}
|
||||||
|
onChange={(e) => handleInputChange(index, 'value', e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddButton onClick={() => handleAddFilter(index)}>추가</AddButton>
|
||||||
|
|
||||||
|
{filterSections.length > 1 && (
|
||||||
|
<RemoveButton onClick={() => handleRemoveFilterSection(index)}>×</RemoveButton>
|
||||||
|
)}
|
||||||
|
</FilterInputSection>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/*<AddFilterButton onClick={handleAddFilterSection}>*/}
|
||||||
|
{/* 필터 추가*/}
|
||||||
|
{/*</AddFilterButton>*/}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FilterWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchFilter;
|
||||||
|
|
||||||
|
const FilterWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FilterToggle = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FilterSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 15px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FilterItem = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FilterName = styled.span`
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FilterValue = styled.span`
|
||||||
|
color: #333;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RemoveButton = styled.button`
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #888;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AddButton = styled.button.attrs({
|
||||||
|
type: 'button'
|
||||||
|
})`
|
||||||
|
background: #1890ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #40a9ff;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FilterInputSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 0 15px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
${TextInput} {
|
||||||
|
width: 160px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AddFilterButton = styled.button.attrs({
|
||||||
|
type: 'button'
|
||||||
|
})`
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
margin: 0 15px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
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 { TextInput, SelectInput, SearchBarAlert, BtnWrapper } from '../../../styles/Components';
|
||||||
import Button from '../button/Button';
|
import Button from '../button/Button';
|
||||||
|
|
||||||
const SearchBarLayout = ({ firstColumnData, secondColumnData, direction, onReset, handleSubmit }) => {
|
const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction, onReset, handleSubmit }) => {
|
||||||
return (
|
return (
|
||||||
<SearchbarStyle direction={direction}>
|
<SearchbarStyle direction={direction}>
|
||||||
<SearchRow>
|
<SearchRow>
|
||||||
@@ -17,6 +17,11 @@ const SearchBarLayout = ({ firstColumnData, secondColumnData, direction, onReset
|
|||||||
))}
|
))}
|
||||||
</SearchRow>
|
</SearchRow>
|
||||||
)}
|
)}
|
||||||
|
{filter && (
|
||||||
|
<SearchRow>
|
||||||
|
{filter}
|
||||||
|
</SearchRow>
|
||||||
|
)}
|
||||||
<SearchRow>
|
<SearchRow>
|
||||||
<BtnWrapper $gap="8px">
|
<BtnWrapper $gap="8px">
|
||||||
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
|
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
|
||||||
|
|||||||
@@ -1,7 +1,34 @@
|
|||||||
import * as XLSX from 'xlsx-js-style';
|
import * as XLSX from 'xlsx-js-style';
|
||||||
import { ExcelDownButton } from '../../../styles/ModuleComponents';
|
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) => {
|
const isNumeric = (value) => {
|
||||||
// 숫자 또는 숫자 문자열인지 확인
|
// 숫자 또는 숫자 문자열인지 확인
|
||||||
return !isNaN(value) && !isNaN(parseFloat(value));
|
return !isNaN(value) && !isNaN(parseFloat(value));
|
||||||
@@ -71,192 +98,409 @@ const ExcelDownloadButton = ({ tableRef, data, fileName = 'download.xlsx', sheet
|
|||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadTableExcel = () => {
|
const updateLoadingState = (newProgress) => {
|
||||||
try {
|
setLastProgress(newProgress);
|
||||||
if (!tableRef || !tableRef.current) {
|
if (onLoadingChange && typeof onLoadingChange === 'function') {
|
||||||
alert('테이블 참조가 없습니다.');
|
onLoadingChange({loading: true, progress: newProgress});
|
||||||
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 downloadDataExcel = () => {
|
const downloadTableExcel = async () => {
|
||||||
try {
|
return new Promise((resolve, reject) => {
|
||||||
if (!data || !data || data.length === 0) {
|
try {
|
||||||
alert('다운로드할 데이터가 없습니다.');
|
if (!tableRef || !tableRef.current) {
|
||||||
return;
|
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 chunkArray = (array, chunkSize) => {
|
||||||
const flattenedData = data.map(item => {
|
const chunks = [];
|
||||||
// 기본 필드
|
for (let i = 0; i < array.length; i += chunkSize) {
|
||||||
const baseData = {
|
chunks.push(array.slice(i, i + chunkSize));
|
||||||
'logTime': item.logTime,
|
}
|
||||||
'GUID': item.userGuid === 'None' ? '' : item.userGuid,
|
return chunks;
|
||||||
'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 processDataChunk = async (chunk, headers, allDataRows, processedCount, totalCount) => {
|
||||||
const actorData = item.header && item.header.Actor ?
|
return new Promise((resolve) => {
|
||||||
flattenObject(item.header.Actor, 'Actor') : {};
|
setTimeout(() => {
|
||||||
|
// 각 청크의 데이터를 처리
|
||||||
// Infos 데이터 플랫하게 추가
|
const rowsData = chunk.map(item => {
|
||||||
let infosData = {};
|
return headers.map(header => {
|
||||||
if (item.body && item.body.Infos && Array.isArray(item.body.Infos)) {
|
const value = item[header] !== undefined ? item[header] : '';
|
||||||
item.body.Infos.forEach((info, index) => {
|
return {
|
||||||
infosData = {
|
v: value,
|
||||||
...infosData,
|
s: dataStyle
|
||||||
...flattenObject(info, `Info`)
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 진행률 계산 및 콜백 호출
|
||||||
|
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,
|
const allKeys = new Set();
|
||||||
...actorData,
|
|
||||||
...infosData
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 모든 항목의 모든 키 수집하여 헤더 생성
|
// 헤더 수집도 청크로 나누기
|
||||||
const allKeys = new Set();
|
for (let i = 0; i < flattenedData.length; i += dataChunkSize) {
|
||||||
flattenedData.forEach(item => {
|
await new Promise(resolve => {
|
||||||
Object.keys(item).forEach(key => allKeys.add(key));
|
setTimeout(() => {
|
||||||
});
|
const end = Math.min(i + dataChunkSize, flattenedData.length);
|
||||||
const headers = Array.from(allKeys);
|
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 headers = Array.from(allKeys);
|
||||||
const wb = XLSX.utils.book_new();
|
updateLoadingState(20);
|
||||||
|
|
||||||
// 데이터 행들 생성
|
// 워크북 생성
|
||||||
const dataRows = flattenedData.map(item => {
|
const wb = XLSX.utils.book_new();
|
||||||
return headers.map(header => {
|
|
||||||
const value = item[header] !== undefined ? item[header] : '';
|
|
||||||
return {
|
|
||||||
v: value,
|
|
||||||
s: dataStyle
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 워크시트 데이터 구성
|
// 헤더 행 생성
|
||||||
const wsData = [
|
const headerRow = headers.map(h => ({
|
||||||
// 헤더 행
|
|
||||||
headers.map(h => ({
|
|
||||||
v: h,
|
v: h,
|
||||||
s: headerStyle
|
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) => {
|
let processedCount = 0;
|
||||||
// 헤더 길이와 데이터 길이 중 최대값으로 열 너비 결정
|
for (const chunk of rowChunks) {
|
||||||
const maxLength = Math.max(
|
await processDataChunk(chunk, headers, allDataRows, processedCount, flattenedData.length);
|
||||||
header.length * 1.5,
|
processedCount += chunk.length;
|
||||||
...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)) };
|
|
||||||
});
|
|
||||||
|
|
||||||
// 워크시트를 워크북에 추가
|
// 메모리 정리를 위한 가비지 컬렉션 힌트
|
||||||
XLSX.utils.book_append_sheet(wb, ws, sheetName);
|
if (processedCount % (chunkSize * 10) === 0) {
|
||||||
|
// 5000행마다 짧은 지연을 두어 가비지 컬렉션 기회 제공
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 엑셀 파일 다운로드
|
updateLoadingState(95);
|
||||||
XLSX.writeFile(wb, fileName);
|
|
||||||
|
// 워크시트 데이터 구성 및 파일 다운로드를 메인 로직과 분리
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('Excel download failed:', error);
|
console.error('Excel download failed:', error);
|
||||||
alert('엑셀 다운로드 중 오류가 발생했습니다.');
|
alert('엑셀 다운로드 중 오류가 발생했습니다.');
|
||||||
|
} finally {
|
||||||
|
// 다운로드 완료 후 짧은 지연 시간을 두어 100% 상태를 잠시 보여줌
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsDownloading(false);
|
||||||
|
if (onLoadingChange) onLoadingChange({loading: false, progress: 100});
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
};
|
}, [tableRef, data, fileName, sheetName, isDownloading, onLoadingChange]);
|
||||||
|
|
||||||
const handleDownload = () => {
|
|
||||||
if (tableRef) {
|
|
||||||
downloadTableExcel();
|
|
||||||
} else if (data) {
|
|
||||||
downloadDataExcel();
|
|
||||||
} else {
|
|
||||||
alert('유효한 데이터 소스가 없습니다.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExcelDownButton onClick={handleDownload}>
|
<ExcelDownButton onClick={handleDownload} disabled={isDownloading}>
|
||||||
엑셀 다운로드
|
{isDownloading ? '다운로드 중...' : '엑셀 다운로드'}
|
||||||
</ExcelDownButton>
|
</ExcelDownButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,29 +2,26 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const Button = styled.button`
|
const Button = styled.button`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
right: 30px;
|
right: 30px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #666666;
|
background-color: #666666;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
display: flex;
|
display: ${props => (props.$show ? 'flex' : 'none')};
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: ${props => (props.visible ? '1' : '0')};
|
border: none;
|
||||||
visibility: ${props => (props.visible ? 'visible' : 'hidden')};
|
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
transition: opacity 0.3s, visibility 0.3s;
|
z-index: 999;
|
||||||
border: none;
|
|
||||||
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2);
|
|
||||||
z-index: 999;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TopButton = () => {
|
const TopButton = () => {
|
||||||
@@ -53,7 +50,7 @@ const TopButton = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
visible={visible}
|
$show={visible}
|
||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
title="맨 위로 이동"
|
title="맨 위로 이동"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import CustomConfirmModal from './modal/CustomConfirmModal';
|
|||||||
import Modal from './modal/Modal';
|
import Modal from './modal/Modal';
|
||||||
import Pagination from './Pagination/Pagination';
|
import Pagination from './Pagination/Pagination';
|
||||||
import DynamoPagination from './Pagination/DynamoPagination';
|
import DynamoPagination from './Pagination/DynamoPagination';
|
||||||
|
import FrontPagination from './Pagination/FrontPagination';
|
||||||
import ViewTableInfo from './Table/ViewTableInfo';
|
import ViewTableInfo from './Table/ViewTableInfo';
|
||||||
import Loading from './Loading';
|
import Loading from './Loading';
|
||||||
|
import DownloadProgress from './DownloadProgress';
|
||||||
import CDivider from './CDivider';
|
import CDivider from './CDivider';
|
||||||
import TopButton from './button/TopButton';
|
import TopButton from './button/TopButton';
|
||||||
export {
|
export {
|
||||||
@@ -42,5 +44,7 @@ export { DateTimeInput,
|
|||||||
Loading,
|
Loading,
|
||||||
CDivider,
|
CDivider,
|
||||||
TopButton,
|
TopButton,
|
||||||
DynamoPagination
|
DynamoPagination,
|
||||||
|
FrontPagination,
|
||||||
|
DownloadProgress
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user