비즈니스 로그 조회 및 파일 다운 수정

This commit is contained in:
2025-06-04 15:19:08 +09:00
parent 9d06246aba
commit dc7934d906
6 changed files with 352 additions and 39 deletions

View File

@@ -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;
}
};

View 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"
}
}

View File

@@ -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"){

View 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;

View File

@@ -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: '닉네임 변경이 완료되었습니다.',

View File

@@ -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 />
</>
}