antBUtton 생성
topButton 이미지 변경 엑셀 버튼 조정 tab 컨트론 생성 detailGrid, layout 생성 modal motion 적용
This commit is contained in:
@@ -22,8 +22,7 @@ export const BusinessLogExport = async (token, params) => {
|
|||||||
try {
|
try {
|
||||||
await Axios.post(`/api/v1/log/generic/excel-export`, params, {
|
await Axios.post(`/api/v1/log/generic/excel-export`, params, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
responseType: 'blob',
|
responseType: 'blob'
|
||||||
timeout: 300000
|
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
responseFileDownload(response, {
|
responseFileDownload(response, {
|
||||||
defaultFileName: 'businessLog'
|
defaultFileName: 'businessLog'
|
||||||
|
|||||||
@@ -204,16 +204,16 @@ export const menuConfig = {
|
|||||||
view: true,
|
view: true,
|
||||||
authLevel: adminAuthLevel.NONE
|
authLevel: adminAuthLevel.NONE
|
||||||
},
|
},
|
||||||
// menubanner: {
|
menubanner: {
|
||||||
// title: '메뉴 배너 관리',
|
title: '메뉴 배너 관리',
|
||||||
// permissions: {
|
permissions: {
|
||||||
// read: authType.menuBannerRead,
|
read: authType.menuBannerRead,
|
||||||
// update: authType.menuBannerUpdate,
|
update: authType.menuBannerUpdate,
|
||||||
// delete: authType.menuBannerDelete
|
delete: authType.menuBannerDelete
|
||||||
// },
|
},
|
||||||
// view: true,
|
view: true,
|
||||||
// authLevel: adminAuthLevel.NONE
|
authLevel: adminAuthLevel.NONE
|
||||||
// },
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -11,7 +11,7 @@ export const TabGameLogList = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const TabEconomicIndexList = [
|
export const TabEconomicIndexList = [
|
||||||
{ value: 'CURRENCY', name: '재화' },
|
{ value: 'CURRENCY', name: '재화(유저)' },
|
||||||
// { value: 'ITEM', name: '아이템' },
|
// { value: 'ITEM', name: '아이템' },
|
||||||
// { value: 'VBP', name: 'VBP' },
|
// { value: 'VBP', name: 'VBP' },
|
||||||
// { value: 'deco', name: '의상/타투' },
|
// { value: 'deco', name: '의상/타투' },
|
||||||
|
|||||||
@@ -158,4 +158,10 @@ export const currencyCodeTypes = {
|
|||||||
sapphire: "19010002",
|
sapphire: "19010002",
|
||||||
ruby: "19010005",
|
ruby: "19010005",
|
||||||
calium: "19010003"
|
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% 완료 시 폴링 중지
|
// 100% 완료 시 폴링 중지
|
||||||
if (percentage >= 100) {
|
if (percentage >= 100) {
|
||||||
|
// console.log("pollProgress data exists polling stop");
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 진행률 정보가 없으면 폴링 중지
|
// 진행률 정보가 없으면 폴링 중지
|
||||||
|
console.log("pollProgress data not exists polling stop");
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
|
|
||||||
|
if (onLoadingChange) {
|
||||||
|
onLoadingChange({ loading: false, progress: 0 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Progress polling error:', error);
|
console.error('Progress polling error:', error);
|
||||||
@@ -61,7 +67,13 @@ const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx',
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
|
console.log("unmount polling stop");
|
||||||
|
onLoadingChange({ loading: false, progress: 0 });
|
||||||
clearInterval(intervalRef.current);
|
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)}`;
|
const taskId = `excel_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||||
taskIdRef.current = taskId;
|
taskIdRef.current = taskId;
|
||||||
|
|
||||||
|
|
||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
if (onLoadingChange) onLoadingChange({loading: true, progress: 0});
|
if (onLoadingChange) onLoadingChange({loading: true, progress: 0});
|
||||||
|
|
||||||
@@ -118,10 +129,10 @@ const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx',
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsDownloading(false);
|
setIsDownloading(false);
|
||||||
|
|
||||||
if (onLoadingChange) {
|
// console.log("handleDownload finally polling stop");
|
||||||
onLoadingChange({ loading: false, progress: 100 });
|
|
||||||
showToast('DOWNLOAD_COMPLETE', { type: alertTypes.success });
|
onLoadingChange({ loading: false, progress: 100 });
|
||||||
}
|
showToast('DOWNLOAD_COMPLETE', { type: alertTypes.success });
|
||||||
|
|
||||||
// 폴링 완전 중지
|
// 폴링 완전 중지
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
@@ -132,7 +143,7 @@ const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx',
|
|||||||
// const end = Date.now();
|
// const end = Date.now();
|
||||||
// showToast(`처리 시간: ${end - start}ms`, { type: alertTypes.info });
|
// showToast(`처리 시간: ${end - start}ms`, { type: alertTypes.info });
|
||||||
// console.log(`처리 시간: ${end - start}ms`);
|
// console.log(`처리 시간: ${end - start}ms`);
|
||||||
}, 2000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}, [params, isDownloading, onLoadingChange, dataSize, pollProgress, showToast]);
|
}, [params, isDownloading, onLoadingChange, dataSize, pollProgress, showToast]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const Button = styled.button`
|
const Button = styled.button`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
right: 30px;
|
right: 30px;
|
||||||
width: 50px;
|
width: 40px;
|
||||||
height: 50px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #666666;
|
background-color: #666666;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -54,7 +55,7 @@ const TopButton = () => {
|
|||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
title="맨 위로 이동"
|
title="맨 위로 이동"
|
||||||
>
|
>
|
||||||
↑
|
<VerticalAlignTopOutlined />
|
||||||
</Button>
|
</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 DownloadProgress from './DownloadProgress';
|
||||||
import CDivider from './CDivider';
|
import CDivider from './CDivider';
|
||||||
import TopButton from './button/TopButton';
|
import TopButton from './button/TopButton';
|
||||||
|
import AntButton from './button/AntButton';
|
||||||
|
import DetailLayout from './Layout/DetailLayout';
|
||||||
|
|
||||||
import CaliTable from './Custom/CaliTable'
|
import CaliTable from './Custom/CaliTable'
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ export { DateTimeInput,
|
|||||||
CheckBox,
|
CheckBox,
|
||||||
Radio,
|
Radio,
|
||||||
Button,
|
Button,
|
||||||
|
AntButton,
|
||||||
ExcelDownButton,
|
ExcelDownButton,
|
||||||
AuthModal,
|
AuthModal,
|
||||||
CompletedModal,
|
CompletedModal,
|
||||||
@@ -52,5 +55,6 @@ export { DateTimeInput,
|
|||||||
DynamoPagination,
|
DynamoPagination,
|
||||||
FrontPagination,
|
FrontPagination,
|
||||||
DownloadProgress,
|
DownloadProgress,
|
||||||
CaliTable
|
CaliTable,
|
||||||
|
DetailLayout
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,34 @@
|
|||||||
import styled from 'styled-components';
|
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;
|
position: fixed;
|
||||||
background: ${props => props.$bgcolor || 'rgba(0, 0, 0, 0.5)'};
|
background: ${props => props.$bgcolor || 'rgba(0, 0, 0, 0.5)'};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -12,30 +40,70 @@ const ModalBg = styled.div`
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ModalWrapper = styled.div`
|
const ModalContainer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: #fff;
|
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
min-width: ${props => props.min || 'auto'};
|
`;
|
||||||
padding: ${props => props.$padding || '30px'};
|
|
||||||
border-radius: 30px;
|
const ModalWrapper = styled(motion.div)`
|
||||||
max-height: 90%;
|
background: #fff;
|
||||||
overflow: auto;
|
min-width: ${props => props.min || 'auto'};
|
||||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
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 Modal = ({ children, $padding, min, $view, $bgcolor }) => {
|
||||||
|
const isVisible = $view !== 'hidden';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalBg $view={$view} $bgcolor={$bgcolor}>
|
<AnimatePresence>
|
||||||
<ModalWrapper $padding={$padding} min={min}>
|
{isVisible && (
|
||||||
{children}
|
<ModalBg
|
||||||
</ModalWrapper>
|
$bgcolor={$bgcolor}
|
||||||
</ModalBg>
|
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;
|
export default Modal;
|
||||||
|
|||||||
Reference in New Issue
Block a user