Compare commits

...

10 Commits

Author SHA1 Message Date
67c048a11d 메뉴배너 detailGrid 적용 2025-06-27 09:28:13 +09:00
f6a0319701 메뉴배너 detailGrid 적용 2025-06-27 09:28:06 +09:00
0368bf77e7 antBUtton 생성
topButton 이미지 변경
엑셀 버튼 조정
tab 컨트론 생성
detailGrid, layout 생성
modal motion 적용
2025-06-27 09:25:41 +09:00
b2b579ead1 전투시스템 스케줄러 game mode 방식 변경 2025-06-19 18:54:15 +09:00
495243a1a3 경제지표 재화 추가
게임 로그 재화지표 export api 추가
경제지표 재화 상세 > 재화 로그 페이지 이동 처리
2025-06-16 15:45:36 +09:00
7993f37e26 qa 주소 변경 2025-06-16 15:43:20 +09:00
93a7c087e1 style, utils 수정 2025-06-12 14:16:46 +09:00
38fa323db6 Log currency 관련 API 호출 추가
components 정리에 따른 호출 위치 수정
excelExportButton에 functionName 추가
게임 로그조회 수정
2025-06-12 14:16:26 +09:00
6f9f0307ac component 정리
currencyLogSearchBar 생성
currencyLogCOntent 생성
excelExportButton api호출 방식 수정
2025-06-12 14:08:11 +09:00
dc7934d906 비즈니스 로그 조회 및 파일 다운 수정 2025-06-04 15:19:08 +09:00
91 changed files with 3180 additions and 1657 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ export * from './Calium';
export * from './Land';
export * from './Menu';
export * from './OpenAI';
export * from './Log';
const apiModules = {};
const allApis = {};

View File

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

View File

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

View File

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

View File

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

View 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"
}
}

View File

@@ -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': '일본어',
};

View File

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

View 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;

View File

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

View File

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

View File

@@ -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 {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useCurrencyIndexSearch(token);
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 [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
const [currencyType, setCurrencyType] = useState('Gold');
const [currencyText, setCurrencyText] = useState('골드');
const totals = useMemo(() => {
if (!dataList?.currency_list?.length) return null;
const [dataList, setDataList] = useState([]);
const [routeData, setRouteData] = useState([]);
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]);
useEffect(() => {
fetchData(sendDate, finishDate, currencyType);
}, [currencyType]);
const fetchData = async (startDate, endDate) => {
const newStartDate = new Date(startDate);
const newEndDate = new Date(endDate);
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 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 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);
// 복사한 데이터를 세션 스토리지에 저장
sessionStorage.setItem(STORAGE_GAME_LOG_CURRENCY_SEARCH, JSON.stringify(params));
navigate('/datamanage/gamelogview');
break;
}
})
});
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);
};
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>
<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>
<EconomicTable>
<TableStyle ref={tableRef}>
<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>
)}
{tableHeaders.map(header => (
<th key={header.id} width={header.width}>{header.label}</th>
))}
</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',
{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>
)}
</TableData>
))
}
{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>
{/* 획득 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>
))
}
</Fragment>
))}
</tbody>
</EconomicTable>
</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;
}
}
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;

View 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: ''
// }
// ];

View 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;

View 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;

View File

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

View 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;

View File

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

View File

@@ -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,12 +40,15 @@ 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%);
`;
const ModalWrapper = styled(motion.div)`
background: #fff;
min-width: ${props => props.min || 'auto'};
padding: ${props => props.$padding || '30px'};
border-radius: 30px;
@@ -27,15 +58,52 @@ const ModalWrapper = styled.div`
`;
const Modal = ({ children, $padding, min, $view, $bgcolor }) => {
const isVisible = $view !== 'hidden';
return (
<>
<ModalBg $view={$view} $bgcolor={$bgcolor}>
<ModalWrapper $padding={$padding} min={min}>
<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;

View File

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

View File

@@ -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); // 조회만 가능 권한

View File

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

View File

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

View File

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

View File

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

View 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;
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
};

View File

@@ -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: '해당 닉네임이 존재합니다.',

View File

@@ -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}
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
onPageChange={handleClientPageChange}
/>}
/>
}
<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;
`;

View File

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

View File

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

View File

@@ -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>
<TabScroll>
<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')}>
거래 로그
{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>
{activeTab === 'itemlog' && <ItemLogContent />}
{activeTab === 'goodslog' && <GoodsLogContent />}
{activeTab === 'tradelog' && <TradeLogContent />}
</>
)}
</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;
`;

View File

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

View File

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

View File

@@ -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,98 +17,40 @@ 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();
setActiveTab(content);
};
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>
<TabScroll>
<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')}>
의상 / 타투
{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>
{activeTab === 'credit' && <CreditContent />}
{activeTab === 'vbp' && <VBPContent />}
{activeTab === 'item' && <ItemContent />}
{activeTab === 'instance' && <InstanceContent />}
{activeTab === 'deco' && <DecoContent />}
</>
)}
</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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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