Compare commits

...

3 Commits

Author SHA1 Message Date
5143b45610 모달 스크롤 추가
detailGrid 수정
전투이벤트, 랜드경매, 이벤트, 메일 상세 수정
랜드경매 예약종료일 제거
2025-08-09 09:50:14 +09:00
f4b629df52 경제지표 재화 보유
경제지표 아이템 보유
게임로그 스냅샷
히스토리 비즈니스로그 기준 변경
2025-08-04 17:40:37 +09:00
2ba8594e6b 칼리움 완료처리 파라미터 변경
우편 상세 확인시 페이지 처리
조회조건 변경시 페이지 초기화
랜드경매 메시지 제거
2025-07-28 14:13:01 +09:00
54 changed files with 3817 additions and 2443 deletions

View File

@@ -36,6 +36,19 @@ export const userTotalIndex = async token => {
}
};
export const dashboardCaliumIndex = async token => {
try {
const res = await Axios.get(`/api/v1/indicators/dashboard/calium/converter`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('dashboardCaliumIndex', e);
}
}
};
// 유저 지표 다운로드
export const userIndexExport = async (token, filename, sendDate, endDate) => {
try {
@@ -187,10 +200,10 @@ export const PlaytimeIndexExport = async (token, filename, sendDate, endDate) =>
// 2. 경제 지표
// 재화 조회 (currency)
export const CurrencyIndexView = async (token, start_dt, end_dt, currency_type) => {
// 재화 획득 조회
export const CurrencyAcquireIndexView = async (token, start_dt, end_dt, currencyType, deltaType) => {
try {
const res = await Axios.get(`/api/v1/indicators/currency/use?start_dt=${start_dt}&end_dt=${end_dt}&currency_type=${currency_type}`, {
const res = await Axios.get(`/api/v1/indicators/currency/list?start_dt=${start_dt}&end_dt=${end_dt}&currency_type=${currencyType}&delta_type=${deltaType}`, {
headers: { Authorization: `Bearer ${token}` },
});
@@ -202,75 +215,10 @@ export const CurrencyIndexView = async (token, start_dt, end_dt, currency_type)
}
};
// 재화 지표 다운로드
export const CurrencyIndexExport = async (token, filename, sendDate, endDate, currencyType) => {
try {
await Axios.get(`/api/v1/indicators/currency/excel-down?file=${filename}&start_dt=${sendDate}&end_dt=${endDate}&currency_type=${currencyType}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('CurrencyIndexExport Error', e);
}
}
};
// VBP
export const VbpIndexView = async (token, start_dt, end_dt) => {
try {
const res = await Axios.get(`/api/v1/indicators/currency/vbp?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('VbpIndexView Error', e);
}
}
};
// VBP 다운로드
export const VBPIndexExport = async (token, filename, sendDate, endDate) => {
try {
await Axios.get(`/api/v1/indicators/currency/excel-down?file=${filename}&start_dt=${sendDate}&end_dt=${endDate}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('VBPIndexExport Error', e);
}
}
};
// Item
export const ItemIndexView = async (token, start_dt, end_dt) => {
export const ItemIndexView = async (token, start_dt, end_dt, itemId, deltaType) => {
try {
const res = await Axios.get(`/api/v1/indicators/currency/item?start_dt=${start_dt}&end_dt=${end_dt}`, {
const res = await Axios.get(`/api/v1/indicators/item/list?start_dt=${start_dt}&end_dt=${end_dt}&item_id=${itemId}&delta_type=${deltaType}`, {
headers: { Authorization: `Bearer ${token}` },
});
@@ -282,27 +230,17 @@ export const ItemIndexView = async (token, start_dt, end_dt) => {
}
};
// Item 다운로드
export const ItemIndexExport = async (token, filename, sendDate, endDate) => {
// Assets
export const AssetsIndexView = async (token, start_dt, end_dt, itemId, deltaType) => {
try {
await Axios.get(`/api/v1/indicators/currency/excel-down?file=${filename}&start_dt=${sendDate}&end_dt=${endDate}`, {
const res = await Axios.get(`/api/v1/indicators/assets/list?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('ItemIndexExport Error', e);
throw new Error('AssetsIndexView Error', e);
}
}
};
@@ -324,137 +262,4 @@ export const InstanceIndexView = async (token, data, start_dt, end_dt) => {
throw new Error('InstanceIndexView Error', e);
}
}
};
// Instance 다운로드
export const InstanceIndexExport = async (token, filename, data, sendDate, endDate) => {
try {
await Axios.get(
`/api/v1/indicators/currency/excel-down?file=${filename}&search_key=${data ? data : ''}
&start_dt=${sendDate}&end_dt=${endDate}`,
{
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
},
).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('InstanceIndexExport Error', e);
}
}
};
// Clothes
export const ClothesIndexView = async (token, data, start_dt, end_dt) => {
try {
const res = await Axios.get(`/api/v1/indicators/currency/clothes?search_key=${data ? data : ''}&start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('ClothesIndexView Error', e);
}
}
};
// Clothes 다운로드
export const ClothesIndexExport = async (token, filename, data, sendDate, endDate) => {
try {
await Axios.get(
`/api/v1/indicators/currency/excel-down?file=${filename}&search_key=${data ? data : ''}
&start_dt=${sendDate}&end_dt=${endDate}`,
{
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
},
).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('ClothesIndexExport Error', e);
}
}
};
// DAU
export const DailyActiveUserView = async (token, start_dt, end_dt) => {
try {
const res = await Axios.get(`/api/v1/indicators/dau/list?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data.dau_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('DailyActiveUserView Error', e);
}
}
};
// DAU 다운로드
export const DailyActiveUserExport = async (token, filename, sendDate, endDate) => {
try {
await Axios.get(`/api/v1/indicators/dau/excel-down?file=${filename}&start_dt=${sendDate}&end_dt=${endDate}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('PlaytimeIndexExport Error', e);
}
}
};
// Daily Medal
export const DailyMedalView = async (token, start_dt, end_dt) => {
try {
const res = await Axios.get(`/api/v1/indicators/daily-medal/list?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data.daily_medal_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('DailyMedalView Error', e);
}
}
};

View File

@@ -242,7 +242,6 @@ export const getUserLoginDetailList = async (token, searchType, searchData, tran
export const GameUserCreateLogExport = async (token, params, fileName) => {
try {
console.log(params);
await Axios.post(`/api/v1/log/user/create/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
@@ -262,7 +261,6 @@ export const GameUserCreateLogExport = async (token, params, fileName) => {
export const GameUserLoginLogExport = async (token, params, fileName) => {
try {
console.log(params);
await Axios.post(`/api/v1/log/user/login/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
@@ -278,4 +276,40 @@ export const GameUserLoginLogExport = async (token, params, fileName) => {
throw new Error('GameUserLoginLogExport Error', e);
}
}
};
export const getUserSnapshotList = async (token, searchType, searchData, startDate, endDate, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/log/user/snapshot/list?search_type=${searchType}&search_data=${searchData}&start_dt=${startDate}&end_dt=${endDate}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('getUserSnapshotList API error:', error);
throw error;
}
};
export const GameUserSnapshotLogExport = async (token, params, fileName) => {
try {
await Axios.post(`/api/v1/log/user/snapshot/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
timeout: 300000
}).then(response => {
responseFileDownload(response, {
defaultFileName: fileName
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('GameUserSnapshotLogExport Error', e);
}
}
};

View File

@@ -1,131 +0,0 @@
{
"baseUrl": "/api/v1/users",
"endpoints": {
"UserView": {
"method": "GET",
"url": "/api/v1/users/find-users",
"dataPath": "data.data.result",
"paramFormat": "query",
"paramMapping": ["search_type", "search_key"]
},
"UserInfoView": {
"method": "GET",
"url": "/api/v1/users/basicinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserChangeNickName": {
"method": "PUT",
"url": "/api/v1/users/change-nickname",
"dataPath": null,
"paramFormat": "body",
"paramMapping": ["guid", "nickname"]
},
"UserChangeAdminLevel": {
"method": "PUT",
"url": "/api/v1/users/change-level",
"dataPath": null,
"paramFormat": "body",
"paramMapping": ["guid", "level"]
},
"UserKick": {
"method": "PUT",
"url": "/api/v1/users/user-kick",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["guid"]
},
"UserAvatarView": {
"method": "GET",
"url": "/api/v1/users/avatarinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserClothView": {
"method": "GET",
"url": "/api/v1/users/clothinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserToolView": {
"method": "GET",
"url": "/api/v1/users/toolslot",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserInventoryView": {
"method": "GET",
"url": "/api/v1/users/inventory",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserInventoryItemDelete": {
"method": "DELETE",
"url": "/api/v1/users/inventory/delete/item",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["guid", "inventory_id"]
},
"UserTattooView": {
"method": "GET",
"url": "/api/v1/users/tattoo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserQuestView": {
"method": "GET",
"url": "/api/v1/users/quest",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserFriendListView": {
"method": "GET",
"url": "/api/v1/users/friendlist",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserMailView": {
"method": "POST",
"url": "/api/v1/users/mail",
"dataPath": "data.data",
"paramFormat": "body",
"paramMapping": ["guid", "page", "limit"]
},
"UserMailDelete": {
"method": "DELETE",
"url": "/api/v1/users/mail/delete",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["mail_id"]
},
"UserMailItemDelete": {
"method": "DELETE",
"url": "/api/v1/users/mail/delete/item",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["mail_id", "item_id"]
},
"UserMailDetailView": {
"method": "GET",
"url": "/api/v1/users/mail/:id",
"dataPath": "data.data",
"paramFormat": "path",
"paramMapping": ["id"]
},
"UserMyhomeView": {
"method": "GET",
"url": "/api/v1/users/myhome",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
}
}
}

View File

@@ -10,19 +10,22 @@ export const TabGameLogList = [
{ value: 'CURRENCYITEM', name: '재화(아이템) 로그' },
{ value: 'USERCREATE', name: '유저생성 로그' },
{ value: 'USERLOGIN', name: '유저로그인 로그' },
{ value: 'SNAPSHOT', name: '스냅샷 로그' },
];
export const TabEconomicIndexList = [
{ value: 'CURRENCY', name: '재화(유저)' },
// { value: 'ITEM', name: '아이템' },
// { value: 'VBP', name: 'VBP' },
// { value: 'deco', name: '의상/타투' },
// { value: 'instance', name: '인스턴스' },
{ value: 'CURRENCY_ACQUIRE', name: '재화 획득' },
{ value: 'CURRENCY_CONSUME', name: '재화 소모' },
{ value: 'ITEM_ACQUIRE', name: '아이템 획득' },
{ value: 'ITEM_CONSUME', name: '아이템 소모' },
{ value: 'CURRENCY_ASSETS', name: '재화 보유' },
{ value: 'ITEM_ASSETS', name: '아이템 보유' },
];
export const TabUserIndexList = [
{ value: 'USER', name: '이용자 지표' },
{ value: 'RETENTION', name: '잔존율' },
{ value: 'CURRENCY', name: '재화' },
// { value: 'SEGMENT', name: 'Segment' },
// { value: 'PLAYTIME', name: '플레이타임' },
];
@@ -468,6 +471,59 @@ export const opDBType = [
{ value: 'MySql', name: 'MySql'},
]
export const opLogCategory = [
{ value: 'SCHEDULER', name: '스케줄러'},
{ value: 'DYNAMODB', name: 'DynamoDB'},
{ value: 'MARIADB', name: 'MariaDB'},
{ value: 'MESSAGE_QUEUE', name: '메시지큐'},
{ value: 'REDIS', name: 'Redis'},
{ value: 'S3', name: 'S3'},
{ value: 'BATCH_JOB', name: '배치잡'},
]
export const opLogAction = [
{ value: 'KICK_USER', name: '유저킥' },
{ value: 'ADMIN_LEVEL', name: 'GM 레벨' },
{ value: 'NICKNAME_CHANGE', name: '아바타명 변경' },
{ value: 'MAIL_ITEM', name: '메일 아이템' },
{ value: 'QUEST_TASK', name: '퀘스트 Task' },
{ value: 'SCHEDULE_CLEANUP', name: '스케줄 캐시정리' },
{ value: 'SCHEDULE_DATA_INIT', name: '스케줄 데이터 초기화' },
{ value: 'SCHEDULE_LAND_OWNER_CHANGE', name: '스케줄 랜드 소유자 변경' },
{ value: 'SCHEDULE_BLACK_LIST', name: '스케줄 이용자 제재' },
{ value: 'SCHEDULE_NOTICE', name: '스케줄 인게임메시지' },
{ value: 'SCHEDULE_MAIL', name: '스케줄 우편' },
{ value: 'SCHEDULE_EVENT', name: '스케줄 이벤트' },
{ value: 'SCHEDULE_BATTLE_EVENT', name: '스케줄 전투 이벤트' },
{ value: 'SCHEDULE_LAND_AUCTION', name: '스케줄 랜드 경매' },
{ value: 'BANNER', name: '메뉴 배너' },
{ value: 'BATTLE_EVENT', name: '전투 이벤트' },
{ value: 'BUILDING', name: '빌딩' },
{ value: 'LAND_OWNER_CHANGE', name: '랜드 소유자 변경' },
{ value: 'LAND_AUCTION', name: '랜드 경매' },
{ value: 'GROUP', name: '그룹' },
{ value: 'ADMIN', name: '운영자' },
{ value: 'ADMIN_GROUP', name: '운영자 그룹' },
{ value: 'ADMIN_DELETE', name: '운영자 삭제' },
{ value: 'AUTH_ADMIN', name: '운영자 권한' },
{ value: 'PASSWORD_INIT', name: '비밀번호 초기화' },
{ value: 'PASSWORD_CHANGE', name: '비밀번호 변경' },
{ value: 'BLACK_LIST', name: '이용자 제재' },
{ value: 'CALIUM_REQUEST', name: '칼리움 요청' },
{ value: 'EVENT', name: '이벤트' },
{ value: 'MAIL', name: '우편' },
{ value: 'NOTICE', name: '인게임메시지' },
{ value: 'DATA_INIT', name: '데이터 초기화' },
{ value: 'DATA', name: '데이터' },
{ value: 'USER', name: '사용자' },
{ value: 'ITEM', name: '아이템' }
]
export const opCommonStatus = [
{ value: 'SUCCESS', name: '성공' },
{ value: 'FAIL', name: '실패' }
]
// export const logAction = [
// { value: "None", name: "ALL" },
// { value: "AIChatDeleteCharacter", name: "NPC 삭제" },

View File

@@ -12,7 +12,7 @@
},
"columns": [
{
"id": "timestamp",
"id": "logTime",
"type": "date",
"width": "200px",
"title": "일시(KST)",
@@ -22,25 +22,38 @@
}
},
{
"id": "dbType",
"id": "category",
"type": "option",
"width": "100px",
"title": "DB타입",
"option_name": "opDBType"
"title": "로그종류",
"option_name": "opLogCategory"
},
{
"id": "historyType",
"id": "action",
"type": "option",
"width": "150px",
"title": "이력종류",
"option_name": "opHistoryType"
"title": "로그액션",
"option_name": "opLogAction"
},
{
"id": "userId",
"id": "status",
"type": "option",
"width": "100px",
"title": "상태",
"option_name": "opCommonStatus"
},
{
"id": "worker",
"type": "text",
"width": "100px",
"title": "작업자"
},
{
"id": "message",
"type": "text",
"width": "100px",
"title": "비고"
},
{
"id": "detail",
"type": "button",

View File

@@ -0,0 +1,153 @@
import React, { Fragment, useMemo, useRef, useState } from 'react';
import {
CircularProgressWrapper,
FormWrapper,
TableStyle,
TableWrapper,
} from '../../styles/Components';
import { useTranslation } from 'react-i18next';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { UserSnapshotLogSearchBar, useUserSnapshotLogSearch } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import Pagination from '../common/Pagination/Pagination';
import {
INITIAL_PAGE_LIMIT,
} from '../../assets/data/adminConstants';
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
import { AnimatedPageWrapper } from '../common/Layout';
import { numberFormatter } from '../../utils';
const UserSnapshotLogContent = ({ active }) => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const [downloadState, setDownloadState] = useState({
loading: false,
progress: 0
});
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
handlePageChange,
updateSearchParams
} = useUserSnapshotLogSearch(token, 500);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '80px' },
{ id: 'accountId', label: 'account ID', width: '80px' },
{ id: 'userGuid', label: 'GUID', width: '180px' },
{ id: 'userNickname', label: '아바타명', width: '150px' },
{ id: 'gold', label: '골드', width: '80px' },
{ id: 'sapphire', label: '사파이어', width: '80px' },
{ id: 'calium', label: '칼리움', width: '80px' },
{ id: 'ruby', label: '루비', width: '80px' },
{ id: 'item_13080002', label: '퀘스트 메달', width: '80px' },
{ id: 'item_13080004', label: '보급품 메달', width: '80px' },
{ id: 'item_13080005', label: '제작 메달', width: '80px' },
{ id: 'item_13080006', label: '에테론 315 포드', width: '80px' },
{ id: 'item_13080007', label: '에테론 316 포드', width: '80px' },
{ id: 'item_13080008', label: '에테론 317 포드', width: '80px' },
{ id: 'item_13080009', label: '에테론 318 포드', width: '80px' },
{ id: 'item_11570001', label: '강화잉크', width: '80px' },
{ id: 'item_11570002', label: '연성잉크', width: '80px' },
{ id: 'lastLogoutTime', label: '마지막 로그아웃 일자', width: '150px' },
];
}, []);
if(!active) return null;
return (
<AnimatedPageWrapper>
<FormWrapper>
<UserSnapshotLogSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo orderType="asc" pageType="B" total={dataList?.total} total_all={dataList?.total_all}>
<ExcelExportButton
functionName="GameUserSnapshotLogExport"
params={searchParams}
fileName={t('FILE_GAME_LOG_USER_SNAPSHOT')}
onLoadingChange={setDownloadState}
dataSize={dataList?.total_all}
/>
{downloadState.loading && (
<CircularProgressWrapper>
<CircularProgress
progress={downloadState.progress}
size={36}
progressColor="#4A90E2"
/>
</CircularProgressWrapper>
)}
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => (
<th key={header.id} width={header.width}>{header.label}</th>
))}
</tr>
</thead>
<tbody>
{dataList?.snapshot_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{item.accountId}</td>
<td>{item.userGuid}</td>
<td>{item.userNickname}</td>
<td>{numberFormatter.formatCurrency(item.gold)}</td>
<td>{numberFormatter.formatCurrency(item.sapphire)}</td>
<td>{numberFormatter.formatCurrency(item.calium)}</td>
<td>{numberFormatter.formatCurrency(item.ruby)}</td>
<td>{item.item_13080002}</td>
<td>{item.item_13080004}</td>
<td>{item.item_13080005}</td>
<td>{item.item_13080006}</td>
<td>{item.item_13080007}</td>
<td>{item.item_13080008}</td>
<td>{item.item_13080009}</td>
<td>{item.item_11570001}</td>
<td>{item.item_11570002}</td>
<td>{item.lastLogoutTime}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{dataList?.snapshot_list &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default UserSnapshotLogContent;

View File

@@ -14,7 +14,7 @@ import {STORAGE_GAME_LOG_CURRENCY_SEARCH, } from '../../assets/data/adminConstan
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
import { useTranslation } from 'react-i18next';
import CurrencyIndexSearchBar from '../searchBar/CurrencyIndexSearchBar';
import CurrencyUserIndexSearchBar from '../searchBar/CurrencyUserIndexSearchBar';
import { useNavigate } from 'react-router-dom';
import { AnimatedPageWrapper } from '../common/Layout';
@@ -132,7 +132,7 @@ const CreditContent = () => {
return (
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyIndexSearchBar
<CurrencyUserIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {

View File

@@ -0,0 +1,126 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption,
} from '../../styles/Components';
import { useCurrencyAcquireIndexSearch, CurrencyAcquireIndexSearchBar } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
const CurrencyAcquireContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useCurrencyAcquireIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'mail', label: '우편', width: '80px' },
{ id: 'ShopSell', label: '상점 판매', width: '80px' },
{ id: 'ShopPurchase', label: '상점 구매', width: '80px' },
{ id: 'seasonPass', label: '시즌 패스', width: '80px' },
{ id: 'claim', label: '클레임', width: '80px' },
{ id: 'quest', label: '퀘스트', width: '80px' },
{ id: 'ugq', label: 'UGQ', width: '80px' },
{ id: 'battleObject', label: '보급품 상자', width: '80px' },
{ id: 'randomBox', label: '랜덤박스', width: '80px' },
{ id: 'landRent', label: '랜드 임대', width: '80px' },
{ id: 'caliumExchange', label: '칼리움 교환소', width: '80px' },
{ id: 'caliumConverter', label: '칼리움 컨버터', width: '80px' },
{ id: 'beaconShop', label: '비컨 상점', width: '80px' },
{ id: 'etc', label: '기타', width: '80px' },
{ id: 'summary', label: '합계', width: '80px' },
];
}, []);
return (
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyAcquireIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_CURRENCY_ACQUIRE')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.currency_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.MailTaken)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopSell)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopPurchase)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.SeasonPassTakeReward)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ClaimReward)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.QuestMainReward || 0) + (item.actionSummary.QuestTaskUpdate || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.UgqAbort)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.RewardProp)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemRandomBoxUse)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.GainLandProfit)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ConvertExchangeCalium)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ConvertCalium)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconSell || 0) + (item.actionSummary.BeaconShopReceivePaymentForSales || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.MoneyChange)}</td>
<td>{numberFormatter.formatCurrency(item.totalDeltaAmount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CurrencyAcquireContent;

View File

@@ -0,0 +1,109 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption
} from '../../styles/Components';
import {
AssetsIndexSearchBar, useAssetsIndexSearch,
} from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
import DailyDashBoard from './DailyCaliumDashBoard';
const CurrencyAssetsContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useAssetsIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'userCount', label: '유저수', width: '100px' },
{ id: 'gold', label: '골드', width: '100px' },
{ id: 'sapphire', label: '사파이어', width: '100px' },
{ id: 'calium', label: '칼리움', width: '100px' },
{ id: 'ruby', label: '루비', width: '100px' }
];
}, []);
return (
<AnimatedPageWrapper>
<DailyDashBoard />
<FormWrapper>
<AssetsIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_ASSETS_CURRENCY')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.assets_list?.map((data, index) => (
<Fragment key={index}>
<tr>
<td>{data.logDay}</td>
<td>{numberFormatter.formatCurrency(data.userCount)}</td>
<td>{numberFormatter.formatCurrency(data.gold)}</td>
<td>{numberFormatter.formatCurrency(data.sapphire)}</td>
<td>{numberFormatter.formatCurrency(data.calium)}</td>
<td>{numberFormatter.formatCurrency(data.ruby)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CurrencyAssetsContent;

View File

@@ -0,0 +1,128 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption,
} from '../../styles/Components';
import {
useCurrencyConsumeIndexSearch, CurrencyConsumeIndexSearchBar,
} from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
const CurrencyConsumeContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useCurrencyConsumeIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'itemBuy', label: '아이템 구매', width: '80px' },
{ id: 'ShopPurchase', label: '상점 구매', width: '80px' },
{ id: 'ShopRePurchase', label: '상점 재구매', width: '80px' },
{ id: 'beaconShop', label: '비컨상점', width: '80px' },
{ id: 'beacon', label: '비컨', width: '80px' },
{ id: 'taxi', label: '택시', width: '80px' },
{ id: 'farming', label: '파밍', width: '80px' },
{ id: 'seasonPass', label: '시즌 패스', width: '80px' },
{ id: 'caliumExchange', label: '칼리움 교환소', width: '80px' },
{ id: 'caliumConverter', label: '칼리움 컨버터', width: '80px' },
{ id: 'rent', label: '랜드 렌탈', width: '80px' },
{ id: 'landAuction', label: '랜드 경매', width: '80px' },
{ id: 'ugq', label: 'UGQ', width: '80px' },
{ id: 'etc', label: '기타', width: '80px' },
{ id: 'summary', label: '합계', width: '80px' },
];
}, []);
return (
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyConsumeIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_CURRENCY_CONSUME')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.currency_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemBuy)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopPurchase)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopRePurchase)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconShopRegisterItem || 0) + (item.actionSummary.BeaconShopPurchaseItem || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconCreate || 0) + (item.actionSummary.BeaconEdit || 0) + (item.actionSummary.BeaconAppearanceCustomize || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.TaxiMove)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.FarmingStart)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.SeasonPassBuyCharged)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ConvertExchangeCalium)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ConvertCalium)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.RentFloor)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.LandAuctionBid)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.UgqAssign)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.MoneyChange ||0) + (item.actionSummary.RenewalShopProducts ||0) + (item.actionSummary.CharacterAppearanceCustomize ||0))}</td>
<td>{numberFormatter.formatCurrency(item.totalDeltaAmount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CurrencyConsumeContent;

View File

@@ -1,110 +0,0 @@
import { useEffect, useState } from 'react';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { DailySearchBar } from '../../components/IndexManage/index';
import { DailyActiveUserExport, DailyActiveUserView } from '../../apis';
const PlayTimeContent = () => {
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
// DAU 데이터
const fetchData = async (startDate, endDate) => {
const startDateToLocal =
startDate.getFullYear() +
'-' +
(startDate.getMonth() + 1 < 9 ? '0' + (startDate.getMonth() + 1) : startDate.getMonth() + 1) +
'-' +
(startDate.getDate() < 9 ? '0' + startDate.getDate() : startDate.getDate());
const endDateToLocal =
endDate.getFullYear() +
'-' +
(endDate.getMonth() + 1 < 9 ? '0' + (endDate.getMonth() + 1) : endDate.getMonth() + 1) +
'-' +
(endDate.getDate() < 9 ? '0' + endDate.getDate() : endDate.getDate());
// await DailyActiveUserView(token, startDateToLocal, endDateToLocal).then(data => {
// console.log(data);
// setDataList(data);
// });
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = (send_dt, end_dt) => {
fetchData(send_dt, end_dt);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_Dau.xlsx';
DailyActiveUserExport(token, fileName, sendDate, finishDate);
};
return (
<>
<DailySearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th rowSpan="1" width="45">
일자
</th>
<th colSpan="1" width="30">
DAU
</th>
{/*<th colSpan="1" width="30">*/}
{/* DALC*/}
{/*</th>*/}
<th colSpan="1" width="30">
DGLC
</th>
{/*<th colSpan="1" width="30">*/}
{/* MaxAU*/}
{/*</th>*/}
</tr>
</thead>
<tbody>
{dataList && (dataList || []).map((data, index) => (
<tr key={index}>
<td>{data.date}</td>
<td>{data.dau}</td>
{/*<td>{data.dalc}</td>*/}
<td>{data.dglc}</td>
{/*<td>{data.maxAu}</td>*/}
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
);
};
export default PlayTimeContent;

View File

@@ -0,0 +1,141 @@
import { useEffect, useState } from 'react';
import { dashboardCaliumIndex } from '../../apis';
import { styled } from 'styled-components';
import TitleArrow from '../../assets/img/icon/icon-title.png';
const DailyDashBoard = () => {
const [boardState, setBoardState] = useState('active');
const [totalData, setTotalData] = useState([]);
const handleBoard = () => {
if (boardState === 'active') {
setBoardState('inactive');
} else {
setBoardState('active');
}
};
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const token = sessionStorage.getItem('token');
await dashboardCaliumIndex(token).then(data => {
setTotalData(data.dashboard_calium);
});
};
const dashboardItems = [
{
title: '컨버터 칼리움 보유량',
value: totalData.total_calium || 0
},
{
title: '컨버터 변환 효율',
value: `${totalData.converter_rate || 0}%`
},
{
title: '인플레이션 가중치',
value: `${totalData.inflation_rate || 0}%`
},
{
title: '칼리움 누적 총량',
value: totalData.cumulative_calium || 0
}
];
return (
<DailyBoardWrapper>
{totalData &&
<DailyBoard>
<BoardTitle onClick={handleBoard} $state={boardState}>
Daily Dashboard
</BoardTitle>
<BoardInfo $state={boardState}>
<BoxWrapper>
{dashboardItems?.map((item, index) => (
<InfoItem key={index}>
<InfoTitle>{item.title}</InfoTitle>
<InfoValue>
{item.value}
</InfoValue>
</InfoItem>
))}
</BoxWrapper>
</BoardInfo>
</DailyBoard>
}
</DailyBoardWrapper>
);
};
export default DailyDashBoard;
const DailyBoardWrapper = styled.div`
padding-top: 20px;
border-top: 1px solid #000;
`;
const DailyBoard = styled.div`
background: #f6f6f6;
border-radius: 10px;
margin-bottom: 20px;
`;
const BoardTitle = styled.div`
font-size: 24px;
font-weight: 600;
line-height: 52px;
padding: 0 10px;
cursor: pointer;
&:after {
content: '';
display: inline-block;
width: 11px;
height: 52px;
margin-left: 10px;
background: url(${TitleArrow}) 50% 50% no-repeat;
position: absolute;
transform: ${props => (props.$state === 'active' ? 'rotate(0)' : 'rotate(180deg)')};
}
`;
const BoardInfo = styled.div`
padding: 20px;
border-top: 1px solid #d9d9d9;
display: ${props => (props.$state === 'active' ? 'block' : 'none')};
`;
const BoxWrapper = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px;
`;
const InfoItem = styled.div`
width: 18%;
background: #fff;
padding: 15px 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
border-radius: 15px;
`;
const InfoTitle = styled.div`
font-size: 20px;
font-weight: 600;
`;
const InfoValue = styled.div`
display: inline-flex;
flex-wrap: wrap;
margin: 5px 0;
gap: 5px 0;
align-items: center;
font-size: 18px;
font-weight: 600;
`;

View File

@@ -26,7 +26,6 @@ const DailyDashBoard = ({ content }) => {
const fetchData = async () => {
const token = sessionStorage.getItem('token');
await userTotalIndex(token).then(data => {
console.log(data);
setTotalData(data.dashboard);
});
};

View File

@@ -1,111 +0,0 @@
import { Fragment, useEffect, useState } from 'react';
import Button from '../common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { DailySearchBar } from './index';
import { DailyMedalView } from '../../apis';
const DailyMedalContent = () => {
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
const fetchData = async (startDate, endDate) => {
const startDateToLocal =
startDate.getFullYear() +
'-' +
(startDate.getMonth() + 1 < 9 ? '0' + (startDate.getMonth() + 1) : startDate.getMonth() + 1) +
'-' +
(startDate.getDate() < 9 ? '0' + startDate.getDate() : startDate.getDate());
const endDateToLocal =
endDate.getFullYear() +
'-' +
(endDate.getMonth() + 1 < 9 ? '0' + (endDate.getMonth() + 1) : endDate.getMonth() + 1) +
'-' +
(endDate.getDate() < 9 ? '0' + endDate.getDate() : endDate.getDate());
setDataList(await DailyMedalView(token, startDateToLocal, endDateToLocal));
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = (send_dt, end_dt) => {
fetchData(send_dt, end_dt);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_Daily_Medal.xlsx';
//DailyActiveUserExport(token, fileName, sendDate, finishDate);
};
return (
<>
<DailySearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead >
<tr>
<th rowSpan="1" width="20">
일자
</th>
<th colSpan="1" width="30">
UserID
</th>
<th colSpan="1" width="30">
닉네임
</th>
<th colSpan="1" width="30">
Item ID
</th>
<th colSpan="1" width="30">
획득량
</th>
</tr>
</thead>
<tbody>
{(dataList || []).map((data, index) => (
<tr key={index}>
<td>{data.date}</td>
<td>{data.dau}</td>
<td>{data.dalc}</td>
<td>{data.dglc}</td>
<td>{data.maxAu}</td>
{Array.from({ length: 24 }, (_, i) => (
<td key={i}>{data['h' + i]}</td>
))}
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
);
};
export default DailyMedalContent;

View File

@@ -0,0 +1,152 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption, TextInput, TableInfoContent, Notice,
} from '../../styles/Components';
import { ItemAcquireIndexSearchBar, useItemAcquireIndexSearch } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
const ItemAcquireContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useItemAcquireIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'mail', label: '우편', width: '80px' },
{ id: 'shopPurchase', label: '상점 구매', width: '80px' },
{ id: 'shopRePurchase', label: '상점 재구매', width: '80px' },
{ id: 'itemBuy', label: '아이템 구매', width: '80px' },
{ id: 'itemUse', label: '아이템 사용', width: '80px' },
{ id: 'seasonPass', label: '시즌 패스', width: '80px' },
{ id: 'claim', label: '클레임', width: '80px' },
{ id: 'quest', label: '퀘스트', width: '80px' },
{ id: 'ugq', label: 'UGQ', width: '80px' },
{ id: 'battleObject', label: '배틀맵', width: '80px' },
{ id: 'runRace', label: '런레이스', width: '80px' },
{ id: 'prop', label: '보급품 상자', width: '80px' },
{ id: 'randomBox', label: '랜덤박스', width: '80px' },
{ id: 'beacon', label: '비컨', width: '80px' },
{ id: 'beaconShop', label: '비컨 상점', width: '80px' },
{ id: 'myHome', label: '마이홈', width: '80px' },
{ id: 'craft', label: '크래프트', width: '80px' },
{ id: 'etc', label: '기타', width: '80px' },
{ id: 'summary', label: '합계', width: '80px' },
];
}, []);
return (
<AnimatedPageWrapper>
<FormWrapper>
<ItemAcquireIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
{dataList?.item_list && dataList.item_list.length > 0 &&
<TableInfoContent>
<TextInput
type="text"
value={dataList.item_list[0].itemId}
width="100px"
readOnly
/>
<TextInput
type="text"
value={dataList.item_list[0].itemName}
width="150px"
readOnly
/>
<Notice $color='#F15F5F'>* 확인되지 않은 액션이 있을 있습니다</Notice>
</TableInfoContent>
}
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_ITEM_ACQUIRE')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.item_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.MailTaken)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopPurchase)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopRePurchase)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemBuy)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemUse)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.SeasonPassTakeReward)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ClaimReward)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.QuestMainReward || 0) + (item.actionSummary.QuestTaskUpdate || 0) + (item.actionSummary.QuestMainTask || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.UgqAbort)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BattleRoundStateUpdate || 0) + (item.actionSummary.BattlePodCombatOccupyReward || 0) + (item.actionSummary.BattleObjectInteraction || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.RunRaceFinishReward || 0) + (item.actionSummary.RunRaceRespawnReward || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.RewardProp)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemRandomBoxUse)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconCreate || 0) + (item.actionSummary.BeaconEdit || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconShopPurchaseItem || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.DeleteMyhome || 0) + (item.actionSummary.SaveMyhome || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.CraftFinish || 0) + (item.actionSummary.CraftStop || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.CheatCommandItem || 0) + (item.actionSummary.CharacterAppearanceUpdate || 0)
+ (item.actionSummary.ItemTattooLevelUp || 0) + (item.actionSummary.UserCreate || 0) + (item.actionSummary.JoinInstance || 0))}</td>
<td>{numberFormatter.formatCurrency(item.totalDeltaCount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default ItemAcquireContent;

View File

@@ -0,0 +1,119 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption
} from '../../styles/Components';
import {
AssetsIndexSearchBar, useAssetsIndexSearch,
} from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
import DailyDashBoard from './DailyCaliumDashBoard';
const ItemAssetsContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useAssetsIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'userCount', label: '유저수', width: '100px' },
{ id: 'item_13080002', label: '퀘스트 메달', width: '100px' },
{ id: 'item_13080004', label: '보급품 메달', width: '100px' },
{ id: 'item_13080005', label: '제작 메달', width: '100px' },
{ id: 'item_13080006', label: '에테론 315 포드', width: '100px' },
{ id: 'item_13080007', label: '에테론 316 포드', width: '100px' },
{ id: 'item_13080008', label: '에테론 317 포드', width: '100px' },
{ id: 'item_13080009', label: '에테론 318 포드', width: '100px' },
{ id: 'item_11570001', label: '강화 잉크', width: '100px' },
{ id: 'item_11570002', label: '연성 잉크', width: '100px' }
];
}, []);
return (
<AnimatedPageWrapper>
<DailyDashBoard />
<FormWrapper>
<AssetsIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_ASSETS_ITEM')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.assets_list?.map((data, index) => (
<Fragment key={index}>
<tr>
<td>{data.logDay}</td>
<td>{numberFormatter.formatCurrency(data.userCount)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080002)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080004)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080005)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080006)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080007)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080008)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080009)}</td>
<td>{numberFormatter.formatCurrency(data.item_11570001)}</td>
<td>{numberFormatter.formatCurrency(data.item_11570002)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default ItemAssetsContent;

View File

@@ -0,0 +1,143 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption, TableInfoContent, TextInput, Label, Notice,
} from '../../styles/Components';
import {
ItemAcquireIndexSearchBar,
ItemConsumeIndexSearchBar,
useItemAcquireIndexSearch,
useItemConsumeIndexSearch,
} from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
import styled from 'styled-components';
const ItemConsumeContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useItemConsumeIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'shopSell', label: '상점 판매', width: '80px' },
{ id: 'itemUse', label: '아이템 사용', width: '80px' },
{ id: 'beaconShop', label: '비컨상점', width: '80px' },
{ id: 'beacon', label: '비컨', width: '80px' },
{ id: 'quest', label: '퀘스트', width: '80px' },
{ id: 'ugq', label: 'UGQ', width: '80px' },
{ id: 'randomBox', label: '랜덤박스', width: '80px' },
{ id: 'myHome', label: '마이홈', width: '80px' },
{ id: 'craft', label: '크래프트', width: '80px' },
{ id: 'etc', label: '기타', width: '80px' },
{ id: 'summary', label: '합계', width: '80px' },
];
}, []);
return (
<AnimatedPageWrapper>
<FormWrapper>
<ItemConsumeIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
{dataList?.item_list && dataList.item_list.length > 0 &&
<TableInfoContent>
<TextInput
type="text"
value={dataList.item_list[0].itemId}
width="100px"
readOnly
/>
<TextInput
type="text"
value={dataList.item_list[0].itemName}
width="300px"
readOnly
/>
<Notice $color='#F15F5F'>* 확인되지 않은 액션이 있을 있습니다</Notice>
</TableInfoContent>
}
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_ITEM_CONSUME')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.item_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopSell)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemUse)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconShopRegisterItem || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconEdit || 0) + (item.actionSummary.BeaconCreate || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.QuestTaskUpdate || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.UgqAbort)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemRandomBoxUse)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.SaveMyhome || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.CraftStart || 0))}</td>
<td>{numberFormatter.formatCurrency(
(item.actionSummary.SummonParty || 0) + (item.actionSummary.ItemDestroy || 0) + (item.actionSummary.CreatePartyInstance || 0) + (item.actionSummary.ItemTattooChangeAttribute || 0)
+ (item.actionSummary.CheatCommandItem || 0) + (item.actionSummary.ItemDestoryByExpiration || 0) + (item.actionSummary.ItemDestroyByUser || 0) + (item.actionSummary.ItemTattooLevelUp || 0)
)}</td>
<td>{numberFormatter.formatCurrency(item.totalDeltaCount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default ItemConsumeContent;

View File

@@ -1,112 +0,0 @@
import { Fragment, useEffect, useState } from 'react';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { PlayTimeSearchBar } from '../../components/IndexManage/index';
import { PlaytimeIndexExport, PlaytimeIndexView } from '../../apis';
const PlayTimeContent = () => {
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
// 이용자 지표 데이터
const fetchData = async (startDate, endDate) => {
const startDateToLocal =
startDate.getFullYear() +
'-' +
(startDate.getMonth() + 1 < 9 ? '0' + (startDate.getMonth() + 1) : startDate.getMonth() + 1) +
'-' +
(startDate.getDate() < 9 ? '0' + startDate.getDate() : startDate.getDate());
const endDateToLocal =
endDate.getFullYear() +
'-' +
(endDate.getMonth() + 1 < 9 ? '0' + (endDate.getMonth() + 1) : endDate.getMonth() + 1) +
'-' +
(endDate.getDate() < 9 ? '0' + endDate.getDate() : endDate.getDate());
setDataList(await PlaytimeIndexView(token, startDateToLocal, endDateToLocal));
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = (send_dt, end_dt) => {
fetchData(send_dt, end_dt);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_PlayTime_Index.xlsx';
PlaytimeIndexExport(token, fileName, sendDate, finishDate);
};
return (
<>
<PlayTimeSearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th rowSpan="2" width="140">
일자
</th>
<th colSpan="4" width="520">
유저수
</th>
<th rowSpan="2" width="160">
누적 플레이타임()
</th>
<th rowSpan="2" width="160">
1인당 평균 플레이타임()
</th>
</tr>
<tr>
<th>30 이내</th>
<th>30 ~ 1시간</th>
<th>1시간 ~ 3시간</th>
<th>3시간 이상</th>
</tr>
</thead>
<tbody>
{dataList.playtime &&
dataList.playtime.map(time => (
<tr key={time.date}>
<td>{time.date}</td>
{time.user_cnt.map((cnt, index) => (
<td className="text-left" key={index}>
{cnt}
</td>
))}
<td className="text-left">{time.total_time}</td>
<td className="text-left">{time.average_time}</td>
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
);
};
export default PlayTimeContent;

View File

@@ -5,8 +5,8 @@ import { AnimatedPageWrapper } from '../common/Layout';
import { useRetentionSearch, RetentionSearchBar } from '../searchBar';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { ExcelDownButton } from '../common';
import { useTranslation } from 'react-i18next';
import CSVDownloadButton from '../common/button/CsvDownButton';
const RetentionContent = () => {
const { t } = useTranslation();
@@ -38,7 +38,7 @@ const RetentionContent = () => {
/>
<TableInfo>
<ListOption>
<ExcelDownButton tableRef={tableRef} fileName={t('FILE_INDEX_USER_RETENTION')} />
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_USER_RETENTION')} />
</ListOption>
</TableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :

View File

@@ -1,90 +0,0 @@
import { Fragment, useEffect, useState } from 'react';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { SegmentSearchBar } from '../../components/IndexManage/index';
import { SegmentIndexExport, SegmentIndexView } from '../../apis';
const SegmentContent = () => {
const token = sessionStorage.getItem('token');
const END_DATE = new Date();
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [finishDate, setFinishDate] = useState(END_DATE);
useEffect(() => {
fetchData(END_DATE);
}, []);
// Retention 지표 데이터
const fetchData = async endDate => {
const endDateToLocal =
endDate.getFullYear() +
'-' +
(endDate.getMonth() + 1 < 9 ? '0' + (endDate.getMonth() + 1) : endDate.getMonth() + 1) +
'-' +
(endDate.getDate() < 9 ? '0' + endDate.getDate() : endDate.getDate());
setDataList(await SegmentIndexView(token, endDateToLocal));
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = end_dt => {
fetchData(end_dt);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_Segment_Index.xlsx';
SegmentIndexExport(token, fileName, finishDate);
};
return (
<>
<SegmentSearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th colSpan="1" width="200">
{dataList && dataList.start_dt} ~ {dataList && dataList.end_dt}
</th>
<th colSpan="2" width="400">
KIP
</th>
</tr>
<tr>
{/* <th>국가</th> */}
<th>세그먼트 분류</th>
<th>AU</th>
<th>AU Percentage by User Segment (%)</th>
</tr>
</thead>
<tbody>
{dataList && dataList.segment &&
dataList.segment.map((segment, index) => (
<tr key={index}>
{/* <td rowSpan="6">TH</td> */}
<td>{segment.type}</td>
<td>{segment.au}</td>
<td>{segment.dif}</td>
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
);
};
export default SegmentContent;

View File

@@ -1,16 +1,14 @@
import { Fragment, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { DailyDashBoard } from '../../components/IndexManage/index';
import { Title, TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { UserIndexSearchBar, DailyDashBoard } from '../../components/IndexManage/index';
import { userIndexView, userIndexExport } from '../../apis';
import Loading from '../common/Loading';
import { userIndexView } from '../../apis';
import { ExcelDownButton } from '../common';
import { useTranslation } from 'react-i18next';
import { formatStringDate } from '../../utils';
import { AnimatedPageWrapper } from '../common/Layout';
import { UserIndexSearchBar } from '../searchBar';
const UserContent = () => {
const token = sessionStorage.getItem('token');

View File

@@ -1,219 +0,0 @@
import { styled } from 'styled-components';
import { useState, useEffect } from 'react';
import { TableStyle, TableInfo, ListOption } from '../../styles/Components';
import Button from '../../components/common/button/Button';
import VBPSearchBar from '../searchBar/VBPSearchBar';
import { VBPIndexExport, VbpIndexView } from '../../apis';
const VBPContent = () => {
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
const [dataList, setDataList] = useState([]);
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
// console.log(dataList);
const fetchData = async (startDate, endDate) => {
const startDateToLocal =
startDate.getFullYear() +
'-' +
(startDate.getMonth() + 1 < 9 ? '0' + (startDate.getMonth() + 1) : startDate.getMonth() + 1) +
'-' +
(startDate.getDate() < 9 ? '0' + startDate.getDate() : startDate.getDate());
const endDateToLocal =
endDate.getFullYear() +
'-' +
(endDate.getMonth() + 1 < 9 ? '0' + (endDate.getMonth() + 1) : endDate.getMonth() + 1) +
'-' +
(endDate.getDate() < 9 ? '0' + endDate.getDate() : endDate.getDate());
setDataList(await VbpIndexView(token, startDateToLocal, endDateToLocal));
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_VBP_Index.xlsx';
VBPIndexExport(token, fileName, sendDate, finishDate);
};
return (
<>
<VBPSearchBar fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<TableWrapper>
<EconomicTable>
<thead>
<tr>
<th colSpan="2" className="text-center" width="300">
Product
</th>
<th width="160">2023-08-07</th>
<th width="160">2023-08-08</th>
<th width="160">2023-08-09</th>
<th width="160">2023-08-10</th>
<th width="160">2023-08-11</th>
<th width="160">2023-08-12</th>
</tr>
</thead>
<tbody>
<tr>
<TableTitle colSpan="2">(Total) VBP 생산량</TableTitle>
<TableData>500000</TableData>
<TableData>500000</TableData>
<TableData>500000</TableData>
<TableData>500000</TableData>
<TableData>500000</TableData>
<TableData>500000</TableData>
</tr>
<tr>
<TableTitle colSpan="2">(Total) VBP 소진량</TableTitle>
<TableData>490000</TableData>
<TableData>490000</TableData>
<TableData>490000</TableData>
<TableData>490000</TableData>
<TableData>490000</TableData>
<TableData>490000</TableData>
</tr>
<tr>
<TableTitle colSpan="2">(Total) VBP 보유량</TableTitle>
<TableData>3.2M</TableData>
<TableData>3.3M</TableData>
<TableData>3.3M</TableData>
<TableData>3.4M</TableData>
<TableData>3.5M</TableData>
<TableData>3.5M</TableData>
</tr>
<tr>
<TableTitle rowSpan="2">GET</TableTitle>
<TableTitle>퀘스트 보상</TableTitle>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
</tr>
<tr>
<TableTitle>시즌패스 보상</TableTitle>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
</tr>
<tr>
<TableTitle rowSpan="2">USE</TableTitle>
<TableTitle>퀘스트 보상</TableTitle>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
</tr>
<tr>
<TableTitle>시즌패스 보상</TableTitle>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
</tr>
{/* {mokupData.map((data, index) => (
<Fragment key={index}>
<tr>
<td>{data.date}</td>
<td>{data.name}</td>
<td>{data.trader}</td>
<td>{data.id}</td>
<td>{data.key}</td>
</tr>
</Fragment>
))} */}
</tbody>
</EconomicTable>
</TableWrapper>
</>
);
};
export default VBPContent;
const TableWrapper = styled.div`
width: 100%;
min-width: 680px;
overflow: auto;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #666666;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
}
${TableStyle} {
width: 100%;
min-width: 900px;
th {
&.cell-nru {
background: #f0f0f0;
border-left: 1px solid #aaa;
border-right: 1px solid #aaa;
}
}
td {
&.blank {
background: #f9f9f9;
}
&.cell-nru {
background: #fafafa;
border-left: 1px solid #aaa;
border-right: 1px solid #aaa;
}
}
}
`;
const TableData = styled.td`
background: ${props => (props.$state === 'danger' ? '#d60000' : props.$state === 'blank' ? '#F9F9F9' : 'transparent')};
color: ${props => (props.$state === 'danger' ? '#fff' : '#2c2c2c')};
`;
const TableTitle = styled.td`
text-align: center;
`;
const EconomicTable = styled(TableStyle)`
${TableData} {
text-align: left;
}
tbody {
tr:nth-child(1),
tr:nth-child(2),
tr:nth-child(3) {
background: #f5fcff;
}
}
`;

View File

@@ -1,27 +1,17 @@
import UserIndexSearchBar from "../searchBar/UserIndexSearchBar";
import RetentionSearchBar from "../searchBar/RetentionSearchBar";
import SegmentSearchBar from "../searchBar/SegmentSearchBar";
import DailyDashBoard from "./DailyDashBoard";
import PlayTimeSearchBar from "../searchBar/PlayTimeSearchBar";
import UserContent from "./UserContent";
import PlayTimeContent from "./PlayTimeContent";
import RetentionContent from "./RetentionContent";
import SegmentContent from "./SegmentContent";
import DailyActiveUserContent from "./DailyActiveUserContent";
import DailyMedalContent from "./DailyMedalContent";
import DailySearchBar from "../searchBar/DailySearchBar";
import CurrencyConsumeContent from "./CurrencyConsumeContent";
import CurrencyAcquireContent from "./CurrencyAcquireContent";
import ItemAcquireContent from "./ItemAcquireContent";
import ItemConsumeContent from "./ItemConsumeContent";
export {
UserIndexSearchBar,
RetentionSearchBar,
SegmentSearchBar,
DailyDashBoard,
PlayTimeSearchBar,
DailyDashBoard,
UserContent,
SegmentContent,
RetentionContent,
PlayTimeContent,
DailySearchBar,
DailyActiveUserContent,
DailyMedalContent,
CurrencyConsumeContent,
CurrencyAcquireContent,
ItemAcquireContent,
ItemConsumeContent
};

View File

@@ -4,6 +4,7 @@ import styled from 'styled-components';
import dayjs from 'dayjs';
import { AnimatedTabs } from '../index';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
/**
* 위치 지정 가능한 그리드 형태 상세 정보 표시 컴포넌트
@@ -51,12 +52,15 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
handler,
min,
max,
step,
format,
required,
showTime,
tabItems,
activeKey,
onTabChange
onTabChange,
maxLength,
rows: textareaRows
} = item;
// 현재 값 가져오기 (formData에서 또는 항목에서)
@@ -85,10 +89,35 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
value={currentValue}
min={min}
max={max}
step={step || 1}
onChange={(value) => onChange(key, value, handler)}
placeholder={placeholder || `${label} 입력`}
/>;
case 'display':
return <Input
{...commonProps}
value={currentValue || ''}
readOnly
style={{
...commonProps.style,
backgroundColor: '#f5f5f5',
cursor: 'default'
}}
placeholder={placeholder || ''}
/>;
case 'textarea':
return <TextArea
{...commonProps}
value={currentValue || ''}
onChange={(e) => onChange(key, e.target.value, handler)}
placeholder={placeholder}
maxLength={maxLength}
rows={textareaRows || 4}
showCount={!!maxLength}
/>;
case 'select':
return (
<Select
@@ -99,7 +128,7 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
>
{options && options.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
{option.name}
</Select.Option>
))}
</Select>
@@ -131,12 +160,25 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
currentValue.end ? dayjs(currentValue.end) : null
] : null)}
format={format || 'YYYY-MM-DD HH:mm:ss'}
onChange={(dates, dateStrings) => {
if (dates) {
onChange={(dates) => {
if (dates && dates.length === 2) {
// 두 개의 별도 필드에 각각 업데이트
if (item.keys) {
onChange(item.keys.start, dates[0], handler);
onChange(item.keys.end, dates[1], handler);
// 두 개의 onChange를 순차적으로 호출하는 대신
// 한 번에 두 필드를 모두 업데이트하는 방식으로 변경
const updatedData = {
...formData,
[item.keys.start]: dates[0],
[item.keys.end]: dates[1]
};
// handler가 있으면 handler 실행, 없으면 직접 onChange 호출
if (handler) {
handler(dates, key, updatedData);
} else {
// onChange를 통해 전체 업데이트된 데이터를 전달
onChange('dateRange_update', updatedData, null);
}
} else {
// 기존 방식 지원 (하위 호환성)
onChange(key, {
@@ -147,8 +189,17 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
} else {
// 두 필드 모두 비우기
if (item.keys) {
onChange(item.keys.start, null, handler);
onChange(item.keys.end, null, handler);
const updatedData = {
...formData,
[item.keys.start]: null,
[item.keys.end]: null
};
if (handler) {
handler(null, key, updatedData);
} else {
onChange('dateRange_update', updatedData, null);
}
} else {
onChange(key, { start: null, end: null }, handler);
}
@@ -204,8 +255,17 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
case 'custom':
return item.render ? item.render(formData, onChange) : null;
case 'label':
default:
return <div>{currentValue}</div>;
return <div style={{
padding: '4px 11px',
minHeight: '32px',
lineHeight: '24px',
fontSize: '15px',
color: currentValue ? '#000' : '#bfbfbf'
}}>
{currentValue || placeholder || ''}
</div>;
}
};

View File

@@ -22,24 +22,36 @@ const DetailLayout = ({
}) => {
// 값 변경 핸들러
const handleChange = (key, value, handler) => {
// 핸들러가 있으면 핸들러 실행
if (handler) {
handler(value, key, formData);
}
let updatedFormData = { ...formData };
// dateRange 전용 업데이트 처리
if (key === 'dateRange_update') {
updatedFormData = value; // value가 이미 완전히 업데이트된 객체
}
// 키가 점 표기법이면 중첩 객체 업데이트
if (key.includes('.')) {
else if (key.includes('.')) {
const [parentKey, childKey] = key.split('.');
onChange({
updatedFormData = {
...formData,
[parentKey]: {
...formData[parentKey],
[childKey]: value
}
});
};
} else {
// 일반 키는 직접 업데이트
onChange({ ...formData, [key]: value });
updatedFormData = {
...formData,
[key]: value
};
}
// 핸들러가 있으면 핸들러 실행 (업데이트된 데이터를 전달)
if (handler) {
handler(value, key, updatedFormData);
} else {
// 핸들러가 없으면 직접 onChange 호출
onChange(updatedFormData);
}
};

View File

@@ -0,0 +1,390 @@
import { ExcelDownButton } from '../../../styles/ModuleComponents';
import { useCallback, useEffect, useState } from 'react';
const CSVDownloadButton = ({ tableRef, data, fileName = 'download.csv', onLoadingChange }) => {
const [isDownloading, setIsDownloading] = useState(false);
const [lastProgress, setLastProgress] = useState(0);
// 타임아웃 감지 및 처리
useEffect(() => {
let timeoutTimer;
if (isDownloading && lastProgress >= 95) {
// 최종 단계에서 타임아웃 감지 타이머 설정
timeoutTimer = setTimeout(() => {
// 진행 상태가 여전히 변하지 않았다면 타임아웃으로 간주
if (isDownloading && lastProgress >= 95) {
console.log("CSV download timeout detected, completing process");
setIsDownloading(false);
if (onLoadingChange) {
onLoadingChange({ loading: false, progress: 100 });
}
}
}, 10000); // 10초 타임아웃 (CSV는 Excel보다 빠르므로 시간 단축)
}
return () => {
if (timeoutTimer) clearTimeout(timeoutTimer);
};
}, [isDownloading, lastProgress, onLoadingChange]);
const flattenObject = (obj, prefix = '') => {
return Object.keys(obj).reduce((acc, key) => {
const prefixedKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
Object.assign(acc, flattenObject(obj[key], prefixedKey));
} else if (Array.isArray(obj[key])) {
// 배열은 JSON 문자열로 변환
acc[prefixedKey] = JSON.stringify(obj[key]);
} else {
acc[prefixedKey] = obj[key];
}
return acc;
}, {});
};
const updateLoadingState = (newProgress) => {
setLastProgress(newProgress);
if (onLoadingChange && typeof onLoadingChange === 'function') {
onLoadingChange({loading: true, progress: newProgress});
}
};
// CSV 문자열 이스케이프 처리
const escapeCSVField = (field) => {
if (field === null || field === undefined) {
return '';
}
const str = String(field);
// 쉼표, 따옴표, 줄바꿈이 있는 경우 따옴표로 감싸고 내부 따옴표는 두 개로 변환
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
return '"' + str.replace(/"/g, '""') + '"';
}
return str;
};
// 배열을 CSV 행으로 변환
const arrayToCSVRow = (array) => {
return array.map(escapeCSVField).join(',');
};
const downloadTableCSV = async () => {
return new Promise((resolve, reject) => {
try {
if (!tableRef || !tableRef.current) {
reject(new Error('테이블 참조가 없습니다.'));
return;
}
updateLoadingState(10);
// 메인 스레드에서 데이터 추출
const tableElement = tableRef.current;
const headerRows = tableElement.getElementsByTagName('thead')[0].getElementsByTagName('tr');
const bodyRows = tableElement.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
// 일반 행만 포함 (상세 행 제외)
const normalBodyRows = Array.from(bodyRows).filter(row => {
const hasTdWithColspan = Array.from(row.cells).some(cell => cell.hasAttribute('colspan'));
return !hasTdWithColspan;
});
// 헤더 데이터 추출
const headers = Array.from(headerRows[0].cells).map(cell => cell.textContent);
updateLoadingState(30);
// 바디 데이터 추출
const bodyData = normalBodyRows.map(row =>
Array.from(row.cells).map(cell => cell.textContent)
);
updateLoadingState(50);
// CSV 생성을 비동기로 처리
setTimeout(() => {
try {
// CSV 문자열 생성
let csvContent = '';
// 헤더 추가
csvContent += arrayToCSVRow(headers) + '\n';
updateLoadingState(70);
// 데이터 행들을 청크로 처리
const chunkSize = 1000;
let currentIndex = 0;
function processDataChunk() {
const end = Math.min(currentIndex + chunkSize, bodyData.length);
for (let i = currentIndex; i < end; i++) {
csvContent += arrayToCSVRow(bodyData[i]) + '\n';
}
currentIndex = end;
const progress = 70 + Math.floor((currentIndex / bodyData.length) * 20);
updateLoadingState(progress);
if (currentIndex < bodyData.length) {
// 아직 처리할 데이터가 남아있으면 다음 청크 처리 예약
setTimeout(processDataChunk, 0);
} else {
// 모든 데이터 처리 완료 후 다운로드
finishCSVDownload(csvContent);
}
}
function finishCSVDownload(csvContent) {
try {
updateLoadingState(95);
// BOM 추가 (한글 깨짐 방지)
const BOM = '\uFEFF';
const csvWithBOM = BOM + csvContent;
// Blob 생성 및 다운로드
const blob = new Blob([csvWithBOM], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
updateLoadingState(100);
resolve();
} catch (error) {
reject(error);
}
}
// 첫 번째 청크 처리 시작
processDataChunk();
} catch (error) {
reject(error);
}
}, 0);
} catch (error) {
reject(error);
}
});
};
const chunkArray = (array, chunkSize) => {
const chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
};
const downloadDataCSV = async () => {
return new Promise(async (resolve, reject) => {
try {
if (!data || data.length === 0) {
reject(new Error('다운로드할 데이터가 없습니다.'));
return;
}
updateLoadingState(5);
// 데이터 플랫 변환 과정을 더 작은 청크로 나누기
const dataChunkSize = 2000;
const dataChunks = chunkArray(data, dataChunkSize);
let flattenedData = [];
for (let i = 0; i < dataChunks.length; i++) {
await new Promise(resolve => {
setTimeout(() => {
// 청크 내 아이템들을 플랫하게 변환
const chunkData = dataChunks[i].map(item => {
// 기본 필드
const baseData = {
'logTime': item.logTime,
'GUID': item.userGuid === 'None' ? '' : item.userGuid,
'Nickname': item.userNickname === 'None' ? '' : item.userNickname,
'Account ID': item.accountId === 'None' ? '' : item.accountId,
'Action': item.action,
'Domain': item.domain === 'None' ? '' : item.domain,
'Tran ID': item.tranId
};
// Actor 데이터 플랫하게 추가
const actorData = item.header && item.header.Actor ?
flattenObject(item.header.Actor, 'Actor') : {};
// Infos 데이터 플랫하게 추가
let infosData = {};
if (item.body && item.body.Infos && Array.isArray(item.body.Infos)) {
item.body.Infos.forEach((info) => {
infosData = {
...infosData,
...flattenObject(info, `Info`)
};
});
}
return {
...baseData,
...actorData,
...infosData
};
});
flattenedData = [...flattenedData, ...chunkData];
const progress = 5 + Math.floor((i + 1) / dataChunks.length * 10);
updateLoadingState(progress);
resolve();
}, 0);
});
}
// 모든 항목의 모든 키 수집하여 헤더 생성
const allKeys = new Set();
// 헤더 수집도 청크로 나누기
for (let i = 0; i < flattenedData.length; i += dataChunkSize) {
await new Promise(resolve => {
setTimeout(() => {
const end = Math.min(i + dataChunkSize, flattenedData.length);
for (let j = i; j < end; j++) {
Object.keys(flattenedData[j]).forEach(key => allKeys.add(key));
}
const progress = 15 + Math.floor((i + dataChunkSize) / flattenedData.length * 5);
updateLoadingState(progress);
resolve();
}, 0);
});
}
const headers = Array.from(allKeys);
updateLoadingState(25);
// CSV 생성
let csvContent = '';
// 헤더 추가
csvContent += arrayToCSVRow(headers) + '\n';
updateLoadingState(30);
// 청크로 데이터 나누기
const chunkSize = 1000;
const rowChunks = chunkArray(flattenedData, chunkSize);
// 각 청크 처리
let processedCount = 0;
for (const chunk of rowChunks) {
await new Promise(resolve => {
setTimeout(() => {
// 각 청크의 데이터를 CSV 행으로 변환
chunk.forEach(item => {
const row = headers.map(header => {
const value = item[header] !== undefined ? item[header] : '';
return value;
});
csvContent += arrayToCSVRow(row) + '\n';
});
processedCount += chunk.length;
// 진행률 계산 및 콜백 호출
const newProgress = Math.min(90, Math.round((processedCount / flattenedData.length) * 60) + 30);
updateLoadingState(newProgress);
resolve();
}, 0);
});
// 메모리 정리를 위한 가비지 컬렉션 힌트
if (processedCount % (chunkSize * 5) === 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
updateLoadingState(95);
// 파일 다운로드
setTimeout(() => {
try {
// BOM 추가 (한글 깨짐 방지)
const BOM = '\uFEFF';
const csvWithBOM = BOM + csvContent;
// Blob 생성 및 다운로드
const blob = new Blob([csvWithBOM], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
updateLoadingState(100);
resolve();
} catch (error) {
reject(error);
}
}, 100);
} catch (error) {
reject(error);
}
});
};
const handleDownload = useCallback(async () => {
if (isDownloading) return; // 이미 다운로드 중이면 중복 실행 방지
setIsDownloading(true);
setLastProgress(0);
if (onLoadingChange) onLoadingChange({loading: true, progress: 0});
try {
if (tableRef) {
await downloadTableCSV();
} else if (data) {
await downloadDataCSV();
} else {
alert('유효한 데이터 소스가 없습니다.');
}
} catch (error) {
console.error('CSV download failed:', error);
alert('CSV 다운로드 중 오류가 발생했습니다.');
} finally {
// 다운로드 완료 후 짧은 지연 시간을 두어 100% 상태를 잠시 보여줌
setTimeout(() => {
setIsDownloading(false);
if (onLoadingChange) onLoadingChange({loading: false, progress: 100});
}, 500);
}
}, [tableRef, data, fileName, isDownloading, onLoadingChange]);
return (
<ExcelDownButton onClick={handleDownload} disabled={isDownloading}>
{isDownloading ? '다운로드 중...' : '엑셀 다운로드'}
</ExcelDownButton>
);
};
export default CSVDownloadButton;

View File

@@ -1,113 +0,0 @@
import * as XLSX from 'xlsx-js-style';
import { ExcelDownButton } from '../../../styles/ModuleComponents';
const ExcelDownloadButton = ({ tableRef, fileName = 'download.xlsx', sheetName = 'Sheet1' }) => {
const isNumeric = (value) => {
// 숫자 또는 숫자 문자열인지 확인
return !isNaN(value) && !isNaN(parseFloat(value));
};
const downloadExcel = () => {
try {
if (!tableRef.current) return;
const tableElement = tableRef.current;
const headerRows = tableElement.getElementsByTagName('thead')[0].getElementsByTagName('tr');
const bodyRows = tableElement.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
// 헤더 데이터 추출
const headers = Array.from(headerRows[0].cells).map(cell => cell.textContent);
// 바디 데이터 추출 및 숫자 타입 처리
const bodyData = Array.from(bodyRows).map(row =>
Array.from(row.cells).map(cell => {
const value = cell.textContent;
return isNumeric(value) ? parseFloat(value) : value;
})
);
// 워크북 생성
const wb = XLSX.utils.book_new();
// 테두리 스타일 정의
const borderStyle = {
style: "thin",
color: { rgb: "000000" }
};
// 스타일 정의
const centerStyle = {
font: {
name: "맑은 고딕",
sz: 11
},
alignment: {
horizontal: 'right',
vertical: 'right'
},
border: {
top: borderStyle,
bottom: borderStyle,
left: borderStyle,
right: borderStyle
}
};
const headerStyle = {
alignment: {
horizontal: 'center',
vertical: 'center'
},
fill: {
fgColor: { rgb: "d9e1f2" },
patternType: "solid"
}
};
// 데이터에 스타일 적용
const wsData = [
// 헤더 행
headers.map(h => ({
v: h,
s: headerStyle
})),
// 데이터 행들
...bodyData.map(row =>
row.map(cell => ({
v: cell,
s: centerStyle
}))
)
];
// 워크시트 생성
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 열 너비 설정 (최소 8, 최대 50)
ws['!cols'] = headers.map((_, index) => {
const maxLength = Math.max(
headers[index].length * 2,
...bodyData.map(row => String(row[index] || '').length * 1.2)
);
return { wch: Math.max(8, Math.min(50, maxLength)) };
});
// 워크시트를 워크북에 추가
XLSX.utils.book_append_sheet(wb, ws, sheetName);
// 엑셀 파일 다운로드
XLSX.writeFile(wb, fileName);
} catch (error) {
console.error('Excel download failed:', error);
alert('엑셀 다운로드 중 오류가 발생했습니다.');
}
};
return (
<ExcelDownButton onClick={downloadExcel}>
엑셀 다운로드
</ExcelDownButton>
);
};
export default ExcelDownloadButton;

View File

@@ -97,20 +97,20 @@ const LogDetailModal = ({ detailView,
const allChangedItems = [];
changedData.forEach((item, itemIndex) => {
if (item.changed && Array.isArray(item.changed)) {
item.changed.forEach((changedItem) => {
if (item.domain.changed && Array.isArray(item.domain.changed)) {
item.domain.changed.forEach((changedItem) => {
allChangedItems.push({
...changedItem,
// 어떤 데이터 항목에서 온 것인지 구분하기 위한 정보 추가
sourceIndex: itemIndex,
sourceInfo: {
dbType: item.dbType,
timestamp: item.timestamp,
operationType: item.operationType,
logTime: item.logTime,
operationType: item.domain.operationType,
historyType: item.historyType,
tableName: item.tableName,
tranId: item.tranId,
userId: item.userId
worker: item.worker
}
});
});
@@ -164,8 +164,8 @@ const LogDetailModal = ({ detailView,
<td>{item.fieldName}</td>
<td>{formatValue(item.newValue)}</td>
<td>{formatValue(item.oldValue)}</td>
<td>{item.sourceInfo.userId}</td>
<td>{convertKTC(item.sourceInfo.timestamp, false)}</td>
<td>{item.sourceInfo.worker}</td>
<td>{convertKTC(item.sourceInfo.logTime, false)}</td>
</tr>
)
})}

View File

@@ -38,6 +38,8 @@ const ModalBg = styled(motion.div)`
min-width: 1080px;
display: ${props => (props.$view === 'hidden' ? 'none' : 'block')};
z-index: 20;
overflow-y: auto;
overflow-x: auto;
`;
const ModalContainer = styled.div`
@@ -52,9 +54,18 @@ const ModalWrapper = styled(motion.div)`
min-width: ${props => props.min || 'auto'};
padding: ${props => props.$padding || '30px'};
border-radius: 30px;
max-height: 90%;
max-height: calc(100vh - 40px);
overflow: auto;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
/*모바일*/
@media (max-width: 768px) {
min-width: unset;
max-width: calc(100vw - 20px);
max-height: calc(100vh - 20px);
padding: ${props => props.$padding || '20px'};
border-radius: 20px;
}
`;
const Modal = ({ children, $padding, min, $view, $bgcolor }) => {

View File

@@ -1,4 +1,4 @@
import React, { useState, Fragment, useEffect } from 'react';
import React, { useState, Fragment, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../common/button/Button';
@@ -18,7 +18,7 @@ import {
FormStatusWarning,
FormButtonContainer,
} from '../../styles/ModuleComponents';
import { Modal, SingleDatePicker, SingleTimePicker } from '../common';
import { DetailLayout, Modal, SingleDatePicker, SingleTimePicker } from '../common';
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
import { convertKTCDate } from '../../utils';
import {
@@ -64,16 +64,6 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}
}, [modalType, content]);
// useEffect(() => {
// if(modalType === TYPE_REGISTRY && configData?.length > 0){
// setResultData(prev => ({
// ...prev,
// round_count: configData[0].default_round_count,
// round_time: configData[0].round_time
// }));
// }
// }, [modalType, configData]);
useEffect(() => {
if (checkCondition()) {
setIsNullValue(false);
@@ -82,104 +72,12 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}
}, [resultData]);
// 시작 날짜 변경 핸들러
const handleStartDateChange = (date) => {
if (!date) return;
const newDate = new Date(date);
if(resultData.repeat_type !== NONE && resultData.event_end_dt){
const endDate = new Date(resultData.event_end_dt);
const startDay = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
showToast('DATE_START_DIFF_END_WARNING', {type: alertTypes.warning});
return;
}
}
setResultData(prev => ({
...prev,
event_start_dt: newDate
}));
};
// 시작 시간 변경 핸들러
const handleStartTimeChange = (time) => {
if (!time) return;
const newDateTime = resultData.event_start_dt
? new Date(resultData.event_start_dt)
: new Date();
newDateTime.setHours(
time.getHours(),
time.getMinutes(),
0,
0
);
setResultData(prev => ({
...prev,
event_start_dt: newDateTime
}));
};
const handleEndTimeChange = (time) => {
if (!time) return;
const newDateTime = resultData.event_end_time
? new Date(resultData.event_end_time)
: new Date();
newDateTime.setHours(
time.getHours(),
time.getMinutes(),
0,
0
);
setResultData(prev => ({
...prev,
event_end_time: newDateTime
}));
};
// 종료 날짜 변경 핸들러
const handleEndDateChange = (date) => {
if (!date || !resultData.event_start_dt) return;
const startDate = new Date(resultData.event_start_dt);
const endDate = new Date(date);
// 일자만 비교하기 위해 년/월/일만 추출
const startDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
showToast('DATE_START_DIFF_END_WARNING', {type: alertTypes.warning});
return;
}
setResultData(prev => ({
...prev,
event_end_dt: endDate
}));
};
const handleConfigChange = (e) => {
const config = configData.find(data => String(data.id) === String(e.target.value));
if (config) {
setResultData({
...resultData,
config_id: config.id,
round_time: config.round_time
});
} else {
showToast('Config not found for value:', e.target.value, {type: alertTypes.warning});
}
}
const opGameMode = useMemo(() => {
return gameModeData?.map(item => ({
value: item.id,
name: `${item.desc}(${item.id})`
})) || [];
}, [gameModeData]);
const handleReset = () => {
setDetailData({});
@@ -282,6 +180,7 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
return (
resultData.event_start_dt !== ''
&& resultData.group_id !== ''
&& resultData.game_mode_id > 0
&& resultData.event_name !== ''
&& (resultData.repeat_type === 'NONE' || (resultData.repeat_type !== 'NONE' && resultData.event_end_dt !== ''))
);
@@ -310,122 +209,115 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}
}
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 2,
type: 'text',
key: 'group_id',
label: '그룹 ID',
disabled: !isView('group'),
width: '150px',
},
{
row: 0,
col: 2,
colSpan: 2,
type: 'text',
key: 'event_name',
label: '이벤트명',
disabled: !isView('name'),
width: '250px',
},
{
row: 1,
col: 0,
colSpan: 2,
type: 'date',
key: 'event_start_dt',
label: '시작일시',
disabled: !isView('start_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '200px',
showTime: true
},
{
row: 1,
col: 2,
colSpan: 2,
type: 'number',
key: 'event_operation_time',
label: '진행시간(분)',
disabled: !isView('operation_time'),
width: '100px',
min: 10,
},
{
row: 3,
col: 0,
colSpan: 2,
type: 'select',
key: 'repeat_type',
label: '반복',
disabled: !isView('repeat'),
width: '150px',
options: battleRepeatType
},
...(resultData?.repeat_type !== 'NONE' ? [{
row: 3,
col: 2,
colSpan: 2,
type: 'date',
key: 'event_end_dt',
label: '종료일',
disabled: !isView('end_dt'),
format: 'YYYY-MM-DD',
width: '200px'
}] : []),
{
row: 4,
col: 0,
colSpan: 2,
type: 'select',
key: 'game_mode_id',
label: '게임 모드',
disabled: !isView('mode'),
width: '150px',
options: opGameMode
},
{
row: 4,
col: 2,
colSpan: 2,
type: 'select',
key: 'hot_time',
label: '핫타임',
disabled: !isView('hot'),
width: '150px',
options: battleEventHotTime.map(value => ({
value: value,
name: `${value}`
}))
},
]
}
];
return (
<>
<Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "전투시스템 이벤트 등록" : isView('modify') ? "전투시스템 이벤트 수정" : "전투시스템 이벤트 상세"}</Title>
<MessageWrapper>
<FormRowGroup>
<FormLabel>그룹 ID</FormLabel>
<FormInput
type="text"
disabled={!isView('group')}
width='150px'
value={resultData?.group_id}
onChange={e => setResultData({ ...resultData, group_id: e.target.value })}
/>
<FormLabel>이벤트명</FormLabel>
<FormInput
type="text"
disabled={!isView('name')}
width='300px'
value={resultData?.event_name}
onChange={e => setResultData({ ...resultData, event_name: e.target.value })}
/>
</FormRowGroup>
<FormRowGroup>
<SingleDatePicker
label="시작일자"
disabled={!isView('start_dt')}
dateLabel="시작 일자"
onDateChange={handleStartDateChange}
selectedDate={resultData?.event_start_dt}
/>
</FormRowGroup>
<FormRowGroup>
<SingleTimePicker
label="시작시간"
disabled={!isView('start_dt')}
selectedTime={resultData?.event_start_dt}
onTimeChange={handleStartTimeChange}
/>
<FormLabel>진행시간()</FormLabel>
<FormInput
type="number"
disabled={!isView('operation_time')}
width='100px'
min={10}
value={resultData?.event_operation_time}
onChange={e => setResultData({ ...resultData, event_operation_time: e.target.value })}
/>
</FormRowGroup>
<FormRowGroup>
<FormLabel>반복</FormLabel>
<SelectInput value={resultData?.repeat_type} onChange={e => setResultData({ ...resultData, repeat_type: e.target.value })} disabled={!isView('repeat')} width="150px">
{battleRepeatType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
{resultData?.repeat_type !== 'NONE' &&
<SingleDatePicker
label="종료일자"
disabled={!isView('end_dt')}
dateLabel="종료 일자"
onDateChange={handleEndDateChange}
selectedDate={resultData?.event_end_dt}
/>
}
</FormRowGroup>
{/*<FormRowGroup>*/}
{/* <FormLabel>라운드 시간</FormLabel>*/}
{/* <SelectInput value={resultData.config_id} onChange={handleConfigChange} disabled={!isView('config')} width="200px">*/}
{/* {configData && configData?.map((data, index) => (*/}
{/* <option key={index} value={data.id}>*/}
{/* {data.desc}({data.id})*/}
{/* </option>*/}
{/* ))}*/}
{/* </SelectInput>*/}
{/* <FormLabel>라운드 수</FormLabel>*/}
{/* <SelectInput value={resultData.round_count} onChange={e => setResultData({ ...resultData, round_count: e.target.value })} disabled={!isView('round')} width="100px">*/}
{/* {battleEventRoundCount.map((data, index) => (*/}
{/* <option key={index} value={data}>*/}
{/* {data}*/}
{/* </option>*/}
{/* ))}*/}
{/* </SelectInput>*/}
{/*</FormRowGroup>*/}
<FormRowGroup>
{/*<FormLabel>배정 포드</FormLabel>*/}
{/*<SelectInput value={resultData.reward_group_id} onChange={e => setResultData({ ...resultData, reward_group_id: e.target.value })} disabled={!isView('reward')} width="200px">*/}
{/* {rewardData && rewardData?.map((data, index) => (*/}
{/* <option key={index} value={data.group_id}>*/}
{/* {data.desc}({data.group_id})*/}
{/* </option>*/}
{/* ))}*/}
{/*</SelectInput>*/}
<FormLabel>게임 모드</FormLabel>
<SelectInput value={resultData.game_mode_id} onChange={e => setResultData({ ...resultData, game_mode_id: e.target.value })} disabled={!isView('mode')} width="200px">
{gameModeData && gameModeData?.map((data, index) => (
<option key={index} value={data.id}>
{data.desc}({data.id})
</option>
))}
</SelectInput>
<FormLabel>핫타임</FormLabel>
<SelectInput value={resultData.hot_time} onChange={e => setResultData({ ...resultData, hot_time: e.target.value })} disabled={!isView('hot')} width="100px">
{battleEventHotTime.map((data, index) => (
<option key={index} value={data}>
{data}
</option>
))}
</SelectInput>
</FormRowGroup>
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
</MessageWrapper>
<BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar>
<FormStatusLabel>
@@ -482,7 +374,7 @@ export const initData = {
reward_group_id: 1,
round_count: 1,
hot_time: 1,
game_mode_id: 1,
game_mode_id: '',
event_start_dt: '',
event_end_dt: '',
event_operation_time: 10

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, Fragment } from 'react';
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, Textarea, SearchBarAlert } from '../../styles/Components';
import { Input, Button as AntButton, Select, Alert, Space, Card, Row, Col } from 'antd';
import { Title, BtnWrapper } from '../../styles/Components';
import Button from '../common/button/Button';
import Modal from '../common/modal/Modal';
import { EventIsItem, EventModify } from '../../apis';
@@ -10,16 +11,13 @@ import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import { authType, benItems, commonStatus, currencyItemCode } from '../../assets/data';
import {
AppendRegistBox, AppendRegistTable, AreaBtnClose,
BtnDelete, DetailInputItem, DetailInputRow,
DetailModalWrapper, RegistGroup, DetailRegistInfo, DetailState,
Item, ItemList, LangArea
DetailRegistInfo, DetailState
} from '../../styles/ModuleComponents';
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../utils';
import DateTimeInput from '../common/input/DateTimeInput';
import { convertKTC, timeDiffMinute, convertKTCDate } from '../../utils';
import { useLoading } from '../../context/LoadingProvider';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { DetailLayout } from '../common';
const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => {
const userInfo = useRecoilValue(authList);
@@ -31,21 +29,14 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
const id = content && content.id;
const updateAuth = userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventUpdate);
const [time, setTime] = useState({
start_hour: '00',
start_min: '00',
end_hour: '00',
end_min: '00',
}); //시간 정보
const [activeLanguage, setActiveLanguage] = useState('KO');
const [item, setItem] = useState('');
const [itemCount, setItemCount] = useState('');
const [itemCount, setItemCount] = useState(1);
const [resource, setResource] = useState('19010001');
const [resourceCount, setResourceCount] = useState('');
const [resourceCount, setResourceCount] = useState(1);
const [resultData, setResultData] = useState({});
const [isNullValue, setIsNullValue] = useState(false);
// 과거 판단
const [isPast, setIsPast] = useState(false);
const [isChanged, setIsChanged] = useState(false);
@@ -65,13 +56,8 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
event_type: content.event_type,
mail_list: content.mail_list,
item_list: content.item_list,
});
setTime({ ...time,
start_hour: String(start_dt_KTC.getHours()).padStart(2, '0'),
start_min: String(start_dt_KTC.getMinutes()).padStart(2, '0'),
end_hour: String(end_dt_KTC.getHours()).padStart(2, '0'),
end_min: String(end_dt_KTC.getMinutes()).padStart(2, '0')
status: content.status,
delete_desc: content.delete_desc
});
start_dt_KTC < (new Date) ? setIsPast(true) : setIsPast(false);
@@ -90,29 +76,97 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
}
}, [updateAuth, isPast]);
useEffect(() => {
if (conditionCheck()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
useEffect(() => {
setItemCheckMsg('');
}, [item]);
// 아이템 수량 숫자 체크
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 getLanguageTabItems = () => {
return resultData.mail_list?.map(mail => ({
key: mail.language,
label: mail.language,
children: (
<div style={{ padding: '10px', minHeight: '400px', height: 'auto' }}>
<Row gutter={[16, 24]}>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
제목 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input
value={mail.title || ''}
placeholder="우편 제목을 입력하세요"
maxLength={30}
readOnly={isReadOnly}
onChange={(e) => updateMailData(mail.language, 'title', e.target.value.trimStart())}
showCount
size="large"
/>
</div>
</Col>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
내용 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input.TextArea
value={mail.content || ''}
placeholder="우편 내용을 입력하세요"
readOnly={isReadOnly}
rows={8}
maxLength={2000}
showCount
onChange={(e) => {
if (e.target.value.length > 2000) return;
updateMailData(mail.language, 'content', e.target.value.trimStart());
}}
style={{
resize: 'vertical',
minHeight: '200px',
maxHeight: '400px'
}}
/>
</div>
</Col>
</Row>
</div>
),
closable: resultData.mail_list?.length > 1 && !isReadOnly, // 마지막 하나가 아니고 읽기전용이 아닐 때만 삭제 가능
})) || [];
};
const updateMailData = (language, field, value) => {
const updatedMailList = resultData.mail_list.map(mail =>
mail.language === language
? { ...mail, [field]: value }
: mail
);
setResultData({ ...resultData, mail_list: updatedMailList });
setIsChanged(true);
};
const handleTabClose = (targetKey) => {
if (resultData.mail_list.length <= 1) return;
const filterList = resultData.mail_list.filter(el => el.language !== targetKey);
setResultData({ ...resultData, mail_list: filterList });
// 삭제된 탭이 현재 활성 탭이었다면 첫 번째 탭으로 변경
if (activeLanguage === targetKey) {
setActiveLanguage(filterList[0]?.language || 'KO');
}
setIsChanged(true);
};
// 아이템 추가
@@ -152,19 +206,6 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
setResultData({ ...resultData, item_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;
@@ -186,45 +227,9 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
setResourceCount('');
};
// 입력창 삭제
const onLangDelete = language => {
let filterList = resultData.mail_list && resultData.mail_list.filter(el => el.language !== language);
if (filterList.length === 1) setBtnValidation(true);
setIsChanged(true);
setResultData({ ...resultData, mail_list: filterList });
};
// 날짜 처리
const handleDateChange = (data, type) => {
const date = new Date(data);
setResultData({
...resultData,
[`${type}_dt`]: combineDateTime(date, time[`${type}_hour`], time[`${type}_min`]),
});
setIsChanged(true);
};
// 시간 처리
const handleTimeChange = (e, type) => {
const { id, value } = e.target;
const newTime = { ...time, [`${type}_${id}`]: value };
setTime(newTime);
const date = resultData[`${type}_dt`] ? new Date(resultData[`${type}_dt`]) : new Date();
setResultData({
...resultData,
[`${type}_dt`]: combineDateTime(date, newTime[`${type}_hour`], newTime[`${type}_min`]),
});
setIsChanged(true);
};
// 확인 버튼 후 다 초기화
const handleReset = () => {
setBtnValidation(false);
setIsNullValue(false);
setIsChanged(false);
};
@@ -281,6 +286,197 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
}
};
// 아이템 목록 렌더링 컴포넌트
const renderItemList = () => {
return (
<div>
{resultData.item_list && resultData.item_list.length > 0 && (
<Space wrap>
{resultData.item_list.map((data, index) => (
<Card
key={index}
title={data.item_name}
size="small"
extra={
!isReadOnly && (
<AntButton
type="text"
danger
size="small"
onClick={() => onItemRemove(index)}
>
X
</AntButton>
)
}
style={{ minWidth: '150px' }}
>
<div>
<div>{data.item}</div>
<div>수량: {data.item_cnt}</div>
</div>
</Card>
))}
</Space>
)}
</div>
);
};
// 아이템 추가 컴포넌트
const renderItemAdd = () => {
return (
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="Item Meta id 입력"
value={item}
onChange={(e) => setItem(e.target.value.trimStart())}
disabled={isReadOnly}
style={{ width: '200px' }}
/>
<Input
type="number"
placeholder="수량"
value={itemCount}
onChange={(e) => setItemCount(e.target.value)}
disabled={isReadOnly}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleItemList}
disabled={itemCount.length === 0 || item.length === 0 || isReadOnly}
>
추가
</AntButton>
</Space.Compact>
);
};
// 자원 추가 컴포넌트
const renderResourceAdd = () => {
return (
<Space.Compact style={{ width: '100%' }}>
<Select
value={resource}
onChange={setResource}
disabled={isReadOnly}
style={{ width: '200px' }}
placeholder="자원 선택"
>
{currencyItemCode.map((data, index) => (
<Select.Option key={index} value={data.value}>
{data.name}
</Select.Option>
))}
</Select>
<Input
type="number"
placeholder="수량"
value={resourceCount}
disabled={isReadOnly}
onChange={(e) => setResourceCount(e.target.value)}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleResourceList}
disabled={resourceCount.length === 0 || resource.length === 0 || isReadOnly}
>
추가
</AntButton>
</Space.Compact>
);
};
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 2,
type: 'dateRange',
keys: {
start: 'start_dt',
end: 'end_dt'
},
label: '이벤트 기간',
disabled: isReadOnly,
format: 'YYYY-MM-DD HH:mm',
showTime: true,
startLabel: '시작 일시',
endLabel: '종료 일시'
},
{
row: 0,
col: 2,
colSpan: 1,
type: 'custom',
key: 'status',
label: '이벤트 상태',
render: () => detailState(resultData.status)
},
...(resultData.status === commonStatus.delete ? [{
row: 0,
col: 3,
colSpan: 1,
type: 'display',
key: 'delete_desc',
label: '삭제 사유',
value: resultData.delete_desc || ''
}] : [{
row: 0,
col: 3,
colSpan: 1,
type: 'custom',
key: 'empty_space',
label: '',
render: () => <div></div>
}]),
{
row: 1,
col: 0,
colSpan: 4,
type: 'tab',
key: 'language_tabs',
tabItems: getLanguageTabItems(),
activeKey: activeLanguage,
onTabChange: setActiveLanguage,
onTabClose: handleTabClose
},
{
row: 2,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_add',
label: '아이템 추가',
render: renderItemAdd
},
{
row: 3,
col: 0,
colSpan: 4,
type: 'custom',
key: 'resource_add',
label: '자원 추가',
render: renderResourceAdd
},
{
row: 4,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_list',
render: renderItemList
}
]
}
];
return (
<>
<Modal min="960px" $view={detailView}>
@@ -297,201 +493,22 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
)}
</DetailRegistInfo>
}
<DetailModalWrapper>
{content &&
<RegistGroup>
<DetailInputRow>
<DateTimeInput
title="이벤트 기간"
dateName="시작 일자"
selectedDate={convertKTCDate(content.start_dt)}
handleSelectedDate={data => handleDateChange(data, 'start')}
onChange={e => handleTimeChange(e, 'start')}
/>
<DateTimeInput
dateName="종료 일자"
selectedDate={convertKTCDate(content.end_dt)}
handleSelectedDate={data => handleDateChange(data, 'end')}
onChange={e => handleTimeChange(e, 'end')}
/>
</DetailInputRow>
<DetailInputRow>
<DetailInputItem>
<InputLabel>이벤트 상태</InputLabel>
<div>{detailState(content.status)}</div>
</DetailInputItem>
{content.status === commonStatus.delete &&
<DetailInputItem>
<InputLabel>삭제 사유</InputLabel>
<div>{content.delete_desc}</div>
</DetailInputItem>
}
</DetailInputRow>
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
</RegistGroup>
}
{resultData.mail_list &&
resultData.mail_list.map(data => {
return (
<Fragment key={data.language}>
<AppendRegistBox>
<LangArea>
언어 : {data.language}
{btnValidation === false && !isReadOnly ? (
<AreaBtnClose
onClick={e => {
e.preventDefault();
onLangDelete(data.language);
}}
/>
) : (
<AreaBtnClose opacity="10%" />
)}
</LangArea>
<AppendRegistTable>
<tbody>
<tr>
<th width="120">
<Label>제목</Label>
</th>
<td>
<DetailInputItem>
<TextInput
placeholder="우편 제목 입력"
maxLength="30"
id={data.language}
value={data.title}
readOnly={isReadOnly}
onChange={e => {
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 });
setIsChanged(true);
}}
/>
</DetailInputItem>
</td>
</tr>
<tr>
<th>
<Label>내용</Label>
</th>
<td>
<Textarea
value={data.content}
readOnly={isReadOnly}
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 });
setIsChanged(true);
}}
/>
</td>
</tr>
</tbody>
</AppendRegistTable>
</AppendRegistBox>
</Fragment>
);
})}
<AppendRegistBox>
<AppendRegistTable>
<tbody>
<tr>
<th width="120">
<Label>아이템 첨부</Label>
</th>
<td>
<DetailInputItem>
<TextInput
placeholder="Item Meta id 입력"
value={item}
onChange={e => {
let list = [];
list = e.target.value.trimStart();
setItem(list);
}}
disabled={isReadOnly}
/>
<TextInput
placeholder="수량"
value={itemCount}
type="number"
onChange={e => handleItemCount(e)}
width="90px"
disabled={isReadOnly}
/>
<Button
text="추가"
theme={itemCount.length === 0 || item.length === 0 ? 'disable' : 'search'}
handleClick={handleItemList}
/>
{itemCheckMsg && <SearchBarAlert>{itemCheckMsg}</SearchBarAlert>}
</DetailInputItem>
</td>
</tr>
<tr>
<th width="120">
<Label>자원 첨부</Label>
</th>
<td>
<DetailInputItem>
<SelectInput onChange={e => setResource(e.target.value)} value={resource} disabled={isReadOnly}>
{currencyItemCode.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
placeholder="수량"
type="number"
value={resourceCount}
disabled={isReadOnly}
onChange={e => handleResourceCount(e)}
width="200px"
/>
<Button
text="추가"
theme={resourceCount.length === 0 || resource.length === 0 ? 'disable' : 'search'}
handleClick={handleResourceList}
width="100px"
height="35px"
errorMessage={isReadOnly} />
</DetailInputItem>
<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>
{!isReadOnly && <BtnDelete onClick={() => onItemRemove(index)}></BtnDelete>}
</Item>
);
})}
</ItemList>
)}
</div>
</td>
</tr>
</tbody>
</AppendRegistTable>
</AppendRegistBox>
</DetailModalWrapper>
{itemCheckMsg && (
<Alert
message={itemCheckMsg}
type="error"
style={{ marginTop: '8px', width: '300px' }}
/>
)}
<BtnWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
<Button
text="확인"

View File

@@ -1,4 +1,4 @@
import { useState, Fragment, useEffect } from 'react';
import React, { useState, Fragment, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../common/button/Button';
import Loading from '../common/Loading';
@@ -21,7 +21,7 @@ import {
NoticeInputItem2, BoxWrapper, FormStatusBar, FormStatusLabel, FormStatusWarning, FormButtonContainer,
} from '../../styles/ModuleComponents';
import { modalTypes } from '../../assets/data';
import {DynamicModal, Modal, DateTimeRangePicker} from '../common';
import { DynamicModal, Modal, DateTimeRangePicker, DetailLayout } from '../common';
import { LandAuctionModify, LandAuctionSingleRegist } from '../../apis';
import {
AUCTION_MIN_MINUTE_TIME,
@@ -36,6 +36,7 @@ import { convertKTCDate } from '../../utils';
import { msToMinutes } from '../../utils/date';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { battleEventHotTime, battleRepeatType } from '../../assets/data/options';
const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, setDetailData, landData, buildingData }) => {
const { t } = useTranslation();
@@ -62,10 +63,8 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
currency_type: content.currency_type,
start_price: content.start_price,
resv_start_dt: convertKTCDate(content.resv_start_dt),
resv_end_dt: convertKTCDate(content.resv_end_dt),
auction_start_dt: convertKTCDate(content.auction_start_dt),
auction_end_dt: convertKTCDate(content.auction_end_dt),
message_list: content.message_list,
});
const land = landData.find(land => land.id === parseInt(content.land_id));
setSelectLand(land);
@@ -86,76 +85,26 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
}
}, [resetDateTime]);
// 입력 수량 처리
const handleCount = e => {
const regex = /^\d*\.?\d{0,2}$/;
if (!regex.test(e.target.value) && e.target.value !== '-') {
return;
}
const opLand = useMemo(() => {
return landData?.map(item => ({
value: item.id,
name: `${item.name}(${item.id})`
})) || [];
}, [landData]);
let count = 0;
if (e.target.value === '-0') {
count = 1;
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
count = plusNum;
} else{
count = e.target.value;
}
setResultData((prevState) => ({
...prevState,
start_price: count,
}));
};
const handleLand = (value, key, currentFormData) => {
let land_id = value;
const handleReservationChange = {
start: (date) => {
setResultData(prev => ({ ...prev, resv_start_dt: date }));
},
end: (date) => {
setResultData(prev => ({ ...prev, resv_end_dt: date }));
}
};
const handleAuctionChange = {
start: (date) => {
setResultData(prev => ({ ...prev, auction_start_dt: date }));
},
end: (date) => {
setResultData(prev => ({ ...prev, auction_end_dt: date }));
}
};
// 입력 글자 제한
const handleInputData = e => {
if (e.target.value.length > 250) {
return;
}
const updatedMessages = resultData.message_list.map(msg =>
msg.language === message_lang
? { ...msg, content: e.target.value.trimStart() }
: msg
);
setResultData(prev => ({
...prev,
message_list: updatedMessages
}));
};
// 언어 선택
const handleLanguage = e => {
setMessage_lang(e.target.value);
if(!resultData.message_list.some(({language}) => language === e.target.value))
setResultData({ ...resultData, message_list: [...resultData.message_list, {language: e.target.value, content: ''}] })
}
const handleLand = e => {
const land_id = e.target.value;
const land = landData.find(land => land.id === parseInt(land_id));
const instance = buildingData.find(building => building.id === parseInt(land.buildingId))?.socket;
setSelectLand(land);
setResultData({ ...resultData, land_id: land_id, land_name: land.name, land_size: land.size, land_socket: instance });
const updatedData = {
...currentFormData,
land_name: land.name,
land_size: land.size,
land_socket: instance
};
setResultData(updatedData);
}
const handleReset = () => {
@@ -256,13 +205,12 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
&& resultData.resv_start_dt !== ''
&& resultData.resv_end_dt !== ''
&& resultData.land_id !== ''
// && resultData.message_list?.every(data => data.content !== '')
);
};
const isView = (label) => {
switch (label) {
case "recv":
case "resv":
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY && content?.status === landAuctionStatusType.wait);
case "auction":
case "price":
@@ -282,131 +230,117 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
}
}
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 4,
type: 'select',
key: 'land_id',
label: '랜드선택',
disabled: !isView('registry'),
width: '400px',
options: opLand,
handler: handleLand
},
{
row: 1,
col: 0,
colSpan: 4,
type: 'display',
key: 'land_name',
label: '랜드 이름',
width: '400px',
placeholder: '랜드를 선택하세요'
},
{
row: 2,
col: 0,
colSpan: 2,
type: 'display',
key: 'land_size',
label: '랜드 크기',
placeholder: '랜드를 선택하세요',
width: '200px'
},
{
row: 2,
col: 2,
colSpan: 2,
type: 'display',
key: 'land_socket',
label: '인스턴스 수',
placeholder: '랜드를 선택하세요',
width: '200px',
},
{
row: 3,
col: 0,
colSpan: 2,
type: 'select',
key: 'currency_type',
label: '입찰재화',
disabled: true,
width: '200px',
options: CurrencyType
},
{
row: 3,
col: 2,
colSpan: 2,
type: 'number',
key: 'start_price',
label: '입찰시작가',
disabled: !isView('price'),
width: '200px',
min: 0,
step: 0.01
},
{
row: 4,
col: 0,
colSpan: 4,
type: 'date',
key: 'resv_start_dt',
label: '예약시작일',
disabled: !isView('resv'),
width: '200px',
format: 'YYYY-MM-DD HH:mm',
showTime: true
},
{
row: 5,
col: 0,
colSpan: 4,
type: 'dateRange',
keys: {
start: 'auction_start_dt',
end: 'auction_end_dt'
},
label: '경매기간',
disabled: !isView('auction'),
width: '400px',
format: 'YYYY-MM-DD HH:mm',
showTime: true
}
]
}
];
return (
<>
<Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "랜드 경매 등록" : isView('modify') ? "랜드 경매 수정" : "랜드 경매 상세"}</Title>
<MessageWrapper>
<FormRowGroup>
<FormLabel>랜드선택</FormLabel>
<SelectInput value={resultData.land_id} onChange={e => handleLand(e)} disabled={!isView('registry')} width="400px">
{landData && landData.map((data, index) => (
<option key={index} value={data.id}>
{data.name}({data.id})
</option>
))}
</SelectInput>
</FormRowGroup>
<FormRowGroup>
<FormLabel>랜드 이름</FormLabel>
<FormInput
type="text"
disabled={true}
width='400px'
value={resultData?.land_name}
/>
</FormRowGroup>
<FormRowGroup>
<FormLabel>랜드 크기</FormLabel>
<FormInput
type="text"
disabled={true}
width='200px'
value={resultData?.land_size}
/>
<FormLabel>인스턴스 </FormLabel>
<FormInput
type="text"
disabled={true}
width='200px'
value={resultData?.land_socket}
/>
</FormRowGroup>
<FormRowGroup>
<FormLabel>입찰 재화</FormLabel>
<SelectInput value={resultData.currency_type} width='200px' disabled={true} >
{CurrencyType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<FormLabel>입찰시작가</FormLabel>
<FormInput
type="number"
name="price"
value={resultData.start_price}
step={"0.01"}
min={0}
width='200px'
disabled={!isView('price')}
onChange={e => handleCount(e)}
/>
</FormRowGroup>
<DateTimeRangePicker
label="예약기간"
startDate={resultData.resv_start_dt}
endDate={resultData.resv_end_dt}
onStartDateChange={handleReservationChange.start}
onEndDateChange={handleReservationChange.end}
pastDate={new Date()}
disabled={!isView('recv')}
startLabel="시작 일자"
endLabel="종료 일자"
reset={resetDateTime}
/>
<DateTimeRangePicker
label="경매기간"
startDate={resultData.auction_start_dt}
endDate={resultData.auction_end_dt}
onStartDateChange={handleAuctionChange.start}
onEndDateChange={handleAuctionChange.end}
pastDate={new Date()}
disabled={!isView('auction')}
startLabel="시작 일자"
endLabel="종료 일자"
reset={resetDateTime}
/>
{/*<NoticeInputRow2>*/}
{/* <InputLabel>*/}
{/* 메세지 작성[경매 시작 5분전 공지 - 미구현]*/}
{/* </InputLabel>*/}
{/* <NoticeInputItem2>*/}
{/* <InputLabel>언어</InputLabel>*/}
{/* <SelectInput onChange={e => handleLanguage(e) } value={message_lang}>*/}
{/* {languageType.map((data, index) => (*/}
{/* <option key={index} value={data.value}>*/}
{/* {data.name}*/}
{/* </option>*/}
{/* ))}*/}
{/* </SelectInput>*/}
{/* </NoticeInputItem2>*/}
{/*</NoticeInputRow2>*/}
{/*<BoxWrapper>*/}
{/* {resultData.message_list.map(content => {*/}
{/* return (*/}
{/* <Fragment key={content.language}>*/}
{/* {message_lang === content.language && (*/}
{/* <FormTextAreaWrapper>*/}
{/* <FormTextArea*/}
{/* name="content"*/}
{/* id={content.language}*/}
{/* value={content.content}*/}
{/* onChange={e => handleInputData(e)}*/}
{/* maxLength={250}*/}
{/* disabled={!isView('message')}*/}
{/* />*/}
{/* </FormTextAreaWrapper>*/}
{/* )}*/}
{/* </Fragment>*/}
{/* );*/}
{/* })}*/}
{/*</BoxWrapper>*/}
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
</MessageWrapper>
<BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar>
<FormStatusLabel>
@@ -463,14 +397,8 @@ export const initData = {
currency_type: 'Calium',
start_price: 0,
resv_start_dt: '',
resv_end_dt: '',
auction_start_dt: '',
auction_end_dt: '',
message_list: [
{ language: 'KO', content: '' },
{ language: 'EN', content: '' },
{ language: 'JA', content: '' },
],
auction_end_dt: ''
}
export const initLandData = {

View File

@@ -2,6 +2,21 @@ import { styled } from 'styled-components';
import RadioInput from '../common/input/Radio';
import React, { useState, useEffect, Fragment } from 'react';
import CheckBox from '../common/input/CheckBox';
import {
Input,
Button as AntButton,
Select,
Alert,
Space,
Card,
Row,
Col,
Checkbox,
Radio,
DatePicker,
TimePicker
} from 'antd';
import dayjs from 'dayjs';
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, DatePickerWrapper, Textarea} from '../../styles/Components';
import Button from '../common/button/Button';
@@ -32,6 +47,7 @@ import { useLoading } from '../../context/LoadingProvider';
import { alertTypes, currencyCodeTypes } from '../../assets/data/types';
import { userType2 } from '../../assets/data/options';
import { STORAGE_MAIL_COPY } from '../../assets/data/adminConstants';
import { DetailLayout } from '../common';
const MailDetailModal = ({ detailView, handleDetailView, content }) => {
const userInfo = useRecoilValue(authList);
@@ -46,10 +62,11 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
const [sendHour, setSendHour] = useState('00');
const [sendMin, setSendMin] = useState('00');
const [activeLanguage, setActiveLanguage] = useState('KO');
const [item, setItem] = useState('');
const [itemCount, setItemCount] = useState('');
const [itemCount, setItemCount] = useState(1);
const [resource, setResource] = useState(currencyCodeTypes.gold);
const [resourceCount, setResourceCount] = useState('');
const [resourceCount, setResourceCount] = useState(1);
const [resultData, setResultData] = useState({
is_reserve: false,
@@ -94,6 +111,7 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
mail_list: content.mail_list,
item_list: content.item_list,
guid: content.target,
send_status: content.send_status,
file_name: content.receive_type === 'MULTIPLE' ? content.target : null
});
@@ -122,6 +140,136 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
}
},[updateAuth, content, resultData])
// 발송 상태 렌더링
const renderSendStatus = () => {
const status = resultData.send_status;
let color = '';
let text = '';
switch (status) {
case 'WAIT':
color = '#FAAD14';
text = '대기';
break;
case 'FINISH':
color = '#52c41a';
text = '완료';
break;
case 'FAIL':
color = '#ff4d4f';
text = '실패';
break;
default:
color = '#d9d9d9';
text = status || '알 수 없음';
}
return (
<span style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: '4px',
backgroundColor: color,
color: 'white',
fontSize: '14px',
fontWeight: '500'
}}>
{text}
</span>
);
};
// 탭 항목 생성
const getLanguageTabItems = () => {
return resultData.mail_list?.map(mail => ({
key: mail.language,
label: mail.language,
children: (
<div style={{ padding: '10px', minHeight: '400px', height: 'auto' }}>
<Row gutter={[16, 24]}>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
제목 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input
value={mail.title || ''}
placeholder="우편 제목을 입력하세요"
maxLength={30}
readOnly={isView}
onChange={(e) => updateMailData(mail.language, 'title', e.target.value.trimStart())}
showCount
size="large"
/>
</div>
</Col>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
내용 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input.TextArea
value={mail.content || ''}
placeholder="우편 내용을 입력하세요"
readOnly={isView}
rows={6}
maxLength={2000}
showCount
onChange={(e) => {
if (e.target.value.length > 2000) return;
updateMailData(mail.language, 'content', e.target.value.trimStart());
}}
style={{
resize: 'vertical',
minHeight: '200px',
maxHeight: '400px'
}}
/>
</div>
</Col>
</Row>
</div>
),
closable: resultData.mail_list?.length > 1 && !isView,
})) || [];
};
// 메일 데이터 업데이트
const updateMailData = (language, field, value) => {
const updatedMailList = resultData.mail_list.map(mail =>
mail.language === language
? { ...mail, [field]: value }
: mail
);
setResultData({ ...resultData, mail_list: updatedMailList });
setIsChanged(true);
};
// 탭 삭제 핸들러
const handleTabClose = (targetKey) => {
if (resultData.mail_list.length <= 1) return;
const filterList = resultData.mail_list.filter(el => el.language !== targetKey);
setResultData({ ...resultData, mail_list: filterList });
if (activeLanguage === targetKey) {
setActiveLanguage(filterList[0]?.language || 'KO');
}
setIsChanged(true);
};
// 아이템 수량 숫자 체크
const handleItemCount = e => {
if (e.target.value === '0' || e.target.value === '-0') {
@@ -338,12 +486,282 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
}
}
const handleDateTimeChange = (datetime) => {
setResultData({ ...resultData, send_dt: datetime.toDate() });
setIsChanged(true);
};
// 아이템 목록 렌더링
const renderItemList = () => {
return (
<div>
{resultData.item_list && resultData.item_list.length > 0 ? (
<Space wrap>
{resultData.item_list.map((data, index) => (
<Card
key={index}
title={data.item_name}
size="small"
extra={
!isView && (
<AntButton
type="text"
danger
size="small"
onClick={() => onItemRemove(index)}
>
X
</AntButton>
)
}
style={{ minWidth: '150px' }}
>
<div>
<div>{data.item}</div>
<div>수량: {data.item_cnt}</div>
</div>
</Card>
))}
</Space>
) : (
<Alert message="등록된 아이템이 없습니다." type="info" showIcon />
)}
</div>
);
};
// 아이템 추가 컴포넌트
const renderItemAdd = () => {
return (
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="Item Meta id 입력"
value={item}
onChange={(e) => setItem(e.target.value.trimStart())}
disabled={isView}
style={{ width: '200px' }}
/>
<Input
type="number"
placeholder="수량"
value={itemCount}
onChange={(e) => setItemCount(e.target.value)}
disabled={isView}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleItemList}
disabled={itemCount.length === 0 || item.length === 0 || isView}
>
추가
</AntButton>
</Space.Compact>
);
};
// 자원 추가 컴포넌트
const renderResourceAdd = () => {
return (
<div>
<Space.Compact style={{ width: '100%', marginBottom: '8px' }}>
<Select
value={resource}
onChange={setResource}
disabled={isView}
style={{ width: '200px' }}
placeholder="자원 선택"
>
{currencyItemCode.map((data, index) => (
<Select.Option key={index} value={data.value}>
{data.name}
</Select.Option>
))}
</Select>
<Input
type="number"
placeholder="수량"
value={resourceCount}
disabled={isView}
onChange={(e) => setResourceCount(e.target.value)}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleResourceList}
disabled={resourceCount.length === 0 || resource.length === 0 || isView}
>
추가
</AntButton>
</Space.Compact>
{resource === currencyCodeTypes.calium && (
<div style={{ fontSize: '12px', color: '#666' }}>
잔여 수량: {caliumTotalData}
</div>
)}
</div>
);
};
// 수신대상 렌더링
const renderReceiver = () => {
return (
<div>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Radio.Group value={resultData.receive_type} disabled>
<Space direction="vertical">
<Radio value="SINGLE">
단일: {resultData.receive_type === 'SINGLE' ? resultData.guid : ''}
</Radio>
<Radio value="MULTIPLE">
복수: {resultData.receive_type === 'MULTIPLE' ? excelFile : ''}
</Radio>
</Space>
</Radio.Group>
</div>
</Space>
</div>
);
};
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 1,
type: 'custom',
key: 'is_reserve',
render: () => (
<Checkbox
checked={resultData.is_reserve}
disabled
onChange={(e) => {
setResultData({ ...resultData, is_reserve: e.target.checked });
setIsChanged(true);
}}
>
예약 발송
</Checkbox>
)
},
...(resultData.is_reserve ? [{
row: 0,
col: 1,
colSpan: 2,
type: 'custom',
key: 'send_dt',
label: '발송 시간',
render: () => (
<DatePicker
showTime
allowClear={false}
value={resultData.send_dt ? dayjs(resultData.send_dt) : null}
onChange={handleDateTimeChange}
disabled={!content?.is_reserve || isView}
format="YYYY-MM-DD HH:mm"
style={{ width: '200px' }}
disabledDate={(current) => current && current < dayjs().startOf('day')}
/>
)
}] : []),
{
row: 1,
col: 0,
colSpan: 1,
type: 'select',
key: 'mail_type',
label: '우편 타입',
value: resultData.mail_type,
disabled: isView,
options: [
{ value: 'SELECT', name: '타입 선택' },
...mailType.filter(data => data.value !== 'ALL')
],
handler: (value) => {
setResultData({ ...resultData, mail_type: value });
setIsChanged(true);
}
},
{
row: 1,
col: 2,
colSpan: 1,
type: 'custom',
key: 'send_status',
label: '발송상태',
render: renderSendStatus
},
{
row: 2,
col: 1,
colSpan: 1,
type: 'select',
key: 'user_type',
label: '수신대상 타입',
value: resultData.user_type,
disabled: true,
options: userType2
},
{
row: 2,
col: 2,
colSpan: 2,
type: 'custom',
key: 'receiver',
label: '수신대상',
render: renderReceiver
},
{
row: 3,
col: 0,
colSpan: 4,
type: 'tab',
key: 'language_tabs',
tabItems: getLanguageTabItems(),
activeKey: activeLanguage,
onTabChange: setActiveLanguage,
onTabClose: handleTabClose
},
{
row: 4,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_add',
label: '아이템 추가',
render: renderItemAdd
},
{
row: 5,
col: 0,
colSpan: 4,
type: 'custom',
key: 'resource_add',
label: '자원 추가',
render: renderResourceAdd
},
{
row: 6,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_list',
render: renderItemList
}
]
}
];
return (
<>
<Modal min="960px" $view={detailView}>
<Title $align="center">우편 상세 정보</Title>
{content && <>
<RegistInfo>
{content && <RegistInfo>
<span>등록자 : {content.create_by}</span>
<span>등록일 : {convertKTC(content.create_dt, false)}</span>
{typeof content.update_by !== 'undefined' && (
@@ -352,321 +770,14 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
<span>수정일 : {convertKTC(content.update_dt, false)}</span>
</>
)}
</RegistInfo>
<ModalWrapper>
<RegistGroup>
<InputRow>
<CheckBox
label="예약 발송"
id="reserve"
checked={resultData && resultData.is_reserve}
setData={e => {
setResultData({ ...resultData, is_reserve: e.target.checked });
setIsChanged(true);
}}
disabled={(content.is_reserve === false) || isView}
/>
{content.is_reserve === false ? (
<></>
) : (
resultData.is_reserve === true && (
<InputItem>
<InputLabel>발송 시간</InputLabel>
<InputGroup>
<DatePickerWrapper>
<DatePickerComponent
readOnly={(content.is_reserve === false) || isView}
name={initialData.send_dt}
selectedDate={resultData ? resultData.send_dt : initialData.send_dt}
handleSelectedDate={data => handleSelectedDate(data)}
pastDate={new Date()}
/>
</DatePickerWrapper>
<SelectInput
onChange={e => handleSendTime(e)}
id="hour"
disabled={(content.is_reserve === false) || isView}
value={
resultData && String(new Date(resultData.send_dt).getHours()) < 10
? '0' + String(new Date(resultData.send_dt).getHours())
: resultData && String(new Date(resultData.send_dt).getHours())
}>
{HourList.map(hour => (
<option value={hour} key={hour}>
{hour}
</option>
))}
</SelectInput>
<SelectInput
onChange={e => {
handleSendTime(e);
setIsChanged(true);
}}
id="min"
disabled={(content.is_reserve === false) || isView}
value={
resultData && String(new Date(resultData.send_dt).getMinutes()) < 10
? '0' + String(new Date(resultData.send_dt).getMinutes())
: resultData && String(new Date(resultData.send_dt).getMinutes())
}>
{MinuteList.map(min => (
<option value={min} key={min}>
{min}
</option>
))}
</SelectInput>
</InputGroup>
</InputItem>
)
)}
<InputItem>
<InputLabel>우편 타입</InputLabel>
<SelectInput
onChange={e => {
setResultData({ ...resultData, mail_type: e.target.value });
setIsChanged(true);
}}
value={resultData.mail_type}
disabled={isView}>
<option value="SELECT">타입 선택</option>
{mailType.filter(data => data.value !== 'ALL').map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</InputItem>
<InputItem>
<InputLabel>발송상태</InputLabel>
<div>
{initialData.send_status === 'WAIT' && <MailState>대기</MailState>}
{initialData.send_status === 'FINISH' && <MailState result="success">완료</MailState>}
{initialData.send_status === 'FAIL' && <MailState result="fail">실패</MailState>}
</div>
</InputItem>
</InputRow>
<MailReceiver>
<InputItem>
<InputLabel>수신대상</InputLabel>
<InputItem>
<SelectInput onChange={e => setResultData({ ...resultData, user_type: e.target.value })}
value={resultData.user_type}
disabled={true}>
{userType2.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</InputItem>
<div>
<InputGroup>
<RadioInput
label="단일"
id="SINGLE"
name="receiver"
value="SINGLE"
disabled={true}
fontWeight="600"
checked={resultData.receive_type === 'SINGLE'}
/>
<TextInput
disabled={true}
value={resultData.receive_type === 'SINGLE' && resultData.guid !== '' ? resultData.guid : ''}
/>
</InputGroup>
<InputGroup>
<RadioInput
label="복수"
id="MULTIPLE"
name="receiver"
value="MULTIPLE"
fontWeight="600"
disabled={true}
/>
<MailRegistUploadBtn
disabled={true}
setResultData={setResultData}
resultData={resultData}
setExcelFile={setExcelFile}
handleDetailDelete={() => {}}
disabledBtn={true}
excelName={excelFile}
setExcelName={setExcelFile}
downloadData={downloadData}
status={initialData.send_status}
/>
</InputGroup>
</div>
</InputItem>
</MailReceiver>
</RegistGroup>
{resultData.mail_list &&
resultData?.mail_list?.map(data => {
return (
<Fragment key={data.language}>
<MailRegistBox>
<LangArea>
언어 : {data.language}
{btnValidation === false ? (
<BtnClose
disabled={true}
onClick={e => {
e.preventDefault();
onLangDelete(data.language);
}}
/>
) : (
<BtnClose opacity="10%" />
)}
</LangArea>
<MailRegistTable>
<tbody>
<tr>
<th width="120">
<Label>제목</Label>
</th>
<td>
<InputItem>
<TextInput
placeholder="우편 제목 입력"
maxLength="30"
id={data.language}
value={data.title}
readOnly={(content.is_reserve === false) || isView}
onChange={e => {
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 });
setIsChanged(true);
}}
/>
</InputItem>
</td>
</tr>
<tr>
<th>
<Label>내용</Label>
</th>
<td>
<Textarea
value={data.content}
readOnly={isView}
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 });
setIsChanged(true);
}}
/>
</td>
</tr>
</tbody>
</MailRegistTable>
</MailRegistBox>
</Fragment>
);
})}
<MailRegistBox>
<MailRegistTable>
<tbody>
<tr>
<th width="120">
<Label>아이템 첨부</Label>
</th>
<td>
<InputItem>
<TextInput
placeholder="Item Meta id 입력"
value={item}
onChange={e => {
let list = [];
list = e.target.value.trimStart();
setItem(list);
}}
disabled={isView}
/>
<TextInput
placeholder="수량"
value={itemCount}
type="number"
onChange={e => handleItemCount(e)}
width="90px"
disabled={isView}
/>
<Button
text="추가"
theme={itemCount.length === 0 || item.length === 0 ? 'disable' : 'search'}
disabled={isView}
handleClick={handleItemList}
/>
</InputItem>
</td>
</tr>
<tr>
<th width="120">
<Label>자원 첨부</Label>
</th>
<td>
<InputItem>
<SelectInput onChange={e => setResource(e.target.value)} value={resource} disabled={isView}>
{currencyItemCode.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
placeholder="수량"
type="number"
value={resourceCount}
disabled={isView}
onChange={e => handleResourceCount(e)}
width="200px"
/>
<Button
text="추가"
theme={resourceCount.length === 0 || resource.length === 0 ? 'disable' : 'search'}
disabled={isView}
handleClick={handleResourceList}
width="100px"
height="35px"
/>
{resource === currencyCodeTypes.calium &&
<Label>(잔여 수량: {caliumTotalData})</Label>}
</InputItem>
<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>
{!isView && <BtnDelete onClick={() => onItemRemove(index)}></BtnDelete>}
</Item>
);
})}
</ItemList>
)}
</div>
</td>
</tr>
</tbody>
</MailRegistTable>
</MailRegistBox>
</ModalWrapper>
</>}
</RegistInfo>}
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
<BtnWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
<Button
text="확인"

View File

@@ -0,0 +1,150 @@
import { useCallback, useEffect, useState } from 'react';
import { InputLabel, TextInput } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { AssetsIndexView, CurrencyAcquireIndexView, ItemIndexView } from '../../apis';
export const useAssetsIndexSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate());
return date;
})(),
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await AssetsIndexView(
token,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result);
return result;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, updateSearchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
start_dt: now,
end_dt: new Date(),
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const AssetsIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default AssetsIndexSearchBar;

View File

@@ -0,0 +1,165 @@
import { InputLabel, SelectInput } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { CurrencyType } from '../../assets/data';
import { CurrencyAcquireIndexView } from '../../apis';
export const useCurrencyAcquireIndexSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate());
return date;
})(),
currency_type: 'Gold',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await CurrencyAcquireIndexView(
token,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.currency_type,
"Acquire",
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result);
return result;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
start_dt: now,
end_dt: new Date(),
currency_type: 'Gold',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const CurrencyAcquireIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>,
<>
<InputLabel>재화종류</InputLabel>
<SelectInput value={searchParams.currency_type} onChange={e => onSearch({ currency_type: e.target.value }, false)} >
{CurrencyType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default CurrencyAcquireIndexSearchBar;

View File

@@ -0,0 +1,165 @@
import { InputLabel, SelectInput } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { CurrencyType } from '../../assets/data';
import { CurrencyAcquireIndexView } from '../../apis';
export const useCurrencyConsumeIndexSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate());
return date;
})(),
currency_type: 'Gold',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await CurrencyAcquireIndexView(
token,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.currency_type,
"Consume",
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result);
return result;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
start_dt: now,
end_dt: new Date(),
currency_type: 'Gold',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const CurrencyConsumeIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>,
<>
<InputLabel>재화종류</InputLabel>
<SelectInput value={searchParams.currency_type} onChange={e => onSearch({ currency_type: e.target.value }, false)} >
{CurrencyType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default CurrencyConsumeIndexSearchBar;

View File

@@ -125,7 +125,7 @@ export const useCurrencyIndexSearch = (token, initialPageSize) => {
};
};
const CurrencyIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const CurrencyUserIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
@@ -147,4 +147,4 @@ const CurrencyIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default CurrencyIndexSearchBar;
export default CurrencyUserIndexSearchBar;

View File

@@ -0,0 +1,171 @@
import { useCallback, useEffect, useState } from 'react';
import { InputLabel, TextInput } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { CurrencyAcquireIndexView, ItemIndexView } from '../../apis';
export const useItemAcquireIndexSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate());
return date;
})(),
item_id: '',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await ItemIndexView(
token,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.item_id,
"Acquire",
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result);
return result;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
start_dt: now,
end_dt: new Date(),
item_id: '',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const ItemAcquireIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const {showToast} = useAlert();
const handleSubmit = event => {
event.preventDefault();
if(searchParams.item_id.length === 0 || searchParams.item_id === ''){
showToast('ITEM_ID_EMPTY_WARNING', {type: alertTypes.warning});
return;
}
onSearch(searchParams, true);
};
const searchList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>,
<>
<InputLabel>아이템 ID</InputLabel>
<TextInput
type="text"
placeholder={'아이템 ID 입력'}
value={searchParams.item_id}
width="300px"
onChange={e => onSearch({ item_id: e.target.value }, false)}
/>
</>,
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default ItemAcquireIndexSearchBar;

View File

@@ -0,0 +1,171 @@
import { useCallback, useEffect, useState } from 'react';
import { InputLabel, TextInput } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { CurrencyAcquireIndexView, ItemIndexView } from '../../apis';
export const useItemConsumeIndexSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate());
return date;
})(),
item_id: '',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await ItemIndexView(
token,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.item_id,
"Consume",
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result);
return result;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
start_dt: now,
end_dt: new Date(),
item_id: '',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const ItemConsumeIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const {showToast} = useAlert();
const handleSubmit = event => {
event.preventDefault();
if(searchParams.item_id.length === 0 || searchParams.item_id === ''){
showToast('ITEM_ID_EMPTY_WARNING', {type: alertTypes.warning});
return;
}
onSearch(searchParams, true);
};
const searchList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>,
<>
<InputLabel>아이템 ID</InputLabel>
<TextInput
type="text"
placeholder={'아이템 ID 입력'}
value={searchParams.item_id}
width="300px"
onChange={e => onSearch({ item_id: e.target.value }, false)}
/>
</>,
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default ItemConsumeIndexSearchBar;

View File

@@ -0,0 +1,175 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import { userSearchType2 } from '../../assets/data/options';
import { getUserLoginDetailList, getUserSnapshotList } from '../../apis';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
export const useUserSnapshotLogSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
search_type: 'GUID',
search_data: '',
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
// const initialLoad = async () => {
// await fetchData(searchParams);
// };
//
// initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await getUserSnapshotList(
token,
params.search_type,
params.search_data,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result.data);
return result.data;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [showToast, token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
search_type: 'GUID',
search_data: '',
start_dt: now,
end_dt: now,
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const UserSnapshotLogSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputGroup>
<SelectInput value={searchParams.search_type} onChange={e => onSearch({search_type: e.target.value }, false)}>
{userSearchType2.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
type="text"
placeholder={searchParams.search_type === 'GUID' ? 'GUID ID 입력' : searchParams.search_type === 'NICKNAME' ? '아바타명 입력' :'Account ID 입력'}
value={searchParams.search_data}
width="260px"
onChange={e => onSearch({ search_data: e.target.value }, false)}
/>
</InputGroup>
</>,
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>,
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default UserSnapshotLogSearchBar;

View File

@@ -27,9 +27,15 @@ import BusinessLogSearchBar, { useBusinessLogSearch } from './BusinessLogSearchB
import CurrencyLogSearchBar, { useCurrencyLogSearch } from './CurrencyLogSearchBar';
import ItemLogSearchBar, { useItemLogSearch } from './ItemLogSearchBar';
import CurrencyItemLogSearchBar, { useCurrencyItemLogSearch } from './CurrencyItemLogSearchBar';
import CurrencyIndexSearchBar, { useCurrencyIndexSearch } from './CurrencyIndexSearchBar';
import CurrencyUserIndexSearchBar, { useCurrencyIndexSearch } from './CurrencyUserIndexSearchBar';
import CurrencyAcquireIndexSearchBar, { useCurrencyAcquireIndexSearch } from './CurrencyAcquireIndexSearchBar';
import CurrencyConsumeIndexSearchBar, { useCurrencyConsumeIndexSearch } from './CurrencyConsumeIndexSearchBar';
import ItemAcquireIndexSearchBar, { useItemAcquireIndexSearch } from './ItemAcquireIndexSearchBar';
import ItemConsumeIndexSearchBar, { useItemConsumeIndexSearch } from './ItemConsumeIndexSearchBar';
import UserCreateLogSearchBar, { useUserCreateLogSearch } from './UserCreateLogSearchBar';
import UserLoginLogSearchBar, { useUserLoginLogSearch } from './UserLoginLogSearchBar';
import UserSnapshotLogSearchBar, { useUserSnapshotLogSearch } from './UserSnapshotLogSearchBar';
import AssetsIndexSearchBar, { useAssetsIndexSearch } from './AssetsIndexSearchBar';
import LandAuctionSearchBar from './LandAuctionSearchBar';
import CaliumRequestSearchBar from './CaliumRequestSearchBar';
@@ -66,15 +72,27 @@ export {
useCurrencyLogSearch,
LandAuctionSearchBar,
CaliumRequestSearchBar,
CurrencyIndexSearchBar,
CurrencyUserIndexSearchBar,
useCurrencyIndexSearch,
useItemLogSearch,
ItemLogSearchBar,
CurrencyItemLogSearchBar,
useCurrencyItemLogSearch,
UserCreateLogSearchBar,
useUserCreateLogSearch,
UserLoginLogSearchBar,
useUserLoginLogSearch,
useItemLogSearch,
ItemLogSearchBar,
CurrencyItemLogSearchBar,
useCurrencyItemLogSearch,
UserCreateLogSearchBar,
useUserCreateLogSearch,
UserLoginLogSearchBar,
useUserLoginLogSearch,
CurrencyAcquireIndexSearchBar,
useCurrencyAcquireIndexSearch,
CurrencyConsumeIndexSearchBar,
useCurrencyConsumeIndexSearch,
ItemAcquireIndexSearchBar,
useItemAcquireIndexSearch,
ItemConsumeIndexSearchBar,
useItemConsumeIndexSearch,
UserSnapshotLogSearchBar,
useUserSnapshotLogSearch,
AssetsIndexSearchBar,
useAssetsIndexSearch
};

View File

@@ -102,7 +102,22 @@ export const useCommonSearchOld = (configPath) => {
}, [configLoaded, config, searchParams, fetchData]);
// 검색 파라미터 업데이트
const updateSearchParams = useCallback((newParams) => {
const updateSearchParams = useCallback((newParams, resetPage = false) => {
const pageField = config?.apiInfo?.pageField || 'currentPage';
setSearchParams(prev => {
// 페이지 필드 자체가 변경되는 경우가 아니라면 1페이지로 리셋
const shouldResetPage = !newParams.hasOwnProperty(pageField) && prev[pageField] > 1;
return {
...prev,
...newParams,
...(shouldResetPage && { [pageField]: 1 })
};
});
}, [config]);
const updateSearchParamsForPagination = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
@@ -142,8 +157,14 @@ export const useCommonSearchOld = (configPath) => {
if (!config) return null;
const pageField = config.apiInfo?.pageField || 'currentPage';
return await handleSearch({ [pageField]: newPage }, true);
}, [handleSearch, config]);
const updatedParams = {
...searchParams,
[pageField]: newPage
};
updateSearchParamsForPagination(updatedParams);
return await fetchData(updatedParams);
}, [searchParams, fetchData, config, updateSearchParamsForPagination]);
// 페이지 크기 변경
const handlePageSizeChange = useCallback(async (newSize) => {

View File

@@ -56,6 +56,7 @@ const resources = {
DOWNLOAD_FAIL: '다운이 실패하였습니다.',
DELETE_STATUS_ONLY_WAIT: '대기상태의 데이터만 삭제가 가능합니다.',
TABLE_DATA_NOT_FOUND: '데이터가 없습니다.',
ITEM_ID_EMPTY_WARNING: '아이템 아이디를 입력해주세요.',
//user
NICKNAME_CHANGES_CONFIRM: '닉네임을 변경하시겠습니까?',
NICKNAME_CHANGES_COMPLETE: '닉네임 변경이 완료되었습니다.',
@@ -164,14 +165,21 @@ const resources = {
FILE_SIZE_OVER_ERROR: "파일의 사이즈가 5MB를 초과하였습니다.",
//파일명칭
FILE_INDEX_USER_CONTENT: 'Caliverse_User_Index.xlsx',
FILE_INDEX_USER_RETENTION: 'Caliverse_Retention.xlsx',
FILE_INDEX_USER_RETENTION: 'Caliverse_Retention.csv',
FILE_INDEX_CURRENCY_ACQUIRE: 'Caliverse_Currency_Acquire.csv',
FILE_INDEX_CURRENCY_CONSUME: 'Caliverse_Currency_Consume.csv',
FILE_INDEX_ITEM_ACQUIRE: 'Caliverse_Item_Acquire.csv',
FILE_INDEX_ITEM_CONSUME: 'Caliverse_Item_Consume.csv',
FILE_INDEX_ASSETS_CURRENCY: 'Caliverse_Assets_Currency.csv',
FILE_INDEX_ASSETS_ITEM: 'Caliverse_Assets_Item.csv',
FILE_CALIUM_REQUEST: 'Caliverse_Calium_Request.xlsx',
FILE_LAND_AUCTION: 'Caliverse_Land_Auction.xlsx',
FILE_BUSINESS_LOG: 'Caliverse_Log.xlsx',
FILE_BUSINESS_LOG: 'Caliverse_Log',
FILE_BATTLE_EVENT: 'Caliverse_Battle_Event.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',
FILE_GAME_LOG_USER_SNAPSHOT: 'Caliverse_Game_Log_User_Snapshot',
FILE_GAME_LOG_ITEM: 'Caliverse_Game_Log_Item',
FILE_GAME_LOG_CURRENCY_ITEM: 'Caliverse_Game_Log_Currecy_Item',
FILE_CURRENCY_INDEX: 'Caliverse_Currency_Index',

View File

@@ -20,6 +20,7 @@ import ItemLogContent from '../../components/DataManage/ItemLogContent';
import CurrencyItemLogContent from '../../components/DataManage/CurrencyItemLogContent';
import UserCreateLogContent from '../../components/DataManage/UserCreateLogContent';
import UserLoginLogContent from '../../components/DataManage/UserLoginLogContent';
import UserSnapshotLogContent from '../../components/DataManage/UserSnapshotLogContent';
registerLocale('ko', ko);
@@ -62,6 +63,7 @@ const GameLogView = () => {
<CurrencyItemLogContent active={activeTab === 'CURRENCYITEM'} />
<UserCreateLogContent active={activeTab === 'USERCREATE'} />
<UserLoginLogContent active={activeTab === 'USERLOGIN'} />
<UserSnapshotLogContent active={activeTab === 'SNAPSHOT'} />
</AnimatedPageWrapper>
);
};

View File

@@ -1,29 +1,19 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import Button from '../../components/common/button/Button';
import { Title, BtnWrapper, ButtonClose, ModalText, TabItem, TabScroll, TabWrapper } from '../../styles/Components';
import { styled } from 'styled-components';
import Modal from '../../components/common/modal/Modal';
import { useNavigate } from 'react-router-dom';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
import CreditContent from '../../components/IndexManage/CreditContent';
import VBPContent from '../../components/IndexManage/VBPContent';
import ItemContent from '../../components/IndexManage/ItemContent';
import InstanceContent from '../../components/IndexManage/InstanceContent';
import DecoContent from '../../components/IndexManage/DecoContent';
import { Title, TabItem, TabScroll, TabWrapper } from '../../styles/Components';
import { withAuth } from '../../hooks/hook';
import { authType } from '../../assets/data';
import { TabEconomicIndexList, TabGameLogList } from '../../assets/data/options';
import { TabEconomicIndexList } from '../../assets/data/options';
import CurrencyAcquireContent from '../../components/IndexManage/CurrencyAcquireContent';
import CurrencyConsumeContent from '../../components/IndexManage/CurrencyConsumeContent';
import ItemAcquireContent from '../../components/IndexManage/ItemAcquireContent';
import ItemConsumeContent from '../../components/IndexManage/ItemConsumeContent';
import CurrencyAssetsContent from '../../components/IndexManage/CurrencyAssetsContent';
import ItemAssetsContent from '../../components/IndexManage/ItemAssetsContent';
const EconomicIndex = () => {
const [activeTab, setActiveTab] = useState('CURRENCY');
const [activeTab, setActiveTab] = useState('CURRENCY_ACQUIRE');
const handleTab = (e, content) => {
e.preventDefault();
@@ -45,11 +35,12 @@ const EconomicIndex = () => {
})}
</TabWrapper>
</TabScroll>
{activeTab === 'CURRENCY' && <CreditContent />}
{/*{activeTab === 'vbp' && <VBPContent />}*/}
{/*{activeTab === 'item' && <ItemContent />}*/}
{/*{activeTab === 'instance' && <InstanceContent />}*/}
{/*{activeTab === 'deco' && <DecoContent />}*/}
{activeTab === 'CURRENCY_ACQUIRE' && <CurrencyAcquireContent />}
{activeTab === 'CURRENCY_CONSUME' && <CurrencyConsumeContent />}
{activeTab === 'ITEM_ACQUIRE' && <ItemAcquireContent />}
{activeTab === 'ITEM_CONSUME' && <ItemConsumeContent />}
{activeTab === 'CURRENCY_ASSETS' && <CurrencyAssetsContent />}
{activeTab === 'ITEM_ASSETS' && <ItemAssetsContent />}
</AnimatedPageWrapper>
);
};

View File

@@ -4,10 +4,11 @@ import { Link } from 'react-router-dom';
import { AnimatedPageWrapper } from '../../components/common/Layout';
import { Title } from '../../styles/Components';
import { UserContent, SegmentContent, PlayTimeContent, RetentionContent, DailyActiveUserContent, DailyMedalContent } from '../../components/IndexManage/index';
import { UserContent, RetentionContent } from '../../components/IndexManage/index';
import { authType } from '../../assets/data';
import { withAuth } from '../../hooks/hook';
import { TabUserIndexList } from '../../assets/data/options';
import CreditContent from '../../components/IndexManage/CreditContent';
const UserIndex = () => {
const [activeTab, setActiveTab] = useState('USER');
@@ -32,12 +33,9 @@ const UserIndex = () => {
})}
</TabWrapper>
{/*{activeTab === 'DAU' && <DailyActiveUserContent />}*/}
{activeTab === 'USER' && <UserContent />}
{activeTab === 'RETENTION' && <RetentionContent />}
{activeTab === 'SEGMENT' && <SegmentContent />}
{activeTab === 'PLAYTIME' && <PlayTimeContent />}
{/*{activeTab === '메달' && <DailyMedalContent />}*/}
{activeTab === 'CURRENCY' && <CreditContent />}
</AnimatedPageWrapper>

View File

@@ -1,21 +1,14 @@
import React, { Fragment, useState } from 'react';
import { Link } from 'react-router-dom';
import { useEffect } from 'react';
import Button from '../../components/common/button/Button';
import CheckBox from '../../components/common/input/CheckBox';
import Modal from '../../components/common/modal/Modal';
import styled from 'styled-components';
import {
Title,
TableWrapper,
BtnWrapper,
TableInfo,
ListOption,
TableStyle,
ButtonClose,
ModalText,
DetailMessage,
} from '../../styles/Components';
@@ -23,14 +16,11 @@ import { NoticeDelete, NoticeDetailView, NoticeListView } from '../../apis/Notic
import { BoardInfoModal, BoardRegistModal } from '../../components/ServiceManage';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
import { useNavigate } from 'react-router-dom';
import { convertKTC, timeDiffMinute } from '../../utils';
import AuthModal from '../../components/common/modal/AuthModal';
import { authType, landAuctionStatusType } from '../../assets/data';
import { convertKTC } from '../../utils';
import { authType } from '../../assets/data';
import { useAlert } from '../../context/AlertProvider';
import { message_type, sendStatus } from '../../assets/data/options';
import { BattleEventDelete, BattleEventDetailView, BattleEventStop } from '../../apis/Battle';
import { alertTypes, battleEventStatusType } from '../../assets/data/types';
import { alertTypes } from '../../assets/data/types';
import { useModal, useTable, withAuth } from '../../hooks/hook';
import { useLoading } from '../../context/LoadingProvider';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
@@ -109,7 +99,7 @@ const Board = () => {
selectedRows.map(data =>
list.push({
message_id: data.id,
id: data.id,
}),
);

View File

@@ -45,6 +45,7 @@ const Items = () => {
pagination,
goToNextPage,
goToPrevPage,
handlePageChange
} = useEnhancedCommonSearch("itemSearch");
const {
@@ -85,7 +86,7 @@ const Items = () => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
setItemCount('1');
handleSearch(updateSearchParams);
handlePageChange(pagination.currentPage);
});
break;
@@ -118,7 +119,7 @@ const Items = () => {
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
handleSearch(updateSearchParams);
handlePageChange(pagination.currentPage);
});
break;

View File

@@ -205,7 +205,6 @@ const LandAuction = () => {
{/*<th width="100">경매 재화</th>*/}
<th width="90">경매 시작가</th>
<th width="200">예약 시작일</th>
<th width="200">예약 종료일</th>
<th width="200">경매 시작일</th>
<th width="200">경매 종료일</th>
<th width="200">낙찰 정산일</th>
@@ -236,7 +235,6 @@ const LandAuction = () => {
{/*<td>{auction.currency_type}</td>*/}
<td>{auction.start_price}</td>
<td>{convertKTC(auction.resv_start_dt)}</td>
<td>{convertKTC(auction.resv_end_dt)}</td>
<td>{convertKTC(auction.auction_start_dt)}</td>
<td>{convertKTC(auction.auction_end_dt)}</td>
<td>{convertKTC(auction.close_end_dt)}</td>

View File

@@ -226,7 +226,7 @@ const Mail = () => {
detailView={modalState.detailModal}
handleDetailView={() =>{
handleModalClose('detail');
handleSearch(updateSearchParams);
handlePageChange(searchParams.currentPage);
}}
content={detailData}
/>

View File

@@ -15,14 +15,14 @@ import { Title, FormWrapper, TableStyle, TableWrapper, PopupMessage } from '../.
import {
StatusWapper,
ChargeBtn,
StatusLabel, TitleItemLabel, TitleItemValue, TitleItem,
StatusLabel,
} from '../../styles/ModuleComponents';
import {Button, ExcelDownButton, Pagination, ViewTableInfo} from '../../components/common';
import { convertKTC, truncateText } from '../../utils';
import { CaliumRequestRegistModal } from '../../components/UserManage';
import { CaliumCharge, LogHistory } from '../../apis';
import { useModal, withAuth } from '../../hooks/hook';
import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage';
import { CommonSearchBar } from '../../components/ServiceManage';
import {
INITIAL_PAGE_LIMIT,
LOG_ACTION_FAIL_CALIUM_ECHO,
@@ -35,7 +35,6 @@ import useCommonSearchOld from '../../hooks/useCommonSearchOld';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import { historyTables } from '../../assets/data/data';
import { useNavigate } from 'react-router-dom';
import { logAction } from '../../assets/data/options';
const CaliumRequest = () => {
const token = sessionStorage.getItem('token');
@@ -46,7 +45,6 @@ const CaliumRequest = () => {
const navigate = useNavigate();
const tableRef = useRef(null);
const [selectCharge, setSelectCharge] = useState({});
const [historyData, setHistoryData] = useState({});
const {
@@ -90,15 +88,15 @@ const CaliumRequest = () => {
});
break;
case "chargedConfirm":
setSelectCharge({id: param.id, count: param.count});
const select = {id: param.id, count: param.count};
showModal('CALIUM_CHARGE_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('charged')
onConfirm: () => handleSubmit('charged', select)
});
break;
case "charged":
await withLoading( async () => {
return await CaliumCharge(token, selectCharge);
return await CaliumCharge(token, param);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('CHARGE_COMPLTED', {type: alertTypes.success});
@@ -110,7 +108,6 @@ const CaliumRequest = () => {
}).catch(error => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() =>{
setSelectCharge({});
handleSearch(updateSearchParams);
});
break;

View File

@@ -43,7 +43,7 @@ const LogView = () => {
switch (action) {
case "detail":
handleModalView('detail');
setDetailData(item.data);
setDetailData(item.domain.data);
break;
default:

View File

@@ -1,170 +0,0 @@
import { Fragment, useState, useEffect } from 'react';
import {LogViewSearchBar} from '../../components/ServiceManage';
import { Title, FormWrapper, SelectInput, TableInfo, ListCount, ListOption, TableStyle, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
import Button from '../../components/common/button/Button';
import Pagination from '../../components/common/Pagination/Pagination';
import Modal from '../../components/common/modal/Modal';
import LogViewModal from '../../components/UserManage/LogViewModal';
import { LogViewList } from '../../apis';
import { useNavigate } from 'react-router-dom';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
import { logOption } from '../../assets/data';
import { convertKTC } from '../../utils';
const LogView = () => {
const navigate = useNavigate();
const userInfo = useRecoilValue(authList);
const [currentPage, setCurrentPage] = useState(1);
const [stateModal, setStateModal] = useState('hidden');
const [dataList, setDataList] = useState([]);
const [detailData, setDetailData] = useState('');
const [searchData, setSearchData] = useState({});
const [orderBy, setOrderBy] = useState('DESC');
const [pageSize, setPageSize] = useState('50');
const handleButtonClick = content => {
setStateModal('view');
setDetailData(content);
};
const handleModal = () => {
if (stateModal === 'hidden') {
setStateModal('view');
} else {
setStateModal('hidden');
}
};
const handleOrderBy = e => {
const order = e.target.value;
setOrderBy(order);
fetchData(
searchData.searchOption,
searchData.data,
searchData.logOption,
searchData.startDate && new Date(searchData.startDate).toISOString(),
searchData.endDate && new Date(searchData.endDate).toISOString(),
order,
pageSize,
);
};
const handlePageSize = e => {
const size = e.target.value;
setPageSize(size);
setCurrentPage(1);
fetchData(
searchData.searchOption,
searchData.data,
searchData.logOption,
searchData.startDate && new Date(searchData.startDate).toISOString(),
searchData.endDate && new Date(searchData.endDate).toISOString(),
orderBy,
size,
1,
);
};
const fetchData = async (searchType, searchKey, historyType, startDt, endDt, order, size) => {
const token = sessionStorage.getItem('token');
setDataList(await LogViewList(token, searchType, searchKey, historyType, startDt, endDt, order ? order : orderBy, size ? size : pageSize, currentPage));
};
useEffect(() => {
fetchData(
searchData.searchOption,
searchData.data,
searchData.logOption,
searchData.startDate && new Date(searchData.startDate).toISOString(),
searchData.endDate && new Date(searchData.endDate).toISOString(),
orderBy,
pageSize,
);
}, [currentPage]);
const handleSearch = (searchType, searchKey, historyType, startDt, endDt) => {
fetchData(searchType, searchKey, historyType, startDt, endDt);
};
return (
<>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === 5) ? (
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={'view'}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={() => navigate(-1)} />
</BtnWrapper>
<ModalText $align="center">
해당 메뉴에 대한 조회 권한이 없습니다.
<br />
권한 등급을 변경 다시 이용해주세요.
</ModalText>
<BtnWrapper $gap="10px">
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={() => navigate(-1)} />
</BtnWrapper>
</Modal>
) : (
<>
<Title>사용 이력 조회</Title>
<FormWrapper action="" $flow="row">
<LogViewSearchBar handleSearch={handleSearch} resultData={setSearchData} />
</FormWrapper>
<TableInfo>
<ListCount>
: {dataList && dataList.total} / {dataList && dataList.total_all}
</ListCount>
<ListOption>
<SelectInput className="input-select" onChange={e => handleOrderBy(e)}>
<option value="DESC">최신일자</option>
<option value="ASC">오래된순</option>
</SelectInput>
<SelectInput className="input-select" onChange={e => handlePageSize(e)}>
<option value="50">50</option>
<option value="100">100</option>
</SelectInput>
</ListOption>
</TableInfo>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th width="200">일시</th>
<th width="200">이름</th>
<th width="50%">ID</th>
<th width="30%">사용 이력</th>
{/* <th width="25%">상세 이력</th> */}
<th width="150">상세 보기</th>
</tr>
</thead>
<tbody>
{dataList.list &&
dataList.list.map((history, index) => (
<Fragment key={index}>
<tr>
<td>{convertKTC(history.create_dt)}</td>
<td>{history.name}</td>
<td>{history.mail}</td>
<td>{logOption.map(data => data.value === history.history_type && data.name)}</td>
<td>
<Button theme="line" text="JSON INFO" handleClick={() => handleButtonClick(history.content)} />
</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
<Pagination postsPerPage={pageSize} totalPosts={dataList && dataList.total_all} setCurrentPage={setCurrentPage} currentPage={currentPage} pageLimit={10} />
<LogViewModal stateModal={stateModal} handleModal={handleModal} data={detailData} />
</>
)}
</>
);
};
export default LogView;

View File

@@ -162,6 +162,14 @@ export const InputItem = styled.div`
gap: 10px;
`;
export const Notice = styled.span`
font-size: 12px;
font-weight: 300;
color: ${props => props.$color || '#999'} !important;
margin-top: 10px;
display: block;
`;
export const BtnWrapper = styled.div`
width: ${props => props.width};
display: flex;
@@ -240,6 +248,20 @@ export const ListCount = styled.div`
}
`;
export const TableInfoContent = styled.div`
position: absolute;
display: flex;
left: 0;
top: 50%;
transform: translate(0, -50%);
color: #686868;
gap: 20px;
span {
color: #333;
font-weight: 500;
}
`;
export const HeaderPaginationContainer = styled.div`
position: absolute;
display: flex;