메뉴 배너 관리
This commit is contained in:
@@ -26,7 +26,7 @@ import {
|
||||
UserBlockRegist,
|
||||
WhiteList,
|
||||
LandAuction,
|
||||
BattleEvent
|
||||
BattleEvent, MenuBanner, MenuBannerRegist,
|
||||
} from './pages/ServiceManage';
|
||||
|
||||
const RouteInfo = () => {
|
||||
@@ -75,6 +75,8 @@ const RouteInfo = () => {
|
||||
<Route path="event/eventregist" element={<EventRegist />} />
|
||||
<Route path="landauction" element={<LandAuction />} />
|
||||
<Route path="battleevent" element={<BattleEvent />} />
|
||||
<Route path="menubanner" element={<MenuBanner />} />
|
||||
<Route path="menubanner/menubannerregist" element={<MenuBannerRegist />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
114
src/apis/Menu.js
Normal file
114
src/apis/Menu.js
Normal file
@@ -0,0 +1,114 @@
|
||||
//운영서비스 관리 - 메뉴배너 api 연결
|
||||
|
||||
import { Axios } from '../utils';
|
||||
|
||||
// 리스트 조회
|
||||
export const MenuBannerView = async (token, searchData, status, startDate, endDate, order, size, currentPage) => {
|
||||
try {
|
||||
const res = await Axios.get(
|
||||
`/api/v1/menu/banner/list?search_data=${searchData}&status=${status}&start_dt=${startDate}&end_dt=${endDate}
|
||||
&orderby=${order}&page_no=${currentPage}&page_size=${size}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
);
|
||||
|
||||
return res.data.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('MenuBannerView Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 상세보기
|
||||
export const MenuBannerDetailView = async (token, id) => {
|
||||
try {
|
||||
const res = await Axios.get(`/api/v1/menu/banner/detail/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
return res.data.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('MenuBannerDetailView Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 등록
|
||||
export const MenuBannerSingleRegist = async (token, params) => {
|
||||
try {
|
||||
const res = await Axios.post(`/api/v1/menu/banner`, params, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('MenuBannerSingleRegist Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 수정
|
||||
export const MenuBannerModify = async (token, id, params) => {
|
||||
try {
|
||||
const res = await Axios.put(`/api/v1/menu/banner/${id}`, params, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('MenuBannerModify Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제
|
||||
export const MenuBannerDelete = async (token, params) => {
|
||||
try {
|
||||
const res = await Axios.delete(`/api/v1/menu/banner/delete`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { list: params },
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('MenuBannerDelete Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const MenuImageUpload = async (token, file) => {
|
||||
const exelFile = new FormData();
|
||||
exelFile.append('file', file);
|
||||
try {
|
||||
const res = await Axios.post(`/api/v1/menu/image-upload`, exelFile, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('MenuImageUpload', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const MenuImageDelete = async (token, filename) => {
|
||||
try {
|
||||
const res = await Axios.get(`/api/v1/menu/image-delete?file=${filename}`, {
|
||||
headers: {Authorization: `Bearer ${token}`},
|
||||
});
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('MenuImageDelete', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -12,3 +12,5 @@ export * from './Item';
|
||||
export * from './Event';
|
||||
export * from './Calium';
|
||||
export * from './Land';
|
||||
export * from './Menu';
|
||||
export * from './OpenAI';
|
||||
|
||||
@@ -7,5 +7,6 @@ export const NONE = 'NONE';
|
||||
export const ONE_MINUTE_MS = 60000;
|
||||
export const ONE_MINUTE_SECOND = 60;
|
||||
export const AUCTION_MIN_MINUTE_TIME = 15; // 15분
|
||||
export const IMAGE_MAX_SIZE = 5242880;
|
||||
|
||||
export { INITIAL_PAGE_SIZE, INITIAL_CURRENT_PAGE, INITIAL_PAGE_LIMIT };
|
||||
|
||||
@@ -73,6 +73,10 @@ export const STATUS_STYLES = {
|
||||
background: '#FFB59B',
|
||||
color: 'white'
|
||||
},
|
||||
DELETE: {
|
||||
background: '#FFB59B',
|
||||
color: 'white'
|
||||
},
|
||||
RUNNING: {
|
||||
background: '#4287f5',
|
||||
color: 'white'
|
||||
|
||||
@@ -194,6 +194,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
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -278,6 +278,13 @@ export const opInputType = [
|
||||
{ value: 'Boolean', name: '부울' },
|
||||
];
|
||||
|
||||
export const opMenuBannerStatus = [
|
||||
{ value: 'ALL', name: '전체' },
|
||||
{ value: 'WAIT', name: '대기' },
|
||||
{ value: 'RUNNING', name: '진행중' },
|
||||
{ value: 'FINISH', name: '만료' },
|
||||
];
|
||||
|
||||
// export const logAction = [
|
||||
// { value: "None", name: "ALL" },
|
||||
// { value: "AIChatDeleteCharacter", name: "NPC 삭제" },
|
||||
|
||||
@@ -49,7 +49,9 @@ export const authType = {
|
||||
battleEventUpdate: 47,
|
||||
battleEventDelete: 48,
|
||||
businessLogRead: 49,
|
||||
|
||||
menuBannerRead: 50,
|
||||
menuBannerUpdate: 51,
|
||||
menuBannerDelete: 52,
|
||||
|
||||
|
||||
levelReader: 999,
|
||||
|
||||
53
src/components/common/button/VerticalDotsButton.js
Normal file
53
src/components/common/button/VerticalDotsButton.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const DotsButton = styled.button`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #f0f0f0;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 점 스타일링 */
|
||||
.dot {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background-color: #333;
|
||||
margin: 2px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const VerticalDotsButton = ({ text, type = 'button', errorMessage, handleClick, size, width, height, borderColor, disabled, name }) => {
|
||||
|
||||
return (
|
||||
<DotsButton
|
||||
onSubmit={e => e.preventDefault()}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
size={size}
|
||||
bordercolor={borderColor}
|
||||
width={width}
|
||||
height={height}
|
||||
name={name}
|
||||
>
|
||||
<div className="dot"></div>
|
||||
<div className="dot"></div>
|
||||
<div className="dot"></div>
|
||||
</DotsButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerticalDotsButton;
|
||||
11
src/i18n.js
11
src/i18n.js
@@ -41,6 +41,7 @@ const resources = {
|
||||
WARNING_NICKNAME_CHECK: '닉네임을 확인해주세요.',
|
||||
WARNING_EMAIL_CHECK: '이메일을 확인해주세요.',
|
||||
WARNING_TYPE_CHECK: '타입을 확인해주세요.',
|
||||
DATE_START_DIFF_END_WARNING :"종료일은 시작일보다 하루 이후여야 합니다.",
|
||||
//db
|
||||
LOG_MEMORY_LIMIT_WARNING: '데이터가 너무 많아 조회할 수 없습니다.\n조회조건 조정 후 다시 조회해주세요.',
|
||||
LOG_MONGGDB_QUERY_WARNING: '조회 중 오류가 발생하였습니다. 잠시 후 다시 한번 진행해 주세요.\n오류가 지속될 경우, 담당자에게 문의해주세요.',
|
||||
@@ -101,7 +102,6 @@ const resources = {
|
||||
SEARCH_LIMIT_FAIL: '인출 가능 수량 조회에 대한 요청 중 오류가 발생하였습니다. 잠시 후 다시 한번 진행해 주세요. 오류가 지속될 경우, 담당자에게 문의해주세요.',
|
||||
//전투시스템
|
||||
BATTLE_EVENT_MODAL_START_DT_WARNING: "시작 시간은 현재 시간으로부터 10분 이후부터 가능합니다.",
|
||||
BATTLE_EVENT_MODAL_START_DIFF_END_WARNING :"종료일은 시작일보다 하루 이후여야 합니다.",
|
||||
BATTLE_EVENT_MODAL_TIME_CHECK_WARNING :"해당 시간에 속하는 이벤트가 존재합니다.",
|
||||
BATTLE_EVENT_REGIST_CONFIRM: "이벤트를 등록하시겠습니까?",
|
||||
BATTLE_EVENT_UPDATE_CONFIRM: "이벤트를 수정하시겠습니까?",
|
||||
@@ -110,6 +110,15 @@ const resources = {
|
||||
BATTLE_EVENT_STOP_5MINUTES_DATE_WARNING: "이벤트 시작 5분 전에는 중단할 수 없습니다.",
|
||||
BATTLE_EVENT_STATUS_RUNNING_WARNING: "이벤트 진행중에는 중단할 수 없습니다.",
|
||||
BATTLE_EVENT_MODAL_STATUS_WARNING: "이벤트가 중단일때만 수정이 가능합니다.",
|
||||
//메뉴
|
||||
MENU_BANNER_REGIST_CONFIRM: "배너를 등록하시겠습니까?",
|
||||
MENU_BANNER_SELECT_DELETE: "선택된 배너를 삭제하시겠습니까?",
|
||||
MENU_BANNER_REGIST_CANCEL: "배너 등록을 취소하시겠습니까?\n\r취소 시 설정된 값은 반영되지 않습니다.",
|
||||
//파일
|
||||
FILE_IMAGE_EXTENSION_WARNING: "png, jpg 확장자의 이미지만 업로드 가능합니다.",
|
||||
FILE_IMAGE_UPLOAD_ERROR: "이미지 업로드 중 오류가 발생했습니다.",
|
||||
FILE_NOT_EXIT_ERROR: "유효하지 않은 파일입니다.",
|
||||
FILE_SIZE_OVER_ERROR: "파일의 사이즈가 5MB를 초과하였습니다.",
|
||||
//파일명칭
|
||||
FILE_INDEX_USER_CONTENT: 'Caliverse_User_Index.xlsx',
|
||||
FILE_CALIUM_REQUEST: 'Caliverse_Calium_Request.xlsx',
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { INITIAL_PAGE_LIMIT, INITIAL_PAGE_SIZE } from '../../assets/data/adminConstants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
DownloadProgress,
|
||||
DynamicModal,
|
||||
ExcelDownButton,
|
||||
@@ -34,6 +35,9 @@ import styled from 'styled-components';
|
||||
import FrontPagination from '../../components/common/Pagination/FrontPagination';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import CircularProgress from '../../components/common/CircularProgress';
|
||||
import VerticalDotsButton from '../../components/common/button/VerticalDotsButton';
|
||||
import MessageInput from '../../components/common/input/MessageInput';
|
||||
import { AnalyzeAI } from '../../apis';
|
||||
|
||||
const BusinessLogView = () => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
@@ -171,6 +175,15 @@ const BusinessLogView = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleMessage = (message) => {
|
||||
const params = {}
|
||||
params.message = message;
|
||||
params.type = 'BUSINESS_LOG'
|
||||
params.conditions = searchParams;
|
||||
AnalyzeAI(token, params);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>비즈니스 로그 조회</Title>
|
||||
@@ -204,6 +217,7 @@ const BusinessLogView = () => {
|
||||
</CircularProgressWrapper>
|
||||
)}
|
||||
</DownloadContainer>
|
||||
<MessageInput onSendMessage={handleMessage} />
|
||||
</ViewTableInfo>
|
||||
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
|
||||
<>
|
||||
|
||||
@@ -23,7 +23,14 @@ import ViewTableInfo from '../../components/common/Table/ViewTableInfo';
|
||||
import { convertKTC, timeDiffMinute } from '../../utils';
|
||||
import EventListSearchBar from '../../components/ServiceManage/searchBar/EventListSearchBar';
|
||||
import CustomConfirmModal from '../../components/common/modal/CustomConfirmModal';
|
||||
import { ModalInputItem, ModalSubText, RegistInputItem, RegistNotice, SubText } from '../../styles/ModuleComponents';
|
||||
import {
|
||||
ModalInputItem,
|
||||
ModalSubText,
|
||||
RegistInputItem,
|
||||
RegistNotice,
|
||||
StatusLabel, StatusWapper,
|
||||
SubText,
|
||||
} from '../../styles/ModuleComponents';
|
||||
|
||||
const Event = () => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
@@ -273,7 +280,11 @@ const Event = () => {
|
||||
<CheckBox name={'select'} id={event.id} setData={e => handleSelectCheckBox(e, event)} />
|
||||
</td>
|
||||
<td>{event.row_num}</td>
|
||||
<td>{eventStatus.map(data => data.value === event.status && data.name)}</td>
|
||||
<StatusWapper>
|
||||
<StatusLabel $status={event.status}>
|
||||
{eventStatus.map(data => data.value === event.status && data.name)}
|
||||
</StatusLabel>
|
||||
</StatusWapper>
|
||||
<td>{convertKTC(event.start_dt)}</td>
|
||||
<td>{convertKTC(event.end_dt)}</td>
|
||||
<MailTitle>{event.title}</MailTitle>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { authType, mailReceiveType, mailSendStatus, mailSendType, mailType } fro
|
||||
import AuthModal from '../../components/common/modal/AuthModal';
|
||||
import ViewTableInfo from '../../components/common/Table/ViewTableInfo';
|
||||
import { convertKTC} from '../../utils';
|
||||
import { StatusLabel, StatusWapper } from '../../styles/ModuleComponents';
|
||||
|
||||
const Mail = () => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
@@ -270,13 +271,11 @@ const Mail = () => {
|
||||
<td>{convertKTC(mail.create_dt)}</td>
|
||||
<td>{convertKTC(mail.send_dt)}</td>
|
||||
<td>{mailSendType.map(data => data.value === mail.send_type && data.name)}</td>
|
||||
<td>
|
||||
{mail.send_status === 'FAIL' ? (
|
||||
<ListState>{mailSendStatus.map(data => data.value === mail.send_status && data.name)}</ListState>
|
||||
) : (
|
||||
mailSendStatus.map(data => data.value === mail.send_status && data.name)
|
||||
)}
|
||||
</td>
|
||||
<StatusWapper>
|
||||
<StatusLabel $status={mail.send_status}>
|
||||
{mailSendStatus.map(data => data.value === mail.send_status && data.name)}
|
||||
</StatusLabel>
|
||||
</StatusWapper>
|
||||
<td>{mailType.map(data => data.value === mail.mail_type && data.name)}</td>
|
||||
<td>{mailReceiveType.map(data => data.value === mail.receive_type && data.name)}</td>
|
||||
<MailTitle>{mail.title}</MailTitle>
|
||||
|
||||
247
src/pages/ServiceManage/MenuBanner.js
Normal file
247
src/pages/ServiceManage/MenuBanner.js
Normal file
@@ -0,0 +1,247 @@
|
||||
import { useState, Fragment, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
import { authList } from '../../store/authList';
|
||||
import {
|
||||
authType,
|
||||
modalTypes,
|
||||
landAuctionStatusType, opYNType,
|
||||
} from '../../assets/data';
|
||||
import { Title, FormWrapper, TableStyle, TableWrapper} from '../../styles/Components';
|
||||
import {
|
||||
CheckBox,
|
||||
Button,
|
||||
DynamicModal,
|
||||
Pagination,
|
||||
ViewTableInfo,
|
||||
} from '../../components/common';
|
||||
import { convertKTC, timeDiffMinute } from '../../utils';
|
||||
import { INITIAL_PAGE_SIZE, INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
|
||||
import { useModal, useTable, withAuth } from '../../hooks/hook';
|
||||
import { StatusWapper, StatusLabel } from '../../styles/ModuleComponents';
|
||||
import { opMenuBannerStatus } from '../../assets/data/options';
|
||||
import MenuBannerSearchBar, { useMenuBannerSearch } from '../../components/ServiceManage/searchBar/MenuBannerSearchBar';
|
||||
import { MenuBannerDelete, MenuBannerDetailView } from '../../apis';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MenuBannerModal from '../../components/ServiceManage/modal/MenuBannerModal';
|
||||
|
||||
const MenuBanner = () => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
const userInfo = useRecoilValue(authList);
|
||||
const { t } = useTranslation();
|
||||
const tableRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [detailData, setDetailData] = useState({});
|
||||
|
||||
const {
|
||||
modalState,
|
||||
handleModalView,
|
||||
handleModalClose
|
||||
} = useModal({
|
||||
detail: 'hidden',
|
||||
deleteConfirm: 'hidden',
|
||||
deleteComplete: 'hidden'
|
||||
});
|
||||
const [alertMsg, setAlertMsg] = useState('');
|
||||
const [modalType, setModalType] = useState('regist');
|
||||
|
||||
const {
|
||||
searchParams,
|
||||
data: dataList,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
handleOrderByChange,
|
||||
updateSearchParams
|
||||
} = useMenuBannerSearch(token, INITIAL_PAGE_SIZE);
|
||||
|
||||
const {
|
||||
selectedRows,
|
||||
handleSelectRow,
|
||||
isRowSelected
|
||||
} = useTable(dataList?.event_list || [], {mode: 'single'});
|
||||
|
||||
|
||||
const handleModalSubmit = async (type, param = null) => {
|
||||
switch (type) {
|
||||
case "regist":
|
||||
setModalType('regist');
|
||||
handleModalView('detail');
|
||||
break;
|
||||
case "detail":
|
||||
await MenuBannerDetailView(token, param).then(data => {
|
||||
setDetailData(data.event_detail);
|
||||
setModalType('modify');
|
||||
handleModalView('detail');
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
const date_check = selectedRows.every(row => {
|
||||
const timeDiff = timeDiffMinute(convertKTC(row.auction_start_dt), (new Date));
|
||||
return timeDiff < 3;
|
||||
});
|
||||
if(date_check){
|
||||
setAlertMsg(t('LAND_AUCTION_DELETE_DATE_WARNING'));
|
||||
return;
|
||||
}
|
||||
if(selectedRows[0].status === landAuctionStatusType.auction_start || selectedRows[0].status === landAuctionStatusType.stl_end){
|
||||
setAlertMsg(t('LAND_AUCTION_DELETE_STATUS_WARNING'));
|
||||
return;
|
||||
}
|
||||
handleModalView('deleteConfirm');
|
||||
break;
|
||||
case "deleteConfirm":
|
||||
let list = [];
|
||||
let isChecked = false;
|
||||
|
||||
selectedRows.map(data => {
|
||||
// const row = dataList.list.find(row => row.id === Number(data.id));
|
||||
// if(row.status !== commonStatus.wait) isChecked = true;
|
||||
list.push({
|
||||
id: data.id,
|
||||
});
|
||||
});
|
||||
|
||||
if(isChecked) {
|
||||
setAlertMsg(t('LAND_AUCTION_WARNING_DELETE'))
|
||||
handleModalClose('deleteConfirm');
|
||||
return;
|
||||
}
|
||||
|
||||
await MenuBannerDelete(token, list).then(data => {
|
||||
handleModalClose('deleteConfirm');
|
||||
if(data.result === "SUCCESS") {
|
||||
handleModalView('deleteComplete');
|
||||
}else if(data.result === "ERROR_AUCTION_STATUS_IMPOSSIBLE"){
|
||||
setAlertMsg(t('LAND_AUCTION_ERROR_DELETE_STATUS'));
|
||||
}else{
|
||||
setAlertMsg(t('DELETE_FAIL'));
|
||||
}
|
||||
}).catch(reason => {
|
||||
setAlertMsg(t('API_FAIL'));
|
||||
});
|
||||
|
||||
break;
|
||||
case "deleteComplete":
|
||||
handleModalClose('deleteComplete');
|
||||
window.location.reload();
|
||||
break;
|
||||
case "warning":
|
||||
setAlertMsg('')
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>메뉴 배너 관리</Title>
|
||||
<FormWrapper>
|
||||
<MenuBannerSearchBar
|
||||
searchParams={searchParams}
|
||||
onSearch={(newParams, executeSearch = true) => {
|
||||
if (executeSearch) {
|
||||
handleSearch(newParams);
|
||||
} else {
|
||||
updateSearchParams(newParams);
|
||||
}
|
||||
}}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
</FormWrapper>
|
||||
<ViewTableInfo total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
|
||||
{userInfo.auth_list?.some(auth => auth.id === authType.battleEventDelete) && (
|
||||
<Button theme={selectedRows.length === 0 ? 'disable' : 'line'} text="선택 삭제" handleClick={() => handleModalSubmit('delete')} />
|
||||
)}
|
||||
{userInfo.auth_list?.some(auth => auth.id === authType.battleEventUpdate) && (
|
||||
<Button
|
||||
theme="primary"
|
||||
text="이미지 등록"
|
||||
type="button"
|
||||
handleClick={e => {
|
||||
e.preventDefault();
|
||||
navigate('/servicemanage/menubanner/menubannerregist');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ViewTableInfo>
|
||||
<TableWrapper>
|
||||
<TableStyle ref={tableRef}>
|
||||
<caption></caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="40"></th>
|
||||
<th width="70">번호</th>
|
||||
<th width="80">등록 상태</th>
|
||||
<th width="150">시작일(KST)</th>
|
||||
<th width="150">종료일(KST)</th>
|
||||
<th width="300">설명 제목</th>
|
||||
<th width="90">링크여부</th>
|
||||
<th width="100">상세보기</th>
|
||||
<th width="150">히스토리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dataList?.list?.map(banner => (
|
||||
<tr key={banner.row_num}>
|
||||
<td>
|
||||
<CheckBox name={'select'} id={banner.id}
|
||||
setData={(e) => handleSelectRow(e, banner)}
|
||||
checked={isRowSelected(banner.id)} />
|
||||
</td>
|
||||
<td>{banner.row_num}</td>
|
||||
<StatusWapper>
|
||||
<StatusLabel $status={banner.status}>
|
||||
{opMenuBannerStatus.find(data => data.value === banner.status)?.name}
|
||||
</StatusLabel>
|
||||
</StatusWapper>
|
||||
<td>{convertKTC(banner.start_dt)}</td>
|
||||
<td>{convertKTC(banner.end_dt)}</td>
|
||||
<td>{banner.title}</td>
|
||||
<td>{opYNType.find(data => data.value === banner.is_link)?.name}</td>
|
||||
<td>
|
||||
<Button theme="line" text="상세보기"
|
||||
handleClick={e => handleModalSubmit('detail', banner.id)} />
|
||||
</td>
|
||||
<td>{banner.update_by}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</TableStyle>
|
||||
</TableWrapper>
|
||||
|
||||
<Pagination postsPerPage={searchParams.pageSize} totalPosts={dataList?.total_all} setCurrentPage={handlePageChange} currentPage={searchParams.currentPage} pageLimit={INITIAL_PAGE_LIMIT} />
|
||||
|
||||
{/*상세*/}
|
||||
<MenuBannerModal modalType={modalType} detailView={modalState.detailModal} handleDetailView={() => handleModalClose('detail')} content={detailData} setDetailData={setDetailData} />
|
||||
|
||||
{/*삭제 확인*/}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.confirmOkCancel}
|
||||
view={modalState.deleteConfirmModal}
|
||||
handleCancel={() => handleModalClose('deleteConfirm')}
|
||||
handleSubmit={() => handleModalSubmit('deleteConfirm')}
|
||||
modalText={t('MENU_BANNER_SELECT_DELETE')}
|
||||
/>
|
||||
{/*삭제 완료*/}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.completed}
|
||||
view={modalState.deleteCompleteModal}
|
||||
handleSubmit={() => handleModalSubmit('deleteComplete')}
|
||||
modalText={t('DEL_COMPLETE')}
|
||||
/>
|
||||
{/* 경고 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.completed}
|
||||
view={alertMsg ? 'view' : 'hidden'}
|
||||
modalText={alertMsg}
|
||||
handleSubmit={() => handleModalSubmit('warning')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default withAuth(authType.battleEventRead)(MenuBanner);
|
||||
415
src/pages/ServiceManage/MenuBannerRegist.js
Normal file
415
src/pages/ServiceManage/MenuBannerRegist.js
Normal file
@@ -0,0 +1,415 @@
|
||||
import React, { useState, Fragment, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Loading from '../../components/common/Loading';
|
||||
import {
|
||||
Title,
|
||||
BtnWrapper,
|
||||
SearchBarAlert,
|
||||
} from '../../styles/Components';
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { MenuBannerSingleRegist } from '../../apis';
|
||||
|
||||
import { authList } from '../../store/authList';
|
||||
import {
|
||||
FormInput, FormInputSuffix, FormInputSuffixWrapper, FormLabel, FormRowGroup,RegistGroup,
|
||||
} from '../../styles/ModuleComponents';
|
||||
import AuthModal from '../../components/common/modal/AuthModal';
|
||||
import { authType, modalTypes } from '../../assets/data';
|
||||
import DynamicModal from '../../components/common/modal/DynamicModal';
|
||||
import { timeDiffMinute } from '../../utils';
|
||||
import { SingleDatePicker, SingleTimePicker } from '../../components/common';
|
||||
import CheckBox from '../../components/common/input/CheckBox';
|
||||
import ImageUploadBtn from '../../components/ServiceManage/ImageUploadBtn';
|
||||
import { useModal } from '../../hooks/hook';
|
||||
|
||||
const MenuBannerRegist = () => {
|
||||
const navigate = useNavigate();
|
||||
const userInfo = useRecoilValue(authList);
|
||||
const { t } = useTranslation();
|
||||
const token = sessionStorage.getItem('token');
|
||||
|
||||
const [loading, setLoading] = useState(false); // 로딩 창
|
||||
const {
|
||||
modalState,
|
||||
handleModalView,
|
||||
handleModalClose
|
||||
} = useModal({
|
||||
cancel: 'hidden',
|
||||
registConfirm: 'hidden',
|
||||
registComplete: 'hidden'
|
||||
});
|
||||
|
||||
const [isNullValue, setIsNullValue] = useState(false); // 데이터 값 체크
|
||||
const [alertMsg, setAlertMsg] = useState('');
|
||||
|
||||
const [resultData, setResultData] = useState(initData); //데이터 정보
|
||||
const [resetDateTime, setResetDateTime] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (checkCondition()) {
|
||||
setIsNullValue(false);
|
||||
} else {
|
||||
setIsNullValue(true);
|
||||
}
|
||||
}, [resultData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetDateTime) {
|
||||
setResetDateTime(false);
|
||||
}
|
||||
}, [resetDateTime]);
|
||||
|
||||
// 시작 날짜 변경 핸들러
|
||||
const handleStartDateChange = (date) => {
|
||||
if (!date) return;
|
||||
|
||||
const newDate = new Date(date);
|
||||
|
||||
if(resultData.end_dt){
|
||||
const endDate = new Date(resultData.end_dt);
|
||||
const startDay = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
|
||||
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
||||
|
||||
if (endDay <= startDay) {
|
||||
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setResultData(prev => ({
|
||||
...prev,
|
||||
start_dt: newDate
|
||||
}));
|
||||
};
|
||||
|
||||
// 시작 시간 변경 핸들러
|
||||
const handleStartTimeChange = (time) => {
|
||||
if (!time) return;
|
||||
|
||||
const newDateTime = resultData.start_dt
|
||||
? new Date(resultData.start_dt)
|
||||
: new Date();
|
||||
|
||||
newDateTime.setHours(
|
||||
time.getHours(),
|
||||
time.getMinutes(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
setResultData(prev => ({
|
||||
...prev,
|
||||
start_dt: newDateTime
|
||||
}));
|
||||
};
|
||||
|
||||
// 종료 날짜 변경 핸들러
|
||||
const handleEndDateChange = (date) => {
|
||||
if (!date || !resultData.start_dt) return;
|
||||
|
||||
const startDate = new Date(resultData.start_dt);
|
||||
const endDate = new Date(date);
|
||||
|
||||
// 일자만 비교하기 위해 년/월/일만 추출
|
||||
const startDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
||||
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
||||
|
||||
if (endDay <= startDay) {
|
||||
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
|
||||
return;
|
||||
}
|
||||
|
||||
setResultData(prev => ({
|
||||
...prev,
|
||||
end_dt: endDate
|
||||
}));
|
||||
};
|
||||
|
||||
// 종료 시간 변경 핸들러
|
||||
const handleEndTimeChange = (time) => {
|
||||
if (!time) return;
|
||||
|
||||
const newDateTime = resultData.end_dt
|
||||
? new Date(resultData.end_dt)
|
||||
: new Date();
|
||||
|
||||
newDateTime.setHours(
|
||||
time.getHours(),
|
||||
time.getMinutes(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
setResultData(prev => ({
|
||||
...prev,
|
||||
end_dt: newDateTime
|
||||
}));
|
||||
};
|
||||
|
||||
// 이미지 업로드
|
||||
const handleImageUpload = (language, file, fileName) => {
|
||||
const imageIndex = resultData.image_list.findIndex(img => img.language === language);
|
||||
|
||||
if (imageIndex !== -1) {
|
||||
const updatedImageList = [...resultData.image_list];
|
||||
updatedImageList[imageIndex] = {
|
||||
...updatedImageList[imageIndex],
|
||||
content: fileName,
|
||||
};
|
||||
|
||||
setResultData({
|
||||
...resultData,
|
||||
image_list: updatedImageList
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 이미지 삭제
|
||||
const handleImageDelete = (language) => {
|
||||
const imageIndex = resultData.image_list.findIndex(img => img.language === language);
|
||||
|
||||
if (imageIndex !== -1) {
|
||||
const updatedImageList = [...resultData.image_list];
|
||||
updatedImageList[imageIndex] = {
|
||||
...updatedImageList[imageIndex],
|
||||
content: '',
|
||||
};
|
||||
|
||||
setResultData({
|
||||
...resultData,
|
||||
image_list: updatedImageList
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (type, param = null) => {
|
||||
switch (type) {
|
||||
case "submit":
|
||||
if (!checkCondition()) return;
|
||||
const timeDiff = timeDiffMinute(resultData.start_dt, (new Date))
|
||||
if(timeDiff < 60) {
|
||||
setAlertMsg(t('EVENT_TIME_LIMIT_ADD'));
|
||||
return;
|
||||
}
|
||||
|
||||
handleModalView('registConfirm');
|
||||
break;
|
||||
case "cancel":
|
||||
|
||||
handleModalClose('cancel');
|
||||
navigate('/servicemanage/menubanner');
|
||||
break;
|
||||
case "registConfirm":
|
||||
setLoading(true);
|
||||
|
||||
const result = await MenuBannerSingleRegist(token, resultData);
|
||||
|
||||
setLoading(false);
|
||||
handleModalClose('registConfirm');
|
||||
handleModalView('registComplete');
|
||||
break;
|
||||
case "registComplete":
|
||||
handleModalClose('registComplete');
|
||||
|
||||
navigate('/servicemanage/menubanner');
|
||||
break;
|
||||
case "warning":
|
||||
setAlertMsg('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const checkCondition = () => {
|
||||
return (
|
||||
(resultData.start_dt.length !== 0) &&
|
||||
(resultData.end_dt.length !== 0) &&
|
||||
resultData.title !== '' &&
|
||||
resultData.image_list.every(data => data.content !== '') &&
|
||||
(resultData.is_link === false || (resultData.is_link === true && resultData.link_list.every(data => data.content !== '')))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) ? (
|
||||
<AuthModal/>
|
||||
) : (
|
||||
<>
|
||||
<Title>메뉴배너 등록</Title>
|
||||
<RegistGroup>
|
||||
<FormRowGroup>
|
||||
<FormLabel>등록기간</FormLabel>
|
||||
<SingleDatePicker
|
||||
label="시작일자"
|
||||
dateLabel="시작 일자"
|
||||
onDateChange={handleStartDateChange}
|
||||
selectedDate={resultData?.start_dt}
|
||||
/>
|
||||
<SingleTimePicker
|
||||
selectedTime={resultData?.start_dt}
|
||||
onTimeChange={handleStartTimeChange}
|
||||
/>
|
||||
<SingleDatePicker
|
||||
label="종료일자"
|
||||
dateLabel="종료 일자"
|
||||
onDateChange={handleEndDateChange}
|
||||
selectedDate={resultData?.end_dt}
|
||||
/>
|
||||
<SingleTimePicker
|
||||
selectedTime={resultData?.end_dt}
|
||||
onTimeChange={handleEndTimeChange}
|
||||
/>
|
||||
</FormRowGroup>
|
||||
|
||||
<FormRowGroup>
|
||||
<FormLabel>배너 제목</FormLabel>
|
||||
<FormInput
|
||||
type="text"
|
||||
width='50%'
|
||||
value={resultData?.title}
|
||||
onChange={e => setResultData({ ...resultData, title: e.target.value })}
|
||||
/>
|
||||
</FormRowGroup>
|
||||
<FormLabel>이미지 첨부</FormLabel>
|
||||
{resultData.image_list.map((data, idx) => (
|
||||
<LanguageWrapper key={idx}>
|
||||
<LanguageLabel>{data.language}</LanguageLabel>
|
||||
<ImageUploadBtn
|
||||
onImageUpload={(file, fileName) => handleImageUpload(data.language, file, fileName)}
|
||||
onFileDelete={() => handleImageDelete(data.language)}
|
||||
fileName={data.content}
|
||||
setAlertMessage={setAlertMsg}
|
||||
/>
|
||||
</LanguageWrapper>
|
||||
))}
|
||||
<FormRowGroup>
|
||||
<CheckBox
|
||||
label="이미지 링크 여부"
|
||||
id="reserve"
|
||||
checked={resultData.is_link}
|
||||
setData={e => setResultData({ ...resultData, is_link: e.target.checked })}
|
||||
/>
|
||||
</FormRowGroup>
|
||||
{resultData?.is_link &&
|
||||
<>
|
||||
<FormRowGroup>
|
||||
<FormLabel>웹 링크</FormLabel>
|
||||
<LanguageWrapper width="50%" >
|
||||
{resultData.link_list.map((data, idx) => (
|
||||
<FormInputSuffixWrapper>
|
||||
<FormInput
|
||||
type="text"
|
||||
value={resultData?.link_list[idx].content}
|
||||
onChange={e => {
|
||||
const updatedLinkList = [...resultData.link_list];
|
||||
updatedLinkList[idx] = { ...updatedLinkList[idx], content: e.target.value };
|
||||
setResultData({ ...resultData, link_list: updatedLinkList });
|
||||
}}
|
||||
suffix="true"
|
||||
/>
|
||||
<FormInputSuffix>{data.language}</FormInputSuffix>
|
||||
</FormInputSuffixWrapper>
|
||||
))}
|
||||
</LanguageWrapper>
|
||||
</FormRowGroup>
|
||||
</>
|
||||
}
|
||||
</RegistGroup>
|
||||
|
||||
{isNullValue && (
|
||||
<SearchBarAlert $align="right" $padding="0 0 15px">
|
||||
{t('NULL_MSG')}
|
||||
</SearchBarAlert>
|
||||
)}
|
||||
|
||||
<BtnWrapper $justify="flex-end" $gap="10px">
|
||||
<Button text="취소" theme="line" handleClick={() => handleModalView('cancel')} />
|
||||
<Button
|
||||
type="submit"
|
||||
text="등록"
|
||||
theme={checkCondition() ? 'primary' : 'disable'}
|
||||
handleClick={() => handleSubmit('submit')}
|
||||
/>
|
||||
</BtnWrapper>
|
||||
|
||||
{/* 등록 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.confirmOkCancel}
|
||||
view={modalState.registConfirmModal}
|
||||
modalText={t('MENU_BANNER_REGIST_CONFIRM')}
|
||||
handleSubmit={() => handleSubmit('registConfirm')}
|
||||
handleCancel={() => handleModalClose('registConfirm')}
|
||||
/>
|
||||
{/* 완료 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.completed}
|
||||
view={modalState.registCompleteModal}
|
||||
modalText={t('REGIST_COMPLTE')}
|
||||
handleSubmit={() => handleSubmit('registComplete')}
|
||||
/>
|
||||
{/* 취소 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.confirmOkCancel}
|
||||
view={modalState.cancelModal}
|
||||
modalText={t('MENU_BANNER_REGIST_CANCEL')}
|
||||
handleCancel={() => handleModalClose('cancel')}
|
||||
handleSubmit={() => handleSubmit('cancel')}
|
||||
/>
|
||||
{/* 경고 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.completed}
|
||||
view={alertMsg ? 'view' : 'hidden'}
|
||||
modalText={alertMsg}
|
||||
handleSubmit={() => handleSubmit('warning')}
|
||||
/>
|
||||
{loading && <Loading/>}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const initData = {
|
||||
title: '',
|
||||
is_link: false,
|
||||
start_dt: '',
|
||||
end_dt: '',
|
||||
image_list: [
|
||||
{ language: 'KO', content: '' },
|
||||
{ language: 'EN', content: '' },
|
||||
{ language: 'JA', content: '' },
|
||||
],
|
||||
link_list: [
|
||||
{ language: 'KO', content: '' },
|
||||
{ language: 'EN', content: '' },
|
||||
{ language: 'JA', content: '' },
|
||||
],
|
||||
}
|
||||
|
||||
export default MenuBannerRegist;
|
||||
|
||||
const LanguageWrapper = styled.div`
|
||||
width: ${props => props.width || '100%'};
|
||||
//margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 90px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const LanguageLabel = styled.h4`
|
||||
color: #444;
|
||||
margin: 0 0 10px 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
@@ -10,3 +10,5 @@ export { default as Event } from './Event';
|
||||
export { default as EventRegist } from './EventRegist';
|
||||
export { default as LandAuction} from './LandAuction'
|
||||
export { default as BattleEvent} from './BattleEvent'
|
||||
export { default as MenuBanner} from './MenuBanner'
|
||||
export { default as MenuBannerRegist} from './MenuBannerRegist'
|
||||
|
||||
@@ -618,4 +618,5 @@ export const TableActionButton = styled.button`
|
||||
&:hover {
|
||||
background: #3a70bc;
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
||||
|
||||
@@ -614,6 +614,10 @@ export const FormInput = styled.input`
|
||||
color: ${props => props.color || '#cccccc'};
|
||||
background: ${props => props.background_color || '#f6f6f6'};
|
||||
}
|
||||
|
||||
${props => props.suffix && `
|
||||
padding-right: 60px; // 라벨 영역을 위한 여백 확보
|
||||
`}
|
||||
`;
|
||||
|
||||
export const FormRowInput = styled.input`
|
||||
@@ -691,6 +695,30 @@ export const FormTextAreaWrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const FormInputSuffixWrapper = styled.div`
|
||||
position: relative;
|
||||
width: ${props => props.width || '100%'};
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
export const FormInputSuffix = styled.div`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 0 12px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
border-left: 1px solid #e2e8f0;
|
||||
`;
|
||||
|
||||
export const TitleItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -740,6 +768,7 @@ export const StatusWapper = styled.td`
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const ExcelDownButton = styled.button`
|
||||
|
||||
Reference in New Issue
Block a user