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 && - } + + } }