Compare commits
10 Commits
9d06246aba
...
67c048a11d
| Author | SHA1 | Date | |
|---|---|---|---|
| 67c048a11d | |||
| f6a0319701 | |||
| 0368bf77e7 | |||
| b2b579ead1 | |||
| 495243a1a3 | |||
| 7993f37e26 | |||
| 93a7c087e1 | |||
| 38fa323db6 | |||
| 6f9f0307ac | |||
| dc7934d906 |
@@ -11,7 +11,7 @@ server {
|
||||
|
||||
# api reverse proxy
|
||||
location /api/ {
|
||||
proxy_pass http://172.40.129.180:23450;
|
||||
proxy_pass http://172.24.128.231:23450;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@hookform/resolvers": "^3.2.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"antd": "^5.26.1",
|
||||
"axios": "^1.4.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"framer-motion": "^12.19.1",
|
||||
"i18next": "^23.15.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -130,3 +130,20 @@ export const BattleRewardView = async (token) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GameModeView = async (token) => {
|
||||
try {
|
||||
const res = await Axios.get(
|
||||
`/api/v1/battle/game-mode/list`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
);
|
||||
|
||||
return res.data.data.game_mode_list;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('GameModeView Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
114
src/apis/Log.js
114
src/apis/Log.js
@@ -1,12 +1,13 @@
|
||||
//운영 정보 관리 - 로그 api 연결
|
||||
|
||||
import { Axios } from '../utils';
|
||||
import { Axios, responseFileDownload } from '../utils';
|
||||
|
||||
// 비즈니스 로그 조회
|
||||
export const BusinessLogList = async (token, params) => {
|
||||
try {
|
||||
const res = await Axios.post(`/api/v1/log/generic/list`, params, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
timeout: 600000
|
||||
});
|
||||
|
||||
return res.data;
|
||||
@@ -16,3 +17,114 @@ export const BusinessLogList = async (token, params) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const BusinessLogExport = async (token, params) => {
|
||||
try {
|
||||
await Axios.post(`/api/v1/log/generic/excel-export`, params, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
responseType: 'blob'
|
||||
}).then(response => {
|
||||
responseFileDownload(response, {
|
||||
defaultFileName: 'businessLog'
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
throw new Error('BusinessLogExport Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getExcelProgress = async (token, taskId) => {
|
||||
try {
|
||||
const response = await Axios.get(`/api/v1/log/progress/${taskId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Progress API error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCurrencyList = async (token, startDate, endDate, order, size, currentPage) => {
|
||||
try {
|
||||
const response = await Axios.get(`/api/v1/log/currency/list?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('getCurrencyList API error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const GameCurrencyLogExport = async (token, params, fileName) => {
|
||||
try {
|
||||
await Axios.post(`/api/v1/log/currency/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('GameCurrencyLogExport Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getCurrencyDetailList = async (token, searchType, searchData, tranId, logAction, currencyType, amountDeltaType, startDate, endDate, order, size, currentPage) => {
|
||||
try {
|
||||
const response = await Axios.get(`/api/v1/log/currency/detail/list?search_type=${searchType}&search_data=${searchData}&tran_id=${tranId}
|
||||
&log_action=${logAction}¤cy_type=${currencyType}&amount_delta_type=${amountDeltaType}&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('getCurrencyDetailList API error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const GameCurrencyDetailLogExport = async (token, params, fileName) => {
|
||||
try {
|
||||
console.log(params);
|
||||
await Axios.post(`/api/v1/log/currency/detail/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('GameCurrencyDetailLogExport Error', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -17,6 +17,7 @@ export * from './Calium';
|
||||
export * from './Land';
|
||||
export * from './Menu';
|
||||
export * from './OpenAI';
|
||||
export * from './Log';
|
||||
|
||||
const apiModules = {};
|
||||
const allApis = {};
|
||||
|
||||
@@ -10,6 +10,7 @@ export const AUCTION_MIN_MINUTE_TIME = 15; // 15분
|
||||
export const IMAGE_MAX_SIZE = 5242880;
|
||||
export const STORAGE_MAIL_COPY = 'copyMailData';
|
||||
export const STORAGE_BUSINESS_LOG_SEARCH = 'businessLogSearchParam';
|
||||
export const STORAGE_GAME_LOG_CURRENCY_SEARCH = 'gameLogCurrencySearchParam';
|
||||
export const LOG_ACTION_FAIL_CALIUM_ECHO = 'FailCaliumEchoSystem';
|
||||
|
||||
export { INITIAL_PAGE_SIZE, INITIAL_CURRENT_PAGE, INITIAL_PAGE_LIMIT };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export {authType, ivenTabType, modalTypes, TabList, tattooSlot, commonStatus, ViewTitleCountType, landAuctionStatusType} from './types'
|
||||
export {authType, ivenTabType, modalTypes, TabUserList, tattooSlot, commonStatus, ViewTitleCountType, landAuctionStatusType} from './types'
|
||||
export {
|
||||
mailSendType,
|
||||
mailType,
|
||||
@@ -7,7 +7,7 @@ export {
|
||||
adminLevelType,
|
||||
logOption,
|
||||
eventStatus,
|
||||
currencyType,
|
||||
currencyItemCode,
|
||||
blockStatus,
|
||||
blockSanctions,
|
||||
blockPeriod,
|
||||
@@ -27,5 +27,6 @@ export {
|
||||
opYNType,
|
||||
opUserSessionType,
|
||||
opMailType,
|
||||
amountDeltaType
|
||||
} from './options'
|
||||
export {benItems, MinuteList, HourList, caliumRequestInitData, STATUS_STYLES, months, PAGE_SIZE_OPTIONS, ORDER_OPTIONS} from './data'
|
||||
@@ -204,16 +204,16 @@ export const menuConfig = {
|
||||
view: true,
|
||||
authLevel: adminAuthLevel.NONE
|
||||
},
|
||||
// menubanner: {
|
||||
// title: '메뉴 배너 관리',
|
||||
// permissions: {
|
||||
// read: authType.menuBannerRead,
|
||||
// update: authType.menuBannerUpdate,
|
||||
// delete: authType.menuBannerDelete
|
||||
// },
|
||||
// view: true,
|
||||
// authLevel: adminAuthLevel.NONE
|
||||
// },
|
||||
menubanner: {
|
||||
title: '메뉴 배너 관리',
|
||||
permissions: {
|
||||
read: authType.menuBannerRead,
|
||||
update: authType.menuBannerUpdate,
|
||||
delete: authType.menuBannerDelete
|
||||
},
|
||||
view: true,
|
||||
authLevel: adminAuthLevel.NONE
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,20 @@ export const languageType = [
|
||||
{ value: 'JA', name: '일본어' },
|
||||
];
|
||||
|
||||
export const TabGameLogList = [
|
||||
{ value: 'CURRENCY', name: '재화 로그' },
|
||||
// { value: 'ITEM', name: '아이템 로그' },
|
||||
// { value: 'TRADE', name: '거래 로그' },
|
||||
];
|
||||
|
||||
export const TabEconomicIndexList = [
|
||||
{ value: 'CURRENCY', name: '재화(유저)' },
|
||||
// { value: 'ITEM', name: '아이템' },
|
||||
// { value: 'VBP', name: 'VBP' },
|
||||
// { value: 'deco', name: '의상/타투' },
|
||||
// { value: 'instance', name: '인스턴스' },
|
||||
];
|
||||
|
||||
export const mailSendType = [
|
||||
{ value: 'ALL', name: '전체' },
|
||||
{ value: 'RESERVE_SEND', name: '예약 발송' },
|
||||
@@ -81,7 +95,7 @@ export const landAuctionStatus = [
|
||||
{ value: 'FAIL', name: '실패' },
|
||||
];
|
||||
|
||||
export const currencyType = [
|
||||
export const currencyItemCode = [
|
||||
{ value: '19010001', name: '골드' },
|
||||
{ value: '19010002', name: '사파이어' },
|
||||
{ value: '19010005', name: '루비' },
|
||||
@@ -205,6 +219,12 @@ export const CurrencyType = [
|
||||
{value: 'Ruby', name: '루비' }
|
||||
]
|
||||
|
||||
export const amountDeltaType = [
|
||||
{value: 'Acquire', name: '획득' },
|
||||
{value: 'Consume', name: '소모' },
|
||||
{value: 'None', name: '' },
|
||||
]
|
||||
|
||||
export const battleEventStatus = [
|
||||
{ value: 'ALL', name: '전체' },
|
||||
{ value: 'WAIT', name: '대기' },
|
||||
|
||||
96
src/assets/data/pages/businessLogSearch.json
Normal file
96
src/assets/data/pages/businessLogSearch.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"initialSearchParams": {
|
||||
"searchType": "GUID",
|
||||
"searchData": "",
|
||||
"logAction": "None",
|
||||
"logDomain": "BASE",
|
||||
"tran_id": "",
|
||||
"startDate": "",
|
||||
"endDate": "",
|
||||
"orderBy": "DESC",
|
||||
"pageSize": 500,
|
||||
"currentPage": 1
|
||||
},
|
||||
|
||||
"searchFields": [
|
||||
{
|
||||
"type": "select",
|
||||
"id": "searchType",
|
||||
"optionsRef": "userSearchType2",
|
||||
"col": 1,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "searchData",
|
||||
"placeholder": "대상 입력",
|
||||
"width": "300px",
|
||||
"col": 1,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "period",
|
||||
"startDateId": "startDate",
|
||||
"endDateId": "endDate",
|
||||
"label": "조회 일자",
|
||||
"col": 1
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "searchContent",
|
||||
"label": "우편 내용",
|
||||
"placeholder": "우편 내용(공백으로 구분)",
|
||||
"width": "300px",
|
||||
"col": 1
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "sendType",
|
||||
"label": "발송 방식",
|
||||
"optionsRef": "mailSendType",
|
||||
"col": 2
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "status",
|
||||
"label": "발송 상태",
|
||||
"optionsRef": "mailSendStatus",
|
||||
"col": 2
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "mailType",
|
||||
"label": "우편 타입",
|
||||
"optionsRef": "mailType",
|
||||
"col": 2
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "receiveType",
|
||||
"label": "수신 대상",
|
||||
"optionsRef": "mailReceiveType",
|
||||
"col": 2
|
||||
}
|
||||
],
|
||||
|
||||
"apiInfo": {
|
||||
"functionName": "MailView",
|
||||
"loadOnMount": true,
|
||||
"paramsMapping": [
|
||||
"searchTitle",
|
||||
"searchContent",
|
||||
"sendType",
|
||||
"status",
|
||||
"mailType",
|
||||
"receiveType",
|
||||
{"param": "startDate", "transform": "toISOString"},
|
||||
{"param": "endDate", "transform": "toISOString"},
|
||||
"orderBy",
|
||||
"pageSize",
|
||||
"currentPage"
|
||||
],
|
||||
"pageField": "currentPage",
|
||||
"pageSizeField": "pageSize",
|
||||
"orderField": "orderBy"
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export const adminAuthLevel = {
|
||||
DEVELOPER: "Developer",
|
||||
}
|
||||
|
||||
export const TabList = [
|
||||
export const TabUserList = [
|
||||
{ title: '기본정보' },
|
||||
{ title: '아바타' },
|
||||
{ title: '의상' },
|
||||
@@ -159,3 +159,9 @@ export const currencyCodeTypes = {
|
||||
ruby: "19010005",
|
||||
calium: "19010003"
|
||||
}
|
||||
|
||||
export const languageNames = {
|
||||
'KO': '한국어',
|
||||
'EN': '영어',
|
||||
'JA': '일본어',
|
||||
};
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { useState } from 'react';
|
||||
|
||||
import Button from '../../components/common/button/Button';
|
||||
|
||||
import DatePicker, { registerLocale } from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { getMonth, getYear } from 'date-fns';
|
||||
import range from 'lodash/range';
|
||||
|
||||
import { TextInput, SelectInput, DatePickerWrapper, InputLabel, BtnWrapper } from '../../styles/Components';
|
||||
|
||||
const GoodsLogSearchBar = () => {
|
||||
const [startDate, setStartDate] = useState(new Date());
|
||||
const [endDate, setEndDate] = useState(new Date());
|
||||
const [selectData, setSelectData] = useState('default');
|
||||
const years = range(1990, getYear(new Date()) + 1, 1);
|
||||
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
|
||||
|
||||
const handleChange = e => {
|
||||
setSelectData(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchbarStyle2>
|
||||
<SearchRow>
|
||||
<SearchItem>
|
||||
<InputLabel>조회 기간</InputLabel>
|
||||
<DatePickerWrapper>
|
||||
<InputGroup>
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
onChange={date => setStartDate(date)}
|
||||
className="datepicker"
|
||||
placeholderText="검색기간 선택"
|
||||
calendarClassName="calendar"
|
||||
dateFormat="yyyy - MM - dd"
|
||||
locale="ko"
|
||||
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
|
||||
<div className="calendar-top">
|
||||
<button
|
||||
className="btn-prev"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
decreaseMonth();
|
||||
}}
|
||||
disabled={prevMonthButtonDisabled}></button>
|
||||
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
|
||||
{years.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
.
|
||||
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
|
||||
{months.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn-next"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
increaseMonth();
|
||||
}}
|
||||
disabled={nextMonthButtonDisabled}></button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
</InputGroup>
|
||||
<span>~</span>
|
||||
<InputGroup>
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={date => setEndDate(date)}
|
||||
className="datepicker"
|
||||
placeholderText="검색기간 선택"
|
||||
calendarClassName="calendar"
|
||||
dateFormat="yyyy - MM - dd"
|
||||
minDate = {startDate}
|
||||
locale="ko"
|
||||
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
|
||||
<div className="calendar-top">
|
||||
<button
|
||||
className="btn-prev"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
decreaseMonth();
|
||||
}}
|
||||
disabled={prevMonthButtonDisabled}></button>
|
||||
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
|
||||
{years.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
.
|
||||
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
|
||||
{months.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn-next"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
increaseMonth();
|
||||
}}
|
||||
disabled={nextMonthButtonDisabled}></button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
</InputGroup>
|
||||
</DatePickerWrapper>
|
||||
</SearchItem>
|
||||
</SearchRow>
|
||||
<SearchRow>
|
||||
<SearchItem>
|
||||
<InputLabel>조회 대상</InputLabel>
|
||||
<TextInput type="text" placeholder="조회 대상 유저의 GUID를 입력하세요." width="600px" />
|
||||
</SearchItem>
|
||||
<BtnWrapper $gap="8px">
|
||||
<Button theme="reset" />
|
||||
<Button theme="gray" text="검색" />
|
||||
</BtnWrapper>
|
||||
</SearchRow>
|
||||
</SearchbarStyle2>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoodsLogSearchBar;
|
||||
|
||||
const SearchbarStyle = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px 0;
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #000;
|
||||
border-bottom: 1px solid #000;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
const SearchbarStyle2 = styled(SearchbarStyle)`
|
||||
flex-flow: column;
|
||||
gap: 20px;
|
||||
`;
|
||||
|
||||
const SearchRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px 0;
|
||||
`;
|
||||
|
||||
const InputGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`;
|
||||
|
||||
const SearchItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-right: 50px;
|
||||
|
||||
${TextInput}, ${SelectInput} {
|
||||
height: 35px;
|
||||
}
|
||||
${TextInput} {
|
||||
padding: 0 10px;
|
||||
max-width: 400px;
|
||||
}
|
||||
`;
|
||||
165
src/components/DataManage/CurrencyLogContent.js
Normal file
165
src/components/DataManage/CurrencyLogContent.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
CircularProgressWrapper,
|
||||
FormWrapper,
|
||||
TableStyle,
|
||||
TableWrapper,
|
||||
} from '../../styles/Components';
|
||||
import { amountDeltaType, CurrencyType } from '../../assets/data';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { numberFormatter } from '../../utils';
|
||||
import { TableSkeleton } from '../Skeleton/TableSkeleton';
|
||||
import { CurrencyLogSearchBar, useCurrencyLogSearch } from '../searchBar';
|
||||
import { TopButton, ViewTableInfo } from '../common';
|
||||
import Pagination from '../common/Pagination/Pagination';
|
||||
import {
|
||||
INITIAL_PAGE_LIMIT,
|
||||
STORAGE_BUSINESS_LOG_SEARCH,
|
||||
STORAGE_GAME_LOG_CURRENCY_SEARCH,
|
||||
} from '../../assets/data/adminConstants';
|
||||
import ExcelExportButton from '../common/button/ExcelExportButton';
|
||||
import CircularProgress from '../common/CircularProgress';
|
||||
|
||||
const CurrencyLogContent = ({ 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,
|
||||
handleOrderByChange,
|
||||
handlePageSizeChange,
|
||||
updateSearchParams
|
||||
} = useCurrencyLogSearch(token, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if(active) {
|
||||
// 세션 스토리지에서 복사된 메일 데이터 가져오기
|
||||
const paramsData = sessionStorage.getItem(STORAGE_GAME_LOG_CURRENCY_SEARCH);
|
||||
|
||||
if (paramsData) {
|
||||
const searchData = JSON.parse(paramsData);
|
||||
|
||||
handleSearch({
|
||||
start_dt: new Date(searchData.start_dt),
|
||||
end_dt: new Date(searchData.end_dt),
|
||||
search_data: searchData.guid
|
||||
});
|
||||
|
||||
// 사용 후 세션 스토리지 데이터 삭제
|
||||
sessionStorage.removeItem(STORAGE_GAME_LOG_CURRENCY_SEARCH);
|
||||
}
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
const tableHeaders = useMemo(() => {
|
||||
return [
|
||||
// { id: 'logDay', label: '일자', width: '120px' },
|
||||
{ id: 'logTime', label: '일시', width: '120px' },
|
||||
{ id: 'accountId', label: 'account ID', width: '80px' },
|
||||
{ id: 'userGuid', label: 'GUID', width: '200px' },
|
||||
{ id: 'userNickname', label: '아바타명', width: '150px' },
|
||||
{ id: 'tranId', label: '트랜잭션 ID', width: '200px' },
|
||||
{ id: 'action', label: '액션', width: '150px' },
|
||||
{ id: 'currencyType', label: '재화종류', width: '120px' },
|
||||
{ id: 'amountDeltaType', label: '증감유형', width: '120px' },
|
||||
{ id: 'deltaAmount', label: '수량', width: '120px' },
|
||||
// { id: 'deltaAmount', label: '수량원본', width: '120px' },
|
||||
{ id: 'currencyAmount', label: '잔량', width: '120px' },
|
||||
];
|
||||
}, []);
|
||||
|
||||
if(!active) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormWrapper>
|
||||
<CurrencyLogSearchBar
|
||||
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="GameCurrencyDetailLogExport"
|
||||
params={searchParams}
|
||||
fileName={t('FILE_GAME_LOG_CURRENCY')}
|
||||
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?.currency_detail_list?.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
<tr>
|
||||
<td>{item.logTime}</td>
|
||||
<td>{item.accountId}</td>
|
||||
<td>{item.userGuid}</td>
|
||||
<td>{item.userNickname}</td>
|
||||
<td>{item.tranId}</td>
|
||||
<td>{item.action}</td>
|
||||
<td>{CurrencyType.find(data => data.value === item.currencyType)?.name}</td>
|
||||
<td>{amountDeltaType.find(data => data.value === item.amountDeltaType)?.name}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.deltaAmount)}</td>
|
||||
{/*<td>{item.deltaAmount}</td>*/}
|
||||
<td>{numberFormatter.formatCurrency(item.currencyAmount)}</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</TableStyle>
|
||||
</TableWrapper>
|
||||
{dataList?.currency_detail_list &&
|
||||
<Pagination
|
||||
postsPerPage={searchParams.page_size}
|
||||
totalPosts={dataList?.total_all}
|
||||
setCurrentPage={handlePageChange}
|
||||
currentPage={searchParams.page_no}
|
||||
pageLimit={INITIAL_PAGE_LIMIT}
|
||||
/>
|
||||
}
|
||||
<TopButton />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CurrencyLogContent;
|
||||
@@ -1,219 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
import RadioInput from '../common/input/Radio';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import DatePicker, { registerLocale } from 'react-datepicker';
|
||||
import { ko } from 'date-fns/esm/locale';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { getMonth, getYear } from 'date-fns';
|
||||
import range from 'lodash/range';
|
||||
|
||||
import { TextInput, SelectInput, DatePickerWrapper, InputLabel, BtnWrapper } from '../../styles/Components';
|
||||
|
||||
const ItemLogSearchBar = () => {
|
||||
const [startDate, setStartDate] = useState(new Date());
|
||||
const [endDate, setEndDate] = useState(new Date());
|
||||
const [selectData, setSelectData] = useState('default');
|
||||
const years = range(1990, getYear(new Date()) + 1, 1);
|
||||
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
|
||||
|
||||
const handleChange = e => {
|
||||
setSelectData(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchbarStyle2>
|
||||
<SearchRow>
|
||||
<RadioGroup>
|
||||
<RadioInput label="기본 조회" id="single" name="receiver" value="default" fontWeight="600" checked={selectData === 'default'} handleChange={handleChange} />
|
||||
<RadioInput label="아이템 소유자 추적" id="multi" name="receiver" value="item" fontWeight="600" checked={selectData === 'item'} handleChange={handleChange} />
|
||||
</RadioGroup>
|
||||
</SearchRow>
|
||||
<SearchRow>
|
||||
<SearchItem>
|
||||
<InputLabel>조회 기간</InputLabel>
|
||||
<DatePickerWrapper>
|
||||
<InputGroup>
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
onChange={date => setStartDate(date)}
|
||||
className="datepicker"
|
||||
placeholderText="검색기간 선택"
|
||||
calendarClassName="calendar"
|
||||
dateFormat="yyyy - MM - dd"
|
||||
locale="ko"
|
||||
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
|
||||
<div className="calendar-top">
|
||||
<button
|
||||
className="btn-prev"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
decreaseMonth();
|
||||
}}
|
||||
disabled={prevMonthButtonDisabled}></button>
|
||||
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
|
||||
{years.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
.
|
||||
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
|
||||
{months.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn-next"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
increaseMonth();
|
||||
}}
|
||||
disabled={nextMonthButtonDisabled}></button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
</InputGroup>
|
||||
<span>~</span>
|
||||
<InputGroup>
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={date => setEndDate(date)}
|
||||
className="datepicker"
|
||||
placeholderText="검색기간 선택"
|
||||
calendarClassName="calendar"
|
||||
dateFormat="yyyy - MM - dd"
|
||||
minDate = {startDate}
|
||||
locale="ko"
|
||||
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
|
||||
<div className="calendar-top">
|
||||
<button
|
||||
className="btn-prev"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
decreaseMonth();
|
||||
}}
|
||||
disabled={prevMonthButtonDisabled}></button>
|
||||
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
|
||||
{years.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
.
|
||||
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
|
||||
{months.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn-next"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
increaseMonth();
|
||||
}}
|
||||
disabled={nextMonthButtonDisabled}></button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
</InputGroup>
|
||||
</DatePickerWrapper>
|
||||
</SearchItem>
|
||||
</SearchRow>
|
||||
<SearchRow>
|
||||
{selectData === 'default' ? (
|
||||
<SearchItem>
|
||||
<InputLabel>조회 대상</InputLabel>
|
||||
<TextInput type="text" placeholder="조회 대상 유저의 GUID를 입력하세요." width="600px" />
|
||||
</SearchItem>
|
||||
) : (
|
||||
<SearchItem>
|
||||
<InputLabel>아이템 ID</InputLabel>
|
||||
<TextInput type="text" placeholder="아이템의 GUID를 입력하세요." width="600px" />
|
||||
</SearchItem>
|
||||
)}
|
||||
|
||||
<BtnWrapper $gap="8px">
|
||||
<Button theme="reset" />
|
||||
<Button theme="gray" text="검색" />
|
||||
</BtnWrapper>
|
||||
</SearchRow>
|
||||
</SearchbarStyle2>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemLogSearchBar;
|
||||
|
||||
const SearchbarStyle = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px 0;
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #000;
|
||||
border-bottom: 1px solid #000;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
const SearchbarStyle2 = styled(SearchbarStyle)`
|
||||
flex-flow: column;
|
||||
gap: 20px;
|
||||
`;
|
||||
|
||||
const SearchRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px 0;
|
||||
`;
|
||||
|
||||
const InputGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`;
|
||||
|
||||
const RadioGroup = styled(InputGroup)`
|
||||
gap: 30px;
|
||||
height: 35px;
|
||||
`;
|
||||
|
||||
const SearchItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-right: 50px;
|
||||
|
||||
${TextInput}, ${SelectInput} {
|
||||
height: 35px;
|
||||
}
|
||||
${TextInput} {
|
||||
padding: 0 10px;
|
||||
max-width: 400px;
|
||||
}
|
||||
`;
|
||||
@@ -1,198 +0,0 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { useState } from 'react';
|
||||
|
||||
import Button from '../../components/common/button/Button';
|
||||
|
||||
import DatePicker, { registerLocale } from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { getMonth, getYear } from 'date-fns';
|
||||
import range from 'lodash/range';
|
||||
|
||||
import { TextInput, SelectInput, DatePickerWrapper, InputLabel, BtnWrapper } from '../../styles/Components';
|
||||
|
||||
const TradeLogSerchBar = () => {
|
||||
const [startDate, setStartDate] = useState(new Date());
|
||||
const [endDate, setEndDate] = useState(new Date());
|
||||
const [selectData, setSelectData] = useState('default');
|
||||
const years = range(1990, getYear(new Date()) + 1, 1);
|
||||
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
|
||||
|
||||
const handleChange = e => {
|
||||
setSelectData(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchbarStyle2>
|
||||
<SearchRow>
|
||||
<SearchItem>
|
||||
<InputLabel>조회 기간</InputLabel>
|
||||
<DatePickerWrapper>
|
||||
<InputGroup>
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
onChange={date => setStartDate(date)}
|
||||
className="datepicker"
|
||||
placeholderText="검색기간 선택"
|
||||
calendarClassName="calendar"
|
||||
dateFormat="yyyy - MM - dd"
|
||||
locale="ko"
|
||||
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
|
||||
<div className="calendar-top">
|
||||
<button
|
||||
className="btn-prev"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
decreaseMonth();
|
||||
}}
|
||||
disabled={prevMonthButtonDisabled}></button>
|
||||
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
|
||||
{years.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
.
|
||||
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
|
||||
{months.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn-next"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
increaseMonth();
|
||||
}}
|
||||
disabled={nextMonthButtonDisabled}></button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
</InputGroup>
|
||||
<span>~</span>
|
||||
<InputGroup>
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={date => setEndDate(date)}
|
||||
className="datepicker"
|
||||
placeholderText="검색기간 선택"
|
||||
calendarClassName="calendar"
|
||||
dateFormat="yyyy - MM - dd"
|
||||
minDate = {startDate}
|
||||
locale="ko"
|
||||
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
|
||||
<div className="calendar-top">
|
||||
<button
|
||||
className="btn-prev"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
decreaseMonth();
|
||||
}}
|
||||
disabled={prevMonthButtonDisabled}></button>
|
||||
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
|
||||
{years.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
.
|
||||
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
|
||||
{months.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn-next"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
increaseMonth();
|
||||
}}
|
||||
disabled={nextMonthButtonDisabled}></button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
<SelectInput>
|
||||
<option value="">00</option>
|
||||
<option value="">01</option>
|
||||
</SelectInput>
|
||||
</InputGroup>
|
||||
</DatePickerWrapper>
|
||||
</SearchItem>
|
||||
</SearchRow>
|
||||
<SearchRow>
|
||||
<SearchItem>
|
||||
<InputLabel>조회 대상</InputLabel>
|
||||
<TextInput type="text" placeholder="조회 대상 유저의 GUID를 입력하세요." width="600px" />
|
||||
</SearchItem>
|
||||
<BtnWrapper $gap="8px">
|
||||
<Button theme="reset" />
|
||||
<Button theme="gray" text="검색" />
|
||||
</BtnWrapper>
|
||||
</SearchRow>
|
||||
</SearchbarStyle2>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeLogSerchBar;
|
||||
|
||||
const SearchbarStyle = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px 0;
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #000;
|
||||
border-bottom: 1px solid #000;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
const SearchbarStyle2 = styled(SearchbarStyle)`
|
||||
flex-flow: column;
|
||||
gap: 20px;
|
||||
`;
|
||||
|
||||
const SearchRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px 0;
|
||||
`;
|
||||
|
||||
const InputGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`;
|
||||
|
||||
const SearchItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-right: 50px;
|
||||
|
||||
${TextInput}, ${SelectInput} {
|
||||
height: 35px;
|
||||
}
|
||||
${TextInput} {
|
||||
padding: 0 10px;
|
||||
max-width: 400px;
|
||||
}
|
||||
`;
|
||||
@@ -1,312 +1,221 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import React, { Fragment, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { CurrencyIndexExport, CurrencyIndexView } from '../../apis';
|
||||
import { SelectInput, TableStyle, TableInfo, ListOption, InputLabel } from '../../styles/Components';
|
||||
import {
|
||||
TableStyle,
|
||||
FormWrapper,
|
||||
TableWrapper, CircularProgressWrapper, TotalRow,
|
||||
} from '../../styles/Components';
|
||||
|
||||
import CreditSeacrhBar from '../../components/IndexManage/CreditSearchBar';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { sumBy } from 'lodash';
|
||||
import { useCurrencyIndexSearch } from '../searchBar';
|
||||
import { Button, TopButton, ViewTableInfo } from '../common';
|
||||
import { TableSkeleton } from '../Skeleton/TableSkeleton';
|
||||
import { numberFormatter } from '../../utils';
|
||||
import {STORAGE_GAME_LOG_CURRENCY_SEARCH, } from '../../assets/data/adminConstants';
|
||||
import ExcelExportButton from '../common/button/ExcelExportButton';
|
||||
import CircularProgress from '../common/CircularProgress';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CurrencyIndexSearchBar from '../searchBar/CurrencyIndexSearchBar';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const CreditContent = () => {
|
||||
const { t } = useTranslation();
|
||||
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 CURRENCY_LIST = [
|
||||
{ "key": "Gold", "name": "골드" },
|
||||
{ "key": "Sapphire", "name": "사파이어" },
|
||||
{ "key": "Calium", "name": "칼리움" },
|
||||
{ "key": "Onyxium", "name": "오닉시움" }
|
||||
];
|
||||
const navigate = useNavigate();
|
||||
const tableRef = useRef(null);
|
||||
const [downloadState, setDownloadState] = useState({
|
||||
loading: false,
|
||||
progress: 0
|
||||
});
|
||||
|
||||
const [sendDate, setSendDate] = useState(START_DATE);
|
||||
const [finishDate, setFinishDate] = useState(END_DATE);
|
||||
const [currencyType, setCurrencyType] = useState('Gold');
|
||||
const [currencyText, setCurrencyText] = useState('골드');
|
||||
const {
|
||||
searchParams,
|
||||
loading: dataLoading,
|
||||
data: dataList,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
updateSearchParams
|
||||
} = useCurrencyIndexSearch(token);
|
||||
|
||||
const [dataList, setDataList] = useState([]);
|
||||
const [routeData, setRouteData] = useState([]);
|
||||
const tableHeaders = useMemo(() => {
|
||||
return [
|
||||
{ id: 'logDay', label: '일자', width: '100px' },
|
||||
{ id: 'accountId', label: 'account ID', width: '80px' },
|
||||
{ id: 'userGuid', label: 'GUID', width: '200px' },
|
||||
{ id: 'userNickname', label: '아바타명', width: '150px' },
|
||||
{ id: 'sapphireAcquired', label: '사파이어 획득량', width: '80px' },
|
||||
{ id: 'sapphireConsumed', label: '사파이어 소모량', width: '80px' },
|
||||
{ id: 'goldAcquired', label: '골드 획득량', width: '80px' },
|
||||
{ id: 'goldConsumed', label: '골드 소모량', width: '80px' },
|
||||
{ id: 'caliumAcquired', label: '칼리움 획득량', width: '80px' },
|
||||
{ id: 'caliumConsumed', label: '칼리움 소모량', width: '80px' },
|
||||
{ id: 'beamAcquired', label: 'BEAM 획득량', width: '80px' },
|
||||
{ id: 'beamConsumed', label: 'BEAM 소모량', width: '80px' },
|
||||
{ id: 'rubyAcquired', label: '루비 획득량', width: '80px' },
|
||||
{ id: 'rubyConsumed', label: '루비 소모량', width: '80px' },
|
||||
{ id: 'sapphireNet', label: '사파이어 계', width: '80px' },
|
||||
{ id: 'goldNet', label: '골드 계', width: '80px' },
|
||||
{ id: 'caliumNet', label: '칼리움 계', width: '80px' },
|
||||
{ id: 'beamNet', label: 'BEAM 계', width: '80px' },
|
||||
{ id: 'rubyNet', label: '루비 계', width: '80px' },
|
||||
{ id: 'totalCurrencies', label: '활동 수', width: '80px' },
|
||||
{ id: 'detail', label: '상세', width: '100px' },
|
||||
];
|
||||
}, []);
|
||||
|
||||
const totals = useMemo(() => {
|
||||
if (!dataList?.currency_list?.length) return null;
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(sendDate, finishDate, currencyType);
|
||||
}, [currencyType]);
|
||||
return dataList.currency_list.reduce((acc, item) => {
|
||||
return {
|
||||
sapphireAcquired: (acc.sapphireAcquired || 0) + (item.sapphireAcquired || 0),
|
||||
sapphireConsumed: (acc.sapphireConsumed || 0) + (item.sapphireConsumed || 0),
|
||||
goldAcquired: (acc.goldAcquired || 0) + (item.goldAcquired || 0),
|
||||
goldConsumed: (acc.goldConsumed || 0) + (item.goldConsumed || 0),
|
||||
caliumAcquired: (acc.caliumAcquired || 0) + (item.caliumAcquired || 0),
|
||||
caliumConsumed: (acc.caliumConsumed || 0) + (item.caliumConsumed || 0),
|
||||
beamAcquired: (acc.beamAcquired || 0) + (item.beamAcquired || 0),
|
||||
beamConsumed: (acc.beamConsumed || 0) + (item.beamConsumed || 0),
|
||||
rubyAcquired: (acc.rubyAcquired || 0) + (item.rubyAcquired || 0),
|
||||
rubyConsumed: (acc.rubyConsumed || 0) + (item.rubyConsumed || 0),
|
||||
sapphireNet: (acc.sapphireNet || 0) + (item.sapphireNet || 0),
|
||||
goldNet: (acc.goldNet || 0) + (item.goldNet || 0),
|
||||
caliumNet: (acc.caliumNet || 0) + (item.caliumNet || 0),
|
||||
beamNet: (acc.beamNet || 0) + (item.beamNet || 0),
|
||||
rubyNet: (acc.rubyNet || 0) + (item.rubyNet || 0),
|
||||
totalCurrencies: (acc.totalCurrencies || 0) + (item.totalCurrencies || 0),
|
||||
};
|
||||
}, {});
|
||||
}, [dataList?.currency_list]);
|
||||
|
||||
const fetchData = async (startDate, endDate) => {
|
||||
const newStartDate = new Date(startDate);
|
||||
const newEndDate = new Date(endDate);
|
||||
const handleModalSubmit = async (type, param = null) => {
|
||||
switch (type) {
|
||||
case "detail":
|
||||
const params = {
|
||||
tab: "CURRENCY",
|
||||
start_dt: (() => {
|
||||
const date = new Date(param.logDay);
|
||||
return date;
|
||||
})(),
|
||||
end_dt: (() => {
|
||||
const date = new Date(param.logDay);
|
||||
date.setDate(date.getDate() + 1);
|
||||
return date;
|
||||
})(),
|
||||
guid: param.userGuid
|
||||
};
|
||||
|
||||
const startDateToLocal =
|
||||
newStartDate.getFullYear() +
|
||||
'-' +
|
||||
(newStartDate.getMonth() + 1 < 9 ? '0' + (newStartDate.getMonth() + 1) : newStartDate.getMonth() + 1) +
|
||||
'-' +
|
||||
(newStartDate.getDate() < 9 ? '0' + newStartDate.getDate() : newStartDate.getDate());
|
||||
|
||||
const endDateToLocal =
|
||||
newEndDate.getFullYear() +
|
||||
'-' +
|
||||
(newEndDate.getMonth() + 1 < 9 ? '0' + (newEndDate.getMonth() + 1) : newEndDate.getMonth() + 1) +
|
||||
'-' +
|
||||
(newEndDate.getDate() < 9 ? '0' + newEndDate.getDate() : newEndDate.getDate());
|
||||
|
||||
setDataList(await CurrencyIndexView(token, startDateToLocal, endDateToLocal, currencyType));
|
||||
|
||||
|
||||
setSendDate(startDateToLocal);
|
||||
setFinishDate(endDateToLocal);
|
||||
setRoutArray(await CurrencyIndexView(token, startDateToLocal, endDateToLocal, currencyType));
|
||||
};
|
||||
|
||||
const handleCurrencyChange = e => {
|
||||
let value = e.target.value;
|
||||
setCurrencyType(value);
|
||||
CURRENCY_LIST.filter(data => data.key === value).map(data => {
|
||||
setCurrencyText(data.name);
|
||||
});
|
||||
};
|
||||
|
||||
//route data
|
||||
const setRoutArray = async (data) => {
|
||||
let routeArray = [];
|
||||
let routeAcqArr = [];
|
||||
let routeConArr = [];
|
||||
|
||||
//생산량 route
|
||||
data.list && data.list[0].daily_data.filter(routeData => routeData.delta_type === 'ACQUIRE').map(routeData => {
|
||||
routeData.data.map(routeData => {
|
||||
if(!routeAcqArr.includes(routeData.route) ){
|
||||
routeAcqArr.push(routeData.route);
|
||||
}
|
||||
})
|
||||
});
|
||||
routeArray.ACQUIRE = routeAcqArr;
|
||||
|
||||
//소진량 route
|
||||
data.list && data.list[0].daily_data.filter(routeData => routeData.delta_type === 'CONSUME').map(routeData => {
|
||||
routeData.data.map(routeData => {
|
||||
if(!routeConArr.includes(routeData.route) ){
|
||||
routeConArr.push(routeData.route);
|
||||
}
|
||||
})
|
||||
});
|
||||
routeArray.CONSUME = routeConArr;
|
||||
|
||||
setRouteData(routeArray);
|
||||
};
|
||||
|
||||
// 엑셀 다운로드
|
||||
const handleXlsxExport = () => {
|
||||
const fileName = 'Caliverse_Credit_Index.xlsx';
|
||||
CurrencyIndexExport(token, fileName, sendDate, finishDate, currencyType);
|
||||
};
|
||||
// 복사한 데이터를 세션 스토리지에 저장
|
||||
sessionStorage.setItem(STORAGE_GAME_LOG_CURRENCY_SEARCH, JSON.stringify(params));
|
||||
navigate('/datamanage/gamelogview');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<CreditSeacrhBar fetchData={fetchData} />
|
||||
<TableInfo2>
|
||||
<SelectInput onChange={handleCurrencyChange}>
|
||||
{CURRENCY_LIST.map((item, index) => (
|
||||
<option value={item.key} key={index}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
<ListOption>
|
||||
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
|
||||
</ListOption>
|
||||
</TableInfo2>
|
||||
<TableWrapper>
|
||||
<EconomicTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2" className="text-center" width="300">
|
||||
Product
|
||||
</th>
|
||||
{dataList.list && uniqBy(dataList.list, 'date').map(data =>
|
||||
<th width="160" key={data.date}>{data.date}</th>
|
||||
<FormWrapper>
|
||||
<CurrencyIndexSearchBar
|
||||
searchParams={searchParams}
|
||||
onSearch={(newParams, executeSearch = true) => {
|
||||
if (executeSearch) {
|
||||
handleSearch(newParams);
|
||||
} else {
|
||||
updateSearchParams(newParams);
|
||||
}
|
||||
}}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
</FormWrapper>
|
||||
<ViewTableInfo >
|
||||
<ExcelExportButton
|
||||
functionName="GameCurrencyLogExport"
|
||||
params={searchParams}
|
||||
fileName={t('FILE_CURRENCY_INDEX')}
|
||||
onLoadingChange={setDownloadState}
|
||||
dataSize={dataList?.length}
|
||||
/>
|
||||
{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>
|
||||
{totals && (
|
||||
<TotalRow>
|
||||
<td colSpan="4">합계</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.sapphireAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.sapphireConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.goldAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.goldConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.caliumAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.caliumConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.beamAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.beamConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.rubyAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.rubyConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.sapphireNet)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.goldNet)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.caliumNet)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.beamNet)}</td>
|
||||
<td>{numberFormatter.formatCurrency(totals.rubyNet)}</td>
|
||||
<td>{totals.totalCurrencies}</td>
|
||||
<td>-</td>
|
||||
</TotalRow>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<TableTitle colSpan="2">(Total) {currencyText} 생산량</TableTitle>
|
||||
{dataList.list &&
|
||||
dataList.list.map((data) =>
|
||||
(data.total).filter(totalData => data.date === totalData.date && totalData.delta_type === 'ACQUIRE')
|
||||
.map((totalData, index) => (
|
||||
<TableData key={index}
|
||||
$state={totalData.dif !== "" && totalData.dif !== "Infinity" ? "danger" : ""}>
|
||||
{totalData.quantity}
|
||||
{
|
||||
totalData.dif !== "" && totalData.dif !== "Infinity"
|
||||
? (<span>({totalData.dif})</span>)
|
||||
: ("")
|
||||
}
|
||||
</TableData>
|
||||
)
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
<tr>
|
||||
<TableTitle colSpan="2">(Total) {currencyText} 소진량</TableTitle>
|
||||
{dataList.list &&
|
||||
dataList.list.map((data) =>
|
||||
(data.total).filter(totalData => data.date === totalData.date && totalData.delta_type === 'CONSUME')
|
||||
.map((totalData, index) => (
|
||||
<TableData key={index}
|
||||
$state={totalData.dif !== "" && totalData.dif !== "Infinity" ? "danger" : ""}>
|
||||
{totalData.quantity}
|
||||
{
|
||||
totalData.dif !== "" && totalData.dif !== "Infinity"
|
||||
? (<span>({totalData.dif})</span>)
|
||||
: ("")
|
||||
}
|
||||
</TableData>
|
||||
)
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
<tr>
|
||||
<TableTitle colSpan="2">(Total) {currencyText} 보유량</TableTitle>
|
||||
{dataList.list &&
|
||||
dataList.list.map((data, index) => (
|
||||
<TableData key={index}>
|
||||
{sumBy(
|
||||
data.total.filter(totalData => data.date === totalData.date && totalData.delta_type === 'ACQUIRE'),
|
||||
'quantity',
|
||||
) -
|
||||
sumBy(
|
||||
data.total.filter(totalData => data.date === totalData.date && totalData.delta_type === 'CONSUME'),
|
||||
'quantity',
|
||||
)}
|
||||
</TableData>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
{/* 획득 GET */}
|
||||
{
|
||||
routeData.ACQUIRE && routeData.ACQUIRE.map((route, index) => (
|
||||
<tr key={index}>
|
||||
<TableTitle>GET</TableTitle>
|
||||
<TableTitle>{route}</TableTitle>
|
||||
{dataList.list &&
|
||||
dataList.list.map((data) =>
|
||||
data.daily_data.filter(dailyData => data.date === dailyData.date && dailyData.delta_type === 'ACQUIRE')
|
||||
.map(dailyData => (dailyData.data).filter(routeData => data.date === routeData.date && routeData.route === route)
|
||||
.map((routeData, i) => (
|
||||
<TableData key={i} data={routeData.date}>{routeData.quantity}</TableData>
|
||||
)))
|
||||
)
|
||||
}
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
{/* 소진 USE CONSUME */}
|
||||
{
|
||||
routeData.CONSUME && routeData.CONSUME.map((route, index) => (
|
||||
<tr key={index}>
|
||||
<TableTitle>USE</TableTitle>
|
||||
<TableTitle>{route}</TableTitle>
|
||||
{dataList.list &&
|
||||
dataList.list.map((data) =>
|
||||
data.daily_data.filter(dailyData => data.date === dailyData.date && dailyData.delta_type === 'CONSUME')
|
||||
.map(dailyData => (dailyData.data).filter(routeData => data.date === routeData.date && routeData.route === route)
|
||||
.map((routeData, i) => (
|
||||
<TableData key={i} data={routeData.date}>{routeData.quantity}</TableData>
|
||||
)))
|
||||
)
|
||||
}
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</EconomicTable>
|
||||
</TableWrapper>
|
||||
{dataList?.currency_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.sapphireAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.sapphireConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.goldAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.goldConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.caliumAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.caliumConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.beamAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.beamConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.rubyAcquired)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.rubyConsumed)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.sapphireNet)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.goldNet)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.caliumNet)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.beamNet)}</td>
|
||||
<td>{numberFormatter.formatCurrency(item.rubyNet)}</td>
|
||||
<td>{item.totalCurrencies}</td>
|
||||
<td>
|
||||
<Button theme="line" text="상세보기"
|
||||
handleClick={e => handleModalSubmit('detail', item)} />
|
||||
</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</TableStyle>
|
||||
</TableWrapper>
|
||||
<TopButton />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreditContent;
|
||||
|
||||
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 TableInfo2 = styled(TableInfo)`
|
||||
justify-content: space-between;
|
||||
${InputLabel} {
|
||||
font-size: 12px;
|
||||
}
|
||||
${SelectInput} {
|
||||
width: auto;
|
||||
min-width: 100px;
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const TableDate = styled.th`
|
||||
color: ${props => (props.$state === 'danger' ? '#d60000' : '#2c2c2c')};
|
||||
`;
|
||||
|
||||
const TableData = styled.td`
|
||||
|
||||
background: ${props => (props.$state === 'danger' ? '#d60000' : props.$state === 'blank' ? '#F9F9F9' : 'transparent')};
|
||||
color: ${props => (props.$state === 'danger' ? '#fff' : '#2c2c2c')};
|
||||
`;
|
||||
|
||||
const perData = styled.span`
|
||||
display: ${props => (props.$view === 'hidden' ? 'none' : 'block')};
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import DecoSearchBar from '../../components/IndexManage/DecoSearchBar';
|
||||
import DecoSearchBar from '../searchBar/DecoSearchBar';
|
||||
import Button from '../../components/common/button/Button';
|
||||
|
||||
import { TableStyle, TableInfo, ListOption } from '../../styles/Components';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import { TableStyle, TableInfo, ListOption } from '../../styles/Components';
|
||||
|
||||
import Button from '../../components/common/button/Button';
|
||||
import InstanceSearchBar from '../../components/IndexManage/InstanceSearchBar';
|
||||
import InstanceSearchBar from '../searchBar/InstanceSearchBar';
|
||||
import { InstanceIndexExport, InstanceIndexView } from '../../apis';
|
||||
|
||||
const InstanceContent = () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import Button from '../../components/common/button/Button';
|
||||
|
||||
import { SelectInput, TableStyle, TableInfo, ListOption, InputLabel, InputGroup } from '../../styles/Components';
|
||||
|
||||
import ItemSearchBar from '../../components/IndexManage/ItemSearchBar';
|
||||
import ItemSearchBar from '../searchBar/ItemSearchBar';
|
||||
import { ItemIndexExport, ItemIndexView } from '../../apis';
|
||||
|
||||
const ItemContent = () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { TableStyle, TableInfo, ListOption } from '../../styles/Components';
|
||||
|
||||
import Button from '../../components/common/button/Button';
|
||||
import VBPSearchBar from '../../components/IndexManage/VBPSearchBar';
|
||||
import VBPSearchBar from '../searchBar/VBPSearchBar';
|
||||
import { VBPIndexExport, VbpIndexView } from '../../apis';
|
||||
|
||||
const VBPContent = () => {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import UserIndexSearchBar from "./UserIndexSearchBar";
|
||||
import RetentionSearchBar from "./RetentionSearchBar";
|
||||
import SegmentSearchBar from "./SegmentSearchBar";
|
||||
import UserIndexSearchBar from "../searchBar/UserIndexSearchBar";
|
||||
import RetentionSearchBar from "../searchBar/RetentionSearchBar";
|
||||
import SegmentSearchBar from "../searchBar/SegmentSearchBar";
|
||||
import DailyDashBoard from "./DailyDashBoard";
|
||||
import PlayTimeSearchBar from "./PlayTimeSearchBar";
|
||||
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 "./DailySearchBar";
|
||||
import DailySearchBar from "../searchBar/DailySearchBar";
|
||||
|
||||
export {
|
||||
UserIndexSearchBar,
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import BoardInfoModal from './modal/BoardInfoModal';
|
||||
import BoardRegistModal from './modal/BoardRegistModal';
|
||||
import MailDetailModal from './modal/MailDetailModal';
|
||||
import LandAuctionModal from './modal/LandAuctionModal'
|
||||
import BattleEventModal from './modal/BattleEventModal'
|
||||
import ReportListAnswerModal from './modal/ReportListAnswerModal';
|
||||
import ReportListDetailModal from './modal/ReportListDetailModal';
|
||||
import UserBlockDetailModal from './modal/UserBlockDetailModal';
|
||||
import OwnerChangeModal from './modal/OwnerChangeModal';
|
||||
import BoardInfoModal from '../modal/BoardInfoModal';
|
||||
import BoardRegistModal from '../modal/BoardRegistModal';
|
||||
import MailDetailModal from '../modal/MailDetailModal';
|
||||
import LandAuctionModal from '../modal/LandAuctionModal'
|
||||
import BattleEventModal from '../modal/BattleEventModal'
|
||||
import ReportListAnswerModal from '../modal/ReportListAnswerModal';
|
||||
import ReportListDetailModal from '../modal/ReportListDetailModal';
|
||||
import UserBlockDetailModal from '../modal/UserBlockDetailModal';
|
||||
import OwnerChangeModal from '../modal/OwnerChangeModal';
|
||||
//searchbar
|
||||
import SearchFilter from './searchBar/SearchFilter';
|
||||
import ReportListSearchBar from './searchBar/ReportListSearchBar';
|
||||
import UserBlockSearchBar from './searchBar/UserBlockSearchBar';
|
||||
import EventListSearchBar from './searchBar/EventListSearchBar';
|
||||
import LandAuctionSearchBar from './searchBar/LandAuctionSearchBar'
|
||||
import MailListSearchBar from './searchBar/MailListSearchBar';
|
||||
import LandInfoSearchBar from './searchBar/LandInfoSearchBar';
|
||||
import BusinessLogSearchBar from './searchBar/BusinessLogSearchBar';
|
||||
import DataInitSearchBar from './searchBar/DataInitSearchBar';
|
||||
import LogViewSearchBar from './searchBar/LogViewSearchBar';
|
||||
import AdminViewSearchBar from './searchBar/AdminViewSearchBar';
|
||||
import CaliumRequestSearchBar from './searchBar/CaliumRequestSearchBar';
|
||||
import SearchFilter from '../searchBar/SearchFilter';
|
||||
import ReportListSearchBar from '../searchBar/ReportListSearchBar';
|
||||
import UserBlockSearchBar from '../searchBar/UserBlockSearchBar';
|
||||
import EventListSearchBar from '../searchBar/EventListSearchBar';
|
||||
import LandAuctionSearchBar from '../searchBar/LandAuctionSearchBar'
|
||||
import MailListSearchBar from '../searchBar/MailListSearchBar';
|
||||
import LandInfoSearchBar from '../searchBar/LandInfoSearchBar';
|
||||
import BusinessLogSearchBar from '../searchBar/BusinessLogSearchBar';
|
||||
import DataInitSearchBar from '../searchBar/DataInitSearchBar';
|
||||
import LogViewSearchBar from '../searchBar/LogViewSearchBar';
|
||||
import AdminViewSearchBar from '../searchBar/AdminViewSearchBar';
|
||||
import CaliumRequestSearchBar from '../searchBar/CaliumRequestSearchBar';
|
||||
import CurrencyLogSearchBar from '../searchBar/CurrencyLogSearchBar';
|
||||
|
||||
import CommonSearchBar from './searchBar/CommonSearchBar';
|
||||
import CommonSearchBar from '../searchBar/CommonSearchBar';
|
||||
import useCommonSearch from '../../hooks/useCommonSearch';
|
||||
|
||||
//etc
|
||||
@@ -35,6 +36,7 @@ export {
|
||||
MailListSearchBar,
|
||||
LandInfoSearchBar,
|
||||
BusinessLogSearchBar,
|
||||
CurrencyLogSearchBar,
|
||||
DataInitSearchBar,
|
||||
LogViewSearchBar,
|
||||
AdminViewSearchBar,
|
||||
|
||||
301
src/components/common/Layout/DetailGrid.js
Normal file
301
src/components/common/Layout/DetailGrid.js
Normal file
@@ -0,0 +1,301 @@
|
||||
import React from 'react';
|
||||
import { Row, Col, Form, Input, Select, DatePicker, TimePicker, InputNumber, Switch, Button, Checkbox } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import dayjs from 'dayjs';
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
/**
|
||||
* 위치 지정 가능한 그리드 형태 상세 정보 표시 컴포넌트
|
||||
* @param {Array} items - 표시할 항목 배열 (row, col, rowSpan, colSpan 속성 추가)
|
||||
* @param {Object} formData - 폼 데이터 객체
|
||||
* @param {Function} onChange - 값 변경 시 호출할 함수
|
||||
* @param {boolean} disabled - 전체 비활성화 여부
|
||||
* @param {number} columns - 그리드의 총 컬럼 수 (기본값: 4)
|
||||
*/
|
||||
const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }) => {
|
||||
// 항목을 행과 열 위치별로 그룹화
|
||||
const positionedItems = {};
|
||||
|
||||
// 각 항목의 위치 및 span 정보 처리
|
||||
items.forEach(item => {
|
||||
const rowIndex = item.row || 0;
|
||||
const colIndex = item.col || 0;
|
||||
|
||||
if (!positionedItems[rowIndex]) {
|
||||
positionedItems[rowIndex] = {};
|
||||
}
|
||||
|
||||
positionedItems[rowIndex][colIndex] = {
|
||||
...item,
|
||||
rowSpan: item.rowSpan || 1,
|
||||
colSpan: item.colSpan || 1
|
||||
};
|
||||
});
|
||||
|
||||
// 행 번호 목록 (정렬)
|
||||
const rows = Object.keys(positionedItems).map(Number).sort((a, b) => a - b);
|
||||
|
||||
// 항목에 따른 컴포넌트 렌더링 함수
|
||||
const renderComponent = (item) => {
|
||||
const {
|
||||
type,
|
||||
key,
|
||||
keys,
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
placeholder,
|
||||
disabled: itemDisabled,
|
||||
width,
|
||||
handler,
|
||||
min,
|
||||
max,
|
||||
format,
|
||||
required,
|
||||
showTime
|
||||
} = item;
|
||||
|
||||
// 현재 값 가져오기 (formData에서 또는 항목에서)
|
||||
const currentValue = formData[key] !== undefined ? formData[key] : value;
|
||||
|
||||
// 컴포넌트 공통 속성
|
||||
const commonProps = {
|
||||
id: key,
|
||||
disabled: disabled || itemDisabled,
|
||||
style: width ? { width, fontSize: '15px' } : { width: '100%', fontSize: '15px' }
|
||||
};
|
||||
|
||||
// 항목 타입에 따른 컴포넌트 렌더링
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return <Input
|
||||
{...commonProps}
|
||||
value={currentValue}
|
||||
onChange={(e) => onChange(key, e.target.value, handler)}
|
||||
placeholder={placeholder || `${label} 입력`}
|
||||
/>;
|
||||
|
||||
case 'number':
|
||||
return <InputNumber
|
||||
{...commonProps}
|
||||
value={currentValue}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={(value) => onChange(key, value, handler)}
|
||||
placeholder={placeholder || `${label} 입력`}
|
||||
/>;
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
{...commonProps}
|
||||
value={currentValue}
|
||||
onChange={(value) => onChange(key, value, handler)}
|
||||
placeholder={placeholder || `${label} 선택`}
|
||||
>
|
||||
{options && options.map((option) => (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
|
||||
case 'date':
|
||||
return (
|
||||
<DatePicker
|
||||
{...commonProps}
|
||||
value={currentValue ? dayjs(currentValue) : null}
|
||||
format={format || 'YYYY-MM-DD'}
|
||||
onChange={(date) => onChange(key, date, handler)}
|
||||
placeholder={placeholder || `${label} 선택`}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'dateRange':
|
||||
return (
|
||||
<RangePicker
|
||||
{...commonProps}
|
||||
showTime={showTime !== false}
|
||||
value={keys ? [
|
||||
formData[keys.start] ? dayjs(formData[keys.start]) : null,
|
||||
formData[keys.end] ? dayjs(formData[keys.end]) : null
|
||||
] : (currentValue ? [
|
||||
currentValue.start ? dayjs(currentValue.start) : null,
|
||||
currentValue.end ? dayjs(currentValue.end) : null
|
||||
] : null)}
|
||||
format={format || 'YYYY-MM-DD HH:mm:ss'}
|
||||
onChange={(dates, dateStrings) => {
|
||||
if (dates) {
|
||||
// 두 개의 별도 필드에 각각 업데이트
|
||||
if (item.keys) {
|
||||
onChange(item.keys.start, dates[0], handler);
|
||||
onChange(item.keys.end, dates[1], handler);
|
||||
} else {
|
||||
// 기존 방식 지원 (하위 호환성)
|
||||
onChange(key, {
|
||||
start: dates[0],
|
||||
end: dates[1]
|
||||
}, handler);
|
||||
}
|
||||
} else {
|
||||
// 두 필드 모두 비우기
|
||||
if (item.keys) {
|
||||
onChange(item.keys.start, null, handler);
|
||||
onChange(item.keys.end, null, handler);
|
||||
} else {
|
||||
onChange(key, { start: null, end: null }, handler);
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={[
|
||||
item.startLabel || '시작 일시',
|
||||
item.endLabel || '종료 일시'
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'time':
|
||||
return (
|
||||
<TimePicker
|
||||
{...commonProps}
|
||||
value={currentValue ? dayjs(currentValue, 'HH:mm') : null}
|
||||
format={format || 'HH:mm'}
|
||||
onChange={(time) => onChange(key, time, handler)}
|
||||
placeholder={placeholder || `${label} 선택`}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'switch':
|
||||
return (
|
||||
<Switch
|
||||
checked={currentValue}
|
||||
onChange={(checked) => onChange(key, checked, handler)}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<Checkbox
|
||||
checked={currentValue}
|
||||
disabled={disabled || itemDisabled}
|
||||
onChange={(e) => onChange(key, e.target.checked, handler)}
|
||||
>
|
||||
{item.checkboxLabel}
|
||||
</Checkbox>
|
||||
);
|
||||
|
||||
case 'status':
|
||||
return <StatusDisplay status={currentValue} />;
|
||||
|
||||
case 'tab':
|
||||
return <AnimatedTabs
|
||||
items={tabItems}
|
||||
activeKey={activeLanguage}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
|
||||
case 'custom':
|
||||
return item.render ? item.render(formData, onChange) : null;
|
||||
|
||||
default:
|
||||
return <div>{currentValue}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
// 각 셀의 폭 계산 (Ant Design의 24-컬럼 시스템 기준)
|
||||
const colWidth = 24 / columns;
|
||||
|
||||
return (
|
||||
<GridContainer>
|
||||
<Form layout="horizontal">
|
||||
{rows.map(rowIndex => {
|
||||
const rowItems = positionedItems[rowIndex];
|
||||
const cols = Object.keys(rowItems).map(Number).sort((a, b) => a - b);
|
||||
|
||||
return (
|
||||
<Row key={`row-${rowIndex}`} gutter={[16, 16]}>
|
||||
{cols.map(colIndex => {
|
||||
const item = rowItems[colIndex];
|
||||
const itemColSpan = Math.min(item.colSpan * colWidth, 24);
|
||||
|
||||
return (
|
||||
<Col
|
||||
key={`${item.key}-${rowIndex}-${colIndex}`}
|
||||
span={itemColSpan}
|
||||
xs={24}
|
||||
sm={itemColSpan}
|
||||
>
|
||||
<Form.Item
|
||||
label={item.label}
|
||||
required={item.required}
|
||||
tooltip={item.tooltip}
|
||||
>
|
||||
{renderComponent(item)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</Form>
|
||||
</GridContainer>
|
||||
);
|
||||
};
|
||||
|
||||
// 상태 표시 컴포넌트
|
||||
const StatusDisplay = ({ status }) => {
|
||||
let color = '';
|
||||
let text = '';
|
||||
|
||||
switch (status) {
|
||||
case 'wait':
|
||||
color = '#faad14';
|
||||
text = '대기';
|
||||
break;
|
||||
case 'running':
|
||||
color = '#52c41a';
|
||||
text = '진행중';
|
||||
break;
|
||||
case 'finish':
|
||||
color = '#d9d9d9';
|
||||
text = '만료';
|
||||
break;
|
||||
case 'fail':
|
||||
color = '#ff4d4f';
|
||||
text = '실패';
|
||||
break;
|
||||
case 'delete':
|
||||
color = '#ff4d4f';
|
||||
text = '삭제';
|
||||
break;
|
||||
default:
|
||||
color = '#1890ff';
|
||||
text = status;
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusTag color={color}>
|
||||
{text}
|
||||
</StatusTag>
|
||||
);
|
||||
};
|
||||
|
||||
const GridContainer = styled.div`
|
||||
width: 100%;
|
||||
padding: 16px 0;
|
||||
font-size: 15px;
|
||||
`;
|
||||
|
||||
const StatusTag = styled.div`
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: ${props => props.color};
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export default DetailGrid;
|
||||
127
src/components/common/Layout/DetailLayout.js
Normal file
127
src/components/common/Layout/DetailLayout.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Card } from 'antd';
|
||||
import DetailGrid from './DetailGrid';
|
||||
|
||||
/**
|
||||
* Ant Design 방식의 DetailModalWrapper 컴포넌트
|
||||
* @param {Array} itemGroups - 표시할 항목 그룹 배열
|
||||
* @param {Object} formData - 폼 데이터 객체
|
||||
* @param {Function} onChange - 값 변경 시 호출할 함수
|
||||
* @param {boolean} disabled - 전체 비활성화 여부
|
||||
* @param {number} columnCount - 한 행에 표시할 컬럼 수 (기본값: 4)
|
||||
* @param {ReactNode} children - 추가 컨텐츠
|
||||
*/
|
||||
const DetailLayout = ({
|
||||
itemGroups,
|
||||
formData,
|
||||
onChange,
|
||||
disabled = false,
|
||||
columnCount = 4,
|
||||
children
|
||||
}) => {
|
||||
// 값 변경 핸들러
|
||||
const handleChange = (key, value, handler) => {
|
||||
// 핸들러가 있으면 핸들러 실행
|
||||
if (handler) {
|
||||
handler(value, key, formData);
|
||||
}
|
||||
|
||||
// 키가 점 표기법이면 중첩 객체 업데이트
|
||||
if (key.includes('.')) {
|
||||
const [parentKey, childKey] = key.split('.');
|
||||
onChange({
|
||||
...formData,
|
||||
[parentKey]: {
|
||||
...formData[parentKey],
|
||||
[childKey]: value
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 일반 키는 직접 업데이트
|
||||
onChange({ ...formData, [key]: value });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DetailWrapper>
|
||||
{itemGroups.map((group, index) => (
|
||||
<Card
|
||||
key={`group-${index}`}
|
||||
title={group.title}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<DetailGrid
|
||||
items={group.items}
|
||||
formData={formData}
|
||||
onChange={handleChange}
|
||||
disabled={disabled || group.disabled}
|
||||
columnCount={group.columnCount || columnCount}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
{children}
|
||||
</DetailWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const DetailWrapper = styled.div`
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
export default DetailLayout;
|
||||
|
||||
//예시
|
||||
// const itemGroupsExample = [
|
||||
// {
|
||||
// title: '기본 정보',
|
||||
// items: [
|
||||
// {
|
||||
// row: 0,
|
||||
// col: 0,
|
||||
// colSpan: 2,
|
||||
// type: 'text',
|
||||
// key: 'title',
|
||||
// label: '제목',
|
||||
// required: true,
|
||||
// disabled: !isView('title'),
|
||||
// width: '300px',
|
||||
// },
|
||||
// {
|
||||
// row: 0,
|
||||
// col: 2,
|
||||
// colSpan: 2,
|
||||
// type: 'number',
|
||||
// key: 'order_id',
|
||||
// label: '순서',
|
||||
// required: true,
|
||||
// disabled: !isView('order_id'),
|
||||
// width: '200px',
|
||||
// min: 1,
|
||||
// },
|
||||
// {
|
||||
// row: 1,
|
||||
// col: 0,
|
||||
// colSpan: 2,
|
||||
// type: 'status',
|
||||
// key: 'status',
|
||||
// label: '상태',
|
||||
// value: resultData.status,
|
||||
// },
|
||||
// {
|
||||
// row: 1,
|
||||
// col: 2,
|
||||
// colSpan: 2,
|
||||
// type: 'switch',
|
||||
// key: 'is_link',
|
||||
// label: '링크 사용',
|
||||
// disabled: !isView('is_link'),
|
||||
// },
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// title: ''
|
||||
// }
|
||||
// ];
|
||||
84
src/components/common/button/AntButton.js
Normal file
84
src/components/common/button/AntButton.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { Button as AntBtn} from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
|
||||
/**
|
||||
* Ant Design의 Button을 사용한 버튼 컴포넌트
|
||||
* @param {string} text - 버튼 텍스트
|
||||
* @param {string} type - 버튼 타입 (primary, default, dashed, link, text)
|
||||
* @param {function} onClick - 클릭 이벤트 핸들러
|
||||
* @param {string} theme - 커스텀 테마 (primary, line, disable, reset, gray, search, find)
|
||||
* @param {boolean} disabled - 비활성화 여부
|
||||
* @param {string} name - 버튼 이름
|
||||
* @param {string} width - 버튼 너비
|
||||
* @param {string} height - 버튼 높이
|
||||
*/
|
||||
const AntButton = ({
|
||||
text,
|
||||
type = 'button',
|
||||
onClick,
|
||||
theme,
|
||||
disabled,
|
||||
name,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
// theme에 따른 Ant Design 버튼 타입 설정
|
||||
let buttonType = 'default';
|
||||
let shape = 'default';
|
||||
let icon = '';
|
||||
let color = 'default';
|
||||
let variant = 'outlined';
|
||||
let buttonProps = {
|
||||
disabled,
|
||||
onClick,
|
||||
name,
|
||||
style: {
|
||||
width: width || 'auto',
|
||||
height: height || '35px',
|
||||
minWidth: 'fit-content',
|
||||
fontSize: '15px',
|
||||
},
|
||||
...props
|
||||
};
|
||||
|
||||
// 테마에 따른 스타일 설정
|
||||
switch (theme) {
|
||||
case 'primary':
|
||||
case 'submit':
|
||||
case 'line':
|
||||
buttonType = 'default';
|
||||
break;
|
||||
case 'disable':
|
||||
buttonProps.disabled = true;
|
||||
break;
|
||||
case 'gray':
|
||||
break;
|
||||
case 'search':
|
||||
case 'find':
|
||||
icon = <SearchOutlined />;
|
||||
break;
|
||||
case 'cancel':
|
||||
color = 'danger';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<AntBtn
|
||||
type={buttonType}
|
||||
shape={shape}
|
||||
icon={icon}
|
||||
variant={variant}
|
||||
color={color}
|
||||
htmlType={type === 'submit' ? 'submit' : 'button'}
|
||||
{...buttonProps}
|
||||
>
|
||||
{text}
|
||||
</AntBtn>
|
||||
);
|
||||
};
|
||||
|
||||
export default AntButton;
|
||||
157
src/components/common/button/ExcelExportButton.js
Normal file
157
src/components/common/button/ExcelExportButton.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { ExcelDownButton } from '../../../styles/ModuleComponents';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { BusinessLogExport, getExcelProgress } from '../../../apis/Log';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
import { alertTypes } from '../../../assets/data/types';
|
||||
import * as APIs from '../../../apis';
|
||||
import { loadConfig } from '../../../utils';
|
||||
|
||||
const ExcelDownloadButton = ({ functionName, params, fileName = 'download.xlsx', sheetName = 'Sheet1', onLoadingChange, disabled, dataSize }) => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
const {showToast} = useAlert();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const taskIdRef = useRef(null);
|
||||
const intervalRef = useRef(null);
|
||||
|
||||
// 진행률 폴링 함수
|
||||
const pollProgress = useCallback(async (taskId) => {
|
||||
try {
|
||||
const response = await getExcelProgress(token, taskId);
|
||||
// console.log(response.data);
|
||||
if (response.data.exists) {
|
||||
const { percentage, message } = response.data;
|
||||
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({
|
||||
loading: true,
|
||||
progress: percentage
|
||||
});
|
||||
}
|
||||
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({ loading: true, progress: percentage });
|
||||
}
|
||||
|
||||
// 100% 완료 시 폴링 중지
|
||||
if (percentage >= 100) {
|
||||
// console.log("pollProgress data exists polling stop");
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
} else {
|
||||
// 진행률 정보가 없으면 폴링 중지
|
||||
console.log("pollProgress data not exists polling stop");
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({ loading: false, progress: 0 });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Progress polling error:', error);
|
||||
// 에러 발생 시 폴링 중지
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
|
||||
setIsDownloading(false);
|
||||
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({ loading: false, progress: 0 });
|
||||
showToast('DOWNLOAD_FAIL', { type: alertTypes.error });
|
||||
}
|
||||
}
|
||||
}, [onLoadingChange]);
|
||||
|
||||
// 컴포넌트 언마운트 시 정리
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
console.log("unmount polling stop");
|
||||
onLoadingChange({ loading: false, progress: 0 });
|
||||
clearInterval(intervalRef.current);
|
||||
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({ loading: false, progress: 0 });
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
if (isDownloading) return; // 이미 다운로드 중이면 중복 실행 방지
|
||||
if (dataSize > 200000){
|
||||
showToast('EXCEL_EXPORT_LENGTH_LIMIT_WARNING', {type: alertTypes.warning});
|
||||
return;
|
||||
}
|
||||
|
||||
// const start = Date.now();
|
||||
const taskId = `excel_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
taskIdRef.current = taskId;
|
||||
|
||||
setIsDownloading(true);
|
||||
if (onLoadingChange) onLoadingChange({loading: true, progress: 0});
|
||||
|
||||
// 진행률 폴링 시작 (1초마다)
|
||||
intervalRef.current = setInterval(() => {
|
||||
pollProgress(taskId);
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
await APIs[functionName](token, {...params, taskId}, fileName);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await pollProgress(taskId);
|
||||
} catch (error) {
|
||||
console.error('Final progress check error:', error);
|
||||
}
|
||||
}, 300);
|
||||
} catch (error) {
|
||||
console.error('BusinessLogExport API error:', error);
|
||||
showToast(error, {type: alertTypes.error});
|
||||
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
setIsDownloading(false);
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange({
|
||||
loading: false,
|
||||
progress: 0
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (!intervalRef.current) return;
|
||||
|
||||
// 2초 후 상태 리셋
|
||||
setTimeout(() => {
|
||||
setIsDownloading(false);
|
||||
|
||||
// console.log("handleDownload finally polling stop");
|
||||
|
||||
onLoadingChange({ loading: false, progress: 100 });
|
||||
showToast('DOWNLOAD_COMPLETE', { type: alertTypes.success });
|
||||
|
||||
// 폴링 완전 중지
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
// const end = Date.now();
|
||||
// showToast(`처리 시간: ${end - start}ms`, { type: alertTypes.info });
|
||||
// console.log(`처리 시간: ${end - start}ms`);
|
||||
}, 1000);
|
||||
}
|
||||
}, [params, isDownloading, onLoadingChange, dataSize, pollProgress, showToast]);
|
||||
|
||||
return (
|
||||
<ExcelDownButton onClick={handleDownload} disabled={isDownloading || dataSize === 0}>
|
||||
{isDownloading ? '다운로드 중...' : '엑셀 다운로드'}
|
||||
</ExcelDownButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExcelDownloadButton;
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
|
||||
const Button = styled.button`
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #666666;
|
||||
color: white;
|
||||
@@ -54,7 +55,7 @@ const TopButton = () => {
|
||||
onClick={scrollToTop}
|
||||
title="맨 위로 이동"
|
||||
>
|
||||
↑
|
||||
<VerticalAlignTopOutlined />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
78
src/components/common/control/AnimatedTabs.js
Normal file
78
src/components/common/control/AnimatedTabs.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { Tabs } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// 통합된 애니메이션 탭 컴포넌트
|
||||
const AnimatedTabs = ({ items, activeKey, onChange }) => {
|
||||
return (
|
||||
<StyledTabs
|
||||
activeKey={activeKey}
|
||||
onChange={onChange}
|
||||
centered={true}
|
||||
>
|
||||
{items.map(item => (
|
||||
<Tabs.TabPane
|
||||
tab={item.label}
|
||||
key={item.key}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeKey}
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
{item.children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</StyledTabs>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledTabs = styled(Tabs)`
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 16px;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
export default AnimatedTabs;
|
||||
@@ -19,6 +19,8 @@ import Loading from './Loading';
|
||||
import DownloadProgress from './DownloadProgress';
|
||||
import CDivider from './CDivider';
|
||||
import TopButton from './button/TopButton';
|
||||
import AntButton from './button/AntButton';
|
||||
import DetailLayout from './Layout/DetailLayout';
|
||||
|
||||
import CaliTable from './Custom/CaliTable'
|
||||
|
||||
@@ -36,6 +38,7 @@ export { DateTimeInput,
|
||||
CheckBox,
|
||||
Radio,
|
||||
Button,
|
||||
AntButton,
|
||||
ExcelDownButton,
|
||||
AuthModal,
|
||||
CompletedModal,
|
||||
@@ -52,5 +55,6 @@ export { DateTimeInput,
|
||||
DynamoPagination,
|
||||
FrontPagination,
|
||||
DownloadProgress,
|
||||
CaliTable
|
||||
CaliTable,
|
||||
DetailLayout
|
||||
};
|
||||
@@ -1,6 +1,34 @@
|
||||
import styled from 'styled-components';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Modal as AntModal } from 'antd';
|
||||
|
||||
const ModalBg = styled.div`
|
||||
// const ModalBg = styled.div`
|
||||
// position: fixed;
|
||||
// background: ${props => props.$bgcolor || 'rgba(0, 0, 0, 0.5)'};
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// min-width: 1080px;
|
||||
// display: ${props => (props.$view === 'hidden' ? 'none' : 'block')};
|
||||
// z-index: 20;
|
||||
// `;
|
||||
//
|
||||
// const ModalWrapper = styled.div`
|
||||
// position: absolute;
|
||||
// background: #fff;
|
||||
// left: 50%;
|
||||
// top: 50%;
|
||||
// transform: translate(-50%, -50%);
|
||||
// min-width: ${props => props.min || 'auto'};
|
||||
// padding: ${props => props.$padding || '30px'};
|
||||
// border-radius: 30px;
|
||||
// max-height: 90%;
|
||||
// overflow: auto;
|
||||
// box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
||||
// `;
|
||||
|
||||
const ModalBg = styled(motion.div)`
|
||||
position: fixed;
|
||||
background: ${props => props.$bgcolor || 'rgba(0, 0, 0, 0.5)'};
|
||||
width: 100%;
|
||||
@@ -12,30 +40,70 @@ const ModalBg = styled.div`
|
||||
z-index: 20;
|
||||
`;
|
||||
|
||||
const ModalWrapper = styled.div`
|
||||
const ModalContainer = styled.div`
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
min-width: ${props => props.min || 'auto'};
|
||||
padding: ${props => props.$padding || '30px'};
|
||||
border-radius: 30px;
|
||||
max-height: 90%;
|
||||
overflow: auto;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
const ModalWrapper = styled(motion.div)`
|
||||
background: #fff;
|
||||
min-width: ${props => props.min || 'auto'};
|
||||
padding: ${props => props.$padding || '30px'};
|
||||
border-radius: 30px;
|
||||
max-height: 90%;
|
||||
overflow: auto;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
const Modal = ({ children, $padding, min, $view, $bgcolor }) => {
|
||||
const isVisible = $view !== 'hidden';
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalBg $view={$view} $bgcolor={$bgcolor}>
|
||||
<ModalWrapper $padding={$padding} min={min}>
|
||||
{children}
|
||||
</ModalWrapper>
|
||||
</ModalBg>
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<ModalBg
|
||||
$bgcolor={$bgcolor}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<ModalContainer>
|
||||
<ModalWrapper
|
||||
$padding={$padding}
|
||||
min={min}
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ModalWrapper>
|
||||
</ModalContainer>
|
||||
</ModalBg>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// const Modal = ({ children, $padding, min, $view, $bgcolor }) => {
|
||||
// return (
|
||||
// <>
|
||||
// <ModalBg $view={$view} $bgcolor={$bgcolor}>
|
||||
// <ModalWrapper $padding={$padding} min={min}>
|
||||
// {children}
|
||||
// </ModalWrapper>
|
||||
// </ModalBg>
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default Modal;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, Fragment, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import {
|
||||
Title,
|
||||
BtnWrapper,
|
||||
SearchBarAlert, SelectInput,
|
||||
} from '../../../styles/Components';
|
||||
} from '../../styles/Components';
|
||||
|
||||
import {
|
||||
FormInput,
|
||||
@@ -17,23 +17,23 @@ import {
|
||||
FormStatusLabel,
|
||||
FormStatusWarning,
|
||||
FormButtonContainer,
|
||||
} from '../../../styles/ModuleComponents';
|
||||
import { Modal, SingleDatePicker, SingleTimePicker } from '../../common';
|
||||
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../../assets/data/adminConstants';
|
||||
import { convertKTCDate } from '../../../utils';
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { Modal, SingleDatePicker, SingleTimePicker } from '../common';
|
||||
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
|
||||
import { convertKTCDate } from '../../utils';
|
||||
import {
|
||||
battleEventHotTime,
|
||||
battleEventRoundCount,
|
||||
battleEventStatus,
|
||||
battleRepeatType,
|
||||
} from '../../../assets/data/options';
|
||||
import { BattleEventModify, BattleEventSingleRegist } from '../../../apis/Battle';
|
||||
import { alertTypes, battleEventStatusType } from '../../../assets/data/types';
|
||||
import { isValidDayRange } from '../../../utils/date';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
import { useLoading } from '../../../context/LoadingProvider';
|
||||
} from '../../assets/data/options';
|
||||
import { BattleEventModify, BattleEventSingleRegist } from '../../apis/Battle';
|
||||
import { alertTypes, battleEventStatusType } from '../../assets/data/types';
|
||||
import { isValidDayRange } from '../../utils/date';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { useLoading } from '../../context/LoadingProvider';
|
||||
|
||||
const BattleEventModal = ({ modalType, detailView, handleDetailView, content, setDetailData, configData, rewardData }) => {
|
||||
const BattleEventModal = ({ modalType, detailView, handleDetailView, content, setDetailData, configData, rewardData, gameModeData }) => {
|
||||
const { t } = useTranslation();
|
||||
const token = sessionStorage.getItem('token');
|
||||
const { showToast, showModal } = useAlert();
|
||||
@@ -46,7 +46,8 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
|
||||
if(modalType === TYPE_MODIFY && content && Object.keys(content).length > 0){
|
||||
setResultData({
|
||||
group_id: content.group_id,
|
||||
event_id: content.event_id,
|
||||
id: content.id,
|
||||
event_id: content.id,
|
||||
event_name: content.event_name,
|
||||
repeat_type: content.repeat_type,
|
||||
config_id: content.config_id,
|
||||
@@ -54,6 +55,7 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
|
||||
round_count: content.round_count,
|
||||
hot_time: content.hot_time,
|
||||
round_time: content.round_time,
|
||||
game_mode_id: content.game_mode_id,
|
||||
status: content.status,
|
||||
event_start_dt: convertKTCDate(content.event_start_dt),
|
||||
event_end_dt: content.event_end_dt,
|
||||
@@ -262,6 +264,7 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
|
||||
case "reward":
|
||||
case "round":
|
||||
case "hot":
|
||||
case "mode":
|
||||
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === battleEventStatusType.stop));
|
||||
default:
|
||||
return modalType === TYPE_MODIFY && (content?.status !== battleEventStatusType.stop);
|
||||
@@ -326,14 +329,14 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
|
||||
}
|
||||
</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.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) => (
|
||||
@@ -344,11 +347,19 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
|
||||
</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})
|
||||
{/*<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>
|
||||
@@ -421,6 +432,7 @@ export const initData = {
|
||||
reward_group_id: 1,
|
||||
round_count: 1,
|
||||
hot_time: 1,
|
||||
game_mode_id: 1,
|
||||
event_start_dt: '',
|
||||
event_end_dt: ''
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { useState, useRef, Fragment, useEffect } from 'react';
|
||||
|
||||
import Button from '../../common/button/Button';
|
||||
import CheckBox from '../../common/input/CheckBox';
|
||||
import Modal from '../../common/modal/Modal';
|
||||
import Button from '../common/button/Button';
|
||||
import CheckBox from '../common/input/CheckBox';
|
||||
import Modal from '../common/modal/Modal';
|
||||
|
||||
import { Title, BtnWrapper, SelectInput, TextInput, DatePickerWrapper, InputLabel, Textarea, ModalText, SearchBarAlert } from '../../../styles/Components';
|
||||
import CloseIcon from '../../../assets/img/icon/icon-close.png';
|
||||
import DatePickerComponent from '../../common/Date/DatePickerComponent';
|
||||
import { HourList, MinuteList } from '../../../assets/data';
|
||||
import { NoticeModify } from '../../../apis';
|
||||
import { Title, BtnWrapper, SelectInput, TextInput, DatePickerWrapper, InputLabel, Textarea, ModalText, SearchBarAlert } from '../../styles/Components';
|
||||
import CloseIcon from '../../assets/img/icon/icon-close.png';
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
import { HourList, MinuteList } from '../../assets/data';
|
||||
import { NoticeModify } from '../../apis';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { convertKTC, convertKTCDate } from '../../../utils';
|
||||
import { languageType } from '../../../assets/data/options';
|
||||
import { CopyBtn } from '../../../styles/ModuleComponents';
|
||||
import { convertKTC, convertKTCDate } from '../../utils';
|
||||
import { languageType } from '../../assets/data/options';
|
||||
import { CopyBtn } from '../../styles/ModuleComponents';
|
||||
|
||||
const BoardInfoModal = ({ detailView, setDetailView, content, id, setIsCopyData, openRegistModal, userInfo }) => {
|
||||
let viewOnly = userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === 17); // 조회만 가능 권한
|
||||
@@ -1,17 +1,17 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { useState, useRef, Fragment, useEffect } from 'react';
|
||||
|
||||
import Button from '../../common/button/Button';
|
||||
import CheckBox from '../../common/input/CheckBox';
|
||||
import Modal from '../../common/modal/Modal';
|
||||
import Button from '../common/button/Button';
|
||||
import CheckBox from '../common/input/CheckBox';
|
||||
import Modal from '../common/modal/Modal';
|
||||
|
||||
import { Title, BtnWrapper, SelectInput, TextInput, DatePickerWrapper, InputLabel, Textarea, ModalText, SearchBarAlert } from '../../../styles/Components';
|
||||
import CloseIcon from '../../../assets/img/icon/icon-close.png';
|
||||
import DatePickerComponent from '../../common/Date/DatePickerComponent';
|
||||
import { HourList, MinuteList, modalTypes } from '../../../assets/data';
|
||||
import { NoticeRegist } from '../../../apis';
|
||||
import { convertKTC, convertKTCDate } from '../../../utils';
|
||||
import DynamicModal from '../../common/modal/DynamicModal';
|
||||
import { Title, BtnWrapper, SelectInput, TextInput, DatePickerWrapper, InputLabel, Textarea, ModalText, SearchBarAlert } from '../../styles/Components';
|
||||
import CloseIcon from '../../assets/img/icon/icon-close.png';
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
import { HourList, MinuteList, modalTypes } from '../../assets/data';
|
||||
import { NoticeRegist } from '../../apis';
|
||||
import { convertKTC, convertKTCDate } from '../../utils';
|
||||
import DynamicModal from '../common/modal/DynamicModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
BoxWrapper, InputGroup2,
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
NoticeInputRow, NoticeInputRow2,
|
||||
NoticeRegistGroup,
|
||||
RegistInputItem, RepeatTime, SubText, SubTextRow, TitleLang,
|
||||
} from '../../../styles/ModuleComponents';
|
||||
import { languageType } from '../../../assets/data/options';
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { languageType } from '../../assets/data/options';
|
||||
|
||||
const BoardRegistModal = ({ registView, setRegistView, copyData, setIsCopyData }) => {
|
||||
const [doubleSubmitFlag, setDoubleSubmitFlag] = useState(false);
|
||||
@@ -1,25 +1,25 @@
|
||||
import { useState, useEffect, Fragment } from 'react';
|
||||
|
||||
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, Textarea, SearchBarAlert } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import Modal from '../../common/modal/Modal';
|
||||
import { EventIsItem, EventModify } from '../../../apis';
|
||||
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, Textarea, SearchBarAlert } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import Modal from '../common/modal/Modal';
|
||||
import { EventIsItem, EventModify } from '../../apis';
|
||||
|
||||
import { authList } from '../../../store/authList';
|
||||
import { authList } from '../../store/authList';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authType, benItems, commonStatus, currencyType } from '../../../assets/data';
|
||||
import { authType, benItems, commonStatus, currencyItemCode } from '../../assets/data';
|
||||
import {
|
||||
AppendRegistBox, AppendRegistTable, AreaBtnClose,
|
||||
BtnDelete, DetailInputItem, DetailInputRow,
|
||||
DetailModalWrapper, RegistGroup, DetailRegistInfo, DetailState,
|
||||
Item, ItemList, LangArea
|
||||
} from '../../../styles/ModuleComponents';
|
||||
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../../utils';
|
||||
import DateTimeInput from '../../common/input/DateTimeInput';
|
||||
import { useLoading } from '../../../context/LoadingProvider';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
import { alertTypes } from '../../../assets/data/types';
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../utils';
|
||||
import DateTimeInput from '../common/input/DateTimeInput';
|
||||
import { useLoading } from '../../context/LoadingProvider';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
|
||||
const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => {
|
||||
const userInfo = useRecoilValue(authList);
|
||||
@@ -177,7 +177,7 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
|
||||
const item_cnt = resultData.item_list[itemIndex].item_cnt;
|
||||
resultData.item_list[itemIndex].item_cnt = Number(item_cnt) + Number(resourceCount);
|
||||
} else {
|
||||
const name = currencyType.find(well => well.value === resource).name;
|
||||
const name = currencyItemCode.find(well => well.value === resource).name;
|
||||
const newItem = { item: resource, item_cnt: resourceCount, item_name: name };
|
||||
resultData.item_list.push(newItem);
|
||||
}
|
||||
@@ -447,7 +447,7 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
|
||||
<td>
|
||||
<DetailInputItem>
|
||||
<SelectInput onChange={e => setResource(e.target.value)} value={resource} disabled={isReadOnly}>
|
||||
{currencyType.map((data, index) => (
|
||||
{currencyItemCode.map((data, index) => (
|
||||
<option key={index} value={data.value}>
|
||||
{data.name}
|
||||
</option>
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState, Fragment, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../common/button/Button';
|
||||
import Loading from '../../common/Loading';
|
||||
import Button from '../common/button/Button';
|
||||
import Loading from '../common/Loading';
|
||||
|
||||
import {
|
||||
Title,
|
||||
BtnWrapper,
|
||||
SearchBarAlert, SelectInput, InputLabel,
|
||||
} from '../../../styles/Components';
|
||||
} from '../../styles/Components';
|
||||
|
||||
import {
|
||||
FormHelperText,
|
||||
@@ -19,23 +19,23 @@ import {
|
||||
FormRowGroup,
|
||||
NoticeInputRow2,
|
||||
NoticeInputItem2, BoxWrapper, FormStatusBar, FormStatusLabel, FormStatusWarning, FormButtonContainer,
|
||||
} from '../../../styles/ModuleComponents';
|
||||
import { modalTypes } from '../../../assets/data';
|
||||
import {DynamicModal, Modal, DateTimeRangePicker} from '../../common';
|
||||
import { LandAuctionModify, LandAuctionSingleRegist } from '../../../apis';
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { modalTypes } from '../../assets/data';
|
||||
import {DynamicModal, Modal, DateTimeRangePicker} from '../common';
|
||||
import { LandAuctionModify, LandAuctionSingleRegist } from '../../apis';
|
||||
import {
|
||||
AUCTION_MIN_MINUTE_TIME,
|
||||
ONE_MINUTE,
|
||||
ONE_MINUTE_MS,
|
||||
TYPE_MODIFY,
|
||||
TYPE_REGISTRY,
|
||||
} from '../../../assets/data/adminConstants';
|
||||
import { landAuctionStatus, landAuctionStatusType, languageType, CurrencyType } from '../../../assets/data';
|
||||
import { useModal } from '../../../hooks/hook';
|
||||
import { convertKTCDate } from '../../../utils';
|
||||
import { msToMinutes } from '../../../utils/date';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
import { alertTypes } from '../../../assets/data/types';
|
||||
} from '../../assets/data/adminConstants';
|
||||
import { landAuctionStatus, landAuctionStatusType, languageType, CurrencyType } from '../../assets/data';
|
||||
import { useModal } from '../../hooks/hook';
|
||||
import { convertKTCDate } from '../../utils';
|
||||
import { msToMinutes } from '../../utils/date';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
|
||||
const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, setDetailData, landData, buildingData }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -1,16 +1,16 @@
|
||||
import { styled } from 'styled-components';
|
||||
import RadioInput from '../../common/input/Radio';
|
||||
import RadioInput from '../common/input/Radio';
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import CheckBox from '../../common/input/CheckBox';
|
||||
import CheckBox from '../common/input/CheckBox';
|
||||
|
||||
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, DatePickerWrapper, Textarea} from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import Modal from '../../common/modal/Modal';
|
||||
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, DatePickerWrapper, Textarea} from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import Modal from '../common/modal/Modal';
|
||||
|
||||
import IconDelete from '../../../assets/img/icon/icon-delete.png';
|
||||
import CloseIcon from '../../../assets/img/icon/icon-close.png';
|
||||
import DatePickerComponent from '../../common/Date/DatePickerComponent';
|
||||
import MailRegistUploadBtn from '../MailRegistUploadBtn';
|
||||
import IconDelete from '../../assets/img/icon/icon-delete.png';
|
||||
import CloseIcon from '../../assets/img/icon/icon-close.png';
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
import MailRegistUploadBtn from '../ServiceManage/MailRegistUploadBtn';
|
||||
import {
|
||||
authType,
|
||||
benItems, commonStatus,
|
||||
@@ -18,20 +18,20 @@ import {
|
||||
mailType,
|
||||
MinuteList,
|
||||
userType,
|
||||
currencyType,
|
||||
} from '../../../assets/data';
|
||||
import { MailCaliumTotalView, MailIsItem, MailModify } from '../../../apis';
|
||||
currencyItemCode,
|
||||
} from '../../assets/data';
|
||||
import { MailCaliumTotalView, MailIsItem, MailModify } from '../../apis';
|
||||
|
||||
import { authList } from '../../../store/authList';
|
||||
import { authList } from '../../store/authList';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { convertKTC, convertKTCDate } from '../../../utils';
|
||||
import { convertKTC, convertKTCDate } from '../../utils';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDataFetch } from '../../../hooks/hook';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
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 { useDataFetch } from '../../hooks/hook';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
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';
|
||||
|
||||
const MailDetailModal = ({ detailView, handleDetailView, content }) => {
|
||||
const userInfo = useRecoilValue(authList);
|
||||
@@ -219,7 +219,7 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const name = currencyType.find(well => well.value === resource).name;
|
||||
const name = currencyItemCode.find(well => well.value === resource).name;
|
||||
const newItem = { item: resource, item_cnt: resourceCount, item_name: name };
|
||||
resultData.item_list.push(newItem);
|
||||
}
|
||||
@@ -618,7 +618,7 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
|
||||
<td>
|
||||
<InputItem>
|
||||
<SelectInput onChange={e => setResource(e.target.value)} value={resource} disabled={isView}>
|
||||
{currencyType.map((data, index) => (
|
||||
{currencyItemCode.map((data, index) => (
|
||||
<option key={index} value={data.value}>
|
||||
{data.name}
|
||||
</option>
|
||||
611
src/components/modal/MenuBannerDetailModal.js
Normal file
611
src/components/modal/MenuBannerDetailModal.js
Normal file
@@ -0,0 +1,611 @@
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
|
||||
import {
|
||||
Title,
|
||||
SelectInput,
|
||||
BtnWrapper,
|
||||
TextInput,
|
||||
Label,
|
||||
InputLabel,
|
||||
Textarea,
|
||||
SearchBarAlert,
|
||||
ButtonGroupWrapper,
|
||||
} from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import Modal from '../common/modal/Modal';
|
||||
import { EventIsItem, EventModify, MenuBannerModify } from '../../apis';
|
||||
|
||||
import { authList } from '../../store/authList';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { authType, benItems, commonStatus, currencyItemCode } from '../../assets/data';
|
||||
import {
|
||||
DetailInputItem, DetailInputRow,
|
||||
DetailModalWrapper, RegistGroup, DetailRegistInfo, DetailState, FormRowGroup, FormLabel, FormInput,
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../utils';
|
||||
import DateTimeInput from '../common/input/DateTimeInput';
|
||||
import { useLoading } from '../../context/LoadingProvider';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes, battleEventStatusType, languageNames } from '../../assets/data/types';
|
||||
import { Tabs, Image as AntImage, Spin } from 'antd';
|
||||
import { TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
|
||||
import { AntButton, DateTimeRangePicker, DetailLayout, SingleTimePicker } from '../common';
|
||||
import AnimatedTabs from '../common/control/AnimatedTabs';
|
||||
|
||||
function renderImageContent(imageData) {
|
||||
if (!imageData) {
|
||||
return <NoImagePlaceholder>이미지가 없습니다</NoImagePlaceholder>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImageWrapper>
|
||||
<AntImage
|
||||
src={imageData.title}
|
||||
alt={`${imageData.language} 배너 이미지`}
|
||||
style={{ width: '100%', maxHeight: '300px', objectFit: 'contain' }}
|
||||
placeholder={
|
||||
<AntImage
|
||||
preview={false}
|
||||
src={imageData.title}
|
||||
width={300}
|
||||
/>
|
||||
}
|
||||
// preview={{
|
||||
// mask: '미리보기',
|
||||
// maskClassName: 'custom-mask',
|
||||
// }}
|
||||
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg=="
|
||||
/>
|
||||
</ImageWrapper>
|
||||
{imageData.content &&
|
||||
<ImageUrlInfo>
|
||||
<UrlLink
|
||||
href={imageData.content}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{imageData.content}
|
||||
</UrlLink>
|
||||
</ImageUrlInfo>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => {
|
||||
const userInfo = useRecoilValue(authList);
|
||||
const { t } = useTranslation();
|
||||
const token = sessionStorage.getItem('token');
|
||||
const {withLoading} = useLoading();
|
||||
const {showModal, showToast} = useAlert();
|
||||
|
||||
const id = content && content.id;
|
||||
const updateAuth = userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.menuBannerUpdate);
|
||||
|
||||
const [time, setTime] = useState({
|
||||
start_hour: '00',
|
||||
start_min: '00',
|
||||
end_hour: '00',
|
||||
end_min: '00',
|
||||
}); //시간 정보
|
||||
|
||||
const [resultData, setResultData] = useState(initData);
|
||||
const [activeLanguage, setActiveLanguage] = useState('KO');
|
||||
// 이미지 프리로드를 위한 상태
|
||||
const [allImagesLoaded, setAllImagesLoaded] = useState(false);
|
||||
const [showTabContent, setShowTabContent] = useState(false);
|
||||
const [loadedImages, setLoadedImages] = useState([]);
|
||||
const [totalImageCount, setTotalImageCount] = useState(0);
|
||||
|
||||
const [tabItems, setTabItems] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if(content){
|
||||
console.log(content);
|
||||
const start_dt_KTC = convertKTCDate(content.start_dt);
|
||||
const end_dt_KTC = convertKTCDate(content.end_dt);
|
||||
|
||||
setResultData({
|
||||
id: content.id,
|
||||
title: content.title,
|
||||
start_dt: start_dt_KTC,
|
||||
end_dt: end_dt_KTC,
|
||||
status: content.status,
|
||||
order_id: content.order_id,
|
||||
is_link: content.is_link,
|
||||
image_list: content.image_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')
|
||||
});
|
||||
}
|
||||
|
||||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (content && content.image_list) {
|
||||
// 초기화
|
||||
setAllImagesLoaded(false);
|
||||
setShowTabContent(false);
|
||||
setLoadedImages([]);
|
||||
|
||||
// 이미지 개수 설정
|
||||
setTotalImageCount(content.image_list ? content.image_list.length : 0);
|
||||
|
||||
// 첫 번째 언어를 활성 언어로 설정
|
||||
if (content.image_list && content.image_list.length > 0) {
|
||||
setActiveLanguage(content.image_list[0].language);
|
||||
}
|
||||
|
||||
// 동적으로 탭 아이템 생성
|
||||
const newTabItems = content.image_list ? content.image_list.map(imageData => ({
|
||||
key: imageData.language,
|
||||
label: languageNames[imageData.language] || imageData.language,
|
||||
children: (
|
||||
<ImageContainer>
|
||||
{renderImageContent(imageData)}
|
||||
</ImageContainer>
|
||||
)
|
||||
})) : [];
|
||||
|
||||
setTabItems(newTabItems);
|
||||
|
||||
// 모든 이미지 프리로딩 시작
|
||||
setTimeout(() => {
|
||||
preloadAllImages();
|
||||
}, 100);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const preloadAllImages = () => {
|
||||
if (!content || !content.image_list || content.image_list.length === 0) {
|
||||
// 이미지가 없는 경우 바로 로딩 완료 처리
|
||||
// console.log('이미지가 없습니다. 로딩 완료 처리합니다.');
|
||||
setAllImagesLoaded(true);
|
||||
setShowTabContent(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`총 ${content.image_list.length}개의 이미지 로딩을 시작합니다.`);
|
||||
|
||||
// 이미지 개수가 0이면 로딩 완료 처리
|
||||
if (content.image_list.length === 0) {
|
||||
setAllImagesLoaded(true);
|
||||
setShowTabContent(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let loadedCount = 0;
|
||||
|
||||
// 이미지 로드 완료 이벤트 핸들러
|
||||
const handleImageLoad = (url) => {
|
||||
loadedCount++;
|
||||
// console.log(`이미지 로드 완료 (${loadedCount}/${content.image_list.length}): ${url}`);
|
||||
|
||||
// 모든 이미지가 로드되었는지 확인
|
||||
if (loadedCount >= content.image_list.length) {
|
||||
// console.log('모든 이미지 로딩 완료!');
|
||||
setAllImagesLoaded(true);
|
||||
setShowTabContent(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 각 이미지에 대해 프리로드 객체 생성
|
||||
content.image_list.forEach(img => {
|
||||
if (img.title) {
|
||||
// console.log(`이미지 로딩 시작: ${img.title}`);
|
||||
const image = new Image();
|
||||
image.onload = () => handleImageLoad(img.title);
|
||||
image.onerror = () => {
|
||||
console.log(`이미지 로드 실패: ${img.title}`);
|
||||
handleImageLoad(img.title); // 오류 시에도 카운트
|
||||
};
|
||||
image.src = img.title; // src 속성은 onload/onerror 핸들러 설정 후에 설정
|
||||
} else {
|
||||
// console.log('이미지 URL이 없습니다.');
|
||||
handleImageLoad('empty'); // URL이 없는 경우에도 카운트
|
||||
}
|
||||
});
|
||||
|
||||
// 안전장치: 5초 후에도 로딩이 완료되지 않으면 강제로 완료 처리
|
||||
setTimeout(() => {
|
||||
if (!allImagesLoaded) {
|
||||
// console.log('시간 초과로 로딩 강제 완료');
|
||||
setAllImagesLoaded(true);
|
||||
setShowTabContent(true);
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// 날짜 처리
|
||||
// const handleDateChange = (data, type) => {
|
||||
// const date = new Date(data);
|
||||
// setResultData({
|
||||
// ...resultData,
|
||||
// [`${type}_dt`]: combineDateTime(date, time[`${type}_hour`], time[`${type}_min`]),
|
||||
// });
|
||||
// };
|
||||
|
||||
// 시간 처리
|
||||
const handleTimeChange = (e, type) => {
|
||||
const { id, value } = e.target;
|
||||
const newTime = { ...time, [`${type}_${id}`]: value };
|
||||
setTime(newTime);
|
||||
|
||||
const date = resultData[`${type}_dt`] ? new Date(resultData[`${type}_dt`]) : new Date();
|
||||
|
||||
setResultData({
|
||||
...resultData,
|
||||
[`${type}_dt`]: combineDateTime(date, newTime[`${type}_hour`], newTime[`${type}_min`]),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDateChange = {
|
||||
start: (date) => {
|
||||
setResultData(prev => ({ ...prev, start_dt: date }));
|
||||
},
|
||||
end: (date) => {
|
||||
setResultData(prev => ({ ...prev, end_dt: date }));
|
||||
}
|
||||
};
|
||||
|
||||
// 확인 버튼 후 다 초기화
|
||||
const handleReset = () => {
|
||||
};
|
||||
|
||||
const checkCondition = () => {
|
||||
return (
|
||||
(resultData.start_dt.length !== 0) &&
|
||||
(resultData.end_dt.length !== 0) &&
|
||||
resultData.title !== '' &&
|
||||
resultData.order_id !== ''
|
||||
);
|
||||
};
|
||||
|
||||
// 탭 변경 핸들러
|
||||
const handleTabChange = (key) => {
|
||||
setActiveLanguage(key);
|
||||
};
|
||||
|
||||
const handleSubmit = async (type, param = null) => {
|
||||
switch (type) {
|
||||
case "submit":
|
||||
if (!checkCondition()) return;
|
||||
|
||||
showModal('MENU_BANNER_UPDATE_SAVE', {
|
||||
type: alertTypes.confirm,
|
||||
onConfirm: () => handleSubmit('updateConfirm')
|
||||
});
|
||||
break;
|
||||
case "updateConfirm":
|
||||
withLoading( async () => {
|
||||
return await MenuBannerModify(token, id, resultData);
|
||||
}).catch(error => {
|
||||
showToast('API_FAIL', {type: alertTypes.error});
|
||||
}).finally(() => {
|
||||
showToast('UPDATE_COMPLETED', {type: alertTypes.success});
|
||||
handleDetailView();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const detailState = (status) => {
|
||||
switch (status) {
|
||||
case commonStatus.wait:
|
||||
return <DetailState>대기</DetailState>;
|
||||
case commonStatus.running:
|
||||
return <DetailState>진행중</DetailState>;
|
||||
case commonStatus.finish:
|
||||
return <DetailState result={commonStatus.finish}>만료</DetailState>;
|
||||
case commonStatus.fail:
|
||||
return <DetailState result={commonStatus.fail}>실패</DetailState>;
|
||||
case commonStatus.delete:
|
||||
return <DetailState result={commonStatus.delete}>삭제</DetailState>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
//true 수정불가, false 수정가능
|
||||
const isView = (fieldName) => {
|
||||
if (!updateAuth) return false;
|
||||
|
||||
if (fieldName === 'editButton') {
|
||||
// updateAuth가 없거나 FINISH 상태면 수정 버튼 숨김 (false 반환)
|
||||
return !updateAuth || content?.status === commonStatus.finish;
|
||||
}
|
||||
|
||||
switch (content?.status) {
|
||||
case commonStatus.running:
|
||||
// RUNNING 상태일 때는 end_dt와 order_id만 수정 가능
|
||||
return fieldName !== 'date' && fieldName !== 'order_id';
|
||||
case commonStatus.wait:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const itemGroups = [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
row: 0,
|
||||
col: 0,
|
||||
colSpan: 2,
|
||||
type: 'text',
|
||||
key: 'title',
|
||||
label: '제목',
|
||||
disabled: !isView('title'),
|
||||
width: '250px',
|
||||
},
|
||||
{
|
||||
row: 0,
|
||||
col: 2,
|
||||
colSpan: 2,
|
||||
type: 'number',
|
||||
key: 'order_id',
|
||||
label: '순서',
|
||||
disabled: !isView('order_id'),
|
||||
width: '100px',
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
row: 1,
|
||||
col: 0,
|
||||
colSpan: 2,
|
||||
type: 'status',
|
||||
key: 'status',
|
||||
label: '상태',
|
||||
value: resultData.status,
|
||||
},
|
||||
{
|
||||
row: 2,
|
||||
col: 0,
|
||||
colSpan: 2,
|
||||
type: 'dateRange',
|
||||
key: 'dateRange',
|
||||
keys: {start: 'start_dt', end: 'end_dt'},
|
||||
label: '기간',
|
||||
disabled: !isView('date'),
|
||||
format: 'YYYY-MM-DD HH:mm'
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal min="960px" $view={detailView}>
|
||||
<Title $align="center">배너 상세 정보</Title>
|
||||
<DetailLayout
|
||||
itemGroups={itemGroups}
|
||||
formData={resultData}
|
||||
onChange={setResultData}
|
||||
disabled={!updateAuth}
|
||||
columnCount={4}
|
||||
/>
|
||||
{/*<DetailModalWrapper>*/}
|
||||
{/* {content &&*/}
|
||||
{/* <RegistGroup>*/}
|
||||
{/* <FormRowGroup>*/}
|
||||
{/* <DetailInputItem>*/}
|
||||
{/* <FormLabel>제목</FormLabel>*/}
|
||||
{/* <FormInput*/}
|
||||
{/* type="text"*/}
|
||||
{/* value={content.title}*/}
|
||||
{/* disabled={isView('title')}*/}
|
||||
{/* onChange={e => setResultData({ ...resultData, title: e.target.value })}*/}
|
||||
{/* width="300px"*/}
|
||||
{/* />*/}
|
||||
{/* </DetailInputItem>*/}
|
||||
{/* <DetailInputItem>*/}
|
||||
{/* <FormLabel>순서</FormLabel>*/}
|
||||
{/* <FormInput*/}
|
||||
{/* placeholder="순서번호"*/}
|
||||
{/* type="number"*/}
|
||||
{/* value={content.order_id}*/}
|
||||
{/* disabled={isView('order_id')}*/}
|
||||
{/* onChange={e => setResultData({ ...resultData, order_id: e.target.value })}*/}
|
||||
{/* width="200px"*/}
|
||||
{/* />*/}
|
||||
{/* </DetailInputItem>*/}
|
||||
{/* </FormRowGroup>*/}
|
||||
{/* <FormRowGroup>*/}
|
||||
{/* <DateTimeRangePicker*/}
|
||||
{/* label="예약기간"*/}
|
||||
{/* startDate={resultData.start_dt}*/}
|
||||
{/* endDate={resultData.end_dt}*/}
|
||||
{/* onStartDateChange={handleDateChange.start}*/}
|
||||
{/* onEndDateChange={handleDateChange.end}*/}
|
||||
{/* pastDate={new Date()}*/}
|
||||
{/* disabled={isView('date')}*/}
|
||||
{/* startLabel="시작 일자"*/}
|
||||
{/* endLabel="종료 일자"*/}
|
||||
{/* // reset={resetDateTime}*/}
|
||||
{/* />*/}
|
||||
{/* </FormRowGroup>*/}
|
||||
{/* <FormRowGroup>*/}
|
||||
{/* <DetailInputItem>*/}
|
||||
{/* <FormLabel>상태</FormLabel>*/}
|
||||
{/* <div>{detailState(content.status)}</div>*/}
|
||||
{/* </DetailInputItem>*/}
|
||||
{/* </FormRowGroup>*/}
|
||||
{/* {content.image_list && content.image_list.length > 0 && (*/}
|
||||
{/* <FormRowGroup style={{display: 'flex', justifyContent: 'center', width: '100%'}}>*/}
|
||||
{/* <DetailInputItem style={{width: '100%'}}>*/}
|
||||
{/* {!showTabContent ? (*/}
|
||||
{/* <LoadingContainer>*/}
|
||||
{/* <Spin size="large" tip="이미지 로딩 중..." />*/}
|
||||
{/* </LoadingContainer>*/}
|
||||
{/* ) : (*/}
|
||||
{/* <ContentWrapper $isLoaded={showTabContent}>*/}
|
||||
{/* <AnimatedTabs*/}
|
||||
{/* items={tabItems}*/}
|
||||
{/* activeKey={activeLanguage}*/}
|
||||
{/* onChange={handleTabChange}*/}
|
||||
{/* />*/}
|
||||
{/* </ContentWrapper>*/}
|
||||
{/* )}*/}
|
||||
|
||||
{/* </DetailInputItem>*/}
|
||||
{/* </FormRowGroup>*/}
|
||||
{/* )}*/}
|
||||
|
||||
{/* </RegistGroup>*/}
|
||||
{/* }*/}
|
||||
{/*</DetailModalWrapper>*/}
|
||||
<ButtonGroupWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
|
||||
<AntButton
|
||||
text="확인"
|
||||
theme="line"
|
||||
name="확인버튼"
|
||||
onClick={() => {
|
||||
handleDetailView();
|
||||
handleReset();
|
||||
setDetailData('');
|
||||
}}
|
||||
/>
|
||||
{!isView('editButton') && (
|
||||
<AntButton
|
||||
type="submit"
|
||||
text="수정"
|
||||
id="수정버튼"
|
||||
theme={checkCondition() ? 'primary' : 'disable'}
|
||||
onClick={() => handleSubmit('submit')}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroupWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuBannerDetailModal;
|
||||
|
||||
const initData = {
|
||||
title: '',
|
||||
is_link: false,
|
||||
start_dt: '',
|
||||
end_dt: '',
|
||||
image_list: [
|
||||
{ language: 'KO', content: '', title: '' },
|
||||
{ language: 'EN', content: '', title: '' },
|
||||
{ language: 'JA', content: '', title: '' },
|
||||
]
|
||||
}
|
||||
|
||||
const StyledTabs = styled(Tabs)`
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 16px;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ImageContainer = styled.div`
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #f0f0f0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const ImageUrlInfo = styled.div`
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const UrlLink = styled.a`
|
||||
color: #1890ff;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`;
|
||||
|
||||
const NoImagePlaceholder = styled.div`
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0f2f5;
|
||||
color: #8c8c8c;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
// 로딩 인디케이터를 위한 컨테이너
|
||||
const LoadingContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
// 컨텐츠 래퍼 - 로딩 상태에 따라 가시성 설정
|
||||
const ContentWrapper = styled.div`
|
||||
width: 100%;
|
||||
opacity: ${props => props.$isLoaded ? 1 : 0};
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
height: ${props => props.$isLoaded ? 'auto' : '0'};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, Fragment, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../common/button/Button';
|
||||
import Loading from '../../common/Loading';
|
||||
import Button from '../common/button/Button';
|
||||
import Loading from '../common/Loading';
|
||||
|
||||
import {
|
||||
Title,
|
||||
BtnWrapper,
|
||||
SearchBarAlert, SelectInput,
|
||||
} from '../../../styles/Components';
|
||||
} from '../../styles/Components';
|
||||
|
||||
import {
|
||||
FormInput,
|
||||
@@ -18,21 +18,21 @@ import {
|
||||
FormStatusLabel,
|
||||
FormStatusWarning,
|
||||
FormButtonContainer,
|
||||
} from '../../../styles/ModuleComponents';
|
||||
import { modalTypes } from '../../../assets/data';
|
||||
import { DynamicModal, Modal, SingleDatePicker, SingleTimePicker } from '../../common';
|
||||
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../../assets/data/adminConstants';
|
||||
import { useModal } from '../../../hooks/hook';
|
||||
import { convertKTCDate } from '../../../utils';
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { modalTypes } from '../../assets/data';
|
||||
import { DynamicModal, Modal, SingleDatePicker, SingleTimePicker } from '../common';
|
||||
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
|
||||
import { useModal } from '../../hooks/hook';
|
||||
import { convertKTCDate } from '../../utils';
|
||||
import {
|
||||
battleEventHotTime,
|
||||
battleEventRoundCount,
|
||||
battleEventStatus,
|
||||
battleRepeatType,
|
||||
} from '../../../assets/data/options';
|
||||
import { BattleEventModify, BattleEventSingleRegist } from '../../../apis/Battle';
|
||||
import { battleEventStatusType } from '../../../assets/data/types';
|
||||
import { isValidDayRange } from '../../../utils/date';
|
||||
} from '../../assets/data/options';
|
||||
import { BattleEventModify, BattleEventSingleRegist } from '../../apis/Battle';
|
||||
import { battleEventStatusType } from '../../assets/data/types';
|
||||
import { isValidDayRange } from '../../utils/date';
|
||||
|
||||
const MenuBannerModal = ({ modalType, detailView, handleDetailView, content, setDetailData, configData, rewardData }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, Fragment, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../common/button/Button';
|
||||
import Loading from '../../common/Loading';
|
||||
import Button from '../common/button/Button';
|
||||
import Loading from '../common/Loading';
|
||||
|
||||
import {
|
||||
Title,
|
||||
BtnWrapper,
|
||||
SearchBarAlert, SelectInput,
|
||||
} from '../../../styles/Components';
|
||||
} from '../../styles/Components';
|
||||
|
||||
import {
|
||||
FormInput,
|
||||
@@ -18,19 +18,19 @@ import {
|
||||
FormStatusLabel,
|
||||
FormStatusWarning,
|
||||
FormButtonContainer, FormGroup, FormItemGroup, SubText,
|
||||
} from '../../../styles/ModuleComponents';
|
||||
import { modalTypes } from '../../../assets/data';
|
||||
import { DynamicModal, Modal, SingleDatePicker, SingleTimePicker } from '../../common';
|
||||
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../../assets/data/adminConstants';
|
||||
import { useModal } from '../../../hooks/hook';
|
||||
import { convertKTCDate } from '../../../utils';
|
||||
import { BattleEventModify, BattleEventSingleRegist } from '../../../apis/Battle';
|
||||
import { alertTypes, battleEventStatusType } from '../../../assets/data/types';
|
||||
import { isValidDayRange } from '../../../utils/date';
|
||||
import CheckBox from '../../common/input/CheckBox';
|
||||
import { LandOwnedChangesRegist, LandOwnerChangesDelete, UserInfoView } from '../../../apis';
|
||||
import { useLoading } from '../../../context/LoadingProvider';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { modalTypes } from '../../assets/data';
|
||||
import { DynamicModal, Modal, SingleDatePicker, SingleTimePicker } from '../common';
|
||||
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
|
||||
import { useModal } from '../../hooks/hook';
|
||||
import { convertKTCDate } from '../../utils';
|
||||
import { BattleEventModify, BattleEventSingleRegist } from '../../apis/Battle';
|
||||
import { alertTypes, battleEventStatusType } from '../../assets/data/types';
|
||||
import { isValidDayRange } from '../../utils/date';
|
||||
import CheckBox from '../common/input/CheckBox';
|
||||
import { LandOwnedChangesRegist, LandOwnerChangesDelete, UserInfoView } from '../../apis';
|
||||
import { useLoading } from '../../context/LoadingProvider';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
|
||||
const OwnerChangeModal = ({ modalType, detailView, handleDetailView, content, setDetailData }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import { Title, BtnWrapper, TextInput, Label, Textarea, InputItem, ModalText, SearchBarAlert } from '../../../styles/Components';
|
||||
import { Title, BtnWrapper, TextInput, Label, Textarea, InputItem, ModalText, SearchBarAlert } from '../../styles/Components';
|
||||
|
||||
import { RepostReplyMessage } from '../../../apis/Report';
|
||||
import { RepostReplyMessage } from '../../apis/Report';
|
||||
|
||||
import Modal from '../../common/modal/Modal';
|
||||
import Button from '../../common/button/Button';
|
||||
import CloseIcon from '../../../assets/img/icon/icon-close.png';
|
||||
import Modal from '../common/modal/Modal';
|
||||
import Button from '../common/button/Button';
|
||||
import CloseIcon from '../../assets/img/icon/icon-close.png';
|
||||
|
||||
const ReportListAnswerModal = ({ answerView, setAnswerView, detailData, replyData, pkId, skId }) => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
@@ -1,10 +1,10 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { Title, BtnWrapper, TextInput, Label, Textarea, InputItem } from '../../../styles/Components';
|
||||
import { Title, BtnWrapper, TextInput, Label, Textarea, InputItem } from '../../styles/Components';
|
||||
|
||||
import Modal from '../../common/modal/Modal';
|
||||
import Button from '../../common/button/Button';
|
||||
import Modal from '../common/modal/Modal';
|
||||
import Button from '../common/button/Button';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { convertKTC } from '../../../utils';
|
||||
import { convertKTC } from '../../utils';
|
||||
|
||||
const ReportListDetailModal = ({ detailView, handleDetailView, handleReply, detailData, replyData, replyAuth }) => {
|
||||
const [dataList, setDataList] = useState([]);
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
import { Title, TableStyle, BtnWrapper, TextInput } from '../../../styles/Components';
|
||||
import Modal from '../../common/modal/Modal';
|
||||
import Button from '../../common/button/Button';
|
||||
import { convertKTC } from '../../../utils';
|
||||
import { blockPeriod, blockSanctions, blockStatus, blockType, commonStatus } from '../../../assets/data';
|
||||
import { Title, TableStyle, BtnWrapper, TextInput } from '../../styles/Components';
|
||||
import Modal from '../common/modal/Modal';
|
||||
import Button from '../common/button/Button';
|
||||
import { convertKTC } from '../../utils';
|
||||
import { blockPeriod, blockSanctions, blockStatus, blockType, commonStatus } from '../../assets/data';
|
||||
|
||||
const UserBlockDetailModal = ({ stateModal, handleModal, data }) => {
|
||||
const [history, setHistory] = useState();
|
||||
@@ -1,9 +1,9 @@
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import CheckBox from '../../common/input/CheckBox';
|
||||
import { SearchBarLayout } from '../../common/SearchBar';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import CheckBox from '../common/input/CheckBox';
|
||||
import { SearchBarLayout } from '../common/SearchBar';
|
||||
import { useState } from 'react';
|
||||
|
||||
const AdminViewSearchBar = ({ handleSearch, groupList, setResultData, setCurrentPage }) => {
|
||||
@@ -1,15 +1,15 @@
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { BattleEventView } from '../../../apis/Battle';
|
||||
import { BattleEventView } from '../../apis/Battle';
|
||||
import {
|
||||
battleEventHotTime,
|
||||
battleEventRoundCount,
|
||||
battleEventStatus,
|
||||
battleRepeatType,
|
||||
eventSearchType,
|
||||
} from '../../../assets/data/options';
|
||||
} from '../../assets/data/options';
|
||||
|
||||
export const useBattleEventSearch = (token, initialPageSize) => {
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
@@ -126,7 +126,7 @@ export const useBattleEventSearch = (token, initialPageSize) => {
|
||||
};
|
||||
};
|
||||
|
||||
const BattleEventSearchBar = ({ searchParams, onSearch, onReset, configData, rewardData }) => {
|
||||
const BattleEventSearchBar = ({ searchParams, onSearch, onReset, configData, rewardData, gameModeData }) => {
|
||||
const handleSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -153,22 +153,33 @@ const BattleEventSearchBar = ({ searchParams, onSearch, onReset, configData, rew
|
||||
</InputGroup>
|
||||
</>,
|
||||
<>
|
||||
<InputLabel>라운드 시간</InputLabel>
|
||||
{/*<InputLabel>라운드 시간</InputLabel>*/}
|
||||
{/*<InputGroup>*/}
|
||||
{/* <SelectInput value={searchParams.configId} onChange={e => onSearch({ configId: e.target.value })}>*/}
|
||||
{/* <option value="ALL">전체</option>*/}
|
||||
{/* {configData?.map((data, index) => (*/}
|
||||
{/* <option key={index} value={data.id}>*/}
|
||||
{/* {data.desc}*/}
|
||||
{/* </option>*/}
|
||||
{/* ))}*/}
|
||||
{/* </SelectInput>*/}
|
||||
{/*</InputGroup>*/}
|
||||
{/*<InputLabel>배정 포드</InputLabel>*/}
|
||||
{/*<InputGroup>*/}
|
||||
{/* <SelectInput value={searchParams.rewardId} onChange={e => onSearch({ rewardId: e.target.value })}>*/}
|
||||
{/* <option value='ALL'>전체</option>*/}
|
||||
{/* {rewardData?.map((data, index) => (*/}
|
||||
{/* <option key={index} value={data.id}>*/}
|
||||
{/* {data.desc}*/}
|
||||
{/* </option>*/}
|
||||
{/* ))}*/}
|
||||
{/* </SelectInput>*/}
|
||||
{/*</InputGroup>*/}
|
||||
<InputLabel>게임모드</InputLabel>
|
||||
<InputGroup>
|
||||
<SelectInput value={searchParams.configId} onChange={e => onSearch({ configId: e.target.value })}>
|
||||
<option value="ALL">전체</option>
|
||||
{configData?.map((data, index) => (
|
||||
<option key={index} value={data.id}>
|
||||
{data.desc}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</InputGroup>
|
||||
<InputLabel>배정 포드</InputLabel>
|
||||
<InputGroup>
|
||||
<SelectInput value={searchParams.rewardId} onChange={e => onSearch({ rewardId: e.target.value })}>
|
||||
<SelectInput value={searchParams.game_mode_id} onChange={e => onSearch({ game_mode_id: e.target.value })}>
|
||||
<option value='ALL'>전체</option>
|
||||
{rewardData?.map((data, index) => (
|
||||
{gameModeData?.map((data, index) => (
|
||||
<option key={index} value={data.id}>
|
||||
{data.desc}
|
||||
</option>
|
||||
@@ -1,11 +1,11 @@
|
||||
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { logAction, logDomain, userSearchType2 } from '../../../assets/data/options';
|
||||
import { BusinessLogList } from '../../../apis/Log';
|
||||
import {SearchFilter} from '../';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
import { alertTypes } from '../../../assets/data/types';
|
||||
import { logAction, logDomain, userSearchType2 } from '../../assets/data/options';
|
||||
import { BusinessLogList } from '../../apis/Log';
|
||||
import {SearchFilter} from '../ServiceManage';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
|
||||
export const useBusinessLogSearch = (token, initialPageSize) => {
|
||||
const {showToast} = useAlert();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useState } from 'react';
|
||||
import { caliumStatus } from '../../../assets/data/options';
|
||||
import { caliumStatus } from '../../assets/data/options';
|
||||
|
||||
const CaliumRequestSearchBar = ({ handleSearch, setResultData }) => {
|
||||
const [searchData, setSearchData] = useState({
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { Fragment } from 'react';
|
||||
import { getOptionsArray } from '../../../utils';
|
||||
import { PageSkeleton } from '../../Skeleton/SearchSkeleton';
|
||||
import { getOptionsArray } from '../../utils';
|
||||
import { PageSkeleton } from '../Skeleton/SearchSkeleton';
|
||||
|
||||
const renderSearchField = (field, searchParams, onSearch) => {
|
||||
const { type, id, label, placeholder, width, optionsRef } = field;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
151
src/components/searchBar/CurrencyIndexSearchBar.js
Normal file
151
src/components/searchBar/CurrencyIndexSearchBar.js
Normal file
@@ -0,0 +1,151 @@
|
||||
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 { getCurrencyDetailList, getCurrencyList } from '../../apis/Log';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
|
||||
export const useCurrencyIndexSearch = (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 getCurrencyList(
|
||||
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.data);
|
||||
return result.data;
|
||||
} 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(),
|
||||
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 CurrencyIndexSearchBar = ({ 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 CurrencyIndexSearchBar;
|
||||
232
src/components/searchBar/CurrencyLogSearchBar.js
Normal file
232
src/components/searchBar/CurrencyLogSearchBar.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { amountDeltaType, CurrencyType, logAction, userSearchType2 } from '../../assets/data/options';
|
||||
import { getCurrencyDetailList } from '../../apis/Log';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
|
||||
export const useCurrencyLogSearch = (token, initialPageSize) => {
|
||||
const {showToast} = useAlert();
|
||||
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
search_type: 'GUID',
|
||||
search_data: '',
|
||||
tran_id: '',
|
||||
log_action: 'None',
|
||||
currency_type: 'None',
|
||||
amount_delta_type: 'None',
|
||||
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 getCurrencyDetailList(
|
||||
token,
|
||||
params.search_type,
|
||||
params.search_data,
|
||||
params.tran_id,
|
||||
params.log_action,
|
||||
params.currency_type,
|
||||
params.amount_delta_type,
|
||||
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);
|
||||
}
|
||||
}, [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: '',
|
||||
tran_id: '',
|
||||
log_action: 'None',
|
||||
currency_type: 'None',
|
||||
amount_delta_type: 'None',
|
||||
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 CurrencyLogSearchBar = ({ 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>트랜잭션 ID</InputLabel>
|
||||
<TextInput
|
||||
type="text"
|
||||
placeholder='트랜잭션 ID 입력'
|
||||
value={searchParams.tran_id}
|
||||
width="300px"
|
||||
onChange={e => onSearch({ tran_id: e.target.value }, false)}
|
||||
/>
|
||||
</>,
|
||||
];
|
||||
|
||||
const optionList = [
|
||||
<>
|
||||
<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.log_action} onChange={e => onSearch({ log_action: e.target.value }, false)} >
|
||||
{logAction.map((data, index) => (
|
||||
<option key={index} value={data.value}>
|
||||
{data.name}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</>,
|
||||
<>
|
||||
<InputLabel>재화종류</InputLabel>
|
||||
<SelectInput value={searchParams.currency_type} onChange={e => onSearch({ currency_type: e.target.value }, false)} >
|
||||
<option value="None">전체</option>
|
||||
{CurrencyType.map((data, index) => (
|
||||
<option key={index} value={data.value}>
|
||||
{data.name}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</>,
|
||||
<>
|
||||
<InputLabel>증감유형</InputLabel>
|
||||
<SelectInput value={searchParams.amount_delta_type} onChange={e => onSearch({ amount_delta_type: e.target.value }, false)} >
|
||||
<option value="None">전체</option>
|
||||
{amountDeltaType.map((data, index) => (
|
||||
<option key={index} value={data.value}>
|
||||
{data.name}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</>,
|
||||
];
|
||||
|
||||
return <SearchBarLayout firstColumnData={searchList} secondColumnData={optionList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
|
||||
};
|
||||
|
||||
export default CurrencyLogSearchBar;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { InputLabel, TextInput } from '../../../styles/Components';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { InputLabel, TextInput } from '../../styles/Components';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { InitHistoryList } from '../../../apis/Data';
|
||||
import { useAlert } from '../../../context/AlertProvider';
|
||||
import { alertTypes } from '../../../assets/data/types';
|
||||
import { InitHistoryList } from '../../apis/Data';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
|
||||
export const useDataInitSearch = (token) => {
|
||||
const {showToast} = useAlert();
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useState } from 'react';
|
||||
import { eventStatus } from '../../../assets/data';
|
||||
import { eventStatus } from '../../assets/data';
|
||||
|
||||
const EventListSearchBar = ({ handleSearch, setResultData }) => {
|
||||
const [searchData, setSearchData] = useState({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
|
||||
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper, InputGroup, DatePickerWrapper } from '../../styles/Components';
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { LandAuctionView } from '../../../apis';
|
||||
import { landAuctionStatus, landSearchType, landSize, userType } from '../../../assets/data';
|
||||
import { LandAuctionView } from '../../apis';
|
||||
import { landAuctionStatus, landSearchType, landSize, userType } from '../../assets/data';
|
||||
|
||||
export const useLandAuctionSearch = (token, initialPageSize) => {
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { LandAuctionView, LandInfoData } from '../../../apis';
|
||||
import { landAuctionStatus, landSearchType, landSize, opLandCategoryType } from '../../../assets/data';
|
||||
import { opLandInfoStatusType } from '../../../assets/data/options';
|
||||
import { LandAuctionView, LandInfoData } from '../../apis';
|
||||
import { landAuctionStatus, landSearchType, landSize, opLandCategoryType } from '../../assets/data';
|
||||
import { opLandInfoStatusType } from '../../assets/data/options';
|
||||
|
||||
export const useLandInfoSearch = (token, initialPageSize) => {
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
@@ -1,5 +1,5 @@
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper } from '../../styles/Components';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { TextInput, InputLabel, SelectInput, BtnWrapper } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { opHistoryType } from '../../../assets/data/options';
|
||||
import { TextInput, InputLabel, SelectInput, BtnWrapper } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { opHistoryType } from '../../assets/data/options';
|
||||
|
||||
const LogViewSearchBar = ({ handleSearch, resultData }) => {
|
||||
const [searchData, setSearchData] = useState({
|
||||
@@ -1,9 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useState } from 'react';
|
||||
import { mailReceiveType, mailSendStatus, mailSendType, mailType } from '../../../assets/data';
|
||||
import { mailReceiveType, mailSendStatus, mailSendType, mailType } from '../../assets/data';
|
||||
|
||||
const MailListSearchBar = ({ handleSearch, setResultData }) => {
|
||||
const [searchData, setSearchData] = useState({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../../common/SearchBar';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||
import { useState } from 'react';
|
||||
|
||||
const ReportListSearchBar = ({ handleSearch, setResultData }) => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
|
||||
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper, InputGroup, DatePickerWrapper, AlertText } from '../../styles/Components';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import { TextInput, InputLabel, SelectInput } from '../../../styles/Components';
|
||||
import { logDomain, opInputType } from '../../../assets/data/options';
|
||||
import { TextInput, InputLabel, SelectInput } from '../../styles/Components';
|
||||
import { logDomain, opInputType } from '../../assets/data/options';
|
||||
|
||||
const TextInputWithHelp = memo(({ helpText, ...props }) => {
|
||||
return (
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
|
||||
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper, InputGroup, DatePickerWrapper } from '../../styles/Components';
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../../styles/Components';
|
||||
import Button from '../../common/button/Button';
|
||||
import { SearchBarLayout } from '../../common/SearchBar';
|
||||
import { blockPeriod, blockSanctions, blockSearchType, blockStatus } from '../../../assets/data';
|
||||
import { userType } from '../../../assets/data/options';
|
||||
import { TextInput, BtnWrapper, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import { SearchBarLayout } from '../common/SearchBar';
|
||||
import { blockPeriod, blockSanctions, blockSearchType, blockStatus } from '../../assets/data';
|
||||
import { userType } from '../../assets/data/options';
|
||||
|
||||
const UserBlockSearchBar = ({ handleSearch, setResultData }) => {
|
||||
const [searchData, setSearchData] = useState({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper } from '../../styles/Components';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { styled } from 'styled-components';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { TextInput, SelectInput, InputLabel, FormWrapper, BtnWrapper, ButtonClose } from '../../styles/Components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Modal from '../../components/common/modal/Modal';
|
||||
import Button from '../common/button/Button';
|
||||
import Modal from '../common/modal/Modal';
|
||||
import { UserView } from '../../apis';
|
||||
|
||||
const UserViewSearchBar = ({ setInfoView, handleTab, setResultData, resultData }) => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { styled } from 'styled-components';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
||||
|
||||
67
src/components/searchBar/index.js
Normal file
67
src/components/searchBar/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// 모든 검색바 컴포넌트 export
|
||||
import SearchFilter from './SearchFilter';
|
||||
import VBPSearchBar from './VBPSearchBar';
|
||||
import DecoSearchBar from './DecoSearchBar';
|
||||
import ItemSearchBar from './ItemSearchBar';
|
||||
import LandSearchBar from './LandSearchBar';
|
||||
import UserSearchBar from './UserSearchBar';
|
||||
import DailySearchBar from './DailySearchBar';
|
||||
import CommonSearchBar from './CommonSearchBar';
|
||||
import CreditSearchBar from './CreditSearchBar';
|
||||
import LogViewSearchBar from './LogViewSearchBar';
|
||||
import SegmentSearchBar from './SegmentSearchBar';
|
||||
import DataInitSearchBar from './DataInitSearchBar';
|
||||
import InstanceSearchBar from './InstanceSearchBar';
|
||||
import LandInfoSearchBar from './LandInfoSearchBar';
|
||||
import MailListSearchBar from './MailListSearchBar';
|
||||
import PlayTimeSearchBar from './PlayTimeSearchBar';
|
||||
import UserViewSearchBar from './UserViewSearchBar';
|
||||
import AdminViewSearchBar from './AdminViewSearchBar';
|
||||
import EventListSearchBar from './EventListSearchBar';
|
||||
import RetentionSearchBar from './RetentionSearchBar';
|
||||
import UserBlockSearchBar from './UserBlockSearchBar';
|
||||
import UserIndexSearchBar from './UserIndexSearchBar';
|
||||
import ReportListSearchBar from './ReportListSearchBar';
|
||||
import BattleEventSearchBar from './BattleEventSearchBar';
|
||||
import BusinessLogSearchBar, { useBusinessLogSearch } from './BusinessLogSearchBar';
|
||||
import CurrencyLogSearchBar, { useCurrencyLogSearch } from './CurrencyLogSearchBar';
|
||||
import CurrencyIndexSearchBar, { useCurrencyIndexSearch } from './CurrencyIndexSearchBar';
|
||||
import LandAuctionSearchBar from './LandAuctionSearchBar';
|
||||
import CaliumRequestSearchBar from './CaliumRequestSearchBar';
|
||||
|
||||
// 모든 SearchBar 컴포넌트 export
|
||||
export {
|
||||
SearchFilter,
|
||||
VBPSearchBar,
|
||||
DecoSearchBar,
|
||||
ItemSearchBar,
|
||||
LandSearchBar,
|
||||
UserSearchBar,
|
||||
DailySearchBar,
|
||||
CommonSearchBar,
|
||||
CreditSearchBar,
|
||||
LogViewSearchBar,
|
||||
SegmentSearchBar,
|
||||
DataInitSearchBar,
|
||||
InstanceSearchBar,
|
||||
LandInfoSearchBar,
|
||||
MailListSearchBar,
|
||||
PlayTimeSearchBar,
|
||||
UserViewSearchBar,
|
||||
AdminViewSearchBar,
|
||||
EventListSearchBar,
|
||||
RetentionSearchBar,
|
||||
UserBlockSearchBar,
|
||||
UserIndexSearchBar,
|
||||
ReportListSearchBar,
|
||||
BattleEventSearchBar,
|
||||
BusinessLogSearchBar,
|
||||
useBusinessLogSearch,
|
||||
CurrencyLogSearchBar,
|
||||
useCurrencyLogSearch,
|
||||
LandAuctionSearchBar,
|
||||
CaliumRequestSearchBar,
|
||||
CurrencyIndexSearchBar,
|
||||
useCurrencyIndexSearch
|
||||
};
|
||||
|
||||
@@ -50,6 +50,9 @@ const resources = {
|
||||
DUPLICATE_USER: "중복된 유저 정보가 있습니다.",
|
||||
COUNT_EMPTY_WARNING: "수량을 입력해주세요.",
|
||||
UPLOAD_FILENAME_SAMPLE_WARNING: "파일명에 sample을 넣을 수 없습니다.\r\n파일명을 변경 후 다시 업로드 해주세요.",
|
||||
EXCEL_EXPORT_LENGTH_LIMIT_WARNING: '엑셀 다운은 10만건 이하까지만 가능합니다.\r\n조건을 조정 후 다시 시도해주세요.',
|
||||
DOWNLOAD_COMPLETE: '다운이 완료되었습니다.',
|
||||
DOWNLOAD_FAIL: '다운이 실패하였습니다.',
|
||||
//user
|
||||
NICKNAME_CHANGES_CONFIRM: '닉네임을 변경하시겠습니까?',
|
||||
NICKNAME_CHANGES_COMPLETE: '닉네임 변경이 완료되었습니다.',
|
||||
@@ -136,6 +139,7 @@ const resources = {
|
||||
MENU_BANNER_REGIST_CONFIRM: "배너를 등록하시겠습니까?",
|
||||
MENU_BANNER_SELECT_DELETE: "선택된 배너를 삭제하시겠습니까?",
|
||||
MENU_BANNER_REGIST_CANCEL: "배너 등록을 취소하시겠습니까?\n\r취소 시 설정된 값은 반영되지 않습니다.",
|
||||
MENU_BANNER_UPDATE_SAVE: "배너 정보 수정사항을 \r\n저장하시겠습니까?",
|
||||
//아이템
|
||||
ITEM_DELETE_CONFIRM: '해당 아이템을 삭제하시겠습니까?\r\n* 한번 삭제한 아이템은 다시 복구할 수 없습니다.',
|
||||
ITEM_RESTORE_CONFIRM: '해당 아이템을 복구하시겠습니까?',
|
||||
@@ -157,6 +161,8 @@ const resources = {
|
||||
FILE_LAND_AUCTION: 'Caliverse_Land_Auction.xlsx',
|
||||
FILE_BUSINESS_LOG: 'Caliverse_Log.xlsx',
|
||||
FILE_BATTLE_EVENT: 'Caliverse_Battle_Event.xlsx',
|
||||
FILE_GAME_LOG_CURRENCY: 'Caliverse_Game_Log_Currency',
|
||||
FILE_CURRENCY_INDEX: 'Caliverse_Currency_Index',
|
||||
//서버 에러메시지
|
||||
DYNAMODB_NOT_USER: '유저 정보를 확인해주세요.',
|
||||
NICKNAME_EXIT_ERROR: '해당 닉네임이 존재합니다.',
|
||||
|
||||
@@ -10,33 +10,30 @@ import {
|
||||
TableDetailContainer,
|
||||
TableDetailFlex,
|
||||
TableDetailColumn,
|
||||
DetailTableInfo,
|
||||
DetailTableInfo, DownloadContainer, CircularProgressWrapper,
|
||||
} from '../../styles/Components';
|
||||
|
||||
import { withAuth } from '../../hooks/hook';
|
||||
import {
|
||||
authType,
|
||||
modalTypes,
|
||||
} from '../../assets/data';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DynamicModal,
|
||||
ExcelDownButton,
|
||||
TopButton,
|
||||
ViewTableInfo,
|
||||
} from '../../components/common';
|
||||
import { TableSkeleton } from '../../components/Skeleton/TableSkeleton';
|
||||
import BusinessLogSearchBar, { useBusinessLogSearch } from '../../components/ServiceManage/searchBar/BusinessLogSearchBar';
|
||||
import BusinessLogSearchBar, { useBusinessLogSearch } from '../../components/searchBar/BusinessLogSearchBar';
|
||||
import styled from 'styled-components';
|
||||
import FrontPagination from '../../components/common/Pagination/FrontPagination';
|
||||
import CircularProgress from '../../components/common/CircularProgress';
|
||||
// import MessageInput from '../../components/common/input/MessageInput';
|
||||
// import { AnalyzeAI } from '../../apis';
|
||||
import {
|
||||
INITIAL_CURRENT_PAGE,
|
||||
INITIAL_PAGE_LIMIT,
|
||||
STORAGE_BUSINESS_LOG_SEARCH,
|
||||
} from '../../assets/data/adminConstants';
|
||||
import ExcelExportButton from '../../components/common/button/ExcelExportButton';
|
||||
import Pagination from '../../components/common/Pagination/Pagination';
|
||||
|
||||
const BusinessLogView = () => {
|
||||
const token = sessionStorage.getItem('token');
|
||||
@@ -49,10 +46,6 @@ const BusinessLogView = () => {
|
||||
progress: 0
|
||||
});
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(INITIAL_CURRENT_PAGE);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(500);
|
||||
const [displayData, setDisplayData] = useState([]);
|
||||
|
||||
const {
|
||||
searchParams,
|
||||
loading: dataLoading,
|
||||
@@ -61,6 +54,7 @@ const BusinessLogView = () => {
|
||||
handleReset,
|
||||
handlePageChange,
|
||||
handleOrderByChange,
|
||||
handlePageSizeChange,
|
||||
updateSearchParams
|
||||
} = useBusinessLogSearch(token, 500);
|
||||
|
||||
@@ -82,25 +76,6 @@ const BusinessLogView = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePageSizeChange = (newSize) => {
|
||||
setItemsPerPage(newSize);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleClientPageChange = useCallback((slicedData) => {
|
||||
setDisplayData(slicedData);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
if (dataList?.generic_list && dataList.generic_list.length > 0) {
|
||||
const initialData = dataList.generic_list.slice(0, itemsPerPage);
|
||||
setDisplayData(initialData);
|
||||
} else {
|
||||
setDisplayData([]);
|
||||
}
|
||||
}, [dataList, itemsPerPage]);
|
||||
|
||||
const toggleRowExpand = (index) => {
|
||||
setExpandedRows(prev => ({
|
||||
...prev,
|
||||
@@ -209,10 +184,12 @@ const BusinessLogView = () => {
|
||||
</FormWrapper>
|
||||
<ViewTableInfo orderType="asc" pageType="B" total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
|
||||
<DownloadContainer>
|
||||
<ExcelDownButton
|
||||
data={dataList?.generic_list}
|
||||
<ExcelExportButton
|
||||
functionName="BusinessLogExport"
|
||||
params={searchParams}
|
||||
fileName={t('FILE_BUSINESS_LOG')}
|
||||
onLoadingChange={setDownloadState}
|
||||
dataSize={dataList?.total_all}
|
||||
/>
|
||||
{downloadState.loading && (
|
||||
<CircularProgressWrapper>
|
||||
@@ -238,7 +215,7 @@ const BusinessLogView = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displayData?.map((item, index) => (
|
||||
{dataList?.generic_list?.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
<tr>
|
||||
<td>{item.logTime}</td>
|
||||
@@ -276,14 +253,14 @@ const BusinessLogView = () => {
|
||||
</TableStyle>
|
||||
</TableWrapper>
|
||||
{dataList?.generic_list &&
|
||||
<FrontPagination
|
||||
data={dataList.generic_list}
|
||||
itemsPerPage={itemsPerPage}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
pageLimit={INITIAL_PAGE_LIMIT}
|
||||
onPageChange={handleClientPageChange}
|
||||
/>}
|
||||
<Pagination
|
||||
postsPerPage={searchParams.page_size}
|
||||
totalPosts={dataList?.total_all}
|
||||
setCurrentPage={handlePageChange}
|
||||
currentPage={searchParams.page_no}
|
||||
pageLimit={INITIAL_PAGE_LIMIT}
|
||||
/>
|
||||
}
|
||||
<TopButton />
|
||||
</>
|
||||
}
|
||||
@@ -292,15 +269,3 @@ const BusinessLogView = () => {
|
||||
};
|
||||
|
||||
export default withAuth(authType.businessLogRead)(BusinessLogView);
|
||||
|
||||
const DownloadContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
const CircularProgressWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
@@ -4,7 +4,7 @@ import { Fragment, useState } from 'react';
|
||||
|
||||
import { Title, TableStyle, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
|
||||
|
||||
import LandSearchBar from '../../components/DataManage/LandSearchBar';
|
||||
import LandSearchBar from '../../components/searchBar/LandSearchBar';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import QuestDetailModal from '../../components/DataManage/QuestDetailModal';
|
||||
import LandDetailModal from '../../components/DataManage/LandDetailModal';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Fragment, useState } from 'react';
|
||||
|
||||
import { Title, TableStyle, ButtonClose, ModalText, BtnWrapper } from '../../styles/Components';
|
||||
|
||||
import UserSearchBar from '../../components/DataManage/UserSearchBar';
|
||||
import UserSearchBar from '../../components/searchBar/UserSearchBar';
|
||||
import Modal from '../../components/common/modal/Modal';
|
||||
import Button from '../../components/common/button/Button';
|
||||
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
|
||||
import Button from '../../components/common/button/Button';
|
||||
import Pagination from '../../components/common/Pagination/Pagination';
|
||||
|
||||
import DatePicker, { registerLocale } from 'react-datepicker';
|
||||
import { registerLocale } from 'react-datepicker';
|
||||
import { ko } from 'date-fns/esm/locale';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { getMonth, getYear } from 'date-fns';
|
||||
import range from 'lodash/range';
|
||||
|
||||
import { Title, SelectInput, BtnWrapper, TableStyle, TableInfo, ListCount, ListOption, ButtonClose, ModalText } from '../../styles/Components';
|
||||
import {
|
||||
Title,
|
||||
SelectInput,
|
||||
TableStyle,
|
||||
TableInfo,
|
||||
ListCount,
|
||||
ListOption,
|
||||
TabScroll, TabItem, TabWrapper,
|
||||
} from '../../styles/Components';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
import ItemLogSearchBar from '../../components/DataManage/ItemLogSearchBar';
|
||||
import GoodsLogSearchBar from '../../components/DataManage/CreditLogSearchBar';
|
||||
import TradeLogSerchBar from '../../components/DataManage/TradeLogSearchBar';
|
||||
import Modal from '../../components/common/modal/Modal';
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { authList } from '../../store/authList';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { withAuth } from '../../hooks/hook';
|
||||
import { authType } from '../../assets/data';
|
||||
import { TabGameLogList } from '../../assets/data/options';
|
||||
import CurrencyLogContent from '../../components/DataManage/CurrencyLogContent';
|
||||
import { STORAGE_GAME_LOG_CURRENCY_SEARCH } from '../../assets/data/adminConstants';
|
||||
|
||||
registerLocale('ko', ko);
|
||||
|
||||
@@ -41,7 +43,6 @@ const ItemLogContent = () => {
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<ItemLogSearchBar />
|
||||
<TableInfo>
|
||||
<ListCount>총 : 117건 / 000 건</ListCount>
|
||||
<ListOption>
|
||||
@@ -98,272 +99,49 @@ const ItemLogContent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const GoodsLogContent = () => {
|
||||
const mokupData = [
|
||||
{
|
||||
date: '2023-08-05 12:11:32',
|
||||
name: '홍길동',
|
||||
id: '16CD2ECD-4798-46CE-9B6B-F952CF11F196',
|
||||
gold: '99800',
|
||||
blue: '400',
|
||||
black: '500',
|
||||
red: '500',
|
||||
action: '소진',
|
||||
location: '유저 거래',
|
||||
goldchange: '-',
|
||||
bluechange: '-100',
|
||||
blackchange: '-',
|
||||
redchange: '-',
|
||||
key: 'User_trade_key',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<GoodsLogSearchBar />
|
||||
<TableInfo>
|
||||
<ListCount>총 : 117건 / 000 건</ListCount>
|
||||
<ListOption>
|
||||
<SelectInput name="" id="" className="input-select">
|
||||
<option value="up">오름차순</option>
|
||||
<option value="down">내림차순</option>
|
||||
</SelectInput>
|
||||
<SelectInput name="" id="" className="input-select">
|
||||
<option value="up">50개</option>
|
||||
<option value="down">100개</option>
|
||||
</SelectInput>
|
||||
<Button theme="line" text="엑셀 다운로드" />
|
||||
</ListOption>
|
||||
</TableInfo>
|
||||
<TableWrapper>
|
||||
<TableStyle>
|
||||
<caption></caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="150">일자</th>
|
||||
<th width="200">아바타명</th>
|
||||
<th width="300">GUID</th>
|
||||
<th width="100">
|
||||
(Total)
|
||||
<br />
|
||||
골드
|
||||
</th>
|
||||
<th width="100">
|
||||
(Total)
|
||||
<br />
|
||||
사파이어
|
||||
</th>
|
||||
<th width="100">
|
||||
(Total)
|
||||
<br />
|
||||
칼리움
|
||||
</th>
|
||||
<th width="100">
|
||||
(Total)
|
||||
<br />
|
||||
오닉시움
|
||||
</th>
|
||||
<th width="80">액션</th>
|
||||
<th width="100">획득 / 소진처</th>
|
||||
<th width="100">
|
||||
(변화량)
|
||||
<br />
|
||||
골드
|
||||
</th>
|
||||
<th width="100">
|
||||
(변화량)
|
||||
<br />
|
||||
사파이어
|
||||
</th>
|
||||
<th width="100">
|
||||
(변화량)
|
||||
<br />
|
||||
칼리움
|
||||
</th>
|
||||
<th width="100">
|
||||
(변화량)
|
||||
<br />
|
||||
오닉시움
|
||||
</th>
|
||||
<th width="300">거래 Key</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{mokupData.map((data, index) => (
|
||||
<Fragment key={index}>
|
||||
<tr>
|
||||
<td>{new Date(data.date).toLocaleString()}</td>
|
||||
<td>{data.name}</td>
|
||||
<td>{data.id}</td>
|
||||
<td>{data.gold}</td>
|
||||
<td>{data.blue}</td>
|
||||
<td>{data.black}</td>
|
||||
<td>{data.red}</td>
|
||||
<td>{data.action}</td>
|
||||
<td>{data.location}</td>
|
||||
<td>{data.goldchange}</td>
|
||||
{/* 변화량 0보다 작을 경우 StateDecrease 적용 */}
|
||||
<td>
|
||||
<StateDecrease>{data.bluechange}</StateDecrease>
|
||||
</td>
|
||||
<td>{data.blackchange}</td>
|
||||
<td>{data.redchange}</td>
|
||||
<td>{data.key}</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</TableStyle>
|
||||
</TableWrapper>
|
||||
<Pagination />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TradeLogContent = () => {
|
||||
const mokupData = [
|
||||
{
|
||||
date: '2023-08-05 12:11:32',
|
||||
name: '홍길동',
|
||||
trader: '칼리버스',
|
||||
id: '16CD2ECD-4798-46CE-9B6B-F952CF11F196',
|
||||
key: 'User_trade_key',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TradeLogSerchBar />
|
||||
<TableInfo>
|
||||
<ListCount>
|
||||
총 : 117건 / <span>000</span> 건
|
||||
</ListCount>
|
||||
<ListOption>
|
||||
<SelectInput name="" id="" className="input-select">
|
||||
<option value="up">오름차순</option>
|
||||
<option value="down">내림차순</option>
|
||||
</SelectInput>
|
||||
<SelectInput name="" id="" className="input-select">
|
||||
<option value="up">50개</option>
|
||||
<option value="down">100개</option>
|
||||
</SelectInput>
|
||||
<Button theme="line" text="엑셀 다운로드" />
|
||||
</ListOption>
|
||||
</TableInfo>
|
||||
<TableWrapper>
|
||||
<TableStyle>
|
||||
<caption></caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="150">일자</th>
|
||||
<th width="150">조회 아바타명</th>
|
||||
<th width="150">거래 대상 아바타명</th>
|
||||
<th width="300">거래 대상 GUID</th>
|
||||
<th width="300">거래 Key</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{mokupData.map((data, index) => (
|
||||
<Fragment key={index}>
|
||||
<tr>
|
||||
<td>{new Date(data.date).toLocaleString()}</td>
|
||||
<td>{data.name}</td>
|
||||
<td>{data.trader}</td>
|
||||
<td>{data.id}</td>
|
||||
<td>{data.key}</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</TableStyle>
|
||||
</TableWrapper>
|
||||
<Pagination />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const GameLogView = () => {
|
||||
const navigate = useNavigate();
|
||||
const userInfo = useRecoilValue(authList);
|
||||
const [activeTab, setActiveTab] = useState('itemlog');
|
||||
const [activeTab, setActiveTab] = useState('CURRENCY');
|
||||
|
||||
useEffect(() => {
|
||||
const paramsData = sessionStorage.getItem(STORAGE_GAME_LOG_CURRENCY_SEARCH);
|
||||
|
||||
if (paramsData) {
|
||||
const searchData = JSON.parse(paramsData);
|
||||
|
||||
setActiveTab(searchData.tab);
|
||||
console.log(searchData);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleTab = (e, content) => {
|
||||
e.preventDefault();
|
||||
setActiveTab(content);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === 14) ? (
|
||||
<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>
|
||||
<TabWrapper>
|
||||
<li>
|
||||
<TabItem $state={activeTab === 'itemlog' ? 'active' : 'none'} onClick={e => handleTab(e, 'itemlog')}>
|
||||
아이템 로그
|
||||
</TabItem>
|
||||
</li>
|
||||
<li>
|
||||
<TabItem $state={activeTab === 'goodslog' ? 'active' : 'none'} onClick={e => handleTab(e, 'goodslog')}>
|
||||
재화 로그
|
||||
</TabItem>
|
||||
</li>
|
||||
<li>
|
||||
<TabItem $state={activeTab === 'tradelog' ? 'active' : 'none'} onClick={e => handleTab(e, 'tradelog')}>
|
||||
거래 로그
|
||||
</TabItem>
|
||||
</li>
|
||||
</TabWrapper>
|
||||
{activeTab === 'itemlog' && <ItemLogContent />}
|
||||
{activeTab === 'goodslog' && <GoodsLogContent />}
|
||||
{activeTab === 'tradelog' && <TradeLogContent />}
|
||||
</>
|
||||
)}
|
||||
<Title>게임 로그 조회</Title>
|
||||
<TabScroll>
|
||||
<TabWrapper>
|
||||
{TabGameLogList.map((el, idx) => {
|
||||
return (
|
||||
<li key={idx}>
|
||||
<TabItem $state={activeTab === el.value ? 'active' : 'none'} onClick={e => handleTab(e, el.value)}>
|
||||
{el.name}
|
||||
</TabItem>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</TabWrapper>
|
||||
</TabScroll>
|
||||
{/*{activeTab === 'ITEM' && <ItemLogContent />}*/}
|
||||
<CurrencyLogContent active={activeTab === 'CURRENCY'} />
|
||||
{/*{activeTab === 'TRADE' && <TradeLogContent />}*/}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameLogView;
|
||||
|
||||
const TabItem = styled(Link)`
|
||||
display: inline-flex;
|
||||
width: 120px;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f9f9f9;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
&:hover {
|
||||
background: #888;
|
||||
color: #fff;
|
||||
}
|
||||
${props =>
|
||||
props.$state === 'active' &&
|
||||
`
|
||||
background: #888;
|
||||
color: #fff;`}
|
||||
`;
|
||||
|
||||
const TabWrapper = styled.ul`
|
||||
display: flex;
|
||||
li:first-child {
|
||||
${TabItem} {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default withAuth(authType.gameLogRead)(GameLogView);
|
||||
|
||||
const TableWrapper = styled.div`
|
||||
width: 100%;
|
||||
@@ -383,7 +161,3 @@ const TableWrapper = styled.div`
|
||||
min-width: max-content;
|
||||
}
|
||||
`;
|
||||
|
||||
const StateDecrease = styled.span`
|
||||
color: #d60000;
|
||||
`;
|
||||
|
||||
@@ -25,9 +25,9 @@ import {
|
||||
import { INITIAL_PAGE_LIMIT, INITIAL_PAGE_SIZE, TYPE_MODIFY } from '../../assets/data/adminConstants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckBox, DynamicModal, ExcelDownButton, Pagination, ViewTableInfo } from '../../components/common';
|
||||
import LandInfoSearchBar, { useLandInfoSearch } from '../../components/ServiceManage/searchBar/LandInfoSearchBar';
|
||||
import LandInfoSearchBar, { useLandInfoSearch } from '../../components/searchBar/LandInfoSearchBar';
|
||||
import { TableSkeleton } from '../../components/Skeleton/TableSkeleton';
|
||||
import OwnerChangeModal from '../../components/ServiceManage/modal/OwnerChangeModal';
|
||||
import OwnerChangeModal from '../../components/modal/OwnerChangeModal';
|
||||
import { opLandInfoStatusType } from '../../assets/data/options';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState, Fragment } from 'react';
|
||||
|
||||
import { Title, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
|
||||
import { TabScroll, Title } from '../../styles/Components';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import UserViewSearchBar from '../../components/DataManage/UserViewSearchBar';
|
||||
import UserViewSearchBar from '../../components/searchBar/UserViewSearchBar';
|
||||
import UserDefaultInfo from '../../components/DataManage/UserDefaultInfo';
|
||||
import UserAvatarInfo from '../../components/DataManage/UserAvatarInfo';
|
||||
import UserDressInfo from '../../components/DataManage/UserDressInfo';
|
||||
@@ -16,17 +16,14 @@ import UserFriendInfo from '../../components/DataManage/UserFriendInfo';
|
||||
import UserTatttooInfo from '../../components/DataManage/UserTattooInfo';
|
||||
import UserQuestInfo from '../../components/DataManage/UserQuestInfo';
|
||||
import UserClaimInfo from '../../components/DataManage/UserClaimInfo';
|
||||
import Modal from '../../components/common/modal/Modal';
|
||||
import Button from '../../components/common/button/Button';
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { authList } from '../../store/authList';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import AuthModal from '../../components/common/modal/AuthModal';
|
||||
import { authType, TabList } from '../../assets/data';
|
||||
import { authType, TabUserList } from '../../assets/data';
|
||||
|
||||
const UserView = () => {
|
||||
const navigate = useNavigate();
|
||||
const userInfo = useRecoilValue(authList);
|
||||
|
||||
const [infoView, setInfoView] = useState('none');
|
||||
@@ -48,7 +45,7 @@ const UserView = () => {
|
||||
<UserWrapper display={infoView}>
|
||||
<TabScroll>
|
||||
<UserTabWrapper>
|
||||
{TabList.map((el, idx) => {
|
||||
{TabUserList.map((el, idx) => {
|
||||
return (
|
||||
<UserTab key={idx} $state={el.title === activeContent ? 'active' : 'unactive'} onClick={() => handleTab(el.title)}>
|
||||
{el.title}
|
||||
@@ -102,10 +99,7 @@ const UserTab = styled.li`
|
||||
color: #fff;
|
||||
background: #888;`}
|
||||
`;
|
||||
const TabScroll = styled.div`
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const UserTabWrapper = styled.ul`
|
||||
border-bottom: 1px solid #888888;
|
||||
display: flex;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import Button from '../../components/common/button/Button';
|
||||
|
||||
import { Title, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
|
||||
import { Title, BtnWrapper, ButtonClose, ModalText, TabItem, TabScroll, TabWrapper } from '../../styles/Components';
|
||||
import { styled } from 'styled-components';
|
||||
|
||||
import Modal from '../../components/common/modal/Modal';
|
||||
@@ -17,11 +17,12 @@ 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 { withAuth } from '../../hooks/hook';
|
||||
import { authType } from '../../assets/data';
|
||||
import { TabEconomicIndexList, TabGameLogList } from '../../assets/data/options';
|
||||
|
||||
const EconomicIndex = () => {
|
||||
const navigate = useNavigate();
|
||||
const userInfo = useRecoilValue(authList);
|
||||
const [activeTab, setActiveTab] = useState('credit');
|
||||
const [activeTab, setActiveTab] = useState('CURRENCY');
|
||||
|
||||
const handleTab = (e, content) => {
|
||||
e.preventDefault();
|
||||
@@ -29,86 +30,27 @@ const EconomicIndex = () => {
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === 10) ? (
|
||||
<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>
|
||||
<TabWrapper>
|
||||
<li>
|
||||
<TabItem $state={activeTab === 'credit' ? 'active' : 'none'} onClick={e => handleTab(e, 'credit')}>
|
||||
재화
|
||||
</TabItem>
|
||||
</li>
|
||||
<li>
|
||||
<TabItem $state={activeTab === 'vbp' ? 'active' : 'none'} onClick={e => handleTab(e, 'vbp')}>
|
||||
VBP
|
||||
</TabItem>
|
||||
</li>
|
||||
<li>
|
||||
<TabItem $state={activeTab === 'item' ? 'active' : 'none'} onClick={e => handleTab(e, 'item')}>
|
||||
아이템
|
||||
</TabItem>
|
||||
</li>
|
||||
<li>
|
||||
<TabItem $state={activeTab === 'instance' ? 'active' : 'none'} onClick={e => handleTab(e, 'instance')}>
|
||||
인스턴스
|
||||
</TabItem>
|
||||
</li>
|
||||
<li>
|
||||
<TabItem $state={activeTab === 'deco' ? 'active' : 'none'} onClick={e => handleTab(e, 'deco')}>
|
||||
의상 / 타투
|
||||
</TabItem>
|
||||
</li>
|
||||
</TabWrapper>
|
||||
{activeTab === 'credit' && <CreditContent />}
|
||||
{activeTab === 'vbp' && <VBPContent />}
|
||||
{activeTab === 'item' && <ItemContent />}
|
||||
{activeTab === 'instance' && <InstanceContent />}
|
||||
{activeTab === 'deco' && <DecoContent />}
|
||||
</>
|
||||
)}
|
||||
<Title>경제 지표</Title>
|
||||
<TabScroll>
|
||||
<TabWrapper>
|
||||
{TabEconomicIndexList.map((el, idx) => {
|
||||
return (
|
||||
<li key={idx}>
|
||||
<TabItem $state={activeTab === el.value ? 'active' : 'none'} onClick={e => handleTab(e, el.value)}>
|
||||
{el.name}
|
||||
</TabItem>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</TabWrapper>
|
||||
</TabScroll>
|
||||
{activeTab === 'CURRENCY' && <CreditContent />}
|
||||
{/*{activeTab === 'vbp' && <VBPContent />}*/}
|
||||
{/*{activeTab === 'item' && <ItemContent />}*/}
|
||||
{/*{activeTab === 'instance' && <InstanceContent />}*/}
|
||||
{/*{activeTab === 'deco' && <DecoContent />}*/}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EconomicIndex;
|
||||
|
||||
const TabItem = styled(Link)`
|
||||
display: inline-flex;
|
||||
width: 120px;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f9f9f9;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
&:hover {
|
||||
background: #888;
|
||||
color: #fff;
|
||||
}
|
||||
${props =>
|
||||
props.$state === 'active' &&
|
||||
`background: #888;
|
||||
color: #fff;`}
|
||||
`;
|
||||
|
||||
const TabWrapper = styled.ul`
|
||||
display: flex;
|
||||
li:first-child {
|
||||
${TabItem} {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default withAuth(authType.economicIndicatorsRead)(EconomicIndex);
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
BattleEventDelete,
|
||||
BattleEventDetailView, BattleEventStop,
|
||||
BattleEventView,
|
||||
BattleRewardView,
|
||||
BattleRewardView, GameModeView,
|
||||
} from '../../apis/Battle';
|
||||
|
||||
import { authList } from '../../store/authList';
|
||||
@@ -36,7 +36,7 @@ import { StatusWapper, StatusLabel } from '../../styles/ModuleComponents';
|
||||
import { battleEventStatus, battleRepeatType } from '../../assets/data/options';
|
||||
import BattleEventSearchBar, {
|
||||
useBattleEventSearch,
|
||||
} from '../../components/ServiceManage/searchBar/BattleEventSearchBar';
|
||||
} from '../../components/searchBar/BattleEventSearchBar';
|
||||
import { getDateOnly, getTimeOnly, secondToMinutes } from '../../utils/date';
|
||||
import { alertTypes, battleEventStatusType } from '../../assets/data/types';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
@@ -91,6 +91,10 @@ const BattleEvent = () => {
|
||||
data: battleRewardData
|
||||
} = useDataFetch(() => BattleRewardView(token), [token]);
|
||||
|
||||
const {
|
||||
data: gameModeData
|
||||
} = useDataFetch(() => GameModeView(token), [token]);
|
||||
|
||||
const endTime = (start_dt, operation_time) =>{
|
||||
const startDate = new Date(start_dt);
|
||||
|
||||
@@ -262,6 +266,7 @@ const BattleEvent = () => {
|
||||
onReset={handleReset}
|
||||
configData={battleConfigData}
|
||||
rewardData={battleRewardData}
|
||||
gameModeData={gameModeData}
|
||||
/>
|
||||
</FormWrapper>
|
||||
<ViewTableInfo total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
|
||||
@@ -296,8 +301,9 @@ const BattleEvent = () => {
|
||||
<th width="100">이벤트 시작시간(KST)</th>
|
||||
<th width="100">이벤트 종료시간(KST)</th>
|
||||
<th width="90">이벤트 상태</th>
|
||||
<th width="90">라운드 시간</th>
|
||||
<th width="90">배정포드</th>
|
||||
<th width="90">게임모드</th>
|
||||
{/*<th width="90">라운드 시간</th>*/}
|
||||
{/*<th width="90">배정포드</th>*/}
|
||||
<th width="70">라운드 수</th>
|
||||
<th width="70">핫타임</th>
|
||||
<th width="100">확인 / 수정</th>
|
||||
@@ -313,7 +319,7 @@ const BattleEvent = () => {
|
||||
checked={isRowSelected(battle.id)} />
|
||||
</td>
|
||||
<td>{battle.group_id}</td>
|
||||
<td>{battle.event_id}</td>
|
||||
<td>{battle.id}</td>
|
||||
<td>{battle.event_name}</td>
|
||||
<StatusWapper>
|
||||
<StatusLabel $status={battle.repeat_type}>
|
||||
@@ -329,8 +335,9 @@ const BattleEvent = () => {
|
||||
{battleEventStatus.find(data => data.value === battle.status).name}
|
||||
</StatusLabel>
|
||||
</StatusWapper>
|
||||
<td>{secondToMinutes(battle.round_time)}분</td>
|
||||
<td>{battle.reward_group_id}</td>
|
||||
<td>{battle.game_mode_id}</td>
|
||||
{/*<td>{secondToMinutes(battle.round_time)}분</td>*/}
|
||||
{/*<td>{battle.reward_group_id}</td>*/}
|
||||
<td>{battle.round_count}</td>
|
||||
<td>{battle.hot_time}</td>
|
||||
<td>
|
||||
@@ -359,6 +366,7 @@ const BattleEvent = () => {
|
||||
setDetailData={setDetailData}
|
||||
configData={battleConfigData}
|
||||
rewardData={battleRewardData}
|
||||
gameModeData={gameModeData}
|
||||
/>
|
||||
|
||||
<LogDetailModal
|
||||
|
||||
@@ -13,7 +13,7 @@ import { authType, commonStatus, modalTypes, eventStatus } from '../../assets/da
|
||||
import { Title, FormWrapper, TableStyle, MailTitle, TableWrapper, TextInput, InputItem } from '../../styles/Components';
|
||||
import CheckBox from '../../components/common/input/CheckBox';
|
||||
import Button from '../../components/common/button/Button';
|
||||
import EventDetailModal from '../../components/ServiceManage/modal/EventDetailModal';
|
||||
import EventDetailModal from '../../components/modal/EventDetailModal';
|
||||
import Pagination from '../../components/common/Pagination/Pagination';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import DynamicModal from '../../components/common/modal/DynamicModal';
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
RegistInputRow, RegistNotice, RegistTable,
|
||||
} from '../../styles/ModuleComponents';
|
||||
import AuthModal from '../../components/common/modal/AuthModal';
|
||||
import { authType, benItems, currencyType } from '../../assets/data';
|
||||
import { authType, benItems, currencyItemCode } from '../../assets/data';
|
||||
import DateTimeInput from '../../components/common/input/DateTimeInput';
|
||||
import { timeDiffMinute } from '../../utils';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
@@ -209,7 +209,7 @@ const EventRegist = () => {
|
||||
const item_cnt = resultData.item_list[itemIndex].item_cnt;
|
||||
resultData.item_list[itemIndex].item_cnt = Number(item_cnt) + Number(resourceCount);
|
||||
} else {
|
||||
const name = currencyType.find(well => well.value === resource).name;
|
||||
const name = currencyItemCode.find(well => well.value === resource).name;
|
||||
const newItem = { item: resource, item_cnt: resourceCount, item_name: name };
|
||||
resultData.item_list.push(newItem);
|
||||
}
|
||||
@@ -403,7 +403,7 @@ const EventRegist = () => {
|
||||
<td>
|
||||
<RegistInputItem>
|
||||
<SelectInput onChange={e => setResource(e.target.value)} value={resource}>
|
||||
{currencyType.filter(data => data.value !== currencyCodeTypes.calium).map((data, index) => (
|
||||
{currencyItemCode.filter(data => data.value !== currencyCodeTypes.calium).map((data, index) => (
|
||||
<option key={index} value={data.value}>
|
||||
{data.name}
|
||||
</option>
|
||||
|
||||
@@ -30,7 +30,7 @@ import { convertKTC, timeDiffMinute } from '../../utils';
|
||||
import { LandAuctionModal, LandAuctionSearchBar } from '../../components/ServiceManage';
|
||||
import { INITIAL_PAGE_SIZE, INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
|
||||
import { useDataFetch, useModal, useTable, withAuth } from '../../hooks/hook';
|
||||
import { useLandAuctionSearch } from '../../components/ServiceManage/searchBar/LandAuctionSearchBar';
|
||||
import { useLandAuctionSearch } from '../../components/searchBar/LandAuctionSearchBar';
|
||||
import { StatusWapper, ChargeBtn, StatusLabel } from '../../styles/ModuleComponents';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
|
||||
@@ -10,7 +10,7 @@ import Button from '../../components/common/button/Button';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MailDetailModal from '../../components/ServiceManage/modal/MailDetailModal';
|
||||
import MailDetailModal from '../../components/modal/MailDetailModal';
|
||||
import Pagination from '../../components/common/Pagination/Pagination';
|
||||
|
||||
import { authList } from '../../store/authList';
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import IconDelete from '../../assets/img/icon/icon-delete.png';
|
||||
import CloseIcon from '../../assets/img/icon/icon-close.png';
|
||||
|
||||
import { benItems, HourList, mailType, MinuteList, currencyType, userType } from '../../assets/data';
|
||||
import { benItems, HourList, mailType, MinuteList, currencyItemCode, userType } from '../../assets/data';
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MailRegistUploadBtn from '../../components/ServiceManage/MailRegistUploadBtn';
|
||||
@@ -243,7 +243,7 @@ const MailRegist = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const name = currencyType.find(well => well.value === resource).name;
|
||||
const name = currencyItemCode.find(well => well.value === resource).name;
|
||||
const newItem = { item: resource, item_cnt: resourceCount, item_name: name };
|
||||
resultData.item_list.push(newItem);
|
||||
}
|
||||
@@ -578,7 +578,7 @@ const MailRegist = () => {
|
||||
<td>
|
||||
<InputItem>
|
||||
<SelectInput onChange={e => setResource(e.target.value)} value={resource}>
|
||||
{currencyType.map((data, index) => (
|
||||
{currencyItemCode.map((data, index) => (
|
||||
<option key={index} value={data.value}>
|
||||
{data.name}
|
||||
</option>
|
||||
|
||||
@@ -14,13 +14,13 @@ import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
|
||||
import { useModal, useTable, withAuth } from '../../hooks/hook';
|
||||
import { MenuBannerDelete, MenuBannerDetailView } from '../../apis';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MenuBannerModal from '../../components/ServiceManage/modal/MenuBannerModal';
|
||||
import tableInfo from '../../assets/data/pages/menuBannerTable.json'
|
||||
import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
import { useLoading } from '../../context/LoadingProvider';
|
||||
import useEnhancedCommonSearch from '../../hooks/useEnhancedCommonSearch';
|
||||
import MenuBannerDetailModal from '../../components/modal/MenuBannerDetailModal';
|
||||
|
||||
const MenuBanner = () => {
|
||||
const tableRef = useRef(null);
|
||||
@@ -38,7 +38,6 @@ const MenuBanner = () => {
|
||||
} = useModal({
|
||||
detail: 'hidden',
|
||||
});
|
||||
const [modalType, setModalType] = useState('regist');
|
||||
|
||||
const {
|
||||
config,
|
||||
@@ -67,9 +66,8 @@ const MenuBanner = () => {
|
||||
const handleAction = async (action, item = null) => {
|
||||
switch (action) {
|
||||
case "detail":
|
||||
await MenuBannerDetailView(token, item).then(data => {
|
||||
setDetailData(data.event_detail);
|
||||
setModalType('modify');
|
||||
await MenuBannerDetailView(token, item.id).then(data => {
|
||||
setDetailData(data.detail);
|
||||
handleModalView('detail');
|
||||
});
|
||||
break;
|
||||
@@ -183,8 +181,7 @@ const MenuBanner = () => {
|
||||
/>
|
||||
|
||||
{/* 상세 */}
|
||||
<MenuBannerModal
|
||||
modalType={modalType}
|
||||
<MenuBannerDetailModal
|
||||
detailView={modalState.detailModal}
|
||||
handleDetailView={() => handleModalClose('detail')}
|
||||
content={detailData}
|
||||
|
||||
@@ -22,10 +22,10 @@ import { INITIAL_PAGE_SIZE, INITIAL_PAGE_LIMIT } from '../../assets/data/adminCo
|
||||
import { useModal, useTable, withAuth } from '../../hooks/hook';
|
||||
import { StatusWapper, StatusLabel } from '../../styles/ModuleComponents';
|
||||
import { opMenuBannerStatus } from '../../assets/data/options';
|
||||
import MenuBannerSearchBar, { useMenuBannerSearch } from '../../components/ServiceManage/searchBar/MenuBannerSearchBar';
|
||||
import MenuBannerSearchBar, { useMenuBannerSearch } from '../../components/searchBar/MenuBannerSearchBar';
|
||||
import { MenuBannerDelete, MenuBannerDetailView } from '../../apis';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MenuBannerModal from '../../components/ServiceManage/modal/MenuBannerModal';
|
||||
import MenuBannerModal from '../../components/modal/MenuBannerModal';
|
||||
import tableInfo from '../../assets/data/pages/menuBannerTable.json'
|
||||
|
||||
const MenuBanner = () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useModal, withAuth } from '../../hooks/hook';
|
||||
import { DynamicModal, TopButton } from '../../components/common';
|
||||
import { opInitDataType, opSuccessType } from '../../assets/data/options';
|
||||
import { InitData } from '../../apis/Data';
|
||||
import DataInitSearchBar, { useDataInitSearch } from '../../components/ServiceManage/searchBar/DataInitSearchBar';
|
||||
import DataInitSearchBar, { useDataInitSearch } from '../../components/searchBar/DataInitSearchBar';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { useLoading } from '../../context/LoadingProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
|
||||
@@ -174,6 +174,21 @@ export const BtnWrapper = styled.div`
|
||||
padding-top: ${props => props.$paddingTop};
|
||||
`;
|
||||
|
||||
/**
|
||||
* 버튼 그룹을 위한 스타일드 컴포넌트
|
||||
*/
|
||||
export const ButtonGroupWrapper = styled.div`
|
||||
width: ${props => props.width};
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-flow: ${props => props.$flow};
|
||||
justify-content: ${props => props.$justify};
|
||||
gap: ${props => props.$gap};
|
||||
margin-top: ${props => props.$marginTop};
|
||||
margin-bottom: ${props => props.$marginBottom};
|
||||
padding-top: ${props => props.$paddingTop};
|
||||
`;
|
||||
|
||||
export const Title = styled.h2`
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
@@ -679,6 +694,12 @@ export const SearchItem = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export const DownloadContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
export const SearchRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -690,3 +711,60 @@ export const SearchRow = styled.div`
|
||||
margin-top: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TabScroll = styled.div`
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export const TabItem = styled(Link)`
|
||||
display: inline-flex;
|
||||
width: 120px;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f9f9f9;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
&:hover {
|
||||
background: #888;
|
||||
color: #fff;
|
||||
}
|
||||
${props =>
|
||||
props.$state === 'active' &&
|
||||
`
|
||||
background: #888;
|
||||
color: #fff;`}
|
||||
`;
|
||||
|
||||
export const TabWrapper = styled.ul`
|
||||
display: flex;
|
||||
li:first-child {
|
||||
${TabItem} {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CircularProgressWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const TotalRow = styled.tr`
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ImagePreview = styled.img`
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px 4px 0 0;
|
||||
background-color: #f6f6f6;
|
||||
`;
|
||||
@@ -87,3 +87,139 @@ export const getFieldLabel = (key, value) => {
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
export const numberFormatter = {
|
||||
formatCurrency: (number, decimals = 2) => {
|
||||
if (number === null || number === undefined) return '0';
|
||||
|
||||
try {
|
||||
const num = typeof number === 'string' ? parseFloat(number) : number;
|
||||
if (isNaN(num)) return '0';
|
||||
|
||||
// 정수인지 확인
|
||||
const isInteger = Number.isInteger(num);
|
||||
|
||||
// 작은 수이거나 소수점이 있는 경우
|
||||
if ((Math.abs(num) < 1 && num !== 0) || !isInteger) {
|
||||
return num.toFixed(decimals);
|
||||
}
|
||||
|
||||
// 정수인 경우
|
||||
return new Intl.NumberFormat('ko-KR', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(num);
|
||||
|
||||
} catch (e) {
|
||||
console.error('Currency formatting error:', e);
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* API 응답으로부터 파일 다운로드를 처리하는 함수
|
||||
* @param {Object} response - API 응답 객체
|
||||
* @param {Object} response.data - 응답 데이터
|
||||
* @param {Object} response.headers - 응답 헤더
|
||||
* @param {Object} options - 추가 옵션
|
||||
* @param {string} options.defaultFileName - 기본 파일명 (기본값: 'download')
|
||||
* @returns {void}
|
||||
* @throws {Error} 데이터가 없거나 파일 형식이 잘못된 경우
|
||||
*/
|
||||
export const responseFileDownload = (response, options = {}) => {
|
||||
const { defaultFileName = 'download' } = options;
|
||||
|
||||
if (!response.data) {
|
||||
console.log(response);
|
||||
throw new Error('No data received');
|
||||
}
|
||||
|
||||
const contentType = response.headers['content-type'] || response.headers['Content-Type'];
|
||||
const contentDisposition = response.headers['content-disposition'] || response.headers['Content-Disposition'];
|
||||
|
||||
// Excel, CSV, ZIP 파일 형식 검증 (CSV 추가)
|
||||
const isValidType = contentType && (
|
||||
contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') ||
|
||||
contentType.includes('text/csv') ||
|
||||
contentType.includes('application/zip')
|
||||
);
|
||||
|
||||
if (!isValidType) {
|
||||
console.log('Invalid content type:', contentType);
|
||||
console.log('Full response:', response);
|
||||
throw new Error(`잘못된 파일 형식입니다. Content-Type: ${contentType}`);
|
||||
}
|
||||
|
||||
let fileName = defaultFileName;
|
||||
let fileExtension = '.csv';
|
||||
let mimeType = 'text/csv; charset=UTF-8';
|
||||
|
||||
// 파일 타입별 처리
|
||||
if (contentType.includes('text/csv')) {
|
||||
// CSV 파일 처리
|
||||
fileExtension = '.csv';
|
||||
mimeType = 'text/csv; charset=UTF-8';
|
||||
fileName = `${defaultFileName}`;
|
||||
} else if (contentType.includes('application/zip')) {
|
||||
// ZIP 파일 처리
|
||||
fileExtension = '.zip';
|
||||
mimeType = 'application/zip';
|
||||
fileName = `${defaultFileName}_multiple_files`;
|
||||
}
|
||||
|
||||
// Content-Disposition에서 파일명 추출
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (fileNameMatch && fileNameMatch[1]) {
|
||||
let extractedFileName = fileNameMatch[1].replace(/['"]/g, '');
|
||||
try {
|
||||
fileName = decodeURIComponent(extractedFileName);
|
||||
} catch (e) {
|
||||
// decodeURIComponent 실패 시 원본 사용
|
||||
fileName = extractedFileName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fileName = fileName + fileExtension;
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data], { type: mimeType });
|
||||
|
||||
// 파일 유효성 검사 (CSV는 더 작을 수 있으므로 조건 완화)
|
||||
if (blob.size === 0) {
|
||||
throw new Error('다운로드된 파일이 비어있습니다.');
|
||||
}
|
||||
|
||||
// CSV 파일은 크기 검사 완화
|
||||
const minSize = contentType.includes('text/csv') ? 100 : 1024;
|
||||
if (blob.size < minSize) {
|
||||
throw new Error(`파일 크기가 너무 작습니다. 올바른 파일이 아닐 수 있습니다. (${blob.size} bytes)`);
|
||||
}
|
||||
|
||||
// 파일 다운로드 실행
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = fileName;
|
||||
link.style.display = 'none';
|
||||
link.rel = 'noopener noreferrer';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 정리
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(href);
|
||||
};
|
||||
|
||||
export const calculateTotals = (data) => {
|
||||
return data?.reduce((acc, row) => {
|
||||
Object.entries(row).forEach(([key, value]) => {
|
||||
if (!isNaN(value) && value !== '') {
|
||||
acc[key] = (acc[key] || 0) + Number(value);
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, {}) || {};
|
||||
};
|
||||
Reference in New Issue
Block a user