antBUtton 생성
topButton 이미지 변경 엑셀 버튼 조정 tab 컨트론 생성 detailGrid, layout 생성 modal motion 적용
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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: '의상/타투' },
|
||||
|
||||
@@ -158,4 +158,10 @@ export const currencyCodeTypes = {
|
||||
sapphire: "19010002",
|
||||
ruby: "19010005",
|
||||
calium: "19010003"
|
||||
}
|
||||
}
|
||||
|
||||
export const languageNames = {
|
||||
'KO': '한국어',
|
||||
'EN': '영어',
|
||||
'JA': '일본어',
|
||||
};
|
||||
|
||||
301
src/components/common/Layout/DetailGrid.js
Normal file
301
src/components/common/Layout/DetailGrid.js
Normal file
@@ -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 <Input
|
||||
{...commonProps}
|
||||
value={currentValue}
|
||||
onChange={(e) => onChange(key, e.target.value, handler)}
|
||||
placeholder={placeholder || `${label} 입력`}
|
||||
/>;
|
||||
|
||||
case 'number':
|
||||
return <InputNumber
|
||||
{...commonProps}
|
||||
value={currentValue}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={(value) => onChange(key, value, handler)}
|
||||
placeholder={placeholder || `${label} 입력`}
|
||||
/>;
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
{...commonProps}
|
||||
value={currentValue}
|
||||
onChange={(value) => onChange(key, value, handler)}
|
||||
placeholder={placeholder || `${label} 선택`}
|
||||
>
|
||||
{options && options.map((option) => (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
|
||||
case 'date':
|
||||
return (
|
||||
<DatePicker
|
||||
{...commonProps}
|
||||
value={currentValue ? dayjs(currentValue) : null}
|
||||
format={format || 'YYYY-MM-DD'}
|
||||
onChange={(date) => onChange(key, date, handler)}
|
||||
placeholder={placeholder || `${label} 선택`}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'dateRange':
|
||||
return (
|
||||
<RangePicker
|
||||
{...commonProps}
|
||||
showTime={showTime !== false}
|
||||
value={keys ? [
|
||||
formData[keys.start] ? dayjs(formData[keys.start]) : null,
|
||||
formData[keys.end] ? dayjs(formData[keys.end]) : null
|
||||
] : (currentValue ? [
|
||||
currentValue.start ? dayjs(currentValue.start) : null,
|
||||
currentValue.end ? dayjs(currentValue.end) : null
|
||||
] : null)}
|
||||
format={format || 'YYYY-MM-DD HH:mm:ss'}
|
||||
onChange={(dates, dateStrings) => {
|
||||
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 (
|
||||
<TimePicker
|
||||
{...commonProps}
|
||||
value={currentValue ? dayjs(currentValue, 'HH:mm') : null}
|
||||
format={format || 'HH:mm'}
|
||||
onChange={(time) => onChange(key, time, handler)}
|
||||
placeholder={placeholder || `${label} 선택`}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'switch':
|
||||
return (
|
||||
<Switch
|
||||
checked={currentValue}
|
||||
onChange={(checked) => onChange(key, checked, handler)}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<Checkbox
|
||||
checked={currentValue}
|
||||
disabled={disabled || itemDisabled}
|
||||
onChange={(e) => onChange(key, e.target.checked, handler)}
|
||||
>
|
||||
{item.checkboxLabel}
|
||||
</Checkbox>
|
||||
);
|
||||
|
||||
case 'status':
|
||||
return <StatusDisplay status={currentValue} />;
|
||||
|
||||
case 'tab':
|
||||
return <AnimatedTabs
|
||||
items={tabItems}
|
||||
activeKey={activeLanguage}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
|
||||
case 'custom':
|
||||
return item.render ? item.render(formData, onChange) : null;
|
||||
|
||||
default:
|
||||
return <div>{currentValue}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
// 각 셀의 폭 계산 (Ant Design의 24-컬럼 시스템 기준)
|
||||
const colWidth = 24 / columns;
|
||||
|
||||
return (
|
||||
<GridContainer>
|
||||
<Form layout="horizontal">
|
||||
{rows.map(rowIndex => {
|
||||
const rowItems = positionedItems[rowIndex];
|
||||
const cols = Object.keys(rowItems).map(Number).sort((a, b) => a - b);
|
||||
|
||||
return (
|
||||
<Row key={`row-${rowIndex}`} gutter={[16, 16]}>
|
||||
{cols.map(colIndex => {
|
||||
const item = rowItems[colIndex];
|
||||
const itemColSpan = Math.min(item.colSpan * colWidth, 24);
|
||||
|
||||
return (
|
||||
<Col
|
||||
key={`${item.key}-${rowIndex}-${colIndex}`}
|
||||
span={itemColSpan}
|
||||
xs={24}
|
||||
sm={itemColSpan}
|
||||
>
|
||||
<Form.Item
|
||||
label={item.label}
|
||||
required={item.required}
|
||||
tooltip={item.tooltip}
|
||||
>
|
||||
{renderComponent(item)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</Form>
|
||||
</GridContainer>
|
||||
);
|
||||
};
|
||||
|
||||
// 상태 표시 컴포넌트
|
||||
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 (
|
||||
<StatusTag color={color}>
|
||||
{text}
|
||||
</StatusTag>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
127
src/components/common/Layout/DetailLayout.js
Normal file
127
src/components/common/Layout/DetailLayout.js
Normal file
@@ -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 (
|
||||
<DetailWrapper>
|
||||
{itemGroups.map((group, index) => (
|
||||
<Card
|
||||
key={`group-${index}`}
|
||||
title={group.title}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<DetailGrid
|
||||
items={group.items}
|
||||
formData={formData}
|
||||
onChange={handleChange}
|
||||
disabled={disabled || group.disabled}
|
||||
columnCount={group.columnCount || columnCount}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
{children}
|
||||
</DetailWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
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: ''
|
||||
// }
|
||||
// ];
|
||||
84
src/components/common/button/AntButton.js
Normal file
84
src/components/common/button/AntButton.js
Normal file
@@ -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 = <SearchOutlined />;
|
||||
break;
|
||||
case 'cancel':
|
||||
color = 'danger';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<AntBtn
|
||||
type={buttonType}
|
||||
shape={shape}
|
||||
icon={icon}
|
||||
variant={variant}
|
||||
color={color}
|
||||
htmlType={type === 'submit' ? 'submit' : 'button'}
|
||||
{...buttonProps}
|
||||
>
|
||||
{text}
|
||||
</AntBtn>
|
||||
);
|
||||
};
|
||||
|
||||
export default AntButton;
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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="맨 위로 이동"
|
||||
>
|
||||
↑
|
||||
<VerticalAlignTopOutlined />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
78
src/components/common/control/AnimatedTabs.js
Normal file
78
src/components/common/control/AnimatedTabs.js
Normal file
@@ -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 (
|
||||
<StyledTabs
|
||||
activeKey={activeKey}
|
||||
onChange={onChange}
|
||||
centered={true}
|
||||
>
|
||||
{items.map(item => (
|
||||
<Tabs.TabPane
|
||||
tab={item.label}
|
||||
key={item.key}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeKey}
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
{item.children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</StyledTabs>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<ModalBg $view={$view} $bgcolor={$bgcolor}>
|
||||
<ModalWrapper $padding={$padding} min={min}>
|
||||
{children}
|
||||
</ModalWrapper>
|
||||
</ModalBg>
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<ModalBg
|
||||
$bgcolor={$bgcolor}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<ModalContainer>
|
||||
<ModalWrapper
|
||||
$padding={$padding}
|
||||
min={min}
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ModalWrapper>
|
||||
</ModalContainer>
|
||||
</ModalBg>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// const Modal = ({ children, $padding, min, $view, $bgcolor }) => {
|
||||
// return (
|
||||
// <>
|
||||
// <ModalBg $view={$view} $bgcolor={$bgcolor}>
|
||||
// <ModalWrapper $padding={$padding} min={min}>
|
||||
// {children}
|
||||
// </ModalWrapper>
|
||||
// </ModalBg>
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default Modal;
|
||||
|
||||
Reference in New Issue
Block a user