diff --git a/src/apis/Log.js b/src/apis/Log.js
index 980287d..96b5acd 100644
--- a/src/apis/Log.js
+++ b/src/apis/Log.js
@@ -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;
+ }
};
\ No newline at end of file
diff --git a/src/assets/data/pages/businessLogSearch.json b/src/assets/data/pages/businessLogSearch.json
new file mode 100644
index 0000000..56039fd
--- /dev/null
+++ b/src/assets/data/pages/businessLogSearch.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js b/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js
index 85ee75e..9cb243f 100644
--- a/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js
+++ b/src/components/ServiceManage/searchBar/BusinessLogSearchBar.js
@@ -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"){
diff --git a/src/components/common/button/ExcelExportButton.js b/src/components/common/button/ExcelExportButton.js
new file mode 100644
index 0000000..6eb3c3b
--- /dev/null
+++ b/src/components/common/button/ExcelExportButton.js
@@ -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 (
+
+ {isDownloading ? '다운로드 중...' : '엑셀 다운로드'}
+
+ );
+};
+
+export default ExcelDownloadButton;
\ No newline at end of file
diff --git a/src/i18n.js b/src/i18n.js
index 393f28f..2180194 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -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: '닉네임 변경이 완료되었습니다.',
diff --git a/src/pages/DataManage/BusinessLogView.js b/src/pages/DataManage/BusinessLogView.js
index 5c80486..c18aafd 100644
--- a/src/pages/DataManage/BusinessLogView.js
+++ b/src/pages/DataManage/BusinessLogView.js
@@ -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 = () => {
-
{downloadState.loading && (
@@ -238,7 +215,7 @@ const BusinessLogView = () => {
- {displayData?.map((item, index) => (
+ {dataList?.generic_list?.map((item, index) => (
| {item.logTime} |
@@ -276,14 +253,14 @@ const BusinessLogView = () => {
{dataList?.generic_list &&
- }
+
+ }
>
}