From 31690556465084c7d8d249547b0f6a80fb11000e Mon Sep 17 00:00:00 2001 From: bcjang Date: Mon, 15 Sep 2025 16:40:04 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B9=BC=EB=A6=AC=EC=9B=80=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EB=82=A0=EC=A7=9C=EC=B2=98=EB=A6=AC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=9E=AD=ED=82=B9=20=EC=8A=A4=EC=BC=80=EC=A4=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/RouteInfo.js | 18 +- src/apis/Rank.js | 99 +++++ src/apis/index.js | 6 +- src/assets/data/apis/index.js | 6 +- src/assets/data/apis/rankingAPI.json | 18 + src/assets/data/data.js | 4 +- src/assets/data/index.js | 5 +- src/assets/data/menuConfig.js | 40 +- src/assets/data/pages/rankingSearch.json | 48 +++ src/assets/data/pages/rankingTable.json | 140 +++++++ src/components/modal/RankingModal.js | 379 ++++++++++++++++++ src/components/searchBar/CommonSearchBar.js | 44 +- src/components/searchBar/ItemLogSearchBar.js | 18 + .../searchBar/RankManageSearchBar.js | 124 ++++++ src/components/searchBar/index.js | 8 +- src/pages/ServiceManage/Ranking.js | 218 ++++++++++ src/pages/ServiceManage/index.js | 6 +- src/pages/UserManage/CaliumRequest.js | 13 +- 18 files changed, 1151 insertions(+), 43 deletions(-) create mode 100644 src/apis/Rank.js create mode 100644 src/assets/data/apis/rankingAPI.json create mode 100644 src/assets/data/pages/rankingSearch.json create mode 100644 src/assets/data/pages/rankingTable.json create mode 100644 src/components/modal/RankingModal.js create mode 100644 src/components/searchBar/RankManageSearchBar.js create mode 100644 src/pages/ServiceManage/Ranking.js diff --git a/src/RouteInfo.js b/src/RouteInfo.js index 0f26fc5..89a73dd 100644 --- a/src/RouteInfo.js +++ b/src/RouteInfo.js @@ -13,11 +13,11 @@ import { LogView, } from './pages/UserManage'; import { EconomicIndex, UserIndex } from './pages/IndexManage'; -import { LandInfoView, CryptView, GameLogView, UserView, BusinessLogView, } from './pages/DataManage'; +import { LandInfoView, GameLogView, UserView, BusinessLogView, MetaItemView, RankManage } from './pages/DataManage'; import { Board, - Event, - EventRegist, + RewardEvent, + RewardEventRegist, Items, Mail, MailRegist, @@ -26,9 +26,9 @@ import { UserBlockRegist, LandAuction, BattleEvent, - MenuBanner, MenuBannerRegist, + MenuBanner, MenuBannerRegist, Ranking, + Event } from './pages/ServiceManage'; -import MetaItemView from './pages/DataManage/MetaItemView'; const RouteInfo = () => { return ( @@ -60,9 +60,9 @@ const RouteInfo = () => { } /> } /> } /> - } /> } /> } /> + } /> } /> @@ -72,12 +72,14 @@ const RouteInfo = () => { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> } /> + } /> + } /> diff --git a/src/apis/Rank.js b/src/apis/Rank.js new file mode 100644 index 0000000..22c9873 --- /dev/null +++ b/src/apis/Rank.js @@ -0,0 +1,99 @@ +//운영서비스 관리 - 랭킹 스케줄 api 연결 + +import { Axios } from '../utils'; + +// 리스트 조회 +export const RankingScheduleView = async (token, title, content, status, startDate, endDate, order, size, currentPage) => { + try { + const res = await Axios.get( + `/api/v1/rank/schedule/list?title=${title}&content=${content}&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('RankingScheduleView Error', e); + } + } +}; + +// 전투시스템 상세보기 +export const RankingScheduleDetailView = async (token, id) => { + try { + const res = await Axios.get(`/api/v1/rank/schedule/detail/${id}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingScheduleDetailView Error', e); + } + } +}; + +// 랭킹스케줄 등록 +export const RankingScheduleSingleRegist = async (token, params) => { + try { + const res = await Axios.post(`/api/v1/rank/schedule`, params, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingScheduleSingleRegist Error', e); + } + } +}; + +// 랭킹스케줄 수정 +export const RankingScheduleModify = async (token, id, params) => { + try { + const res = await Axios.put(`/api/v1/rank/schedule/${id}`, params, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingScheduleModify Error', e); + } + } +}; + +// 랭킹스케줄 삭제 +export const RankingScheduleDelete = async (token, id) => { + try { + const res = await Axios.delete(`/api/v1/rank/schedule/delete?id=${id}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingScheduleDelete Error', e); + } + } +}; + +export const RankingDataView = async (token) => { + try { + const res = await Axios.get( + `/api/v1/dictionary/ranking/list`, + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); + + return res.data.data.ranking_list; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingDataView Error', e); + } + } +}; \ No newline at end of file diff --git a/src/apis/index.js b/src/apis/index.js index c7338ed..4216904 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -12,12 +12,16 @@ export * from './BlackList'; export * from './Users'; export * from './Indicators'; export * from './Item'; -export * from './Event'; +export * from './RewardEvent'; export * from './Calium'; export * from './Land'; export * from './Menu'; // export * from './OpenAI'; export * from './Log'; +export * from './Data'; +export * from './Dictionary'; +export * from './Rank'; +export * from './Event'; const apiModules = {}; const allApis = {}; diff --git a/src/assets/data/apis/index.js b/src/assets/data/apis/index.js index 6c483bd..51ded41 100644 --- a/src/assets/data/apis/index.js +++ b/src/assets/data/apis/index.js @@ -1,9 +1,13 @@ import itemAPI from './itemAPI.json'; import menuBannerAPI from './menuBannerAPI.json'; import historyAPI from './historyAPI.json'; +import eventAPI from './eventAPI.json'; +import rankingAPI from './rankingAPI.json'; export { itemAPI, menuBannerAPI, - historyAPI + historyAPI, + eventAPI, + rankingAPI, }; \ No newline at end of file diff --git a/src/assets/data/apis/rankingAPI.json b/src/assets/data/apis/rankingAPI.json new file mode 100644 index 0000000..64ae66d --- /dev/null +++ b/src/assets/data/apis/rankingAPI.json @@ -0,0 +1,18 @@ +{ + "baseUrl": "/api/v1/rank/schedule", + "endpoints": { + "RankingScheduleView": { + "method": "GET", + "url": "/list", + "dataPath": "data", + "paramFormat": "query" + }, + "RankingScheduleDetailView": { + "method": "GET", + "url": "/detail/:id", + "dataPath": "data.data", + "paramFormat": "query", + "paramMapping": ["id"] + } + } +} \ No newline at end of file diff --git a/src/assets/data/data.js b/src/assets/data/data.js index 4f654b7..fe58ded 100644 --- a/src/assets/data/data.js +++ b/src/assets/data/data.js @@ -209,10 +209,12 @@ export const historyTables = { userBlock: 'black_list', landAuction: 'land_auction', landOwnerChange: 'land_ownership_changes', - event: 'event', + rewardEvent: 'event', mail: 'mail', notice: 'notice', battleEvent: 'battle_event', caliumRequest: 'calium_request', menuBanner: 'menu_banner', + event: 'world_event', + rankingSchedule: 'ranking_schedule', } \ No newline at end of file diff --git a/src/assets/data/index.js b/src/assets/data/index.js index 7f27c2f..dcd13d5 100644 --- a/src/assets/data/index.js +++ b/src/assets/data/index.js @@ -1,4 +1,4 @@ -export {authType, ivenTabType, modalTypes, TabUserList, tattooSlot, commonStatus, ViewTitleCountType, landAuctionStatusType} from './types' +export {authType, ivenTabType, modalTypes, tattooSlot, commonStatus, ViewTitleCountType, landAuctionStatusType} from './types' export { mailSendType, mailType, @@ -27,6 +27,7 @@ export { opYNType, opUserSessionType, opMailType, - amountDeltaType + amountDeltaType, + TabUserList } from './options' export {benItems, MinuteList, HourList, caliumRequestInitData, STATUS_STYLES, months, PAGE_SIZE_OPTIONS, ORDER_OPTIONS} from './data' \ No newline at end of file diff --git a/src/assets/data/menuConfig.js b/src/assets/data/menuConfig.js index 0caec3e..0b97020 100644 --- a/src/assets/data/menuConfig.js +++ b/src/assets/data/menuConfig.js @@ -103,14 +103,6 @@ export const menuConfig = { view: false, authLevel: adminAuthLevel.NONE }, - // cryptview: { - // title: '크립토 조회', - // permissions: { - // read: authType.cryptoRead - // }, - // view: false, - // authLevel: adminAuthLevel.NONE - // }, businesslogview: { title: '비즈니스 로그 조회', permissions: { @@ -122,7 +114,15 @@ export const menuConfig = { itemdictionary: { title: '아이템 백과사전 조회', permissions: { - read: authType.businessLogRead + read: authType.itemDictionaryRead + }, + view: true, + authLevel: adminAuthLevel.NONE + }, + rankmanage: { + title: '랭킹 점수 관리', + permissions: { + read: authType.rankManagerRead }, view: true, authLevel: adminAuthLevel.NONE @@ -172,7 +172,7 @@ export const menuConfig = { // view: true, // authLevel: adminAuthLevel.NONE // }, - event: { + rewardevent: { title: '보상 이벤트 관리', permissions: { read: authType.eventRead, @@ -222,6 +222,26 @@ export const menuConfig = { view: true, authLevel: adminAuthLevel.NONE }, + ranking: { + title: '랭킹 스케줄러', + permissions: { + read: authType.rankingRead, + update: authType.rankingUpdate, + delete: authType.rankingDelete + }, + view: true, + authLevel: adminAuthLevel.NONE + }, + event: { + title: '통합 이벤트 관리', + permissions: { + read: authType.worldEventRead, + update: authType.worldEventUpdate, + delete: authType.worldEventDelete + }, + view: true, + authLevel: adminAuthLevel.NONE + }, } } }; \ No newline at end of file diff --git a/src/assets/data/pages/rankingSearch.json b/src/assets/data/pages/rankingSearch.json new file mode 100644 index 0000000..ce6bcb3 --- /dev/null +++ b/src/assets/data/pages/rankingSearch.json @@ -0,0 +1,48 @@ +{ + "initialSearchParams": { + "searchData": "", + "status": "ALL", + "startDate": "", + "endDate": "", + "orderBy": "DESC", + "pageSize": 50, + "currentPage": 1 + }, + + "searchFields": [ + { + "type": "text", + "id": "searchData", + "label": "제목", + "placeholder": "제목 입력", + "width": "300px", + "col": 1 + }, + { + "type": "select", + "id": "status", + "label": "상태", + "optionsRef": "opMenuBannerStatus", + "col": 1 + }, + { + "type": "period", + "startDateId": "startDate", + "endDateId": "endDate", + "label": "기간", + "col": 1 + } + ], + + "apiInfo": { + "endpointName": "RankingScheduleView", + "loadOnMount": true, + "paramTransforms": [ + {"param": "startDate", "transform": "toISOString"}, + {"param": "endDate", "transform": "toISOString"} + ], + "pageField": "page_no", + "pageSizeField": "page_size", + "orderField": "orderBy" + } +} \ No newline at end of file diff --git a/src/assets/data/pages/rankingTable.json b/src/assets/data/pages/rankingTable.json new file mode 100644 index 0000000..eb17f41 --- /dev/null +++ b/src/assets/data/pages/rankingTable.json @@ -0,0 +1,140 @@ +{ + "id": "rankingTable", + "selection": { + "type": "single", + "idField": "id" + }, + "header": { + "countType": "total", + "orderType": "desc", + "pageType": "default", + "buttons": [ + { + "id": "delete", + "text": "선택 삭제", + "theme": "line", + "disableWhen": "noSelection", + "requiredAuth": "rankingDelete", + "action": "delete" + }, + { + "id": "register", + "text": "랭킹 스케줄 등록", + "theme": "primary", + "requiredAuth": "rankingUpdate", + "action": "regist" + } + ] + }, + "columns": [ + { + "id": "checkbox", + "type": "checkbox", + "width": "40px", + "title": "" + }, + { + "id": "row_num", + "type": "text", + "width": "70px", + "title": "번호" + }, + { + "id": "status", + "type": "status", + "width": "100px", + "title": "상태", + "option_name": "opCommonStatus" + }, + { + "id": "title", + "type": "text", + "title": "제목" + }, + { + "id": "meta_id", + "type": "text", + "title": "랭킹모드", + "width": "100px" + }, + { + "id": "event_action_id", + "type": "text", + "title": "이벤트 액션 그룹", + "width": "150px" + }, + { + "id": "start_dt", + "type": "date", + "width": "200px", + "title": "시작일(KST)", + "format": { + "type": "function", + "name": "convertKTC" + } + }, + { + "id": "end_dt", + "type": "date", + "width": "200px", + "title": "종료일(KST)", + "format": { + "type": "function", + "name": "convertKTC" + } + }, + { + "id": "base_dt", + "type": "date", + "width": "200px", + "title": "기준일(KST)", + "format": { + "type": "function", + "name": "convertKTC" + } + }, + { + "id": "refresh_interval", + "type": "text", + "width": "120px", + "title": "새로고침 주기" + }, + { + "id": "initialization_interval", + "type": "text", + "width": "120px", + "title": "초기화 주기" + }, + { + "id": "snapshot_interval", + "type": "text", + "width": "120px", + "title": "스냅샷 주기" + }, + { + "id": "detail", + "type": "button", + "width": "120px", + "title": "상세보기", + "text": "상세보기", + "action": { + "type": "modal", + "target": "detailModal", + "dataParam": { + "id": "id" + } + } + }, + { + "id": "history", + "type": "button", + "width": "120px", + "title": "히스토리", + "text": "히스토리" + } + ], + "sort": { + "defaultColumn": "row_num", + "defaultDirection": "desc" + } +} \ No newline at end of file diff --git a/src/components/modal/RankingModal.js b/src/components/modal/RankingModal.js new file mode 100644 index 0000000..8a7293e --- /dev/null +++ b/src/components/modal/RankingModal.js @@ -0,0 +1,379 @@ +import React, { useState, Fragment, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import Button from '../common/button/Button'; + +import { + Title, + BtnWrapper, + SearchBarAlert, SelectInput, +} from '../../styles/Components'; + +import { + FormInput, + FormLabel, + MessageWrapper, + FormRowGroup, + FormStatusBar, + FormStatusLabel, + FormStatusWarning, + FormButtonContainer, +} from '../../styles/ModuleComponents'; +import { DetailLayout, Modal, SingleDatePicker, SingleTimePicker } from '../common'; +import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants'; +import { convertKTCDate } from '../../utils'; +import { + battleEventHotTime, + battleEventRoundCount, + battleEventStatus, + battleRepeatType, opCommonStatus, +} from '../../assets/data/options'; +import { BattleEventModify, BattleEventSingleRegist } from '../../apis/Battle'; +import { alertTypes, battleEventStatusType, commonStatus } from '../../assets/data/types'; +import { isValidDayRange } from '../../utils/date'; +import { useAlert } from '../../context/AlertProvider'; +import { useLoading } from '../../context/LoadingProvider'; +import { RankingScheduleModify, RankingScheduleSingleRegist } from '../../apis'; + +const RankingModal = ({ modalType, detailView, handleDetailView, content, setDetailData, rankingData, eventActionData }) => { + const { t } = useTranslation(); + const token = sessionStorage.getItem('token'); + const { showToast, showModal } = useAlert(); + const {withLoading} = useLoading(); + + const [isNullValue, setIsNullValue] = useState(false); + const [resultData, setResultData] = useState(initData); + + useEffect(() => { + if(modalType === TYPE_MODIFY && content && Object.keys(content).length > 0){ + setResultData({ + guid: content.guid, + id: content.id, + title: content.title, + meta_id: content.meta_id, + event_action_id: content.event_action_id, + refresh_interval: content.refresh_interval, + initialization_interval: content.initialization_interval, + snapshot_interval: content.snapshot_interval, + status: content.status, + start_dt: convertKTCDate(content.start_dt), + end_dt: convertKTCDate(content.end_dt), + base_dt: convertKTCDate(content.base_dt), + }); + } + }, [modalType, content]); + + useEffect(() => { + if (checkCondition()) { + setIsNullValue(false); + } else { + setIsNullValue(true); + } + }, [resultData]); + + const opRankingMode = useMemo(() => { + return rankingData?.map(item => ({ + value: item.id, + name: `${item.desc}(${item.id})` + })) || []; + }, [rankingData]); + + const opEventActionMode = useMemo(() => { + return eventActionData?.map(item => ({ + value: item.id, + name: `${item.description}(${item.id})` + })) || []; + }, [eventActionData]); + + const handleReset = () => { + setDetailData({}); + setResultData(initData); + handleDetailView(); + } + + const handleSubmit = async (type, param = null) => { + switch (type) { + case "submit": + if (!checkCondition()) return; + + // const minAllowedTime = new Date(new Date().getTime() + 10 * 60000); + // const startDt = resultData.event_start_dt; + // const endDt = resultData.event_end_dt; + // if (modalType === TYPE_REGISTRY && startDt < minAllowedTime) { + // showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning}); + // return; + // } + // if(resultData.repeat_type !== 'NONE' && !isValidDayRange(startDt, endDt)) { + // showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning}); + // return; + // } + // + // //화면에 머물면서 상태는 안바꼈을 경우가 있기에 시작시간 지났을경우 차단 + // if (modalType === TYPE_REGISTRY && startDt < new Date()) { + // showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning}); + // return; + // } + + showModal(isView('modify') ? 'SCHEDULE_UPDATE_CONFIRM' : 'SCHEDULE_REGIST_CONFIRM', { + type: alertTypes.confirm, + onConfirm: () => handleSubmit('registConfirm') + }); + + break; + case "registConfirm": + + const params = { + ...resultData + }; + + if(isView('modify')){ + await withLoading( async () => { + return await RankingScheduleModify(token, content?.id, params); + }).then(data => { + if(data.result === "SUCCESS") { + showToast('UPDATE_COMPLETED', {type: alertTypes.success}); + }else{ + showToast('UPDATE_FAIL', {type: alertTypes.error}); + } + }).catch(reason => { + showToast('API_FAIL', {type: alertTypes.error}); + }).finally(() => { + handleReset(); + }); + } + else{ + await withLoading( async () => { + return await RankingScheduleSingleRegist(token, params); + }).then(data => { + if(data.result === "SUCCESS") { + showToast('REGIST_COMPLTE', {type: alertTypes.success}); + }else{ + showToast('REGIST_FAIL', {type: alertTypes.error}); + } + }).catch(reason => { + showToast('API_FAIL', {type: alertTypes.error}); + }).finally(() => { + handleReset(); + }); + } + break; + } + } + + const checkCondition = () => { + return ( + resultData.start_dt !== '' + && resultData.end_dt !== '' + && resultData.base_dt !== '' + && resultData.meta_id > 0 + && resultData.event_action_id > 0 + && resultData.title !== '' + && resultData.refresh_interval > 0 + && resultData.initialization_interval > 0 + && resultData.snapshot_interval > 0 + ); + }; + + const isView = (label) => { + switch (label) { + case "modify": + return modalType === TYPE_MODIFY && (content?.status === commonStatus.wait); + case "registry": + return modalType === TYPE_REGISTRY + case "start_dt": + case "end_dt": + case "base_dt": + case "name": + case "refresh_interval": + case "init_interval": + case "snapshot_interval": + case "mode": + case "eventActionMode": + return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === commonStatus.wait)); + default: + return modalType === TYPE_MODIFY && (content?.status !== commonStatus.wait); + } + } + + const itemGroups = [ + { + items: [ + { + row: 0, + col: 0, + colSpan: 2, + type: 'text', + key: 'title', + label: '스케줄러명', + disabled: !isView('name'), + width: '300px', + }, + { + row: 1, + col: 0, + colSpan: 2, + type: 'date', + key: 'start_dt', + label: '시작일시', + disabled: !isView('start_dt'), + format: 'YYYY-MM-DD HH:mm', + width: '200px', + showTime: true + }, + { + row: 1, + col: 2, + colSpan: 2, + type: 'date', + key: 'end_dt', + label: '종료일시', + disabled: !isView('end_dt'), + format: 'YYYY-MM-DD HH:mm', + width: '200px', + showTime: true + }, + { + row: 2, + col: 0, + colSpan: 2, + type: 'date', + key: 'base_dt', + label: '기준일시', + disabled: !isView('base_dt'), + format: 'YYYY-MM-DD HH:mm', + width: '200px', + showTime: true + }, + { + row: 2, + col: 2, + colSpan: 2, + type: 'number', + key: 'refresh_interval', + label: '새로고침 주기(분)', + disabled: !isView('refresh_interval'), + width: '100px', + min: 0, + }, + { + row: 3, + col: 0, + colSpan: 2, + type: 'number', + key: 'initialization_interval', + label: '초기화 주기(분)', + disabled: !isView('init_interval'), + width: '100px', + min: 0, + }, + { + row: 3, + col: 2, + colSpan: 2, + type: 'number', + key: 'snapshot_interval', + label: '스냅샷 주기(분)', + disabled: !isView('snapshot_interval'), + width: '100px', + min: 0, + }, + + { + row: 4, + col: 0, + colSpan: 2, + type: 'select', + key: 'meta_id', + label: '랭킹 모드', + disabled: !isView('mode'), + width: '150px', + options: opRankingMode + }, + { + row: 4, + col: 2, + colSpan: 2, + type: 'select', + key: 'event_action_id', + label: '이벤트 액션 그룹', + disabled: !isView('eventActionMode'), + width: '150px', + options: opEventActionMode + }, + ] + } + ]; + + return ( + <> + + {isView('registry') ? "랭킹 스케줄러 등록" : isView('modify') ? "랭킹 스케줄러 수정" : "랭킹 스케줄러 상세"} + + {!isView() && isNullValue && {t('REQUIRED_VALUE_CHECK')}} + + + + 현재상태: {opCommonStatus.find(data => data.value === content?.status)?.name || "등록"} + + + {isView('registry') ? '' : t('SCHEDULE_MODAL_STATUS_WARNING')} + + + + {isView() ? +