toast 메시지 추가
alert 글로벌화 loading 글로벌화
This commit is contained in:
112
src/components/common/Custom/CaliTable.js
Normal file
112
src/components/common/Custom/CaliTable.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { DetailMessage, TableStyle, TableWrapper } from '../../../styles/Components';
|
||||
import { StatusLabel } from '../../../styles/ModuleComponents';
|
||||
import { Button, CheckBox } from '../index';
|
||||
import { convertKTC, getOptionsArray } from '../../../utils';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
const CaliTable = ({
|
||||
columns,
|
||||
data,
|
||||
selectedRows = [],
|
||||
onSelectRow,
|
||||
onAction,
|
||||
refProp
|
||||
}) => {
|
||||
|
||||
const renderCell = (column, item) => {
|
||||
const { type, id, option_name, format, action } = column;
|
||||
const value = item[id];
|
||||
|
||||
const options = getOptionsArray(option_name);
|
||||
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return value;
|
||||
|
||||
case 'date':
|
||||
return convertKTC(value);
|
||||
|
||||
case 'status':
|
||||
const statusOption = options.find(opt => opt.value === value);
|
||||
return (
|
||||
<StatusWapper>
|
||||
<StatusLabel $status={value}>
|
||||
{statusOption ? statusOption.name : value}
|
||||
</StatusLabel>
|
||||
</StatusWapper>
|
||||
);
|
||||
|
||||
case 'button':
|
||||
return (
|
||||
<Button
|
||||
theme="line"
|
||||
text={column.text || "액션"}
|
||||
handleClick={() => onAction(id, item)}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<CheckBox
|
||||
name={column.name || 'select'}
|
||||
id={item.id}
|
||||
setData={(e) => onSelectRow(e, item)}
|
||||
checked={selectedRows.some(row => row.id === item.id)}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'option':
|
||||
const dataOption = options.find(opt => opt.value === value);
|
||||
return (
|
||||
dataOption ? dataOption.name : value
|
||||
);
|
||||
|
||||
case "link":
|
||||
return (
|
||||
<DetailMessage onClick={() => onAction(action)}>
|
||||
{value.content.length > 20 ? value.content.slice(0, 20) + '...' : value.content || ''}
|
||||
</DetailMessage>
|
||||
);
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableWrapper>
|
||||
<TableStyle ref={refProp}>
|
||||
<caption></caption>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<th key={index} width={column.width || 'auto'}>
|
||||
{column.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((item, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{columns.map((column, colIndex) => (
|
||||
<td key={colIndex}>
|
||||
{renderCell(column, item)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</TableStyle>
|
||||
</TableWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaliTable;
|
||||
|
||||
const StatusWapper = styled.div`
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '../../../styles/ModuleComponents';
|
||||
import { HourList, MinuteList } from '../../../assets/data';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
import { alertTypes } from '../../../assets/data/types';
|
||||
|
||||
const DateTimeRangePicker = ({
|
||||
label,
|
||||
@@ -19,10 +21,11 @@ const DateTimeRangePicker = ({
|
||||
disabled,
|
||||
startLabel = '시작 일자',
|
||||
endLabel = '종료 일자',
|
||||
reset = false,
|
||||
setAlert
|
||||
reset = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { showToast } = useAlert();
|
||||
|
||||
const [startHour, setStartHour] = useState('00');
|
||||
const [startMin, setStartMin] = useState('00');
|
||||
const [endHour, setEndHour] = useState('00');
|
||||
@@ -64,7 +67,7 @@ const DateTimeRangePicker = ({
|
||||
newDate.setHours(parseInt(endHour), parseInt(endMin));
|
||||
|
||||
if (startDate && newDate < startDate) {
|
||||
setAlert(t('TIME_START_DIFF_END'));
|
||||
showToast('TIME_START_DIFF_END', {type: alertTypes.warning});
|
||||
newDate = new Date(startDate);
|
||||
}
|
||||
|
||||
@@ -99,7 +102,7 @@ const DateTimeRangePicker = ({
|
||||
}
|
||||
|
||||
if (startDate && newDate < startDate) {
|
||||
setAlert(t('TIME_START_DIFF_END'));
|
||||
showToast('TIME_START_DIFF_END', {type: alertTypes.warning});
|
||||
newDate = new Date(startDate)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { styled } from 'styled-components';
|
||||
import { TextInput, SelectInput, SearchBarAlert, BtnWrapper } from '../../../styles/Components';
|
||||
import Button from '../button/Button';
|
||||
|
||||
const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction, onReset, handleSubmit }) => {
|
||||
const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction, onReset, handleSubmit, isSearch = true }) => {
|
||||
return (
|
||||
<SearchbarStyle direction={direction}>
|
||||
<SearchRow>
|
||||
@@ -22,12 +22,14 @@ const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction,
|
||||
{filter}
|
||||
</SearchRow>
|
||||
)}
|
||||
<SearchRow>
|
||||
<BtnWrapper $gap="8px">
|
||||
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
|
||||
<Button theme="reset" handleClick={onReset} type="button" />
|
||||
</BtnWrapper>
|
||||
</SearchRow>
|
||||
{isSearch &&
|
||||
<SearchRow>
|
||||
<BtnWrapper $gap="8px">
|
||||
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
|
||||
<Button theme="reset" handleClick={onReset} type="button" />
|
||||
</BtnWrapper>
|
||||
</SearchRow>
|
||||
}
|
||||
</SearchbarStyle>
|
||||
);
|
||||
};
|
||||
|
||||
80
src/components/common/Table/TableHeader.js
Normal file
80
src/components/common/Table/TableHeader.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authList } from '../../../store/authList';
|
||||
import { authType } from '../../../assets/data';
|
||||
import { Button, ExcelDownButton, ViewTableInfo } from '../index';
|
||||
|
||||
const TableHeader = ({
|
||||
config,
|
||||
tableRef,
|
||||
total,
|
||||
total_all,
|
||||
handleOrderBy,
|
||||
handlePageSize,
|
||||
selectedRows = [],
|
||||
onAction,
|
||||
navigate
|
||||
}) => {
|
||||
const userInfo = useRecoilValue(authList);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleButtonClick = (button, e) => {
|
||||
e?.preventDefault();
|
||||
|
||||
if (button.action === 'navigate' && button.navigateTo && navigate) {
|
||||
navigate(button.navigateTo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onAction) {
|
||||
onAction(button.action, button.id);
|
||||
}
|
||||
};
|
||||
|
||||
const renderButton = (button, index) => {
|
||||
const hasAuth = button.requiredAuth ?
|
||||
userInfo.auth_list?.some(auth => auth.id === authType[button.requiredAuth]) :
|
||||
true;
|
||||
|
||||
if (!hasAuth) return null;
|
||||
|
||||
if (button.component === 'ExcelDownButton') {
|
||||
return (
|
||||
<ExcelDownButton
|
||||
key={index}
|
||||
tableRef={tableRef}
|
||||
fileName={button.props?.fileName ? t(button.props.fileName) : ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const buttonTheme = button.disableWhen === 'noSelection' && selectedRows.length === 0
|
||||
? 'disable'
|
||||
: button.theme;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
theme={buttonTheme}
|
||||
text={button.text}
|
||||
handleClick={(e) => handleButtonClick(button, e)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewTableInfo
|
||||
total={total}
|
||||
total_all={total_all}
|
||||
handleOrderBy={handleOrderBy}
|
||||
handlePageSize={handlePageSize}
|
||||
orderType={config.orderType}
|
||||
pageType={config.pageType}
|
||||
countType={config.countType}
|
||||
>
|
||||
{config.buttons.map(renderButton)}
|
||||
</ViewTableInfo>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableHeader;
|
||||
@@ -4,28 +4,28 @@ import {
|
||||
SelectInput,
|
||||
TableInfo,
|
||||
} from '../../../styles/Components';
|
||||
import { ViewTitleCountType } from '../../../assets/data';
|
||||
import { ORDER_OPTIONS, PAGE_SIZE_OPTIONS, ViewTitleCountType } from '../../../assets/data';
|
||||
import { TitleItem, TitleItemLabel, TitleItemValue } from '../../../styles/ModuleComponents';
|
||||
|
||||
const ViewTableInfo = ({children, total, total_all, orderType, handleOrderBy, pageType, handlePageSize, countType = ViewTitleCountType.total}) => {
|
||||
const ViewTableInfo = ({
|
||||
children,
|
||||
total,
|
||||
total_all,
|
||||
orderType = 'desc',
|
||||
handleOrderBy,
|
||||
pageType = 'default',
|
||||
handlePageSize,
|
||||
countType = ViewTitleCountType.total
|
||||
}) => {
|
||||
return (
|
||||
<TableInfo>
|
||||
{total !== undefined && total_all !== undefined &&
|
||||
<ListCount>
|
||||
{ countType === ViewTitleCountType.total && `총 : ${total ?? 0} 건 / ${total_all ?? 0} 건`}
|
||||
{ countType === ViewTitleCountType.calium &&
|
||||
<>
|
||||
<TitleItem>
|
||||
<TitleItemLabel>누적 충전</TitleItemLabel>
|
||||
<TitleItemValue color='#b7e0c3' fontWeight='bold'>{total_all ?? 0}</TitleItemValue>
|
||||
</TitleItem>
|
||||
<TitleItem>
|
||||
<TitleItemLabel>잔여 수량</TitleItemLabel>
|
||||
<TitleItemValue color='#B39063' fontWeight='bold'>{total ?? 0}</TitleItemValue>
|
||||
</TitleItem>
|
||||
</>
|
||||
}
|
||||
</ListCount>}
|
||||
<ListCount>
|
||||
{COUNT_TYPE_RENDERERS[countType] ?
|
||||
COUNT_TYPE_RENDERERS[countType](total, total_all) :
|
||||
COUNT_TYPE_RENDERERS[ViewTitleCountType.total](total, total_all)}
|
||||
</ListCount>
|
||||
}
|
||||
<ListOption>
|
||||
<OrderBySelect orderType={orderType} handleOrderBy={handleOrderBy} />
|
||||
<PageSelect pageType={pageType} handlePageSize={handlePageSize} />
|
||||
@@ -35,36 +35,44 @@ const ViewTableInfo = ({children, total, total_all, orderType, handleOrderBy, pa
|
||||
);
|
||||
};
|
||||
|
||||
const OrderBySelect = ({orderType, handleOrderBy}) => {
|
||||
return(
|
||||
orderType === "asc" ?
|
||||
<SelectInput className="input-select" onChange={e => handleOrderBy(e.target.value)}>
|
||||
<option value="ASC">오름차순</option>
|
||||
<option value="DESC">내림차순</option>
|
||||
</SelectInput>
|
||||
:
|
||||
<SelectInput className="input-select" onChange={e => handleOrderBy(e.target.value)}>
|
||||
<option value="DESC">내림차순</option>
|
||||
<option value="ASC">오름차순</option>
|
||||
</SelectInput>
|
||||
);
|
||||
}
|
||||
const COUNT_TYPE_RENDERERS = {
|
||||
[ViewTitleCountType.total]: (total, total_all) => `총 : ${total ?? 0} 건 / ${total_all ?? 0} 건`,
|
||||
[ViewTitleCountType.calium]: (total, total_all) => (
|
||||
<>
|
||||
<TitleItem>
|
||||
<TitleItemLabel>누적 충전</TitleItemLabel>
|
||||
<TitleItemValue color='#b7e0c3' fontWeight='bold'>{total_all ?? 0}</TitleItemValue>
|
||||
</TitleItem>
|
||||
<TitleItem>
|
||||
<TitleItemLabel>잔여 수량</TitleItemLabel>
|
||||
<TitleItemValue color='#B39063' fontWeight='bold'>{total ?? 0}</TitleItemValue>
|
||||
</TitleItem>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
const PageSelect = ({pageType, handlePageSize}) => {
|
||||
return(
|
||||
pageType === "B" ?
|
||||
<SelectInput name="" id="" className="input-select" onChange={e => handlePageSize(e.target.value)}>
|
||||
<option value="500">500개</option>
|
||||
<option value="1000">1000개</option>
|
||||
<option value="5000">5000개</option>
|
||||
<option value="10000">10000개</option>
|
||||
</SelectInput>
|
||||
:
|
||||
<SelectInput name="" id="" className="input-select" onChange={e => handlePageSize(e.target.value)}>
|
||||
<option value="50">50개</option>
|
||||
<option value="100">100개</option>
|
||||
</SelectInput>
|
||||
const OrderBySelect = ({ orderType, handleOrderBy }) => {
|
||||
const options = ORDER_OPTIONS[orderType] || ORDER_OPTIONS.desc;
|
||||
|
||||
return (
|
||||
<SelectInput className="input-select" onChange={e => handleOrderBy(e.target.value)}>
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const PageSelect = ({ pageType, handlePageSize }) => {
|
||||
const options = PAGE_SIZE_OPTIONS[pageType] || PAGE_SIZE_OPTIONS.default;
|
||||
|
||||
return (
|
||||
<SelectInput name="" id="" className="input-select" onChange={e => handlePageSize(e.target.value)}>
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewTableInfo;
|
||||
156
src/components/common/alert/ToastAlert.js
Normal file
156
src/components/common/alert/ToastAlert.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled, { keyframes, css, createGlobalStyle } from 'styled-components';
|
||||
import { alertTypes } from '../../../assets/data/types';
|
||||
|
||||
const ToastAlert = ({ id, message, type = alertTypes.info, position = 'top-center', onClose }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(true);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContainer $type={type} $position={position} $isVisible={isVisible}>
|
||||
<IconWrapper $type={type}>
|
||||
<ToastIcon type={type} />
|
||||
</IconWrapper>
|
||||
<ToastMessage>{message}</ToastMessage>
|
||||
<CloseButton onClick={handleClose}>×</CloseButton>
|
||||
</ToastContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const ToastIcon = ({ type }) => {
|
||||
switch (type) {
|
||||
case alertTypes.success:
|
||||
return <span>✓</span>;
|
||||
case alertTypes.error:
|
||||
return <span>✗</span>;
|
||||
case alertTypes.warning:
|
||||
return <span>⚠</span>;
|
||||
case alertTypes.info:
|
||||
default:
|
||||
return <span>ℹ</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
`;
|
||||
|
||||
const fadeOut = keyframes`
|
||||
from { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
to { opacity: 0; transform: translateX(-50%) translateY(-20px); }
|
||||
`;
|
||||
|
||||
// 위치에 따른 스타일 지정 함수
|
||||
const getPositionStyle = (position) => {
|
||||
const positions = {
|
||||
'top-left': css`
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
`,
|
||||
'top-center': css`
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
`,
|
||||
'top-right': css`
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
`,
|
||||
'bottom-left': css`
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
`,
|
||||
'bottom-center': css`
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
`,
|
||||
'bottom-right': css`
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
`
|
||||
};
|
||||
|
||||
return positions[position] || positions['top-center'];
|
||||
};
|
||||
|
||||
// 타입에 따른 스타일 지정 함수
|
||||
const getTypeStyle = (type) => {
|
||||
const types = {
|
||||
[alertTypes.success]: css`
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border-color: #c3e6cb;
|
||||
`,
|
||||
[alertTypes.error]: css`
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border-color: #f5c6cb;
|
||||
`,
|
||||
[alertTypes.warning]: css`
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border-color: #ffeeba;
|
||||
`,
|
||||
[alertTypes.info]: css`
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border-color: #bee5eb;
|
||||
`
|
||||
};
|
||||
|
||||
return types[type] || types['info'];
|
||||
};
|
||||
|
||||
const ToastContainer = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 250px;
|
||||
max-width: 450px;
|
||||
padding: 12px 15px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 10px;
|
||||
z-index: 9999;
|
||||
|
||||
animation: ${props => props.$isExiting ? fadeOut : fadeIn} 0.3s ease forwards;
|
||||
|
||||
${props => getPositionStyle(props.$position)}
|
||||
${props => getTypeStyle(props.$type)}
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
const ToastMessage = styled.div`
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default ToastAlert;
|
||||
@@ -14,10 +14,14 @@ import Pagination from './Pagination/Pagination';
|
||||
import DynamoPagination from './Pagination/DynamoPagination';
|
||||
import FrontPagination from './Pagination/FrontPagination';
|
||||
import ViewTableInfo from './Table/ViewTableInfo';
|
||||
import TableHeader from './Table/TableHeader';
|
||||
import Loading from './Loading';
|
||||
import DownloadProgress from './DownloadProgress';
|
||||
import CDivider from './CDivider';
|
||||
import TopButton from './button/TopButton';
|
||||
|
||||
import CaliTable from './Custom/CaliTable'
|
||||
|
||||
export {
|
||||
DatePickerComponent,
|
||||
DateTimeRangePicker,
|
||||
@@ -41,10 +45,12 @@ export { DateTimeInput,
|
||||
Modal,
|
||||
Pagination,
|
||||
ViewTableInfo,
|
||||
TableHeader,
|
||||
Loading,
|
||||
CDivider,
|
||||
TopButton,
|
||||
DynamoPagination,
|
||||
FrontPagination,
|
||||
DownloadProgress
|
||||
DownloadProgress,
|
||||
CaliTable
|
||||
};
|
||||
@@ -53,10 +53,6 @@ const DynamicModal = ({modalType, view, handleSubmit, handleCancel, modalText, c
|
||||
);
|
||||
case modalTypes.childOkCancel:
|
||||
return (
|
||||
// <ModalWrapper view={view} modalText={modalText} handleCancel={handleCancel} children={children} >
|
||||
// <CancelButton handleClick={handleCancel} />
|
||||
// <OkButton handleClick={handleSubmit} />
|
||||
// </ModalWrapper>
|
||||
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={view}>
|
||||
<BtnWrapper $justify="flex-end">
|
||||
<ButtonClose onClick={handleCancel} />
|
||||
|
||||
Reference in New Issue
Block a user