diff --git a/src/apis/Rank.js b/src/apis/Rank.js index 22c9873..6c98148 100644 --- a/src/apis/Rank.js +++ b/src/apis/Rank.js @@ -21,6 +21,40 @@ export const RankingScheduleView = async (token, title, content, status, startDa } }; +export const RankingScheduleSimpleView = async (token) => { + try { + const res = await Axios.get( + `/api/v1/rank/schedule/simple-list`, + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); + + return res.data.data.list; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingScheduleSimpleView Error', e); + } + } +}; + +export const RankingSnapshotView = async (token, guid) => { + try { + const res = await Axios.get( + `/api/v1/rank/snapshot/list?guid=${guid}`, + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); + + return res.data.data.ranking_snapshot_list; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingSnapshotView Error', e); + } + } +}; + // 전투시스템 상세보기 export const RankingScheduleDetailView = async (token, id) => { try { @@ -36,6 +70,20 @@ export const RankingScheduleDetailView = async (token, id) => { } }; +export const RankerListView = async (token, guid, snapshot) => { + try { + const res = await Axios.get(`/api/v1/rank/ranker/list?guid=${guid}&snapshot=${snapshot}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankerListView Error', e); + } + } +}; + // 랭킹스케줄 등록 export const RankingScheduleSingleRegist = async (token, params) => { try { @@ -96,4 +144,74 @@ export const RankingDataView = async (token) => { throw new Error('RankingDataView Error', e); } } +}; + +export const RankingInfoView = async (token, guid) => { + try { + const res = await Axios.get(`/api/v1/rank/info?guid=${guid}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingInfoView Error', e); + } + } +}; + +export const RankerInfoModify = async (token, params) => { + try { + const res = await Axios.put(`/api/v1/rank/info`, params, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankerInfoModify Error', e); + } + } +}; + +export const RankingUpdate = async (token, guid) => { + try { + const res = await Axios.put(`/api/v1/rank/ranking/${guid}`, {},{ + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingUpdate Error', e); + } + } +}; + +export const RankingInit = async (token, guid) => { + try { + const res = await Axios.put(`/api/v1/rank/ranking/init/${guid}`, {},{ + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingInit Error', e); + } + } +}; + +export const RankingSnapshot = async (token, guid) => { + try { + const res = await Axios.put(`/api/v1/rank/ranking/snapshot/${guid}`, {},{ + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.data; + } catch (e) { + if (e instanceof Error) { + throw new Error('RankingSnapshot Error', e); + } + } }; \ No newline at end of file diff --git a/src/assets/data/data.js b/src/assets/data/data.js index fe58ded..0e7c38e 100644 --- a/src/assets/data/data.js +++ b/src/assets/data/data.js @@ -153,15 +153,27 @@ export const FieldLabels = { 'create_by': '생성자', 'status': '상태', 'deleted': '삭제 여부', + 'guid': 'GUID', + 'meta_id': '메타 ID', + 'start_dt': '시작일', + 'end_dt': '종료일', + 'base_dt': '기준일', + 'title': '제목', // 이벤트 필드 관련 'eventId': '이벤트 ID', 'eventName': '이벤트 명', + 'event_name': '이벤트 명', 'repeatType': '반복 타입', - 'eventOperationTime': '운영 시간(초)', - 'eventStartDt': '시작 시간', - 'eventEndDt': '종료 시간', + 'repeat_type': '반복 타입', + 'eventOperationTime': '진행 시간(분)', + 'event_operation_time': '진행 시간(분)', + 'eventStartDt': '이벤트 시작일', + 'event_start_dt': '이벤트 시작일', + 'eventEndDt': '이벤트 종료일', + 'event_end_dt': '이벤트 종료일', 'roundTime': '라운드 시간(초)', + 'round_time': '라운드 시간(초)', 'roundCount': '라운드 수', 'hotTime': '핫타임', 'configId': '설정 ID', @@ -203,6 +215,20 @@ export const FieldLabels = { 'gacha_group_id': '랜덤박스 그룹 ID', 'ugq_action': 'UGQ 사용 가능 여부', 'linked_land': '연결된 랜드 ID', + + //스케줄 + 'refresh_interval': '새로고침 주기', + 'initialization_interval': '초기화 주기', + 'snapshot_interval': '스냅샷 주기', + 'event_action_id': '이벤트 액션 그룹', + 'global_event_action_id': '이벤트 액션 그룹', + 'personal_event_action_id': '개인제작 액션 그룹', + 'max_point': '기여도 목표점수', + + //메뉴 + 'image_list': '이미지 목록', + 'is_link': '링크 여부', + 'order_id': '정렬' }; export const historyTables = { diff --git a/src/assets/data/menuConfig.js b/src/assets/data/menuConfig.js index 9d8e3fa..8b03463 100644 --- a/src/assets/data/menuConfig.js +++ b/src/assets/data/menuConfig.js @@ -130,7 +130,16 @@ export const menuConfig = { rankmanage: { title: '랭킹 점수 관리', permissions: { - read: authType.rankManagerRead + read: authType.rankManagerRead, + update: authType.rankManagerUpdate, + }, + view: true, + authLevel: adminAuthLevel.NONE + }, + rankview: { + title: '랭킹 시스템 조회', + permissions: { + read: authType.rankInfoRead, }, view: true, authLevel: adminAuthLevel.NONE diff --git a/src/assets/data/options.js b/src/assets/data/options.js index f5dd37b..3775be5 100644 --- a/src/assets/data/options.js +++ b/src/assets/data/options.js @@ -45,9 +45,9 @@ export const TabUserIndexList = [ ]; export const TabRankManageList = [ - { value: 'PIONEER', name: '개척자 랭킹 보드' }, - { value: 'RUN_RACE', name: '점프 러너 랭킹 보드' }, - { value: 'BATTLE_OBJECT', name: '컴뱃 존 랭킹 보드' } + { value: 'RANK', name: '랭킹 보드' }, + // { value: 'RUN_RACE', name: '점프 러너 랭킹 보드' }, + // { value: 'BATTLE_OBJECT', name: '컴뱃 존 랭킹 보드' } ]; export const mailSendType = [ @@ -245,6 +245,12 @@ export const itemSearchType = [ { value: 'NAME', name: '아이템명' }, ]; +export const instanceSearchType = [ + { value: 'ID', name: '인스턴스 ID' }, + { value: 'NAME', name: '인스턴스명' }, + { value: 'BUILDING', name: '빌딩 ID' }, +]; + export const blockType = [ { value: '', name: '선택' }, { value: 'Access_Restrictions', name: '접근 제한' }, @@ -419,6 +425,22 @@ export const opPropRecipeType = [ { value: 'Add', name: '등록 필요' }, ]; +export const opInstanceContentsType = [ + { value: 'ALL', name: '전체' }, + { value: 'Concert', name: 'Concert' }, + { value: 'Movie', name: 'Movie' }, + { value: 'Meeting', name: 'Meeting' }, + { value: 'MyHome', name: 'MyHome' }, + { value: 'Normal', name: 'Normal' }, +]; + +export const opInstanceAccessType = [ + { value: 'ALL', name: '전체' }, + { value: 'Public', name: 'Public' }, + { value: 'Item', name: 'Item' }, + { value: 'Belong', name: 'Belong' }, +]; + export const opEquipType = [ { value: 0, name: '미장착' }, { value: 1, name: '의상장착' }, @@ -654,6 +676,18 @@ export const opCommonStatus = [ { value: 'RUNNING', name: '진행중' }, ] +export const opRankingType = [ + { value: 'PIONEER', name: '개척자' }, + { value: 'RUNNER1', name: '점프러너 - 맵1' }, + { value: 'RUNNER2', name: '점프러너 - 맵2' }, + { value: 'RUNNER3', name: '점프러너 - 맵3' }, + { value: 'RUNNER4', name: '점프러너 - 맵4' }, + { value: 'BATTLE_FFA', name: '컴뱃존 - FFA' }, + { value: 'BATTLE_TEAM', name: '컴뱃존 - TEAM' }, + { value: 'EVENT_CONTRIBUTION', name: '월드 이벤트 - 기여도' }, + { value: 'EVENT_CRAFT', name: '월드 이벤트 - 개인 제작' }, +] + // export const logAction = [ // { value: "None", name: "ALL" }, // { value: "AIChatDeleteCharacter", name: "NPC 삭제" }, @@ -1128,6 +1162,10 @@ export const logAction = [ { value: "QuestMailSend", name: "QuestMailSend" }, { value: "QuestMainTask", name: "QuestMainTask" }, { value: "QuestTaskUpdate", name: "QuestTaskUpdate" }, + { value: "RankingStart", name: "RankingStart" }, + { value: "RankingFinish", name: "RankingFinish" }, + { value: "RankingScoreUpdate", name: "RankingScoreUpdate" }, + { value: "RankingEventActionScore", name: "RankingEventActionScore" }, { value: "RefuseFriendRequest", name: "RefuseFriendRequest" }, { value: "RenameFriendFolder", name: "RenameFriendFolder" }, { value: "RenameMyhome", name: "RenameMyhome" }, @@ -1199,6 +1237,7 @@ export const logAction = [ { value: "UserLogout", name: "UserLogout" }, { value: "UserLogoutSnapShot", name: "UserLogoutSnapShot" }, { value: "UserReport", name: "UserReport" }, + { value: "WorldEventActionScore", name: "WorldEventActionScore" }, { value: "Warp", name: "Warp" }, { value: "igmApiLogin", name: "igmApiLogin" } ]; @@ -1239,6 +1278,7 @@ export const logDomain = [ { value: "Cart", name: "Cart" }, { value: "Currency", name: "Currency" }, { value: "CustomDefineUi", name: "CustomDefineUi" }, + { value: "EventActionScore", name: "EventActionScore" }, { value: "EscapePosition", name: "EscapePosition" }, { value: "Friend", name: "Friend" }, { value: "Farming", name: "Farming" }, @@ -1275,6 +1315,8 @@ export const logDomain = [ { value: "QuestMain", name: "QuestMain" }, { value: "QuestUgq", name: "QuestUgq" }, { value: "QuestMail", name: "QuestMail" }, + { value: "Ranking", name: "Ranking" }, + { value: "Ranker", name: "Ranker" }, { value: "RenewalShopProducts", name: "RenewalShopProducts" }, { value: "Rental", name: "Rental" }, { value: "RewardProp", name: "RewardProp" }, diff --git a/src/assets/data/pages/rankingTable.json b/src/assets/data/pages/rankingTable.json index eb17f41..8c276fb 100644 --- a/src/assets/data/pages/rankingTable.json +++ b/src/assets/data/pages/rankingTable.json @@ -9,6 +9,30 @@ "orderType": "desc", "pageType": "default", "buttons": [ + { + "id": "update", + "text": "강제 새로고침", + "theme": "line", + "disableWhen": "noSelection", + "requiredAuth": "rankingUpdate", + "action": "update" + }, + { + "id": "init", + "text": "강제 초기화", + "theme": "line", + "disableWhen": "noSelection", + "requiredAuth": "rankingUpdate", + "action": "rankingInit" + }, + { + "id": "snapshot", + "text": "강제 스냅샷", + "theme": "line", + "disableWhen": "noSelection", + "requiredAuth": "rankingUpdate", + "action": "rankingSnapshot" + }, { "id": "delete", "text": "선택 삭제", diff --git a/src/assets/data/types.js b/src/assets/data/types.js index e667e3b..9fd5452 100644 --- a/src/assets/data/types.js +++ b/src/assets/data/types.js @@ -62,6 +62,8 @@ export const authType = { worldEventUpdate: 60, worldEventDelete: 61, craftingDictionaryRead: 62, + rankInfoRead: 63, + instanceDictionaryRead: 64, levelReader: 999, diff --git a/src/components/DataManage/RankPioneerInfo.js b/src/components/DataManage/RankPioneerInfo.js new file mode 100644 index 0000000..2e2dfdd --- /dev/null +++ b/src/components/DataManage/RankPioneerInfo.js @@ -0,0 +1,99 @@ +import styled from 'styled-components'; +import { useState, useEffect } from 'react'; +import { UserToolView } from '../../apis/Users'; +import { TableSkeleton } from '../Skeleton/TableSkeleton'; + +const RankPioneerInfo = ({ userInfo }) => { + const [dataList, setDataList] = useState(); + const [rowData, setRowData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if(userInfo && Object.keys(userInfo).length > 0) { + fetchData(); + } + }, []); + + useEffect(() => { + if(dataList && dataList.slot_list) + setRowData([ + { title: 'GUID', itemNo: dataList.slot_list.slot1?.tool_id, itemName: dataList?.slot_list.slot1?.tool_name }, + { title: '아바타명', itemNo: dataList.slot_list.slot2?.tool_id, itemName: dataList?.slot_list.slot2?.tool_name }, + { title: '점수', itemNo: dataList.slot_list.slot3?.tool_id, itemName: dataList?.slot_list.slot3?.tool_name }, + ]) + }, [dataList]) + + const fetchData = async () => { + const token = sessionStorage.getItem('token'); + await UserToolView(token, userInfo.guid).then(data => { + setDataList(data); + setLoading(false); + }); + }; + + return ( + loading ? : + <> + + + + + + + + + {rowData && rowData.map((el, idx) => { + return ( + + {el.title} + {el.itemNo} + {el.itemName} + + ); + })} + + + + + ); +}; + +export default RankPioneerInfo; + +const UserInfoTable = styled.table` + width: 100%; + max-width: ${props => props.$maxwidth || 'auto'}; + font-size: 13px; + border-radius: 15px; + overflow: hidden; + tr:first-child { + th, + td { + border-top: 0; + } + } + th, + td { + height: 36px; + vertical-align: middle; + border-top: 1px solid #d9d9d9; + } + th { + width: 120px; + background: #888; + color: #fff; + font-weight: 700; + } + td { + background: #fff; + padding: 0 20px; + } +`; + +const ToolWrapper = styled.div` + ${UserInfoTable} { + td { + border-left: 1px solid #d9d9d9; + } + } +`; diff --git a/src/components/DataManage/RankingSnapshotInfo.js b/src/components/DataManage/RankingSnapshotInfo.js new file mode 100644 index 0000000..371f7a2 --- /dev/null +++ b/src/components/DataManage/RankingSnapshotInfo.js @@ -0,0 +1,84 @@ +import React, { useRef } from 'react'; +import { TableSkeleton } from '../Skeleton/TableSkeleton'; +import { RankInfoSearchBar, useRankInfoSearch } from '../searchBar'; +import { useDataFetch } from '../../hooks/hook'; +import { RankingScheduleSimpleView } from '../../apis'; +import { FormWrapper, TableStyle, TableWrapper } from '../../styles/Components'; +import { ExcelDownButton, ViewTableInfo } from '../common'; +import { useTranslation } from 'react-i18next'; +import { formatTimeFromSeconds } from '../../utils'; + +const RankingSnapshotInfo = () => { + const token = sessionStorage.getItem('token'); + const tableRef = useRef(null); + const { t } = useTranslation(); + + const { + config, + loading: dataLoading, + searchParams, + data: dataList, + handleSearch, + handleReset, + updateSearchParams, + snapshotData + } = useRankInfoSearch(token); + + const { + data: rankingScheduleData + } = useDataFetch(() => RankingScheduleSimpleView(token), [token]); + + return ( + <> + + { + if (executeSearch) { + handleSearch(newParams); + } else { + updateSearchParams(newParams); + } + }} + onReset={handleReset} + scheduleData={rankingScheduleData} + snapshotData={snapshotData} + /> + + + + + {dataLoading ? : + + + + + + 순위 + account ID + GUID + 아바타명 + 점수 + + + + {dataList?.ranker_List?.map((rank, index) => ( + + {rank.rank} + {rank.account_id} + {rank.user_guid} + {rank.nickname} + {rank.score_type === "Time" ? formatTimeFromSeconds(rank.score) : rank.score} + + ))} + + + + } + + ); +}; + +export default RankingSnapshotInfo; + diff --git a/src/components/DataManage/UserRankInfo.js b/src/components/DataManage/UserRankInfo.js new file mode 100644 index 0000000..7cef225 --- /dev/null +++ b/src/components/DataManage/UserRankInfo.js @@ -0,0 +1,201 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import NicknameChangeModal from '../../components/DataManage/NicknameChangeModal'; +import { UserInfoView } from '../../apis/Users'; +import { authType } from '../../assets/data'; +import { useTranslation } from 'react-i18next'; +import { useRecoilValue } from 'recoil'; +import { authList } from '../../store/authList'; +import { UserDefault } from '../../styles/ModuleComponents'; +import { TableSkeleton } from '../Skeleton/TableSkeleton'; +import { UserInfoSkeleton } from '../Skeleton/UserInfoSkeleton'; +import { useModal } from '../../hooks/hook'; +import { useAlert } from '../../context/AlertProvider'; +import { useLoading } from '../../context/LoadingProvider'; +import { alertTypes } from '../../assets/data/types'; +import { Button, Col, Descriptions, InputNumber, Row, Space, Table } from 'antd'; +import { RankerInfoModify, RankingInfoView } from '../../apis'; +import { InputItem, TextInput } from '../../styles/Components'; +import CustomConfirmModal from '../common/modal/CustomConfirmModal'; +import InputConfirmModal from '../common/modal/InputConfirmModal'; +import { opRankingType } from '../../assets/data/options'; +import { formatTimeFromSeconds } from '../../utils'; + +const UserRankInfo = ({ userInfo }) => { + const { t } = useTranslation(); + const authInfo = useRecoilValue(authList); + const token = sessionStorage.getItem('token'); + const {showModal, showToast} = useAlert(); + const {withLoading} = useLoading(); + + const { + modalState, + handleModalView, + handleModalClose + } = useModal({ + valueChange: 'hidden' + }); + const [dataList, setDataList] = useState({}); + const [loading, setLoading] = useState(true); + const [authUpdate, setAuthUpdate] = useState(false); + const [selectRow, setSelectRow] = useState(); + const [updateValue, setUpdateValue] = useState(); + const [comment, setComment] = useState(); + + useEffect(() => { + setAuthUpdate(authInfo?.auth_list?.some(auth => auth.id === authType.rankManagerUpdate)); + }, [authInfo]); + + useEffect(() => { + if(userInfo && Object.keys(userInfo).length > 0) { + fetchData(); + } + }, [userInfo]); + + const fetchData = async () => { + setLoading(true); + await RankingInfoView(token, userInfo.guid).then(data => { + setDataList(data.user_ranking_info); + }).catch(error => { + showToast(error, {type: alertTypes.error}); + }).finally(() => { + setLoading(false); + }); + }; + + const findAndFormatScore = (rankingType) => { + if (!dataList?.rankingItems) return ''; + const item = dataList.rankingItems.find(item => item.rankingType === rankingType); + + if (!item || item.score === null || item.score === undefined) return ''; + + // scoreType이 "Time"이면 시:분:초 형식으로 변환 (초 단위로 가정) + if (item.scoreType === "Time") { + return formatTimeFromSeconds(item.score); + } + + // 그 외의 경우는 숫자 그대로 반환 + return item.score.toString(); + }; + + const columns = [ + { + dataIndex: 'label', + width: 200, + onCell: () => ({ + style: { + backgroundColor: '#f0f0f0', + fontWeight: 'bold' + } + }) + }, + { + dataIndex: 'content', + width: 300, + }, + { + dataIndex: 'action', + }, + ]; + + const generateTableData = () => { + if (!dataList) return []; + + const baseData = [ + { + key: 'guid', + label: 'GUID', + content: userInfo?.guid || '', + action: null + }, + { + key: 'nickname', + label: '아바타명', + content: dataList?.nickname || userInfo?.nickname || '', + action: null + } + ]; + + // opRankingType을 기반으로 동적 생성 + const rankingData = opRankingType.map(rankType => { + const formattedScore = findAndFormatScore(rankType.value); + const hasScore = formattedScore !== ''; + + return { + key: rankType.value.toLowerCase(), + label: rankType.name, + content: formattedScore, + action: authUpdate && hasScore ? ( + + ) : null + }; + }); + + return [...baseData, ...rankingData]; + }; + + const data = generateTableData(); + + const handleSubmit = async (type, param = null) => { + let params = {}; + + switch (type) { + case "valueChange": + setSelectRow(param); + const comment = dataList.rankingItems.find(item => item.rankingType === param)?.scoreType === 'Time' ? '초단위로 입력해주세요.' : '점수'; + setComment(comment); + handleModalView('valueChange'); + break; + case "valueChangeConfirm": + const item = dataList.rankingItems.find(item => item.rankingType === selectRow); + params.guid = item.guid; + params.score = updateValue; + params.user_guid = userInfo.guid; + + await withLoading(async () => { + return await RankerInfoModify(token, params); + }).then(data => { + showToast('UPDATE_COMPLETED', {type: alertTypes.success}); + fetchData(); + }).catch(error => { + showToast(error, {type: alertTypes.error}); + }).finally(() => { + handleModalClose('valueChange'); + }); + break; + } + } + + return ( + loading ? : + <> +
+ + + + + handleSubmit('valueChangeConfirm')} + handleCancel={() => handleModalClose('valueChange')} + handleClose={() => handleModalClose('valueChange')} + value={updateValue} + setValue={setUpdateValue} + inputText={t('UPDATE_VALUE_COMMENT',{comment:comment})} + /> + + ); +}; +export default UserRankInfo; \ No newline at end of file diff --git a/src/components/DataManage/index.js b/src/components/DataManage/index.js new file mode 100644 index 0000000..c0fc13b --- /dev/null +++ b/src/components/DataManage/index.js @@ -0,0 +1,24 @@ +export { default as UserDefaultInfo } from './UserDefaultInfo'; +export { default as UserAvatarInfo } from './UserAvatarInfo'; +export { default as UserDressInfo } from './UserDressInfo'; +export { default as UserToolInfo } from './UserToolInfo'; +export { default as UserInventoryInfo } from './UserInventoryInfo'; +export { default as UserMailInfo } from './UserMailInfo'; +export { default as UserMyHomeInfo } from './UserMyHomeInfo'; +export { default as UserFriendInfo } from './UserFriendInfo'; +export { default as UserTattooInfo } from './UserTattooInfo'; +export { default as UserQuestInfo } from './UserQuestInfo'; +export { default as UserClaimInfo } from './UserClaimInfo'; +export { default as CurrencyLogContent } from './CurrencyLogContent'; +export { default as ItemLogContent } from './ItemLogContent'; +export { default as CurrencyItemLogContent } from './CurrencyItemLogContent'; +export { default as UserLoginLogContent } from './UserLoginLogContent'; +export { default as UserCreateLogContent } from './UserCreateLogContent'; +export { default as UserSnapshotLogContent } from './UserSnapshotLogContent'; +export { default as RankPioneerInfo } from './RankPioneerInfo'; +export { default as LandDetailModal } from './LandDetailModal'; +export { default as MailDetailModal } from './MailDetailModal'; +export { default as QuestDetailModal } from './QuestDetailModal'; +export { default as NicknameChangeModal } from './NicknameChangeModal'; +export { default as UserRankInfo } from './UserRankInfo'; +export { default as RankingSnapshotInfo } from './RankingSnapshotInfo'; diff --git a/src/components/common/modal/InputConfirmModal.js b/src/components/common/modal/InputConfirmModal.js new file mode 100644 index 0000000..102ced9 --- /dev/null +++ b/src/components/common/modal/InputConfirmModal.js @@ -0,0 +1,47 @@ +import { BtnWrapper, ButtonClose, InputItem, ModalText } from '../../../styles/Components'; +import Button from '../button/Button'; +import Modal from './Modal'; +import { Input, InputNumber } from 'antd'; +import React from 'react'; + +const InputConfirmModal = ({view, handleClose, handleCancel, handleSubmit, inputText, inputType, value, setValue}) => { + return ( + + + + + + +

{inputText}

+ { inputType === 'number' && + setValue(value)} + />} + { inputType === 'text' && + setValue(value)} + />} + +
+
+ +
- + diff --git a/src/components/modal/EventModal.js b/src/components/modal/EventModal.js index cf77ca2..df70a5a 100644 --- a/src/components/modal/EventModal.js +++ b/src/components/modal/EventModal.js @@ -59,8 +59,8 @@ const EventModal = ({ modalType, detailView, handleDetailView, content, setDetai const opEventActionMode = useMemo(() => { return eventActionData?.map(item => ({ - value: item.id, - name: `${item.description}(${item.id})` + value: item.groupId, + name: `${item.description}(${item.groupId})` })) || []; }, [eventActionData]); diff --git a/src/components/modal/RankingModal.js b/src/components/modal/RankingModal.js index 8a7293e..3bb6cba 100644 --- a/src/components/modal/RankingModal.js +++ b/src/components/modal/RankingModal.js @@ -79,8 +79,8 @@ const RankingModal = ({ modalType, detailView, handleDetailView, content, setDet const opEventActionMode = useMemo(() => { return eventActionData?.map(item => ({ - value: item.id, - name: `${item.description}(${item.id})` + value: item.groupId, + name: `${item.description}(${item.groupId})` })) || []; }, [eventActionData]); @@ -96,8 +96,9 @@ const RankingModal = ({ modalType, detailView, handleDetailView, content, setDet if (!checkCondition()) return; // const minAllowedTime = new Date(new Date().getTime() + 10 * 60000); - // const startDt = resultData.event_start_dt; - // const endDt = resultData.event_end_dt; + const startDt = resultData.start_dt; + const endDt = resultData.end_dt; + const baseDt = resultData.base_dt; // if (modalType === TYPE_REGISTRY && startDt < minAllowedTime) { // showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning}); // return; @@ -106,12 +107,16 @@ const RankingModal = ({ modalType, detailView, handleDetailView, content, setDet // 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; - // } + + if(startDt > baseDt){ + showToast('SCHEDULE_MODAL_START_DIFF_BASE_WARNING', {type: alertTypes.warning}); + return; + } + + if(startDt > endDt){ + showToast('SCHEDULE_MODAL_START_DIFF_END_WARNING', {type: alertTypes.warning}); + return; + } showModal(isView('modify') ? 'SCHEDULE_UPDATE_CONFIRM' : 'SCHEDULE_REGIST_CONFIRM', { type: alertTypes.confirm, @@ -132,7 +137,11 @@ const RankingModal = ({ modalType, detailView, handleDetailView, content, setDet if(data.result === "SUCCESS") { showToast('UPDATE_COMPLETED', {type: alertTypes.success}); }else{ - showToast('UPDATE_FAIL', {type: alertTypes.error}); + if(data.data.message){ + showToast(data.data.message, {type: alertTypes.error}); + }else{ + showToast('UPDATE_FAIL', {type: alertTypes.error}); + } } }).catch(reason => { showToast('API_FAIL', {type: alertTypes.error}); @@ -147,7 +156,11 @@ const RankingModal = ({ modalType, detailView, handleDetailView, content, setDet if(data.result === "SUCCESS") { showToast('REGIST_COMPLTE', {type: alertTypes.success}); }else{ - showToast('REGIST_FAIL', {type: alertTypes.error}); + if(data.data.message){ + showToast(data.data.message, {type: alertTypes.error}); + }else{ + showToast('REGIST_FAIL', {type: alertTypes.error}); + } } }).catch(reason => { showToast('API_FAIL', {type: alertTypes.error}); @@ -168,7 +181,6 @@ const RankingModal = ({ modalType, detailView, handleDetailView, content, setDet && resultData.event_action_id > 0 && resultData.title !== '' && resultData.refresh_interval > 0 - && resultData.initialization_interval > 0 && resultData.snapshot_interval > 0 ); }; @@ -368,9 +380,9 @@ export const initData = { start_dt: '', end_dt: '', base_dt: '', - refresh_interval: 60, - initialization_interval: 0, - snapshot_interval: 1440, + refresh_interval: 5, + initialization_interval: 240, + snapshot_interval: 60, meta_id: '', event_action_id: '', } diff --git a/src/components/searchBar/RankInfoSearchBar.js b/src/components/searchBar/RankInfoSearchBar.js new file mode 100644 index 0000000..57cc35e --- /dev/null +++ b/src/components/searchBar/RankInfoSearchBar.js @@ -0,0 +1,162 @@ +import { InputLabel, SelectInput, InputGroup } from '../../styles/Components'; +import { SearchBarLayout } from '../common/SearchBar'; +import { useCallback, useEffect, useState } from 'react'; +import { RankingSnapshotView, RankerListView } from '../../apis'; + +export const useRankInfoSearch = (token) => { + const [searchParams, setSearchParams] = useState({ + guid: '', + snapshot: '', + orderBy: 'DESC', + currentPage: 1 + }); + + const [loading, setLoading] = useState(false); + const [data, setData] = useState(null); + const [snapshotData, setSnapshotData] = useState([]); + + + useEffect(() => { + fetchData(searchParams); // 컴포넌트 마운트 시 초기 데이터 로드 + }, [token]); + + const fetchSnapshotData = useCallback(async (guid) => { + if (!guid) { + setSnapshotData([]); + return; + } + + try { + const result = await RankingSnapshotView( + token, + guid + ); + setSnapshotData(result || []); + } catch (error) { + console.error('Error fetching snapshot data:', error); + setSnapshotData([]); + } + }, [token]); + + useEffect(() => { + if (searchParams.guid) { + fetchSnapshotData(searchParams.guid); + } else { + setSnapshotData([]); + } + }, [searchParams.guid, fetchSnapshotData]); + + const fetchData = useCallback(async (params) => { + try { + if(params.snapshot === ''){ + setData(); + return; + } + + setLoading(true); + const result = await RankerListView( + token, + params.guid, + params.snapshot, + ); + setData(result); + return result; + } catch (error) { + console.error('Error fetching auction data:', error); + throw error; + } finally { + setLoading(false); + } + }, [token]); + + const updateSearchParams = useCallback((newParams) => { + setSearchParams(prev => ({ + ...prev, + ...newParams + })); + }, []); + + const handleSearch = useCallback(async (newParams = {}) => { + const updatedParams = { + ...searchParams, + ...newParams, + currentPage: newParams.currentPage || 1 // Reset to first page on new search + }; + updateSearchParams(updatedParams); + return await fetchData(updatedParams); + }, [searchParams, fetchData]); + + const handleReset = useCallback(async () => { + const resetParams = { + guid: '', + snapshot: '', + orderBy: 'DESC', + currentPage: 1 + }; + setSearchParams(resetParams); + return await fetchData(resetParams); + }, [fetchData]); + + const handlePageChange = useCallback(async (newPage) => { + return await handleSearch({ currentPage: newPage }); + }, [handleSearch]); + + const handleOrderByChange = useCallback(async (newOrder) => { + return await handleSearch({ orderBy: newOrder }); + }, [handleSearch]); + + return { + searchParams, + loading, + data, + snapshotData, + handleSearch, + handleReset, + handlePageChange, + handleOrderByChange, + updateSearchParams + }; +}; + +const RankInfoSearchBar = ({ searchParams, onSearch, onReset, scheduleData, snapshotData }) => { + const handleSubmit = event => { + event.preventDefault(); + + onSearch(searchParams); + }; + + const searchList = [ + <> + 랭킹스케줄 + + onSearch({ guid: e.target.value, snapshot: '' }, false)}> + + {scheduleData?.map((data, index) => ( + + ))} + + + 스냅샷 + + onSearch({ snapshot: e.target.value }, false)}> + + + {snapshotData?.map((data, index) => ( + + ))} + + + + ]; + return ; +}; + +export default RankInfoSearchBar; \ No newline at end of file diff --git a/src/components/searchBar/index.js b/src/components/searchBar/index.js index 80cf4f8..3579a84 100644 --- a/src/components/searchBar/index.js +++ b/src/components/searchBar/index.js @@ -40,6 +40,7 @@ import RankManageSearchBar, { useRankManageSearch } from './RankManageSearchBar' import LandAuctionSearchBar from './LandAuctionSearchBar'; import CaliumRequestSearchBar from './CaliumRequestSearchBar'; import UserSearchBar, {useUserSearch} from './UserSearchBar'; +import RankInfoSearchBar, {useRankInfoSearch} from './RankInfoSearchBar'; // 모든 SearchBar 컴포넌트 export export { @@ -101,5 +102,7 @@ export { useRankManageSearch, UserSearchBar, useUserSearch, + RankInfoSearchBar, + useRankInfoSearch, }; diff --git a/src/hooks/useRDSPagination.js b/src/hooks/useRDSPagination.js index 54c9209..a4892cd 100644 --- a/src/hooks/useRDSPagination.js +++ b/src/hooks/useRDSPagination.js @@ -1,4 +1,5 @@ import { useCallback, useState } from 'react'; +import { INITIAL_PAGE_SIZE } from '../assets/data/adminConstants'; /** * RDS 스타일의 페이지네이션을 위한 훅 @@ -12,7 +13,7 @@ export const useRDSPagination = (fetchFunction, initialState = {}) => { // 페이지네이션 상태 const [pagination, setPagination] = useState({ currentPage: initialState.currentPage || 1, - pageSize: initialState.pageSize || 10, + pageSize: initialState.pageSize || INITIAL_PAGE_SIZE, totalItems: initialState.totalItems || 0, totalPages: initialState.totalPages || 0 }); diff --git a/src/pages/DataManage/RankInfoView.js b/src/pages/DataManage/RankInfoView.js new file mode 100644 index 0000000..0944ef0 --- /dev/null +++ b/src/pages/DataManage/RankInfoView.js @@ -0,0 +1,52 @@ +import React, { useRef } from 'react'; + +import { Title} from '../../styles/Components'; + +import { authType } from '../../assets/data'; +import { withAuth } from '../../hooks/hook'; +import { AnimatedPageWrapper } from '../../components/common/Layout'; +import { TableSkeleton } from '../../components/Skeleton/TableSkeleton'; +import { Col, Divider, Row } from 'antd'; +import {RankingSnapshotInfo} from '../../components/DataManage'; + +const RankInfoView = () => { + + + return ( + + 랭킹 시스템 조회 + + + + + + + + + + + + + + ); +}; + +export default withAuth(authType.rankInfoRead)(RankInfoView); diff --git a/src/pages/DataManage/RankManage.js b/src/pages/DataManage/RankManage.js index 75a516f..aa40ec1 100644 --- a/src/pages/DataManage/RankManage.js +++ b/src/pages/DataManage/RankManage.js @@ -5,10 +5,6 @@ import { AnimatedPageWrapper } from '../../components/common/Layout' import styled from 'styled-components'; -import UserDefaultInfo from '../../components/DataManage/UserDefaultInfo'; -import UserAvatarInfo from '../../components/DataManage/UserAvatarInfo'; -import UserDressInfo from '../../components/DataManage/UserDressInfo'; - import { authType } from '../../assets/data'; import { withAuth } from '../../hooks/hook'; import { TabRankManageList } from '../../assets/data/options'; @@ -17,13 +13,13 @@ import { useUserSearch, } from '../../components/searchBar'; import { AnimatedTabs } from '../../components/common'; -import { UserRankPioneerInfo } from '../../components/DataManage'; +import { UserRankInfo } from '../../components/DataManage'; const RankManage = () => { const token = sessionStorage.getItem('token'); const [infoView, setInfoView] = useState('none'); - const [activeTab, setActiveTab] = useState('PIONEER'); + const [activeTab, setActiveTab] = useState('RANK'); const [resultData, setResultData] = useState(); const { @@ -50,9 +46,7 @@ const RankManage = () => { label: el.name, children: (() => { switch(el.value) { - case 'PIONEER': return ; - case 'RUN_RACE': return ; - case 'BATTLE_OBJECT': return ; + case 'RANK': return ; default: return null; } })() diff --git a/src/pages/DataManage/index.js b/src/pages/DataManage/index.js index 772ad81..0ecf36f 100644 --- a/src/pages/DataManage/index.js +++ b/src/pages/DataManage/index.js @@ -5,3 +5,4 @@ export { default as BusinessLogView} from './BusinessLogView'; export { default as MetaItemView} from './MetaItemView'; export { default as MetaCraftingView} from './MetaCraftingView'; export { default as RankManage} from './RankManage'; +export { default as RankInfoView} from './RankInfoView'; diff --git a/src/pages/ServiceManage/EventRegist.js b/src/pages/ServiceManage/EventRegist.js deleted file mode 100644 index 3ea2d07..0000000 --- a/src/pages/ServiceManage/EventRegist.js +++ /dev/null @@ -1,467 +0,0 @@ -import React, { useState, Fragment, useEffect } from 'react'; -import Button from '../../components/common/button/Button'; - -import { - Title, - BtnWrapper, - TextInput, - SelectInput, - Label, - InputLabel, - Textarea, - SearchBarAlert, -} from '../../styles/Components'; - -import { useNavigate } from 'react-router-dom'; -import { EventIsItem, EventSingleRegist } from '../../apis'; - -import { authList } from '../../store/authList'; -import { useRecoilValue } from 'recoil'; -import { useTranslation } from 'react-i18next'; -import { - AppendRegistBox, AppendRegistTable, AreaBtnClose, - BtnDelete, - Item, - ItemList, LangArea, ModalItem, ModalItemList, RegistGroup, - RegistInputItem, - RegistInputRow, RegistNotice, RegistTable, -} from '../../styles/ModuleComponents'; -import AuthModal from '../../components/common/modal/AuthModal'; -import { authType, benItems, currencyItemCode } from '../../assets/data'; -import DateTimeInput from '../../components/common/input/DateTimeInput'; -import { timeDiffMinute } from '../../utils'; -import { useAlert } from '../../context/AlertProvider'; -import { alertTypes, currencyCodeTypes } from '../../assets/data/types'; -import { useLoading } from '../../context/LoadingProvider'; -import { AnimatedPageWrapper } from '../../components/common/Layout'; - -const EventRegist = () => { - const navigate = useNavigate(); - const userInfo = useRecoilValue(authList); - const { t } = useTranslation(); - const token = sessionStorage.getItem('token'); - const { showToast, showModal } = useAlert(); - const { withLoading} = useLoading(); - - const [item, setItem] = useState(''); // 아이템 값 - const [itemCount, setItemCount] = useState(''); // 아이템 개수 - const [resource, setResource] = useState(currencyCodeTypes.gold); // 자원 값 - const [resourceCount, setResourceCount] = useState(''); // 자원 개수 - - const [isNullValue, setIsNullValue] = useState(false); - const [btnValidation, setBtnValidation] = useState(false); - const [itemCheckMsg, setItemCheckMsg] = useState(''); - - const [time, setTime] = useState({ - start_hour: '00', - start_min: '00', - end_hour: '00', - end_min: '00', - }); //시간 정보 - - const [resultData, setResultData] = useState({ - is_reserve: false, - start_dt: '', - end_dt: '', - event_type: 'ATTD', - mail_list: [ - { - title: '', - content: '', - language: 'KO', - }, - { - title: '', - content: '', - language: 'EN', - }, - { - title: '', - content: '', - language: 'JA', - } - ], - item_list: [], - guid: '', - }); //데이터 정보 - - useEffect(() => { - if (checkCondition()) { - setIsNullValue(false); - } else { - setIsNullValue(true); - } - }, [resultData]); - - useEffect(() => { - setItemCheckMsg(''); - }, [item]); - - const combineDateTime = (date, hour, min) => { - if (!date) return null; - return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, min); - }; - - // 날짜 처리 - const handleDateChange = (data, type) => { - const date = new Date(data); - setResultData({ - ...resultData, - [`${type}_dt`]: combineDateTime(date, time[`${type}_hour`], time[`${type}_min`]), - }); - }; - - // 시간 처리 - const handleTimeChange = (e, type) => { - const { id, value } = e.target; - const newTime = { ...time, [`${type}_${id}`]: value }; - setTime(newTime); - - const date = resultData[`${type}_dt`] ? resultData[`${type}_dt`] : new Date(); - - setResultData({ - ...resultData, - [`${type}_dt`]: combineDateTime(date, newTime[`${type}_hour`], newTime[`${type}_min`]), - }); - }; - - // 아이템 수량 숫자 체크 - const handleItemCount = e => { - if (e.target.value === '0' || e.target.value === '-0') { - setItemCount('1'); - e.target.value = '1'; - } else if (e.target.value < 0) { - let plusNum = Math.abs(e.target.value); - setItemCount(plusNum); - } else { - setItemCount(e.target.value); - } - }; - - // 아이템 추가 - const handleItemList = async () => { - if(benItems.includes(item)){ - showToast('MAIL_ITEM_ADD_BEN', {type: alertTypes.warning}); - return; - } - if(item.length === 0 || itemCount.length === 0) return; - - const token = sessionStorage.getItem('token'); - const result = await EventIsItem(token, {item: item}); - - if(result.data.result === "ERROR"){ - setItemCheckMsg(t('NOT_ITEM')); - return; - } - - const itemIndex = resultData.item_list.findIndex((data) => data.item === item); - if (itemIndex !== -1) { - showToast('MAIL_ITEM_ADD_DUPL', {type: alertTypes.warning}); - return; - } - const newItem = { item: item, item_cnt: itemCount, item_name: result.data.data.item_info.item_name }; - - resultData.item_list.push(newItem); - setItem(''); - setItemCount(''); - }; - - // 추가된 아이템 삭제 - const onItemRemove = id => { - let filterList = resultData.item_list && resultData.item_list.filter(item => item !== resultData.item_list[id]); - setResultData({ ...resultData, item_list: filterList }); - }; - - // 입력창 삭제 - const onLangDelete = language => { - let filterList = resultData.mail_list && resultData.mail_list.filter(el => el.language !== language); - - if (filterList.length === 1) { - setBtnValidation(true); - } else { - setBtnValidation(false); - } - - setResultData({ ...resultData, mail_list: filterList }); - }; - - // 자원 수량 숫자 체크 - const handleResourceCount = e => { - if (e.target.value === '0' || e.target.value === '-0') { - setResourceCount('1'); - e.target.value = '1'; - } else if (e.target.value < 0) { - let plusNum = Math.abs(e.target.value); - setResourceCount(plusNum); - } else { - setResourceCount(e.target.value); - } - }; - - // 자원 추가 - const handleResourceList = (e) => { - if(resource.length === 0 || resourceCount.length === 0) return; - - const itemIndex = resultData.item_list.findIndex( - (item) => item.item === resource - ); - - if (itemIndex !== -1) { - const item_cnt = resultData.item_list[itemIndex].item_cnt; - resultData.item_list[itemIndex].item_cnt = Number(item_cnt) + Number(resourceCount); - } else { - const name = currencyItemCode.find(well => well.value === resource).name; - const newItem = { item: resource, item_cnt: resourceCount, item_name: name }; - resultData.item_list.push(newItem); - } - setResourceCount(''); - }; - - const handleSubmit = async (type, param = null) => { - switch (type) { - case "submit": - if (!checkCondition()) return; - const timeDiff = timeDiffMinute(resultData.start_dt, (new Date)) - if(timeDiff < 60) { - showToast('EVENT_TIME_LIMIT_ADD', {type: alertTypes.warning}); - return; - } - - showModal('', { - type: alertTypes.confirmChildren, - onConfirm: () => handleSubmit('registConfirm'), - children: - {t('EVENT_REGIST_CONFIRM')} - {resultData.item_list && ( - - {resultData.item_list.map((data, index) => { - return ( - - - {data.item_name} {data.item_cnt.toLocaleString()} - - - ); - })} - - )} - - }); - break; - - case "registConfirm": - await withLoading(async () => { - return await EventSingleRegist(token, resultData); - }).then((result) => { - showToast('REGIST_COMPLTE', {type: alertTypes.success}); - }).catch(() => { - showToast('API_FAIL', {type: alertTypes.error}); - }).finally(() => { - callbackPage(); - }); - break; - } - } - - const callbackPage = () => { - navigate('/servicemanage/event'); - } - - const checkCondition = () => { - return ( - resultData.mail_list.every(data => data.content !== '' && data.title !== '') && - (resultData.start_dt.length !== 0) && - (resultData.end_dt.length !== 0) - ); - }; - - return ( - - {userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) ? ( - - ) : ( - <> - 이벤트 등록 - - - - 이벤트 타입 - setResultData({ ...resultData, event_type: e.target.value })} value={resultData.event_type}> - - - - handleDateChange(data, 'start')} - onChange={e => handleTimeChange(e, 'start')} - /> - handleDateChange(data, 'end')} - onChange={e => handleTimeChange(e, 'end')} - /> - - - {resultData.mail_list.map((data, idx) => { - return ( - - - - 언어 : {data.language} - {btnValidation === false ? ( - { - e.preventDefault(); - onLangDelete(data.language); - }} - /> - ) : ( - - )} - - - - - - - - - -
{getFieldLabel(item.sourceInfo.operationType)}{item.fieldName}{getFieldLabel(item.fieldName)} {formatValue(item.newValue)} {formatValue(item.oldValue)} {item.sourceInfo.worker}
- - - - { - if (e.target.value.length > 30) { - return; - } - let list = [...resultData.mail_list]; - let findIndex = resultData.mail_list && resultData.mail_list.findIndex(item => item.language === e.target.id); - list[findIndex].title = e.target.value.trimStart(); - - setResultData({ ...resultData, mail_list: list }); - }} - /> - - 29 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({data.title.length}/30자) -
- - -