diff --git a/src/RouteInfo.js b/src/RouteInfo.js
index 3d6cf37..d6f7035 100644
--- a/src/RouteInfo.js
+++ b/src/RouteInfo.js
@@ -26,7 +26,7 @@ import {
UserBlockRegist,
WhiteList,
LandAuction,
- BattleEvent
+ BattleEvent, MenuBanner, MenuBannerRegist,
} from './pages/ServiceManage';
const RouteInfo = () => {
@@ -75,6 +75,8 @@ const RouteInfo = () => {
} />
} />
} />
+ } />
+ } />
diff --git a/src/apis/Menu.js b/src/apis/Menu.js
new file mode 100644
index 0000000..ba7da11
--- /dev/null
+++ b/src/apis/Menu.js
@@ -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);
+ }
+ }
+};
diff --git a/src/apis/index.js b/src/apis/index.js
index d45d0c4..5997932 100644
--- a/src/apis/index.js
+++ b/src/apis/index.js
@@ -12,3 +12,5 @@ export * from './Item';
export * from './Event';
export * from './Calium';
export * from './Land';
+export * from './Menu';
+export * from './OpenAI';
diff --git a/src/assets/data/adminConstants.js b/src/assets/data/adminConstants.js
index 6896528..c2a465d 100644
--- a/src/assets/data/adminConstants.js
+++ b/src/assets/data/adminConstants.js
@@ -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 };
diff --git a/src/assets/data/data.js b/src/assets/data/data.js
index c4c7d77..c5efe05 100644
--- a/src/assets/data/data.js
+++ b/src/assets/data/data.js
@@ -73,6 +73,10 @@ export const STATUS_STYLES = {
background: '#FFB59B',
color: 'white'
},
+ DELETE: {
+ background: '#FFB59B',
+ color: 'white'
+ },
RUNNING: {
background: '#4287f5',
color: 'white'
diff --git a/src/assets/data/menuConfig.js b/src/assets/data/menuConfig.js
index 4304482..01c17c8 100644
--- a/src/assets/data/menuConfig.js
+++ b/src/assets/data/menuConfig.js
@@ -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
+ },
}
}
};
\ No newline at end of file
diff --git a/src/assets/data/options.js b/src/assets/data/options.js
index df01888..d07b1b4 100644
--- a/src/assets/data/options.js
+++ b/src/assets/data/options.js
@@ -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 삭제" },
diff --git a/src/assets/data/types.js b/src/assets/data/types.js
index e5fda3b..b97e302 100644
--- a/src/assets/data/types.js
+++ b/src/assets/data/types.js
@@ -49,7 +49,9 @@ export const authType = {
battleEventUpdate: 47,
battleEventDelete: 48,
businessLogRead: 49,
-
+ menuBannerRead: 50,
+ menuBannerUpdate: 51,
+ menuBannerDelete: 52,
levelReader: 999,
diff --git a/src/components/common/button/VerticalDotsButton.js b/src/components/common/button/VerticalDotsButton.js
new file mode 100644
index 0000000..8abf5b1
--- /dev/null
+++ b/src/components/common/button/VerticalDotsButton.js
@@ -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 (
+ e.preventDefault()}
+ type={type}
+ disabled={disabled}
+ onClick={handleClick}
+ size={size}
+ bordercolor={borderColor}
+ width={width}
+ height={height}
+ name={name}
+ >
+
+
+
+
+ );
+};
+
+export default VerticalDotsButton;
\ No newline at end of file
diff --git a/src/i18n.js b/src/i18n.js
index d0952de..70ed332 100644
--- a/src/i18n.js
+++ b/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',
diff --git a/src/pages/DataManage/BusinessLogView.js b/src/pages/DataManage/BusinessLogView.js
index 4633d77..cb59577 100644
--- a/src/pages/DataManage/BusinessLogView.js
+++ b/src/pages/DataManage/BusinessLogView.js
@@ -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 (
<>
비즈니스 로그 조회
@@ -204,6 +217,7 @@ const BusinessLogView = () => {
)}
+
{dataLoading ? :
<>
diff --git a/src/pages/ServiceManage/Event.js b/src/pages/ServiceManage/Event.js
index 5925b5a..3ade82c 100644
--- a/src/pages/ServiceManage/Event.js
+++ b/src/pages/ServiceManage/Event.js
@@ -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 = () => {
handleSelectCheckBox(e, event)} />
{event.row_num} |
- {eventStatus.map(data => data.value === event.status && data.name)} |
+
+
+ {eventStatus.map(data => data.value === event.status && data.name)}
+
+
{convertKTC(event.start_dt)} |
{convertKTC(event.end_dt)} |
{event.title}
diff --git a/src/pages/ServiceManage/Mail.js b/src/pages/ServiceManage/Mail.js
index a15ca0a..5e63367 100644
--- a/src/pages/ServiceManage/Mail.js
+++ b/src/pages/ServiceManage/Mail.js
@@ -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 = () => {
{convertKTC(mail.create_dt)} |
{convertKTC(mail.send_dt)} |
{mailSendType.map(data => data.value === mail.send_type && data.name)} |
-
- {mail.send_status === 'FAIL' ? (
- {mailSendStatus.map(data => data.value === mail.send_status && data.name)}
- ) : (
- mailSendStatus.map(data => data.value === mail.send_status && data.name)
- )}
- |
+
+
+ {mailSendStatus.map(data => data.value === mail.send_status && data.name)}
+
+
{mailType.map(data => data.value === mail.mail_type && data.name)} |
{mailReceiveType.map(data => data.value === mail.receive_type && data.name)} |
{mail.title}
diff --git a/src/pages/ServiceManage/MenuBanner.js b/src/pages/ServiceManage/MenuBanner.js
new file mode 100644
index 0000000..c857241
--- /dev/null
+++ b/src/pages/ServiceManage/MenuBanner.js
@@ -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 (
+ <>
+ 메뉴 배너 관리
+
+ {
+ if (executeSearch) {
+ handleSearch(newParams);
+ } else {
+ updateSearchParams(newParams);
+ }
+ }}
+ onReset={handleReset}
+ />
+
+
+ {userInfo.auth_list?.some(auth => auth.id === authType.battleEventDelete) && (
+
+
+
+
+
+
+ |
+ 번호 |
+ 등록 상태 |
+ 시작일(KST) |
+ 종료일(KST) |
+ 설명 제목 |
+ 링크여부 |
+ 상세보기 |
+ 히스토리 |
+
+
+
+ {dataList?.list?.map(banner => (
+
+ |
+ handleSelectRow(e, banner)}
+ checked={isRowSelected(banner.id)} />
+ |
+ {banner.row_num} |
+
+
+ {opMenuBannerStatus.find(data => data.value === banner.status)?.name}
+
+
+ {convertKTC(banner.start_dt)} |
+ {convertKTC(banner.end_dt)} |
+ {banner.title} |
+ {opYNType.find(data => data.value === banner.is_link)?.name} |
+
+ handleModalSubmit('detail', banner.id)} />
+ |
+ {banner.update_by} |
+
+ ))}
+
+
+
+
+
+
+ {/*상세*/}
+ handleModalClose('detail')} content={detailData} setDetailData={setDetailData} />
+
+ {/*삭제 확인*/}
+ handleModalClose('deleteConfirm')}
+ handleSubmit={() => handleModalSubmit('deleteConfirm')}
+ modalText={t('MENU_BANNER_SELECT_DELETE')}
+ />
+ {/*삭제 완료*/}
+ handleModalSubmit('deleteComplete')}
+ modalText={t('DEL_COMPLETE')}
+ />
+ {/* 경고 모달 */}
+ handleModalSubmit('warning')}
+ />
+ >
+ )
+};
+
+export default withAuth(authType.battleEventRead)(MenuBanner);
diff --git a/src/pages/ServiceManage/MenuBannerRegist.js b/src/pages/ServiceManage/MenuBannerRegist.js
new file mode 100644
index 0000000..3f611d9
--- /dev/null
+++ b/src/pages/ServiceManage/MenuBannerRegist.js
@@ -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) ? (
+
+ ) : (
+ <>
+ 메뉴배너 등록
+
+
+ 등록기간
+
+
+
+
+
+
+
+ 배너 제목
+ setResultData({ ...resultData, title: e.target.value })}
+ />
+
+ 이미지 첨부
+ {resultData.image_list.map((data, idx) => (
+
+ {data.language}
+ handleImageUpload(data.language, file, fileName)}
+ onFileDelete={() => handleImageDelete(data.language)}
+ fileName={data.content}
+ setAlertMessage={setAlertMsg}
+ />
+
+ ))}
+
+ setResultData({ ...resultData, is_link: e.target.checked })}
+ />
+
+ {resultData?.is_link &&
+ <>
+
+ 웹 링크
+
+ {resultData.link_list.map((data, idx) => (
+
+ {
+ const updatedLinkList = [...resultData.link_list];
+ updatedLinkList[idx] = { ...updatedLinkList[idx], content: e.target.value };
+ setResultData({ ...resultData, link_list: updatedLinkList });
+ }}
+ suffix="true"
+ />
+ {data.language}
+
+ ))}
+
+
+ >
+ }
+
+
+ {isNullValue && (
+
+ {t('NULL_MSG')}
+
+ )}
+
+
+ handleModalView('cancel')} />
+ handleSubmit('submit')}
+ />
+
+
+ {/* 등록 모달 */}
+ handleSubmit('registConfirm')}
+ handleCancel={() => handleModalClose('registConfirm')}
+ />
+ {/* 완료 모달 */}
+ handleSubmit('registComplete')}
+ />
+ {/* 취소 모달 */}
+ handleModalClose('cancel')}
+ handleSubmit={() => handleSubmit('cancel')}
+ />
+ {/* 경고 모달 */}
+ handleSubmit('warning')}
+ />
+ {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;
+`;
+
diff --git a/src/pages/ServiceManage/index.js b/src/pages/ServiceManage/index.js
index b648f17..5861ea2 100644
--- a/src/pages/ServiceManage/index.js
+++ b/src/pages/ServiceManage/index.js
@@ -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'
diff --git a/src/styles/Components.js b/src/styles/Components.js
index 1f83660..cc69f88 100644
--- a/src/styles/Components.js
+++ b/src/styles/Components.js
@@ -618,4 +618,5 @@ export const TableActionButton = styled.button`
&:hover {
background: #3a70bc;
}
-`;
\ No newline at end of file
+`;
+
diff --git a/src/styles/ModuleComponents.js b/src/styles/ModuleComponents.js
index 310a804..ae8eb59 100644
--- a/src/styles/ModuleComponents.js
+++ b/src/styles/ModuleComponents.js
@@ -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`