메뉴 배너 관리

This commit is contained in:
2025-04-21 14:14:34 +09:00
parent 1598fa93b6
commit d2ac5b338e
18 changed files with 935 additions and 13 deletions

View File

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

View File

@@ -12,3 +12,5 @@ export * from './Item';
export * from './Event';
export * from './Calium';
export * from './Land';
export * from './Menu';
export * from './OpenAI';

View File

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

View File

@@ -73,6 +73,10 @@ export const STATUS_STYLES = {
background: '#FFB59B',
color: 'white'
},
DELETE: {
background: '#FFB59B',
color: 'white'
},
RUNNING: {
background: '#4287f5',
color: 'white'

View File

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

View File

@@ -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 삭제" },

View File

@@ -49,7 +49,9 @@ export const authType = {
battleEventUpdate: 47,
battleEventDelete: 48,
businessLogRead: 49,
menuBannerRead: 50,
menuBannerUpdate: 51,
menuBannerDelete: 52,
levelReader: 999,

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

View File

@@ -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',

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -618,4 +618,5 @@ export const TableActionButton = styled.button`
&:hover {
background: #3a70bc;
}
`;
`;

View File

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