toast 메시지 추가

alert 글로벌화
loading 글로벌화
This commit is contained in:
2025-04-25 15:33:21 +09:00
parent d2ac5b338e
commit 826459f304
50 changed files with 3211 additions and 2385 deletions

View 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;
`;

View File

@@ -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)
}

View File

@@ -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>
);
};

View 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;

View File

@@ -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;

View 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;

View File

@@ -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
};

View File

@@ -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} />