From d2ac5b338ecc994ecb17e54706d3886e4bd98a9e Mon Sep 17 00:00:00 2001 From: bcjang Date: Mon, 21 Apr 2025 14:14:34 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EB=B0=B0=EB=84=88=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/RouteInfo.js | 4 +- src/apis/Menu.js | 114 +++++ src/apis/index.js | 2 + src/assets/data/adminConstants.js | 1 + src/assets/data/data.js | 4 + src/assets/data/menuConfig.js | 10 + src/assets/data/options.js | 7 + src/assets/data/types.js | 4 +- .../common/button/VerticalDotsButton.js | 53 +++ src/i18n.js | 11 +- src/pages/DataManage/BusinessLogView.js | 14 + src/pages/ServiceManage/Event.js | 15 +- src/pages/ServiceManage/Mail.js | 13 +- src/pages/ServiceManage/MenuBanner.js | 247 +++++++++++ src/pages/ServiceManage/MenuBannerRegist.js | 415 ++++++++++++++++++ src/pages/ServiceManage/index.js | 2 + src/styles/Components.js | 3 +- src/styles/ModuleComponents.js | 29 ++ 18 files changed, 935 insertions(+), 13 deletions(-) create mode 100644 src/apis/Menu.js create mode 100644 src/components/common/button/VerticalDotsButton.js create mode 100644 src/pages/ServiceManage/MenuBanner.js create mode 100644 src/pages/ServiceManage/MenuBannerRegist.js 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) && ( +