Compare commits

...

4 Commits

Author SHA1 Message Date
760153c700 인스턴스 조회 추가 2025-11-28 16:40:32 +09:00
8dae810e3a 인스턴스 조회 추가 2025-11-28 16:40:18 +09:00
ac9bcdda8b 로그인정보 만료시 비밀번호 초기화 추가,
랭킹 강제 초기화 버튼 추가,
랭킹 시스템 조회 및 수정
2025-11-28 16:39:39 +09:00
3264e94093 로그인 비밀번호 초기화 추가 2025-10-27 18:54:45 +09:00
36 changed files with 1503 additions and 620 deletions

2
.gitignore vendored
View File

@@ -32,3 +32,5 @@ yarn-error.log*
/.idea/misc.xml
/.idea/modules.xml
/.idea/vcs.xml
/.idea/git_toolbox_blame.xml
/.idea/git_toolbox_prj.xml

View File

@@ -21,6 +21,8 @@ import {
MetaItemView,
RankManage,
MetaCraftingView,
RankInfoView,
MetaInstanceView
} from './pages/DataManage';
import {
Board,
@@ -72,6 +74,8 @@ const RouteInfo = () => {
<Route path="itemdictionary" element={<MetaItemView />} />
<Route path="craftdictionary" element={<MetaCraftingView />} />
<Route path="rankmanage" element={<RankManage />} />
<Route path="rankview" element={<RankInfoView />} />
<Route path="instancedictionary" element={<MetaInstanceView />} />
</Route>
<Route path="/servicemanage">
<Route path="board" element={<Board />} />

View File

@@ -1,79 +0,0 @@
import { Route, Routes } from 'react-router-dom';
import { Layout, LoginLayout, MainLayout } from './components/common/Layout';
import LoginBg from './assets/img/login-bg.png';
import { Login } from './pages/Login';
import LoginFail from './pages/LoginFail';
import { AccountEdit, AccountRegist, PasswordReset } from './pages/Account';
import Main from './pages/Main';
import {
AdminView,
AuthSetting,
AuthSettingUpdate,
CaliumRequest,
CaliumRequestRegist,
LogView,
} from './pages/UserManage';
import { EconomicIndex, UserIndex } from './pages/IndexManage';
import { ContentsView, CryptView, GameLogView, UserView } from './pages/DataManage';
import {
Board,
Event,
EventRegist,
Items,
Mail,
MailRegist,
ReportList,
UserBlock,
UserBlockRegist,
WhiteList,
} from './pages/ServiceManage';
const RouteInfo = () => {
return (
<Routes>
<Route element={<LoginLayout $bgimg={LoginBg} $padding="50px" />}>
<Route path="/" element={<Login />} />
<Route path="/fail" element={<LoginFail />} />
<Route path="/account/regist" element={<AccountRegist />} />
<Route path="/account/pwdreset" element={<PasswordReset />} />
<Route path="/account/edit" element={<AccountEdit />} />
</Route>
<Route element={<MainLayout />}>
<Route path="/main" element={<Main />} />
</Route>
<Route element={<Layout />}>
<Route path="/usermanage/">
<Route path="adminview" element={<AdminView />} />
<Route path="logview" element={<LogView />} />
<Route path="authsetting" element={<AuthSetting />} />
<Route path="authsetting/:id" element={<AuthSettingUpdate />} />
<Route path="caliumrequest" element={<CaliumRequest />} />
</Route>
<Route path="/indexmanage">
<Route path="userindex" element={<UserIndex />} />
<Route path="economicindex" element={<EconomicIndex />} />
</Route>
<Route path="/datamanage">
<Route path="userview" element={<UserView />} />
<Route path="contentsview" element={<ContentsView />} />
<Route path="gamelogview" element={<GameLogView />} />
<Route path="cryptview" element={<CryptView />} />
</Route>
<Route path="/servicemanage">
<Route path="board" element={<Board />} />
<Route path="whitelist" element={<WhiteList />} />
<Route path="mail" element={<Mail />} />
<Route path="mail/mailregist" element={<MailRegist />} />
<Route path="userblock" element={<UserBlock />} />
<Route path="userblock/userblockregist" element={<UserBlockRegist />} />
<Route path="reportlist" element={<ReportList />} />
<Route path="items" element={<Items />} />
<Route path="event" element={<Event />} />
<Route path="event/eventregist" element={<EventRegist />} />
</Route>
</Route>
</Routes>
)
}
export default RouteInfo;

View File

@@ -85,10 +85,10 @@ export const AdminDeleteUser = async (token, params) => {
}
};
export const AdminChangePw = async (token, params) => {
export const AdminChangePw = async ( params) => {
try {
const res = await Axios.post('/api/v1/admin/init-password', params, {
headers: { Authorization: `Bearer ${token}` },
headers: { },
});
return res.data;

View File

@@ -77,6 +77,43 @@ export const CraftingDictionaryExport = async (token, params) => {
}
};
export const getInstanceDictionaryList = async (token, searchType, searchData, contentsType, accessType, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/dictionary/instance/list?search_type=${searchType}&search_data=${searchData}
&contents_type=${contentsType}&access_type=${accessType}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('getInstanceDictionaryList API error:', error);
throw error;
}
};
export const InstanceDictionaryExport = async (token, params) => {
try {
await Axios.get(`/api/v1/dictionary/instance/excel-export?search_type=${params.search_type}&search_data=${params.search_data}
&contents_type=${params.contents_type}&access_type=${params.access_type}
&lang=${params.lang}&task_id=${params.taskId}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob'
}).then(response => {
responseFileDownload(response, {
defaultFileName: 'instanceDictionary'
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('InstanceDictionaryExport Error', e);
}
}
};
export const BrandView = async (token) => {
try {
const res = await Axios.get(

View File

@@ -1,21 +0,0 @@
//AI api 연결
import { Axios } from '../utils';
export const AnalyzeAI = async (token, params) => {
try {
const res = await Axios.post('/api/v1/ai/analyze', params, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('analyzeAI Error', e);
}
}
};

View File

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

View File

@@ -4,6 +4,7 @@ import historyAPI from './historyAPI.json';
import eventAPI from './eventAPI.json';
import rankingAPI from './rankingAPI.json';
import metaCraftingAPI from './metaCraftingAPI.json';
import metaInstanceAPI from './metaInstanceAPI.json';
export {
itemAPI,
@@ -11,5 +12,6 @@ export {
historyAPI,
eventAPI,
rankingAPI,
metaCraftingAPI
metaCraftingAPI,
metaInstanceAPI
};

View File

@@ -0,0 +1,11 @@
{
"baseUrl": "/api/v1/dictionary/instance",
"endpoints": {
"getInstanceDictionaryList": {
"method": "GET",
"url": "/list",
"dataPath": "data",
"paramFormat": "query"
}
}
}

View File

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

View File

@@ -100,7 +100,7 @@ export const menuConfig = {
permissions: {
read: authType.gameLogRead
},
view: false,
view: true,
authLevel: adminAuthLevel.NONE
},
businesslogview: {
@@ -127,10 +127,27 @@ export const menuConfig = {
view: true,
authLevel: adminAuthLevel.NONE
},
instancedictionary: {
title: '인스턴스 조회',
permissions: {
read: authType.instanceDictionaryRead
},
view: true,
authLevel: adminAuthLevel.NONE
},
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

View File

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

View File

@@ -0,0 +1,49 @@
{
"initialSearchParams": {
"search_type": "ID",
"search_data": "",
"contents_type": "ALL",
"access_type": "ALL",
"orderBy": "DESC",
"pageSize": 50,
"currentPage": 1
},
"searchFields": [
{
"type": "select",
"id": "search_type",
"optionsRef": "instanceSearchType",
"col": 1
},
{
"type": "text",
"id": "search_data",
"placeholder": "인스턴스 입력",
"width": "300px",
"col": 1
},
{
"type": "select",
"id": "contents_type",
"label": "컨텐츠 타입",
"optionsRef": "opInstanceContentsType",
"col": 1
},
{
"type": "select",
"id": "access_type",
"label": "입장 방식",
"optionsRef": "opInstanceAccessType",
"col": 1
}
],
"apiInfo": {
"endpointName": "getInstanceDictionaryList",
"loadOnMount": true,
"pageField": "page_no",
"pageSizeField": "page_size",
"orderField": "orderBy"
}
}

View File

@@ -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": "선택 삭제",

View File

@@ -62,6 +62,8 @@ export const authType = {
worldEventUpdate: 60,
worldEventDelete: 61,
craftingDictionaryRead: 62,
rankInfoRead: 63,
instanceDictionaryRead: 64,
levelReader: 999,

View File

@@ -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 ? <TableSkeleton width='30%' count={4} /> :
<>
<ToolWrapper>
<UserInfoTable $maxwidth="570px">
<colgroup>
<col width="120" />
<col width="30%" />
<col width="70%" />
</colgroup>
<tbody>
{rowData && rowData.map((el, idx) => {
return (
<tr key={idx}>
<th>{el.title}</th>
<td>{el.itemNo}</td>
<td>{el.itemName}</td>
</tr>
);
})}
</tbody>
</UserInfoTable>
</ToolWrapper>
</>
);
};
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;
}
}
`;

View File

@@ -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 (
<>
<FormWrapper>
<RankInfoSearchBar
config={config}
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
scheduleData={rankingScheduleData}
snapshotData={snapshotData}
/>
</FormWrapper>
<ViewTableInfo>
<ExcelDownButton tableRef={tableRef} fileName={t('FILE_RANKING_SNAPSHOT')} />
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<TableWrapper>
<TableStyle ref={tableRef}>
<caption></caption>
<thead>
<tr>
<th width="80">순위</th>
<th width="120">account ID</th>
<th width="200">GUID</th>
<th width="150">아바타명</th>
<th width="80">점수</th>
</tr>
</thead>
<tbody>
{dataList?.ranker_List?.map((rank, index) => (
<tr key={index}>
<td>{rank.rank}</td>
<td>{rank.account_id}</td>
<td>{rank.user_guid}</td>
<td>{rank.nickname}</td>
<td>{rank.score_type === "Time" ? formatTimeFromSeconds(rank.score) : rank.score}</td>
</tr>
))}
</tbody>
</TableStyle>
</TableWrapper>
}
</>
);
};
export default RankingSnapshotInfo;

View File

@@ -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 ? (
<Button
type="primary"
onClick={() => handleSubmit('valueChange', rankType.value)}
>
수정
</Button>
) : 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 ? <TableSkeleton width='30%' count={5} /> :
<>
<div>
<UserDefault>
<Table
columns={columns}
dataSource={data}
pagination={false}
showHeader={false}
bordered
/>
</UserDefault>
</div>
<InputConfirmModal
inputType='number'
view={modalState.valueChangeModal}
handleSubmit={() => handleSubmit('valueChangeConfirm')}
handleCancel={() => handleModalClose('valueChange')}
handleClose={() => handleModalClose('valueChange')}
value={updateValue}
setValue={setUpdateValue}
inputText={t('UPDATE_VALUE_COMMENT',{comment:comment})}
/>
</>
);
};
export default UserRankInfo;

View File

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

View File

@@ -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 (
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={view}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleClose} />
</BtnWrapper>
<ModalText $align="center">
<InputItem>
<p>{inputText}</p>
{ inputType === 'number' &&
<InputNumber
style={{width: '100%'}}
value={value}
min={0}
step={1}
onChange={(value) => setValue(value)}
/>}
{ inputType === 'text' &&
<Input
value={value}
onChange={(value) => setValue(value)}
/>}
</InputItem>
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleCancel} />
<Button
text="확인"
theme="primary"
type="submit"
size="large"
width="100%"
handleClick={handleSubmit}
/>
</BtnWrapper>
</Modal>
);
}
export default InputConfirmModal;

