비즈니스 로그 조회 및 파일 다운 수정
This commit is contained in:
@@ -7,6 +7,7 @@ export const BusinessLogList = async (token, params) => {
|
||||
try {
|
||||
const res = await Axios.post(`/api/v1/log/generic/list`, params, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
timeout: 600000
|
||||
});
|
||||
|
||||
return res.data;
|
||||
@@ -15,4 +16,100 @@ export const BusinessLogList = async (token, params) => {
|
||||
throw new Error('BusinessLogList Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const BusinessLogExport = async (token, params) => {
|
||||
try {
|
||||
await Axios.post(`/api/v1/log/generic/excel-export`, params, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
responseType: 'blob',
|
||||
timeout: 300000
|
||||
}).then(response => {
|
||||
if (!response.data) {
|
||||
console.log(response);
|
||||
throw new Error('No data received');
|
||||
}
|
||||
|
||||
const contentType = response.headers['content-type'] || response.headers['Content-Type'];
|
||||
const contentDisposition = response.headers['content-disposition'] || response.headers['Content-Disposition'];
|
||||
|
||||
// Excel이나 ZIP 파일이 아닌 경우
|
||||
if (!contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') &&
|
||||
!contentType.includes('application/zip')) {
|
||||
console.log(response);
|
||||
throw new Error(`잘못된 파일 형식입니다. Content-Type: ${contentType}`);
|
||||
}
|
||||
|
||||
let fileName = 'businessLog';
|
||||
let fileExtension = '.xlsx';
|
||||
let mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
|
||||
// ZIP 파일인지 확인
|
||||
if (contentType && contentType.includes('application/zip')) {
|
||||
fileExtension = '.zip';
|
||||
mimeType = 'application/zip';
|
||||
fileName = 'businessLog_multiple_files';
|
||||
}
|
||||
|
||||
// Content-Disposition에서 파일명 추출
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (fileNameMatch && fileNameMatch[1]) {
|
||||
const extractedFileName = decodeURIComponent(fileNameMatch[1].replace(/['"]/g, ''));
|
||||
fileName = extractedFileName;
|
||||
}
|
||||
} else {
|
||||
// Content-Disposition이 없으면 기본 파일명 사용
|
||||
fileName = fileName + fileExtension;
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data], { type: mimeType });
|
||||
|
||||
// 빈 파일 체크
|
||||
if (blob.size === 0) {
|
||||
throw new Error('다운로드된 파일이 비어있습니다.');
|
||||
}
|
||||
|
||||
// 너무 작은 파일 체크 (실제 Excel 파일은 최소 몇 KB 이상)
|
||||
if (blob.size < 1024) { // 1KB 미만
|
||||
throw new Error('파일 크기가 너무 작습니다. 올바른 Excel 파일이 아닐 수 있습니다.');
|
||||
}
|
||||
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = fileName;
|
||||
link.style.display = 'none';
|
||||
link.rel = 'noopener noreferrer';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(href);
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('BusinessLogExport Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getExcelProgress = async (token, taskId) => {
|
||||
try {
|
||||
const response = await Axios.get(`/api/v1/log/progress/${taskId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Progress API error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
96
src/assets/data/pages/businessLogSearch.json
Normal file
96
src/assets/data/pages/businessLogSearch.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"initialSearchParams": {
|
||||
"searchType": "GUID",
|
||||
"searchData": "",
|
||||
"logAction": "None",
|
||||
"logDomain": "BASE",
|
||||
"tran_id": "",
|
||||
"startDate": "",
|
||||
"endDate": "",
|
||||
"orderBy": "DESC",
|
||||
"pageSize": 500,
|
||||
"currentPage": 1
|
||||
},
|
||||
|
||||
"searchFields": [
|
||||
{
|
||||
"type": "select",
|
||||
"id": "searchType",
|
||||
"optionsRef": "userSearchType2",
|
||||
"col": 1,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "searchData",
|
||||
"placeholder": "대상 입력",
|
||||
"width": "300px",
|
||||
"col": 1,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "period",
|
||||
"startDateId": "startDate",
|
||||
"endDateId": "endDate",
|
||||
"label": "조회 일자",
|
||||
"col": 1
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "searchContent",
|
||||
"label": "우편 내용",
|
||||
"placeholder": "우편 내용(공백으로 구분)",
|
||||
"width": "300px",
|
||||
"col": 1
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "sendType",
|
||||
"label": "발송 방식",
|
||||
"optionsRef": "mailSendType",
|
||||
"col": 2
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "status",
|
||||
"label": "발송 상태",
|
||||
"optionsRef": "mailSendStatus",
|
||||
"col": 2
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "mailType",
|
||||
"label": "우편 타입",
|
||||
"optionsRef": "mailType",
|
||||
"col": 2
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "receiveType",
|
||||
"label": "수신 대상",
|
||||
"optionsRef": "mailReceiveType",
|
||||
"col": 2
|
||||
}
|
||||
],
|
||||
|
||||
"apiInfo": {
|
||||
"functionName": "MailView",
|
||||
"loadOnMount": true,
|
||||
"paramsMapping": [
|
||||
"searchTitle",
|
||||
"searchContent",
|
||||
"sendType",
|
||||
"status",
|
||||
"mailType",
|
||||
"receiveType",
|
||||
{"param": "startDate", "transform": "toISOString"},
|
||||
{"param": "endDate", "transform": "toISOString"},
|
||||
"orderBy",
|
||||
"pageSize",
|
||||
"currentPage"
|
||||
],
|
||||
"pageField": "currentPage",
|
||||
"pageSizeField": "pageSize",
|
||||
"orderField": "orderBy"
|
||||
}
|
||||
}
|
||||
@@ -49,10 +49,14 @@ export const useBusinessLogSearch = (token, initialPageSize) => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const start = Date.now();
|
||||
const result = await BusinessLogList(
|
||||
token,
|
||||
params
|
||||
);
|
||||
const end = Date.now();
|
||||
showToast(`처리 시간: ${end - start}ms`, {type: alertTypes.info});
|
||||
console.log(`처리 시간: ${end - start}ms`);
|
||||
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
|
||||
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
|
||||
}else if(result.result === "ERROR_MONGODB_QUERY"){
|
||||
|
||||
137
src/components/common/button/ExcelExportButton.js
Normal file
137
src/components/common/button/ExcelExportButton.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import { ExcelDownButton } from '../../../styles/ModuleComponents';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { BusinessLogExport, getExcelProgress } from '../../../apis/Log';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
import { alertTypes } from '../../../assets/data/types';
|
||||
|
||||
const ExcelDownloadButton = ({ params, fileName = 'download.xlsx', sheetName = 'Sheet1', onLoadingChange, disabled, dataSize }) => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
const {showToast} = useAlert();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const taskIdRef = useRef(null);
|
||||
const intervalRef = useRef(null);
|
||||
|
||||
// 진행률 폴링 함수
|
||||
const pollProgress = useCallback(async (taskId) => {
|
||||
try {
|
||||
const response = await getExcelProgress(token, taskId);
|
||||
// console.log(response.data);
|
||||
if (response.data.exists) {
|
||||
const { percentage, message } = response.data;
|
||||
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({
|
||||
loading: true,
|
||||
progress: percentage
|
||||
});
|
||||
}
|
||||
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({ loading: true, progress: percentage });
|
||||
}
|
||||
|
||||
// 100% 완료 시 폴링 중지
|
||||
if (percentage >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
} else {
|
||||
// 진행률 정보가 없으면 폴링 중지
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Progress polling error:', error);
|
||||
// 에러 발생 시 폴링 중지
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, [onLoadingChange]);
|
||||
|
||||
// 컴포넌트 언마운트 시 정리
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
if (isDownloading) return; // 이미 다운로드 중이면 중복 실행 방지
|
||||
if (dataSize > 200000){
|
||||
showToast('EXCEL_EXPORT_LENGTH_LIMIT_WARNING', {type: alertTypes.warning});
|
||||
return;
|
||||
}
|
||||
|
||||
// const start = Date.now();
|
||||
const taskId = `excel_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
taskIdRef.current = taskId;
|
||||
|
||||
|
||||
setIsDownloading(true);
|
||||
if (onLoadingChange) onLoadingChange({loading: true, progress: 0});
|
||||
|
||||
// 진행률 폴링 시작 (1초마다)
|
||||
intervalRef.current = setInterval(() => {
|
||||
pollProgress(taskId);
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
await BusinessLogExport(token, {...params, taskId});
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await pollProgress(taskId);
|
||||
} catch (error) {
|
||||
console.error('Final progress check error:', error);
|
||||
}
|
||||
}, 300);
|
||||
} catch (error) {
|
||||
console.error('BusinessLogExport API error:', error);
|
||||
showToast(error, {type: alertTypes.error});
|
||||
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
setIsDownloading(false);
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({
|
||||
loading: false,
|
||||
progress: 0
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (!intervalRef.current) return;
|
||||
|
||||
// 2초 후 상태 리셋
|
||||
setTimeout(() => {
|
||||
setIsDownloading(false);
|
||||
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({ loading: false, progress: 100 });
|
||||
showToast('DOWNLOAD_COMPLETE', { type: alertTypes.success });
|
||||
}
|
||||
|
||||
// 폴링 완전 중지
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
// const end = Date.now();
|
||||
// showToast(`처리 시간: ${end - start}ms`, { type: alertTypes.info });
|
||||
// console.log(`처리 시간: ${end - start}ms`);
|
||||
}, 2000);
|
||||
}
|
||||
}, [params, isDownloading, onLoadingChange, dataSize, pollProgress, showToast]);
|
||||
|
||||
return (
|
||||
<ExcelDownButton onClick={handleDownload} disabled={isDownloading || dataSize === 0}>
|
||||
{isDownloading ? '다운로드 중...' : '엑셀 다운로드'}
|
||||
</ExcelDownButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExcelDownloadButton;
|
||||
@@ -50,6 +50,8 @@ const resources = {
|
||||
DUPLICATE_USER: "중복된 유저 정보가 있습니다.",
|
||||
COUNT_EMPTY_WARNING: "수량을 입력해주세요.",
|
||||
UPLOAD_FILENAME_SAMPLE_WARNING: "파일명에 sample을 넣을 수 없습니다.\r\n파일명을 변경 후 다시 업로드 해주세요.",
|
||||
EXCEL_EXPORT_LENGTH_LIMIT_WARNING: '엑셀 다운은 10만건 이하까지만 가능합니다.\r\n조건을 조정 후 다시 시도해주세요.',
|
||||
DOWNLOAD_COMPLETE: '다운이 완료되었습니다.',
|
||||
//user
|
||||
NICKNAME_CHANGES_CONFIRM: '닉네임을 변경하시겠습니까?',
|
||||
NICKNAME_CHANGES_COMPLETE: '닉네임 변경이 완료되었습니다.',
|
||||
|
||||
@@ -16,27 +16,25 @@ import {
|
||||
import { withAuth } from '../../hooks/hook';
|
||||
import {
|
||||
authType,
|
||||
modalTypes,
|
||||
} from '../../assets/data';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DynamicModal,
|
||||
ExcelDownButton,
|
||||
TopButton,
|
||||
ViewTableInfo,
|
||||
} from '../../components/common';
|
||||
import { TableSkeleton } from '../../components/Skeleton/TableSkeleton';
|
||||
import BusinessLogSearchBar, { useBusinessLogSearch } from '../../components/ServiceManage/searchBar/BusinessLogSearchBar';
|
||||
import styled from 'styled-components';
|
||||
import FrontPagination from '../../components/common/Pagination/FrontPagination';
|
||||
import CircularProgress from '../../components/common/CircularProgress';
|
||||
// import MessageInput from '../../components/common/input/MessageInput';
|
||||
// import { AnalyzeAI } from '../../apis';
|
||||
import {
|
||||
INITIAL_CURRENT_PAGE,
|
||||
INITIAL_PAGE_LIMIT,
|
||||
STORAGE_BUSINESS_LOG_SEARCH,
|
||||
} from '../../assets/data/adminConstants';
|
||||
import ExcelExportButton from '../../components/common/button/ExcelExportButton';
|
||||
import Pagination from '../../components/common/Pagination/Pagination';
|
||||
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
|
||||
|
||||
const BusinessLogView = () => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
@@ -49,10 +47,6 @@ const BusinessLogView = () => {
|
||||
progress: 0
|
||||
});
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(INITIAL_CURRENT_PAGE);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(500);
|
||||
const [displayData, setDisplayData] = useState([]);
|
||||
|
||||
const {
|
||||
searchParams,
|
||||
loading: dataLoading,
|
||||
@@ -61,6 +55,7 @@ const BusinessLogView = () => {
|
||||
handleReset,
|
||||
handlePageChange,
|
||||
handleOrderByChange,
|
||||
handlePageSizeChange,
|
||||
updateSearchParams
|
||||
} = useBusinessLogSearch(token, 500);
|
||||
|
||||
@@ -82,25 +77,6 @@ const BusinessLogView = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePageSizeChange = (newSize) => {
|
||||
setItemsPerPage(newSize);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleClientPageChange = useCallback((slicedData) => {
|
||||
setDisplayData(slicedData);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
if (dataList?.generic_list && dataList.generic_list.length > 0) {
|
||||
const initialData = dataList.generic_list.slice(0, itemsPerPage);
|
||||
setDisplayData(initialData);
|
||||
} else {
|
||||
setDisplayData([]);
|
||||
}
|
||||
}, [dataList, itemsPerPage]);
|
||||
|
||||
const toggleRowExpand = (index) => {
|
||||
setExpandedRows(prev => ({
|
||||
...prev,
|
||||
@@ -209,10 +185,11 @@ const BusinessLogView = () => {
|
||||
</FormWrapper>
|
||||
<ViewTableInfo orderType="asc" pageType="B" total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
|
||||
<DownloadContainer>
|
||||
<ExcelDownButton
|
||||
data={dataList?.generic_list}
|
||||
<ExcelExportButton
|
||||
params={searchParams}
|
||||
fileName={t('FILE_BUSINESS_LOG')}
|
||||
onLoadingChange={setDownloadState}
|
||||
dataSize={dataList?.total_all}
|
||||
/>
|
||||
{downloadState.loading && (
|
||||
<CircularProgressWrapper>
|
||||
@@ -238,7 +215,7 @@ const BusinessLogView = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displayData?.map((item, index) => (
|
||||
{dataList?.generic_list?.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
<tr>
|
||||
<td>{item.logTime}</td>
|
||||
@@ -276,14 +253,14 @@ const BusinessLogView = () => {
|
||||
</TableStyle>
|
||||
</TableWrapper>
|
||||
{dataList?.generic_list &&
|
||||
<FrontPagination
|
||||
data={dataList.generic_list}
|
||||
itemsPerPage={itemsPerPage}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
pageLimit={INITIAL_PAGE_LIMIT}
|
||||
onPageChange={handleClientPageChange}
|
||||
/>}
|
||||
<Pagination
|
||||
postsPerPage={searchParams.page_size}
|
||||
totalPosts={dataList?.total_all}
|
||||
setCurrentPage={handlePageChange}
|
||||
currentPage={searchParams.page_no}
|
||||
pageLimit={INITIAL_PAGE_LIMIT}
|
||||
/>
|
||||
}
|
||||
<TopButton />
|
||||
</>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user