diff --git a/src/apis/Log.js b/src/apis/Log.js
index 1d0261e..62254f8 100644
--- a/src/apis/Log.js
+++ b/src/apis/Log.js
@@ -22,8 +22,7 @@ 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
+ responseType: 'blob'
}).then(response => {
responseFileDownload(response, {
defaultFileName: 'businessLog'
diff --git a/src/assets/data/menuConfig.js b/src/assets/data/menuConfig.js
index 125c3f7..ccd1bba 100644
--- a/src/assets/data/menuConfig.js
+++ b/src/assets/data/menuConfig.js
@@ -204,16 +204,16 @@ export const menuConfig = {
view: true,
authLevel: adminAuthLevel.NONE
},
- // menubanner: {
- // title: '메뉴 배너 관리',
- // permissions: {
- // read: authType.menuBannerRead,
- // update: authType.menuBannerUpdate,
- // delete: authType.menuBannerDelete
- // },
- // view: true,
- // authLevel: adminAuthLevel.NONE
- // },
+ menubanner: {
+ title: '메뉴 배너 관리',
+ permissions: {
+ read: authType.menuBannerRead,
+ update: authType.menuBannerUpdate,
+ delete: authType.menuBannerDelete
+ },
+ view: true,
+ authLevel: adminAuthLevel.NONE
+ },
}
}
};
\ No newline at end of file
diff --git a/src/assets/data/options.js b/src/assets/data/options.js
index 02e2558..f2ebabf 100644
--- a/src/assets/data/options.js
+++ b/src/assets/data/options.js
@@ -11,7 +11,7 @@ export const TabGameLogList = [
];
export const TabEconomicIndexList = [
- { value: 'CURRENCY', name: '재화' },
+ { value: 'CURRENCY', name: '재화(유저)' },
// { value: 'ITEM', name: '아이템' },
// { value: 'VBP', name: 'VBP' },
// { value: 'deco', name: '의상/타투' },
diff --git a/src/assets/data/types.js b/src/assets/data/types.js
index b4d6af3..3ea799a 100644
--- a/src/assets/data/types.js
+++ b/src/assets/data/types.js
@@ -158,4 +158,10 @@ export const currencyCodeTypes = {
sapphire: "19010002",
ruby: "19010005",
calium: "19010003"
-}
\ No newline at end of file
+}
+
+export const languageNames = {
+ 'KO': '한국어',
+ 'EN': '영어',
+ 'JA': '일본어',
+};
diff --git a/src/components/common/Layout/DetailGrid.js b/src/components/common/Layout/DetailGrid.js
new file mode 100644
index 0000000..baca099
--- /dev/null
+++ b/src/components/common/Layout/DetailGrid.js
@@ -0,0 +1,301 @@
+import React from 'react';
+import { Row, Col, Form, Input, Select, DatePicker, TimePicker, InputNumber, Switch, Button, Checkbox } from 'antd';
+import styled from 'styled-components';
+import dayjs from 'dayjs';
+const { RangePicker } = DatePicker;
+
+/**
+ * 위치 지정 가능한 그리드 형태 상세 정보 표시 컴포넌트
+ * @param {Array} items - 표시할 항목 배열 (row, col, rowSpan, colSpan 속성 추가)
+ * @param {Object} formData - 폼 데이터 객체
+ * @param {Function} onChange - 값 변경 시 호출할 함수
+ * @param {boolean} disabled - 전체 비활성화 여부
+ * @param {number} columns - 그리드의 총 컬럼 수 (기본값: 4)
+ */
+const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }) => {
+ // 항목을 행과 열 위치별로 그룹화
+ const positionedItems = {};
+
+ // 각 항목의 위치 및 span 정보 처리
+ items.forEach(item => {
+ const rowIndex = item.row || 0;
+ const colIndex = item.col || 0;
+
+ if (!positionedItems[rowIndex]) {
+ positionedItems[rowIndex] = {};
+ }
+
+ positionedItems[rowIndex][colIndex] = {
+ ...item,
+ rowSpan: item.rowSpan || 1,
+ colSpan: item.colSpan || 1
+ };
+ });
+
+ // 행 번호 목록 (정렬)
+ const rows = Object.keys(positionedItems).map(Number).sort((a, b) => a - b);
+
+ // 항목에 따른 컴포넌트 렌더링 함수
+ const renderComponent = (item) => {
+ const {
+ type,
+ key,
+ keys,
+ label,
+ value,
+ options,
+ placeholder,
+ disabled: itemDisabled,
+ width,
+ handler,
+ min,
+ max,
+ format,
+ required,
+ showTime
+ } = item;
+
+ // 현재 값 가져오기 (formData에서 또는 항목에서)
+ const currentValue = formData[key] !== undefined ? formData[key] : value;
+
+ // 컴포넌트 공통 속성
+ const commonProps = {
+ id: key,
+ disabled: disabled || itemDisabled,
+ style: width ? { width, fontSize: '15px' } : { width: '100%', fontSize: '15px' }
+ };
+
+ // 항목 타입에 따른 컴포넌트 렌더링
+ switch (type) {
+ case 'text':
+ return onChange(key, e.target.value, handler)}
+ placeholder={placeholder || `${label} 입력`}
+ />;
+
+ case 'number':
+ return onChange(key, value, handler)}
+ placeholder={placeholder || `${label} 입력`}
+ />;
+
+ case 'select':
+ return (
+
+ );
+
+ case 'date':
+ return (
+ onChange(key, date, handler)}
+ placeholder={placeholder || `${label} 선택`}
+ />
+ );
+
+ case 'dateRange':
+ return (
+ {
+ if (dates) {
+ // 두 개의 별도 필드에 각각 업데이트
+ if (item.keys) {
+ onChange(item.keys.start, dates[0], handler);
+ onChange(item.keys.end, dates[1], handler);
+ } else {
+ // 기존 방식 지원 (하위 호환성)
+ onChange(key, {
+ start: dates[0],
+ end: dates[1]
+ }, handler);
+ }
+ } else {
+ // 두 필드 모두 비우기
+ if (item.keys) {
+ onChange(item.keys.start, null, handler);
+ onChange(item.keys.end, null, handler);
+ } else {
+ onChange(key, { start: null, end: null }, handler);
+ }
+ }
+ }}
+ placeholder={[
+ item.startLabel || '시작 일시',
+ item.endLabel || '종료 일시'
+ ]}
+ />
+ );
+
+ case 'time':
+ return (
+ onChange(key, time, handler)}
+ placeholder={placeholder || `${label} 선택`}
+ />
+ );
+
+ case 'switch':
+ return (
+ onChange(key, checked, handler)}
+ />
+ );
+
+ case 'checkbox':
+ return (
+ onChange(key, e.target.checked, handler)}
+ >
+ {item.checkboxLabel}
+
+ );
+
+ case 'status':
+ return ;
+
+ case 'tab':
+ return
+
+ case 'custom':
+ return item.render ? item.render(formData, onChange) : null;
+
+ default:
+ return {currentValue}
;
+ }
+ };
+
+ // 각 셀의 폭 계산 (Ant Design의 24-컬럼 시스템 기준)
+ const colWidth = 24 / columns;
+
+ return (
+
+
+
+ );
+};
+
+// 상태 표시 컴포넌트
+const StatusDisplay = ({ status }) => {
+ let color = '';
+ let text = '';
+
+ switch (status) {
+ case 'wait':
+ color = '#faad14';
+ text = '대기';
+ break;
+ case 'running':
+ color = '#52c41a';
+ text = '진행중';
+ break;
+ case 'finish':
+ color = '#d9d9d9';
+ text = '만료';
+ break;
+ case 'fail':
+ color = '#ff4d4f';
+ text = '실패';
+ break;
+ case 'delete':
+ color = '#ff4d4f';
+ text = '삭제';
+ break;
+ default:
+ color = '#1890ff';
+ text = status;
+ }
+
+ return (
+
+ {text}
+
+ );
+};
+
+const GridContainer = styled.div`
+ width: 100%;
+ padding: 16px 0;
+ font-size: 15px;
+`;
+
+const StatusTag = styled.div`
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ background-color: ${props => props.color};
+ color: white;
+ font-size: 15px;
+ font-weight: 500;
+`;
+
+export default DetailGrid;
\ No newline at end of file
diff --git a/src/components/common/Layout/DetailLayout.js b/src/components/common/Layout/DetailLayout.js
new file mode 100644
index 0000000..a3107d2
--- /dev/null
+++ b/src/components/common/Layout/DetailLayout.js
@@ -0,0 +1,127 @@
+import React from 'react';
+import styled from 'styled-components';
+import { Card } from 'antd';
+import DetailGrid from './DetailGrid';
+
+/**
+ * Ant Design 방식의 DetailModalWrapper 컴포넌트
+ * @param {Array} itemGroups - 표시할 항목 그룹 배열
+ * @param {Object} formData - 폼 데이터 객체
+ * @param {Function} onChange - 값 변경 시 호출할 함수
+ * @param {boolean} disabled - 전체 비활성화 여부
+ * @param {number} columnCount - 한 행에 표시할 컬럼 수 (기본값: 4)
+ * @param {ReactNode} children - 추가 컨텐츠
+ */
+const DetailLayout = ({
+ itemGroups,
+ formData,
+ onChange,
+ disabled = false,
+ columnCount = 4,
+ children
+ }) => {
+ // 값 변경 핸들러
+ const handleChange = (key, value, handler) => {
+ // 핸들러가 있으면 핸들러 실행
+ if (handler) {
+ handler(value, key, formData);
+ }
+
+ // 키가 점 표기법이면 중첩 객체 업데이트
+ if (key.includes('.')) {
+ const [parentKey, childKey] = key.split('.');
+ onChange({
+ ...formData,
+ [parentKey]: {
+ ...formData[parentKey],
+ [childKey]: value
+ }
+ });
+ } else {
+ // 일반 키는 직접 업데이트
+ onChange({ ...formData, [key]: value });
+ }
+ };
+
+ return (
+
+ {itemGroups.map((group, index) => (
+
+
+
+ ))}
+ {children}
+
+ );
+};
+
+const DetailWrapper = styled.div`
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+`;
+
+export default DetailLayout;
+
+//예시
+// const itemGroupsExample = [
+// {
+// title: '기본 정보',
+// items: [
+// {
+// row: 0,
+// col: 0,
+// colSpan: 2,
+// type: 'text',
+// key: 'title',
+// label: '제목',
+// required: true,
+// disabled: !isView('title'),
+// width: '300px',
+// },
+// {
+// row: 0,
+// col: 2,
+// colSpan: 2,
+// type: 'number',
+// key: 'order_id',
+// label: '순서',
+// required: true,
+// disabled: !isView('order_id'),
+// width: '200px',
+// min: 1,
+// },
+// {
+// row: 1,
+// col: 0,
+// colSpan: 2,
+// type: 'status',
+// key: 'status',
+// label: '상태',
+// value: resultData.status,
+// },
+// {
+// row: 1,
+// col: 2,
+// colSpan: 2,
+// type: 'switch',
+// key: 'is_link',
+// label: '링크 사용',
+// disabled: !isView('is_link'),
+// },
+// ]
+// },
+// {
+// title: ''
+// }
+// ];
\ No newline at end of file
diff --git a/src/components/common/button/AntButton.js b/src/components/common/button/AntButton.js
new file mode 100644
index 0000000..a524527
--- /dev/null
+++ b/src/components/common/button/AntButton.js
@@ -0,0 +1,84 @@
+import React from 'react';
+import { Button as AntBtn} from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+
+/**
+ * Ant Design의 Button을 사용한 버튼 컴포넌트
+ * @param {string} text - 버튼 텍스트
+ * @param {string} type - 버튼 타입 (primary, default, dashed, link, text)
+ * @param {function} onClick - 클릭 이벤트 핸들러
+ * @param {string} theme - 커스텀 테마 (primary, line, disable, reset, gray, search, find)
+ * @param {boolean} disabled - 비활성화 여부
+ * @param {string} name - 버튼 이름
+ * @param {string} width - 버튼 너비
+ * @param {string} height - 버튼 높이
+ */
+const AntButton = ({
+ text,
+ type = 'button',
+ onClick,
+ theme,
+ disabled,
+ name,
+ width,
+ height,
+ ...props
+ }) => {
+ // theme에 따른 Ant Design 버튼 타입 설정
+ let buttonType = 'default';
+ let shape = 'default';
+ let icon = '';
+ let color = 'default';
+ let variant = 'outlined';
+ let buttonProps = {
+ disabled,
+ onClick,
+ name,
+ style: {
+ width: width || 'auto',
+ height: height || '35px',
+ minWidth: 'fit-content',
+ fontSize: '15px',
+ },
+ ...props
+ };
+
+ // 테마에 따른 스타일 설정
+ switch (theme) {
+ case 'primary':
+ case 'submit':
+ case 'line':
+ buttonType = 'default';
+ break;
+ case 'disable':
+ buttonProps.disabled = true;
+ break;
+ case 'gray':
+ break;
+ case 'search':
+ case 'find':
+ icon = ;
+ break;
+ case 'cancel':
+ color = 'danger';
+ break;
+ default:
+ break;
+ }
+
+ return (
+
+ {text}
+
+ );
+};
+
+export default AntButton;
\ No newline at end of file
diff --git a/src/components/common/button/ExcelExportButton.js b/src/components/common/button/ExcelExportButton.js
index 9272d3e..a004982 100644
--- a/src/components/common/button/ExcelExportButton.js
+++ b/src/components/common/button/ExcelExportButton.js
@@ -34,13 +34,19 @@ const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx',
// 100% 완료 시 폴링 중지
if (percentage >= 100) {
+ // console.log("pollProgress data exists polling stop");
clearInterval(intervalRef.current);
intervalRef.current = null;
}
} else {
// 진행률 정보가 없으면 폴링 중지
+ console.log("pollProgress data not exists polling stop");
clearInterval(intervalRef.current);
intervalRef.current = null;
+
+ if (onLoadingChange) {
+ onLoadingChange({ loading: false, progress: 0 });
+ }
}
} catch (error) {
console.error('Progress polling error:', error);
@@ -61,7 +67,13 @@ const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx',
useEffect(() => {
return () => {
if (intervalRef.current) {
+ console.log("unmount polling stop");
+ onLoadingChange({ loading: false, progress: 0 });
clearInterval(intervalRef.current);
+
+ if (onLoadingChange) {
+ onLoadingChange({ loading: false, progress: 0 });
+ }
}
};
}, []);
@@ -77,7 +89,6 @@ const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx',
const taskId = `excel_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
taskIdRef.current = taskId;
-
setIsDownloading(true);
if (onLoadingChange) onLoadingChange({loading: true, progress: 0});
@@ -118,10 +129,10 @@ const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx',
setTimeout(() => {
setIsDownloading(false);
- if (onLoadingChange) {
- onLoadingChange({ loading: false, progress: 100 });
- showToast('DOWNLOAD_COMPLETE', { type: alertTypes.success });
- }
+ // console.log("handleDownload finally polling stop");
+
+ onLoadingChange({ loading: false, progress: 100 });
+ showToast('DOWNLOAD_COMPLETE', { type: alertTypes.success });
// 폴링 완전 중지
if (intervalRef.current) {
@@ -132,7 +143,7 @@ const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx',
// const end = Date.now();
// showToast(`처리 시간: ${end - start}ms`, { type: alertTypes.info });
// console.log(`처리 시간: ${end - start}ms`);
- }, 2000);
+ }, 1000);
}
}, [params, isDownloading, onLoadingChange, dataSize, pollProgress, showToast]);
diff --git a/src/components/common/button/TopButton.js b/src/components/common/button/TopButton.js
index 2b4ab90..1ec9035 100644
--- a/src/components/common/button/TopButton.js
+++ b/src/components/common/button/TopButton.js
@@ -1,12 +1,13 @@
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
+import { VerticalAlignTopOutlined } from '@ant-design/icons';
const Button = styled.button`
position: fixed;
bottom: 30px;
right: 30px;
- width: 50px;
- height: 50px;
+ width: 40px;
+ height: 40px;
border-radius: 50%;
background-color: #666666;
color: white;
@@ -54,7 +55,7 @@ const TopButton = () => {
onClick={scrollToTop}
title="맨 위로 이동"
>
- ↑
+
);
};
diff --git a/src/components/common/control/AnimatedTabs.js b/src/components/common/control/AnimatedTabs.js
new file mode 100644
index 0000000..5dcaef9
--- /dev/null
+++ b/src/components/common/control/AnimatedTabs.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import { Tabs } from 'antd';
+import styled from 'styled-components';
+import { motion, AnimatePresence } from 'framer-motion';
+
+// 통합된 애니메이션 탭 컴포넌트
+const AnimatedTabs = ({ items, activeKey, onChange }) => {
+ return (
+
+ {items.map(item => (
+
+
+
+ {item.children}
+
+
+
+ ))}
+
+ );
+};
+
+const StyledTabs = styled(Tabs)`
+ margin-top: 20px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .ant-tabs-nav {
+ margin-bottom: 16px;
+ width: 80%;
+ }
+
+ .ant-tabs-nav-wrap {
+ justify-content: center;
+ }
+
+ .ant-tabs-tab {
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s;
+ }
+
+ .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
+ color: #1890ff;
+ font-weight: 600;
+ }
+
+ .ant-tabs-ink-bar {
+ background-color: #1890ff;
+ }
+
+ .ant-tabs-content-holder {
+ width: 100%;
+ overflow: hidden;
+ }
+`;
+
+export default AnimatedTabs;
diff --git a/src/components/common/index.js b/src/components/common/index.js
index d198906..595585c 100644
--- a/src/components/common/index.js
+++ b/src/components/common/index.js
@@ -19,6 +19,8 @@ import Loading from './Loading';
import DownloadProgress from './DownloadProgress';
import CDivider from './CDivider';
import TopButton from './button/TopButton';
+import AntButton from './button/AntButton';
+import DetailLayout from './Layout/DetailLayout';
import CaliTable from './Custom/CaliTable'
@@ -36,6 +38,7 @@ export { DateTimeInput,
CheckBox,
Radio,
Button,
+ AntButton,
ExcelDownButton,
AuthModal,
CompletedModal,
@@ -52,5 +55,6 @@ export { DateTimeInput,
DynamoPagination,
FrontPagination,
DownloadProgress,
- CaliTable
+ CaliTable,
+ DetailLayout
};
\ No newline at end of file
diff --git a/src/components/common/modal/Modal.js b/src/components/common/modal/Modal.js
index 349be6e..7ef1f9e 100644
--- a/src/components/common/modal/Modal.js
+++ b/src/components/common/modal/Modal.js
@@ -1,6 +1,34 @@
import styled from 'styled-components';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Modal as AntModal } from 'antd';
-const ModalBg = styled.div`
+// const ModalBg = styled.div`
+// position: fixed;
+// background: ${props => props.$bgcolor || 'rgba(0, 0, 0, 0.5)'};
+// width: 100%;
+// height: 100%;
+// top: 0;
+// left: 0;
+// min-width: 1080px;
+// display: ${props => (props.$view === 'hidden' ? 'none' : 'block')};
+// z-index: 20;
+// `;
+//
+// const ModalWrapper = styled.div`
+// position: absolute;
+// background: #fff;
+// left: 50%;
+// top: 50%;
+// transform: translate(-50%, -50%);
+// min-width: ${props => props.min || 'auto'};
+// padding: ${props => props.$padding || '30px'};
+// border-radius: 30px;
+// max-height: 90%;
+// overflow: auto;
+// box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
+// `;
+
+const ModalBg = styled(motion.div)`
position: fixed;
background: ${props => props.$bgcolor || 'rgba(0, 0, 0, 0.5)'};
width: 100%;
@@ -12,30 +40,70 @@ const ModalBg = styled.div`
z-index: 20;
`;
-const ModalWrapper = styled.div`
+const ModalContainer = styled.div`
position: absolute;
- background: #fff;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
- min-width: ${props => props.min || 'auto'};
- padding: ${props => props.$padding || '30px'};
- border-radius: 30px;
- max-height: 90%;
- overflow: auto;
- box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
+`;
+
+const ModalWrapper = styled(motion.div)`
+ background: #fff;
+ min-width: ${props => props.min || 'auto'};
+ padding: ${props => props.$padding || '30px'};
+ border-radius: 30px;
+ max-height: 90%;
+ overflow: auto;
+ box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
`;
const Modal = ({ children, $padding, min, $view, $bgcolor }) => {
+ const isVisible = $view !== 'hidden';
+
return (
<>
-
-
- {children}
-
-
+
+ {isVisible && (
+
+
+
+ {children}
+
+
+
+ )}
+
>
);
};
+// const Modal = ({ children, $padding, min, $view, $bgcolor }) => {
+// return (
+// <>
+//
+//
+// {children}
+//
+//
+// >
+// );
+// };
+
export default Modal;