View File

@@ -161,7 +161,7 @@ const LogDetailModal = ({ detailView,
return (
<tr key={index}>
<td>{getFieldLabel(item.sourceInfo.operationType)}</td>
<td>{item.fieldName}</td>
<td>{getFieldLabel(item.fieldName)}</td>
<td>{formatValue(item.newValue)}</td>
<td>{formatValue(item.oldValue)}</td>
<td>{item.sourceInfo.worker}</td>

View File

@@ -1,5 +1,5 @@
import styled from 'styled-components';
import { useState } from 'react';
import React, { useState } from 'react';
import LoginModal from './LoginModal';
import Button from '../../components/common/button/Button';
@@ -7,14 +7,22 @@ import Modal from '../common/modal/Modal';
import { Title, BtnWrapper, ButtonClose } from '../../styles/Components';
import { TextInput } from '../../styles/Components';
import { AuthLogin } from '../../apis';
import { AdminChangePw, AuthLogin } from '../../apis';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { authList } from '../../store/authList';
import { alertTypes } from '../../assets/data/types';
import ToastAlert from '../common/alert/ToastAlert';
import { useTranslation } from 'react-i18next';
import Loading from '../common/Loading';
const LoginForm = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [stateModal, setStateModal] = useState('hidden');
const [errorText, setErrorText] = useState('');
const navigate = useNavigate();
const [toast, setToast] = useState(null)
const [isLoading, setIsLoading] = useState(false);
const handleModal = () => {
if (stateModal === 'hidden') {
@@ -28,6 +36,25 @@ const LoginForm = () => {
const values = watch();
const showToast = (message, type = alertTypes.info) => {
const toastData = {
id: Date.now(),
message,
type,
position: 'top-center'
};
setToast(toastData);
// 5초 후 자동으로 토스트 제거
setTimeout(() => {
setToast(null);
}, 5000);
};
const closeToast = () => {
setToast(null);
};
const onSubmit = async data => {
const result = await AuthLogin(data);
setErrorText(result.data.message);
@@ -45,6 +72,24 @@ const LoginForm = () => {
}
};
const handlePasswordInit = async () => {
setIsLoading(true);
await AdminChangePw({ email: values.email })
.then(res => {
if (res.status === 200) {
showToast(t('PASSWORD_INIT_COMPLETE'), alertTypes.success);
} else {
showToast(t('PASSWORD_INIT_ERROR'), alertTypes.error);
}
}).catch(err => {
showToast(t('API_FAIL'), alertTypes.error);
}).finally(() => {
setIsLoading(false);
handleModal('hidden');
});
}
return (
<>
<FormWrapper action="" $flow="column" onSubmit={handleSubmit(onSubmit)}>
@@ -93,15 +138,37 @@ const LoginForm = () => {
text="확인"
theme="line"
size="large"
width="100%"
width="50%"
handleClick={e => {
e.preventDefault();
handleModal('hidden');
}}
/>
<Button
text="비밀번호 초기화"
theme="line"
size="large"
width="50%"
handleClick={e => {
e.preventDefault();
handlePasswordInit();
}}
/>
</BtnWrapper>
</Modal>
)}
{toast && (
<ToastAlert
key={toast.id}
id={toast.id}
message={toast.message}
type={toast.type}
position={toast.position}
onClose={closeToast}
/>
)}
{isLoading && <Loading />}
</>
);
};

View File

@@ -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]);

View File

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

View File

@@ -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 = [
<>
<InputLabel>랭킹스케줄</InputLabel>
<InputGroup>
<SelectInput value={searchParams.guid} onChange={e => onSearch({ guid: e.target.value, snapshot: '' }, false)}>
<option value=""></option>
{scheduleData?.map((data, index) => (
<option key={index} value={data.guid}>
{data.title}
</option>
))}
</SelectInput>
</InputGroup>
<InputLabel>스냅샷</InputLabel>
<InputGroup>
<SelectInput value={searchParams.snapshot} onChange={e => onSearch({ snapshot: e.target.value }, false)}>
<option value="">
{!searchParams.guid ? '먼저 랭킹스케줄을 선택하세요' :
snapshotData === '' || snapshotData === undefined ? '로딩 중...' :
'스냅샷을 선택하세요'}
</option>
{snapshotData?.map((data, index) => (
<option key={index} value={data.snapshot_index}>
{data.snapshot_index}({data.snapshot_time})
</option>
))}
</SelectInput>
</InputGroup>
</>
];
return <SearchBarLayout firstColumnData={searchList} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default RankInfoSearchBar;

View File

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

View File

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

View File

@@ -35,6 +35,8 @@ const resources = {
SAVE_COMPLETED: '저장이 완료되었습니다.',
SAVE_CONFIRM: '저장 하시겠습니까?',
UPDATE_CONFIRM: '수정하시겠습니까?',
UPDATE_VALUE: '변경할 값을 입력해주세요.',
UPDATE_VALUE_COMMENT: '변경할 값을 입력해주세요.\n({{comment}})',
LENGTH_TEXT_LIMIT_100: '요청사유는 100글자 까지만 입력하실 수 있습니다.({{count}}/100)',
LENGTH_NUMBER_POINT_2: '숫자, 소수점 둘째자리',
EXCEL_SELECT: 'Excel 파일을 선택해주세요.',
@@ -55,8 +57,12 @@ const resources = {
DOWNLOAD_COMPLETE: '다운이 완료되었습니다.',
DOWNLOAD_FAIL: '다운이 실패하였습니다.',
DELETE_STATUS_ONLY_WAIT: '대기상태의 데이터만 삭제가 가능합니다.',
UPDATE_STATUS_ONLY_RUNNING: '진행상태의 데이터만 새로고침이 가능합니다.',
TABLE_DATA_NOT_FOUND: '데이터가 없습니다.',
ITEM_ID_EMPTY_WARNING: '아이템 아이디를 입력해주세요.',
//login
PASSWORD_INIT_COMPLETE: '비밀번호 초기화 메일이 발송되었습니다.\r\n메일을 확인해주세요.',
PASSWORD_INIT_ERROR: '비밀번호 초기화에 실패하였습니다. 잠시 후 다시 한번 진행해 주세요.\n오류가 지속될 경우, 담당자에게 문의해주세요.',
//user
NICKNAME_CHANGES_CONFIRM: '닉네임을 변경하시겠습니까?',
NICKNAME_CHANGES_COMPLETE: '닉네임 변경이 완료되었습니다.',
@@ -148,6 +154,15 @@ const resources = {
SCHEDULE_SELECT_DELETE: "선택된 스케줄을 삭제하시겠습니까?",
SCHEDULE_REGIST_CONFIRM: "스케줄을 등록하시겠습니까?",
SCHEDULE_UPDATE_CONFIRM: "스케줄을 수정하시겠습니까?",
SCHEDULE_MODAL_START_DIFF_BASE_WARNING: "기준시간은 시작 시간보다 작을 수 없습니다.",
SCHEDULE_MODAL_START_DIFF_END_WARNING: "종료시간은 시작 시간보다 작을 수 없습니다.",
SCHEDULE_REFRESH_GUID_NULL_WARNING: "재조회 후 다시 시도해주세요.",
SCHEDULE_SELECT_UPDATE: "선택된 스케줄의 랭킹을 강제 새로고침 하시겠습니까?",
SCHEDULE_SELECT_INIT: "선택된 스케줄의 랭킹을 강제 초기화 하시겠습니까?",
SCHEDULE_SELECT_SNAPSHOT: "선택된 스케줄의 랭킹을 강제 스냅샷 하시겠습니까?",
SCHEDULE_REFRESH_COMPLETE: '랭킹 새로고침을 서버에 요청하였습니다.\n변경사항은 잠시 후 확인해주세요.',
SCHEDULE_INIT_COMPLETE: '랭킹 초기화를 서버에 요청하였습니다.\n변경사항은 잠시 후 확인해주세요.',
SCHEDULE_SNAPSHOT_COMPLETE: '랭킹 스냅샷을 서버에 요청하였습니다.\n변경사항은 잠시 후 확인해주세요.',
//메뉴 배너
MENU_BANNER_TITLE: "메뉴 배너 관리",
MENU_BANNER_CREATE: "메뉴 배너 등록",
@@ -185,6 +200,7 @@ const resources = {
FILE_LAND_AUCTION: 'Caliverse_Land_Auction.xlsx',
FILE_BUSINESS_LOG: 'Caliverse_Log',
FILE_BATTLE_EVENT: 'Caliverse_Battle_Event.xlsx',
FILE_RANKING_SNAPSHOT: 'Caliverse_Ranking_Snapshot.xlsx',
FILE_GAME_LOG_CURRENCY: 'Caliverse_Game_Log_Currency',
FILE_GAME_LOG_USER_CREATE: 'Caliverse_Game_Log_User_Create',
FILE_GAME_LOG_USER_LOGIN: 'Caliverse_Game_Log_User_Login',

View File

@@ -0,0 +1,207 @@
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AnimatedPageWrapper } from '../../components/common/Layout';
import {
Title,
TableStyle,
FormWrapper,
TableWrapper,
DownloadContainer, CircularProgressWrapper,
} from '../../styles/Components';
import { withAuth } from '../../hooks/hook';
import {
authType,
} from '../../assets/data';
import { useTranslation } from 'react-i18next';
import {
AnimatedTabs,
ViewTableInfo,
} from '../../components/common';
import { TableSkeleton } from '../../components/Skeleton/TableSkeleton';
import CircularProgress from '../../components/common/CircularProgress';
import {
INITIAL_PAGE_LIMIT,
} from '../../assets/data/adminConstants';
import ExcelExportButton from '../../components/common/button/ExcelExportButton';
import Pagination from '../../components/common/Pagination/Pagination';
import { CommonSearchBar } from '../../components/searchBar';
import { languageNames } from '../../assets/data/types';
import useCommonSearch from '../../hooks/useCommonSearch';
const MetaInstanceView = () => {
const token = sessionStorage.getItem('token');
const { t } = useTranslation();
const tableRef = useRef(null);
const [activeLanguage, setActiveLanguage] = useState('ko');
const [downloadState, setDownloadState] = useState({
loading: false,
progress: 0
});
const {
config,
searchParams,
data: dataList,
handleSearch,
handleReset,
handleOrderByChange,
updateSearchParams,
loading,
configLoaded,
handlePageChange,
handlePageSizeChange
} = useCommonSearch("metaInstanceSearch");
useEffect(()=>{
setDownloadState({
loading: false,
progress: 0
});
},[dataList]);
const tableHeaders = useMemo(() => {
return [
{ id: 'instance_id', label: '인스턴스 ID', width: '100px' },
{ id: 'instance_name', label: '인스턴스 명', width: '200px' },
{ id: 'owner', label: '소유권', width: '200px' },
{ id: 'building_id', label: '빌딩 ID', width: '100px' },
{ id: 'building_socket', label: '빌딩 소켓 넘버', width: '80px' },
{ id: 'contents_type', label: '컨텐츠 타입', width: '150px' },
{ id: 'map_id', label: '맵 ID', width: '80px' },
{ id: 'limit_count', label: '제한 인원 수', width: '100px'},
{ id: 'over_limit', label: '정원 초과 시 추가 생성 여부', width: '90px' },
{ id: 'access_type', label: '입장 방식', width: '80px' },
{ id: 'access_id', label: '입장시 필요 아이템', width: '100px' },
{ id: 'voice_chat', label: '음성채팅 옵션', width: '100px' },
{ id: 'view_type', label: '시야 타입', width: '100px' },
];
}, []);
const renderTableForLanguage = useCallback((languageKey) => {
// 해당 언어의 아이템 리스트 가져오기
const languageInstanceList = dataList?.instance_list?.[languageKey] || [];
return (
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => (
<th key={header.id} width={header.width}>{header.label}</th>
))}
</tr>
</thead>
<tbody>
{languageInstanceList?.map((item, index) => (
<Fragment key={`${languageKey}-${index}`}>
<tr>
<td>{item.instance_id}</td>
<td>{item.instance_name || '-'}</td>
<td>{item.owner || '-'}</td>
<td>{item.building_id || '-'}</td>
<td>{item.building_socket}</td>
<td>{item.contents_type}</td>
<td>{item.map_id}</td>
<td>{item.limit_count}</td>
<td>{item.over_limit}</td>
<td>{item.access_type}</td>
<td>{item.access_id}</td>
<td>{item.voice_chat}</td>
<td>{item.view_type}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{languageInstanceList.length > 0 &&
<Pagination
postsPerPage={searchParams.pageSize}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams?.currentPage}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
</>
);
}, [dataList, loading, tableHeaders, searchParams, handlePageChange]);
// 언어별 탭 아이템 생성
const tabItems = useMemo(() => {
// 실제 데이터에서 사용 가능한 언어만 탭으로 생성
const availableLanguages = dataList?.instance_list ? Object.keys(dataList.instance_list) : ['ko', 'en', 'ja'];
return availableLanguages.map(langKey => ({
key: langKey,
label: languageNames[langKey.charAt(0).toUpperCase() + langKey.slice(1)] || langKey.toUpperCase(),
children: renderTableForLanguage(langKey)
}));
}, [dataList, renderTableForLanguage]);
const handleTabChange = (key) => {
setActiveLanguage(key);
};
const excelParams = useMemo(() => ({
...searchParams,
lang: activeLanguage
}), [searchParams, activeLanguage]);
return (
<AnimatedPageWrapper>
<Title>인스턴스 조회</Title>
<FormWrapper>
<CommonSearchBar
config={config}
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo orderType="asc" total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
<DownloadContainer>
<ExcelExportButton
functionName="InstanceDictionaryExport"
params={excelParams}
fileName={t('FILE_DICTIONARY_CRAFTING')}
onLoadingChange={setDownloadState}
dataSize={dataList?.total_all}
/>
{downloadState.loading && (
<CircularProgressWrapper>
<CircularProgress
progress={downloadState.progress}
size={36}
progressColor="#4A90E2"
/>
</CircularProgressWrapper>
)}
</DownloadContainer>
</ViewTableInfo>
{
loading ? <TableSkeleton width='100%' count={40} /> :
<AnimatedTabs
items={tabItems}
activeKey={activeLanguage}
onChange={handleTabChange}
tabPosition="left"
/>
}
</AnimatedPageWrapper>
);
};
export default withAuth(authType.instanceDictionaryRead)(MetaInstanceView);

View File

@@ -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 (
<AnimatedPageWrapper>
<Title>랭킹 시스템 조회</Title>
<Row
gutter={[24, 16]}
style={{
minHeight: 'calc(100vh - 200px)',
alignItems: 'stretch'
}}
>
<Col
span={11}
style={{
display: 'flex',
flexDirection: 'column'
}}
>
<RankingSnapshotInfo />
</Col>
<Col span={0} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Divider type="vertical" style={{ height: '100%', margin: 0 }} />
</Col>
<Col
span={12}
style={{
display: 'flex',
flexDirection: 'column'
}}
>
<RankingSnapshotInfo />
</Col>
</Row>
</AnimatedPageWrapper>
);
};
export default withAuth(authType.rankInfoRead)(RankInfoView);

View File

@@ -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 <UserRankPioneerInfo userInfo={resultData} />;
case 'RUN_RACE': return <UserAvatarInfo userInfo={resultData} />;
case 'BATTLE_OBJECT': return <UserDressInfo userInfo={resultData} />;
case 'RANK': return <UserRankInfo userInfo={resultData} />;
default: return null;
}
})()

View File

@@ -5,3 +5,5 @@ 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';
export { default as MetaInstanceView} from './MetaInstanceView';

View File

@@ -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: <ModalItem>
{t('EVENT_REGIST_CONFIRM')}
{resultData.item_list && (
<ModalItemList>
{resultData.item_list.map((data, index) => {
return (
<Item key={index}>
<span>
{data.item_name} {data.item_cnt.toLocaleString()}
</span>
</Item>
);
})}
</ModalItemList>
)}
</ModalItem>
});
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 (
<AnimatedPageWrapper>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) ? (
<AuthModal/>
) : (
<>
<Title>이벤트 등록</Title>
<RegistGroup>
<RegistInputRow>
<RegistInputItem>
<InputLabel>이벤트 타입</InputLabel>
<SelectInput onChange={e => setResultData({ ...resultData, event_type: e.target.value })} value={resultData.event_type}>
<option value="ATTD">출석 이벤트</option>
</SelectInput>
</RegistInputItem>
<DateTimeInput
title="시작 시간"
dateName="시작 일자"
selectedDate={resultData.start_dt}
handleSelectedDate={data => handleDateChange(data, 'start')}
onChange={e => handleTimeChange(e, 'start')}
/>
<DateTimeInput
title="종료 시간"
dateName="종료 일자"
selectedDate={resultData.end_dt}
handleSelectedDate={data => handleDateChange(data, 'end')}
onChange={e => handleTimeChange(e, 'end')}
/>
</RegistInputRow>
</RegistGroup>
{resultData.mail_list.map((data, idx) => {
return (
<Fragment key={idx}>
<AppendRegistBox>
<LangArea>
언어 : {data.language}
{btnValidation === false ? (
<AreaBtnClose
onClick={e => {
e.preventDefault();
onLangDelete(data.language);
}}
/>
) : (
<AreaBtnClose opacity="10%" />
)}
</LangArea>
<RegistTable>
<tbody>
<tr>
<th width="120">
<Label>제목</Label>
</th>
<td>
<RegistInputItem>
<TextInput
placeholder="우편 제목 입력"
maxLength="30"
id={data.language}
value={data.title}
onChange={e => {
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 });
}}
/>
</RegistInputItem>
<RegistNotice $color={data.title.length > 29 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({data.title.length}/30)</RegistNotice>
</td>
</tr>
<tr>
<th>
<Label>내용</Label>
</th>
<td>
<Textarea
maxLength="2000"
value={data.content}
id={data.language}
onChange={e => {
if (e.target.value.length > 2000) {
return;
}
let list = [...resultData.mail_list];
let findIndex = resultData.mail_list && resultData.mail_list.findIndex(item => item.language === e.target.id);
list[findIndex].content = e.target.value.trimStart();
setResultData({ ...resultData, mail_list: list });
}}
/>
<RegistNotice $color={data.content.length > 1999 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({data.content.length}/2000)</RegistNotice>
</td>
</tr>
</tbody>
</RegistTable>
</AppendRegistBox>
</Fragment>
);
})}
<AppendRegistBox>
<AppendRegistTable>
<tbody>
<tr>
<th width="120">
<Label>아이템 첨부</Label>
</th>
<td>
<RegistInputItem>
<TextInput placeholder="Item Meta id 입력" value={item} onChange={e => setItem(e.target.value.trimStart())} />
<TextInput placeholder="수량" type="number" value={itemCount} onChange={e => handleItemCount(e)} width="100px" />
<Button text="추가" theme={itemCount.length === 0 || item.length === 0 ? 'disable' : 'search'} handleClick={handleItemList} width="100px" height="35px" />
{itemCheckMsg && <SearchBarAlert>{itemCheckMsg}</SearchBarAlert>}
</RegistInputItem>
</td>
</tr>
<tr>
<th width="120">
<Label>자원 첨부</Label>
</th>
<td>
<RegistInputItem>
<SelectInput onChange={e => setResource(e.target.value)} value={resource}>
{currencyItemCode.filter(data => data.value !== currencyCodeTypes.calium).map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput placeholder="수량" type="number" value={resourceCount} onChange={e => handleResourceCount(e)} width="200px" />
<Button text="추가" theme={resourceCount.length === 0 || resource.length === 0 ? 'disable' : 'search'} handleClick={handleResourceList} width="100px" height="35px" />
</RegistInputItem>
<div>
{resultData.item_list && (
<ItemList>
{resultData.item_list.map((data, index) => {
return (
<Item key={index}>
<span>
{data.item_name}[{data.item}] ({data.item_cnt})
</span>
<BtnDelete onClick={() => onItemRemove(index)}></BtnDelete>
</Item>
);
})}
</ItemList>
)}
</div>
</td>
</tr>
</tbody>
</AppendRegistTable>
</AppendRegistBox>
{isNullValue && (
<SearchBarAlert $align="right" $padding="0 0 15px">
{t('NULL_MSG')}
</SearchBarAlert>
)}
<BtnWrapper $justify="flex-end" $gap="10px">
<Button
text="취소"
theme="line"
handleClick={() => showModal('EVENT_REGIST_CANCEL', {
type: alertTypes.confirm,
onConfirm: () => callbackPage()
})}
/>
<Button
type="submit"
text="등록"
theme={checkCondition() ? 'primary' : 'disable'}
handleClick={() => handleSubmit('submit')}
/>
</BtnWrapper>
</>
)}
</AnimatedPageWrapper>
);
};
export default EventRegist;

