인스턴스 조회 추가

This commit is contained in:
2025-11-28 16:40:18 +09:00
parent ac9bcdda8b
commit 8dae810e3a
12 changed files with 376 additions and 25 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

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

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

@@ -100,7 +100,7 @@ export const menuConfig = {
permissions: {
read: authType.gameLogRead
},
view: false,
view: true,
authLevel: adminAuthLevel.NONE
},
businesslogview: {
@@ -127,6 +127,14 @@ export const menuConfig = {
view: true,
authLevel: adminAuthLevel.NONE
},
instancedictionary: {
title: '인스턴스 조회',
permissions: {
read: authType.instanceDictionaryRead
},
view: true,
authLevel: adminAuthLevel.NONE
},
rankmanage: {
title: '랭킹 점수 관리',
permissions: {

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

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

@@ -6,3 +6,4 @@ 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

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