View File

@@ -15,8 +15,8 @@ import { useDataFetch, useModal, useTable, withAuth } from '../../hooks/hook';
import {
EventActionView,
LogHistory,
RankingDataView, RankingScheduleDelete,
RankingScheduleDetailView,
RankingDataView, RankingInit, RankingScheduleDelete,
RankingScheduleDetailView, RankingSnapshot, RankingUpdate,
} from '../../apis';
import { CommonSearchBar } from '../../components/ServiceManage';
import { useAlert } from '../../context/AlertProvider';
@@ -101,6 +101,102 @@ const Ranking = () => {
handleModalView('detail');
});
break;
case "update":
const sel = selectedRows[0];
const guid = sel.guid;
if(guid === null || guid === undefined || guid === "") {
showToast('SCHEDULE_REFRESH_GUID_NULL_WARNING', {type: alertTypes.warning});
return;
}
if(sel.status !== CommonStatus.running) {
showToast('UPDATE_STATUS_ONLY_RUNNING', {type: alertTypes.warning});
return;
}
showModal('SCHEDULE_SELECT_UPDATE', {
type: alertTypes.confirm,
onConfirm: () => handleAction('updateConfirm', guid)
});
break;
case "updateConfirm":
await withLoading(async () => {
return await RankingUpdate(token, item);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('SCHEDULE_REFRESH_COMPLETE', {type: alertTypes.success});
}else{
showToast(data.data.message, {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
});
break;
case "rankingInit":
const initSel = selectedRows[0];
const initGuid = initSel.guid;
if(initGuid === null || initGuid === undefined || initGuid === "") {
showToast('SCHEDULE_REFRESH_GUID_NULL_WARNING', {type: alertTypes.warning});
return;
}
if(initSel.status !== CommonStatus.running) {
showToast('UPDATE_STATUS_ONLY_RUNNING', {type: alertTypes.warning});
return;
}
showModal('SCHEDULE_SELECT_INIT', {
type: alertTypes.confirm,
onConfirm: () => handleAction('rankingInitConfirm', initGuid)
});
break;
case "rankingInitConfirm":
await withLoading(async () => {
return await RankingInit(token, item);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('SCHEDULE_INIT_COMPLETE', {type: alertTypes.success});
}else{
showToast(data.data.message, {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
});
break;
case "rankingSnapshot":
const snapshotSel = selectedRows[0];
const snapshotSGuid = snapshotSel.guid;
if(snapshotSGuid === null || snapshotSGuid === undefined || snapshotSGuid === "") {
showToast('SCHEDULE_REFRESH_GUID_NULL_WARNING', {type: alertTypes.warning});
return;
}
if(snapshotSel.status !== CommonStatus.running) {
showToast('UPDATE_STATUS_ONLY_RUNNING', {type: alertTypes.warning});
return;
}
showModal('SCHEDULE_SELECT_SNAPSHOT', {
type: alertTypes.confirm,
onConfirm: () => handleAction('rankingSnapshotConfirm', snapshotSGuid)
});
break;
case "rankingSnapshotConfirm":
await withLoading(async () => {
return await RankingSnapshot(token, item);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('SCHEDULE_SNAPSHOT_COMPLETE', {type: alertTypes.success});
}else{
showToast(data.data.message, {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
});
break;
case "delete":
showModal('SCHEDULE_SELECT_DELETE', {

View File

@@ -14,11 +14,15 @@ import { useNavigate } from 'react-router-dom';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
import AuthModal from '../../components/common/modal/AuthModal';
import { alertTypes } from '../../assets/data/types';
import { useLoading } from '../../context/LoadingProvider';
function AdminView() {
const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList);
const navigate = useNavigate();
const {withLoading} = useLoading();
const {showToast} = useLoading();
const [currentPage, setCurrentPage] = useState(1);
@@ -258,14 +262,24 @@ function AdminView() {
handleInitialModalClose();
};
const handlePasswordInitialize = () => {
AdminChangePw(token, { email: selectedEmail });
const handlePasswordInitialize = async () => {
await withLoading(async () => {
return await AdminChangePw({ email: selectedEmail });
}).then(res => {
if (res.status === 200) {
showToast('PASSWORD_INIT_COMPLETE', { type: alertTypes.success });
} else {
showToast('PASSWORD_INIT_ERROR', { type: alertTypes.error });
}
}).error(err => {
showToast('API_FAIL', { type: alertTypes.error });
}).finally(() => {
handleInitialModalClose();
handleConfirmeModalClose();
});
// console.log(selectedEmail);
setConfirmText('비밀번호 초기화가');
handleInitialModalClose();
handleConfirmeModalClose();
};
// 전체 선택 구현
const handleAllSelect = () => {

View File

@@ -180,7 +180,7 @@ export const responseFileDownload = (response, options = {}) => {
const contentType = response.headers['content-type'] || response.headers['Content-Type'];
const contentDisposition = response.headers['content-disposition'] || response.headers['Content-Disposition'];
// Excel, CSV, ZIP 파일 형식 검증 (CSV 추가)
// Excel, CSV, ZIP 파일 형식 검증
const isValidType = contentType && (
contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') ||
contentType.includes('text/csv') ||
@@ -263,4 +263,39 @@ export const calculateTotals = (data) => {
});
return acc;
}, {}) || {};
};
};
/**
* 밀리초를 시:분:초 형식(HH:MM:SS)으로 변환
* @param {number} milliseconds - 변환할 밀리초
* @returns {string} HH:MM:SS 형식의 시간 문자열
*/
export const formatTimeFromMilliseconds = (milliseconds) => {
if (milliseconds === null || milliseconds === undefined || isNaN(milliseconds)) {
return '';
}
const totalSeconds = Math.floor(milliseconds / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
/**
* 초를 시:분:초 형식(HH:MM:SS)으로 변환
* @param {number} totalSeconds - 변환할 초
* @returns {string} HH:MM:SS 형식의 시간 문자열
*/
export const formatTimeFromSeconds = (totalSeconds) => {
if (totalSeconds === null || totalSeconds === undefined || isNaN(totalSeconds)) {
return '';
}
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};