Compare commits

...

15 Commits

Author SHA1 Message Date
d3470e3d03 nginx 파일 제한 증가 2025-07-21 16:33:02 +09:00
99943c0b19 퀘스트 강제 완료
경제지표 재화 헤더 스타일 변경
2025-07-18 15:18:45 +09:00
26114c9a9b 코드 정리 2025-07-17 14:40:07 +09:00
952701f68b 게임로그 유저생성 로그 조회
게임로그 유저로그인 로그 조회
2025-07-17 14:38:07 +09:00
7041d4a649 유저 지표 잔존율 생성 2025-07-16 18:39:30 +09:00
7fa9abcad4 탭 모션 적용 2025-07-16 18:38:38 +09:00
943b146496 마이홈 리스트형식으로 변경 2025-07-14 13:53:14 +09:00
88585c1b24 전투이벤트 최대 진행시간 예외처리 2025-07-13 11:29:31 +09:00
991462c0d7 게임로그 아이템
게임로그 재화(아이템) 추가
2025-07-13 11:29:04 +09:00
bab594918e datepicker 옵션 변경 2025-07-07 14:26:11 +09:00
c4099c0cf0 메뉴 앤트디자인 메뉴로 교체
헤더 Breadcrumb 추가, profile 수정
2025-07-07 14:25:02 +09:00
0d8fb7b327 전투이벤트 진행시간 추가
진행시간 기준 종료시간 계산
2025-07-01 18:05:59 +09:00
d4db33bcf0 detailGrid 탭 추가 2025-07-01 14:41:07 +09:00
38dac99278 비즈니스로그 타입 예외처리 2025-07-01 14:40:41 +09:00
28094e1c48 배너 detailGrid 적용
배너 수정 및 삭제
2025-07-01 14:40:13 +09:00
77 changed files with 4634 additions and 2733 deletions

View File

@@ -3,6 +3,10 @@ server {
listen [::]:8080;
server_name localhost;
client_max_body_size 100M;
client_body_timeout 300s;
client_header_timeout 300s;
location / {
root /usr/share/nginx/admintool;
index index.html index.htm;
@@ -16,6 +20,11 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_request_buffering off;
}
error_page 500 502 503 504 /50x.html;

View File

@@ -3,6 +3,10 @@ server {
listen [::]:8080;
server_name localhost;
client_max_body_size 100M;
client_body_timeout 300s;
client_header_timeout 300s;
location / {
root /usr/share/nginx/admintool;
index index.html index.htm;
@@ -16,6 +20,11 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_request_buffering off;
}
error_page 500 502 503 504 /50x.html;

View File

@@ -3,6 +3,10 @@ server {
listen [::]:8080;
server_name localhost;
client_max_body_size 100M;
client_body_timeout 300s;
client_header_timeout 300s;
location / {
root /usr/share/nginx/admintool;
index index.html index.htm;
@@ -16,6 +20,11 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_request_buffering off;
}
error_page 500 502 503 504 /50x.html;

View File

@@ -3,6 +3,10 @@ server {
listen [::]:8080;
server_name localhost;
client_max_body_size 100M;
client_body_timeout 300s;
client_header_timeout 300s;
location / {
root /usr/share/nginx/admintool;
index index.html index.htm;
@@ -16,6 +20,11 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_request_buffering off;
}
error_page 500 502 503 504 /50x.html;

1744
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -62,10 +62,14 @@ export const userIndexExport = async (token, filename, sendDate, endDate) => {
};
// Retention
export const RetentionIndexView = async (token, start_dt, end_dt) => {
export const RetentionIndexView = async (token, startDate, endDate, order, size, currentPage) => {
try {
const res = await Axios.get(`/api/v1/indicators/retention/list?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
const res = await Axios.get(`/api/v1/indicators/retention/list?start_dt=${startDate}&end_dt=${endDate}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
});
return res.data.data;

View File

@@ -127,4 +127,155 @@ export const GameCurrencyDetailLogExport = async (token, params, fileName) => {
throw new Error('GameCurrencyDetailLogExport Error', e);
}
}
};
export const getItemDetailList = async (token, searchType, searchData, tranId, logAction, itemLargeType, itemSmallType, countDeltaType, startDate, endDate, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/log/item/detail/list?search_type=${searchType}&search_data=${searchData}&tran_id=${tranId}
&log_action=${logAction}&item_large_type=${itemLargeType}&item_small_type=${itemSmallType}&count_delta_type=${countDeltaType}&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('getItemDetailList API error:', error);
throw error;
}
};
export const GameItemDetailLogExport = async (token, params, fileName) => {
try {
console.log(params);
await Axios.post(`/api/v1/log/item/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('GameItemDetailLogExport Error', e);
}
}
};
export const getCurrencyItemList = async (token, searchType, searchData, tranId, logAction, currencyType, amountDeltaType, startDate, endDate, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/log/currency-item/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('getItemDetailList API error:', error);
throw error;
}
};
export const GameCurrencyItemLogExport = async (token, params, fileName) => {
try {
console.log(params);
await Axios.post(`/api/v1/log/currency-item/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('GameCurrencyItemLogExport Error', e);
}
}
};
export const getUserCreateList = async (token, searchType, searchData, startDate, endDate, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/log/user/create/list?search_type=${searchType}&search_data=${searchData}&start_dt=${startDate}&end_dt=${endDate}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('getUserCreateList API error:', error);
throw error;
}
};
export const getUserLoginDetailList = async (token, searchType, searchData, tranId, startDate, endDate, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/log/user/login/list?search_type=${searchType}&search_data=${searchData}&tran_id=${tranId}
&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('getUserLoginDetailList API error:', error);
throw error;
}
};
export const GameUserCreateLogExport = async (token, params, fileName) => {
try {
console.log(params);
await Axios.post(`/api/v1/log/user/create/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
timeout: 300000
}).then(response => {
responseFileDownload(response, {
defaultFileName: fileName
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('GameUserCreateLogExport Error', e);
}
}
};
export const GameUserLoginLogExport = async (token, params, fileName) => {
try {
console.log(params);
await Axios.post(`/api/v1/log/user/login/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
timeout: 300000
}).then(response => {
responseFileDownload(response, {
defaultFileName: fileName
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('GameUserLoginLogExport Error', e);
}
}
};

View File

@@ -67,11 +67,10 @@ export const MenuBannerModify = async (token, id, params) => {
};
// 삭제
export const MenuBannerDelete = async (token, params) => {
export const MenuBannerDelete = async (token, id) => {
try {
const res = await Axios.delete(`/api/v1/menu/banner/delete`, {
headers: { Authorization: `Bearer ${token}` },
data: { list: params },
const res = await Axios.delete(`/api/v1/menu/banner/delete?id=${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
return res.data;

View File

@@ -185,6 +185,21 @@ export const UserQuestView = async (token, guid) => {
}
};
//퀘스트 테스크 완료
export const UserQuestTaskComplete = async (token, params) => {
try {
const res = await Axios.post(`/api/v1/users/quest/task`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('UserQuestTaskComplete Error', e);
}
}
};
// 친구목록 조회
export const UserFriendListView = async (token, guid) => {
try {

View File

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

View File

@@ -11,6 +11,10 @@ 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 STORAGE_GAME_LOG_ITEM_SEARCH = 'gameLogItemSearchParam';
export const STORAGE_GAME_LOG_USER_CREATE_SEARCH = 'gameLogUserCreateSearchParam';
export const STORAGE_GAME_LOG_USER_LOGIN_SEARCH = 'gameLogUserLoginSearchParam';
export const LOG_ACTION_FAIL_CALIUM_ECHO = 'FailCaliumEchoSystem';
export const BATTLE_EVENT_OPERATION_TIME_WAIT_SECONDS = 300;
export { INITIAL_PAGE_SIZE, INITIAL_CURRENT_PAGE, INITIAL_PAGE_LIMIT };

View File

@@ -66,11 +66,11 @@ export const STATUS_STYLES = {
color: 'white'
},
WAIT: {
background: '#DEBB46',
background: '#FAAD14',
color: 'black'
},
FAIL: {
background: '#D33B27',
background: '#ff4d4f',
color: 'white'
},
FINISH: {
@@ -78,11 +78,11 @@ export const STATUS_STYLES = {
color: 'black'
},
REJECT: {
background: '#D33B27',
background: '#ff4d4f',
color: 'white'
},
CANCEL: {
background: '#D33B27',
background: '#ff4d4f',
color: 'white'
},
RESV_START: {
@@ -106,7 +106,7 @@ export const STATUS_STYLES = {
color: 'white'
},
REGISTER: {
background: '#DEBB46',
background: '#FAAD14',
color: 'black'
},
STOP: {
@@ -192,4 +192,5 @@ export const historyTables = {
notice: 'notice',
battleEvent: 'battle_event',
caliumRequest: 'calium_request',
menuBanner: 'menu_banner',
}

View File

@@ -103,14 +103,14 @@ export const menuConfig = {
view: false,
authLevel: adminAuthLevel.NONE
},
cryptview: {
title: '크립토 조회',
permissions: {
read: authType.cryptoRead
},
view: false,
authLevel: adminAuthLevel.NONE
},
// cryptview: {
// title: '크립토 조회',
// permissions: {
// read: authType.cryptoRead
// },
// view: false,
// authLevel: adminAuthLevel.NONE
// },
businesslogview: {
title: '비즈니스 로그 조회',
permissions: {

View File

@@ -6,8 +6,10 @@ export const languageType = [
export const TabGameLogList = [
{ value: 'CURRENCY', name: '재화 로그' },
// { value: 'ITEM', name: '아이템 로그' },
// { value: 'TRADE', name: '거래 로그' },
{ value: 'ITEM', name: '아이템 로그' },
{ value: 'CURRENCYITEM', name: '재화(아이템) 로그' },
{ value: 'USERCREATE', name: '유저생성 로그' },
{ value: 'USERLOGIN', name: '유저로그인 로그' },
];
export const TabEconomicIndexList = [
@@ -18,6 +20,13 @@ export const TabEconomicIndexList = [
// { value: 'instance', name: '인스턴스' },
];
export const TabUserIndexList = [
{ value: 'USER', name: '이용자 지표' },
{ value: 'RETENTION', name: '잔존율' },
// { value: 'SEGMENT', name: 'Segment' },
// { value: 'PLAYTIME', name: '플레이타임' },
];
export const mailSendType = [
{ value: 'ALL', name: '전체' },
{ value: 'RESERVE_SEND', name: '예약 발송' },
@@ -95,6 +104,17 @@ export const landAuctionStatus = [
{ value: 'FAIL', name: '실패' },
];
export const questStatus = [
{ value: 'WAIT', name: '미완료' },
{ value: 'COMPLETE', name: '완료' },
{ value: 'RUNNING', name: '진행중' },
];
export const questCompleteStatusType = [
{ value: 0, name: '미완료' },
{ value: 1, name: '완료' }
]
export const currencyItemCode = [
{ value: '19010001', name: '골드' },
{ value: '19010002', name: '사파이어' },
@@ -225,6 +245,24 @@ export const amountDeltaType = [
{value: 'None', name: '' },
]
export const countDeltaType = [
{value: 'Acquire', name: '획득' },
{value: 'Consume', name: '소모' }
]
export const itemTypeLarge = [
{value: 'TOOL', name: '도구' },
{value: 'EXPENDABLE', name: '소모품' },
{value: 'TICKET', name: '티켓' },
{value: 'RAND_BOX', name: '랜덤 박스' },
{value: 'CLOTH', name: '의상' },
{value: 'AVATAR', name: '아바타' },
{value: 'PROP', name: '프랍(오브젝트)' },
{value: 'TATTOO', name: '타투' },
{value: 'CURRENCY', name: '재화' },
{value: 'SET_BOX', name: '세트 박스' }
]
export const battleEventStatus = [
{ value: 'ALL', name: '전체' },
{ value: 'WAIT', name: '대기' },

View File

@@ -14,14 +14,14 @@
"text": "선택 삭제",
"theme": "line",
"disableWhen": "noSelection",
"requiredAuth": "battleEventDelete",
"requiredAuth": "menuBannerDelete",
"action": "delete"
},
{
"id": "register",
"text": "이미지 등록",
"theme": "primary",
"requiredAuth": "battleEventUpdate",
"requiredAuth": "menuBannerUpdate",
"action": "navigate",
"navigateTo": "/servicemanage/menubanner/menubannerregist"
}
@@ -40,6 +40,12 @@
"width": "70px",
"title": "번호"
},
{
"id": "order_id",
"type": "text",
"width": "70px",
"title": "순서"
},
{
"id": "status",
"type": "status",
@@ -94,10 +100,11 @@
}
},
{
"id": "update_by",
"type": "text",
"width": "150px",
"title": "히스토리"
"id": "history",
"type": "button",
"width": "120px",
"title": "히스토리",
"text": "히스토리"
}
],
"sort": {

View File

@@ -161,7 +161,7 @@ export const currencyCodeTypes = {
}
export const languageNames = {
'KO': '한국어',
'EN': '영어',
'JA': '일본어',
'Ko': '한국어',
'En': '영어',
'Ja': '일본어',
};

View File

@@ -0,0 +1,168 @@
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 { CurrencyItemLogSearchBar, useCurrencyItemLogSearch } 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';
import { GameCurrencyItemLogExport } from '../../apis';
import { AnimatedPageWrapper } from '../common/Layout';
const CurrencyItemLogContent = ({ 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
} = useCurrencyItemLogSearch(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: '150px' },
{ 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: '80px' },
// { id: 'deltaAmount', label: '수량원본', width: '120px' },
{ id: 'currencyAmount', label: '잔량', width: '80px' },
{ id: 'itemId', label: '아이템ID', width: '150px' },
];
}, []);
if(!active) return null;
return (
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyItemLogSearchBar
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="GameCurrencyItemLogExport"
params={searchParams}
fileName={t('FILE_GAME_LOG_CURRENCY_ITEM')}
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_item_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>{numberFormatter.formatCurrency(item.currencyAmount)}</td>
<td>{item.itemIDs}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{dataList?.currency_item_list &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CurrencyItemLogContent;

View File

@@ -21,6 +21,7 @@ import {
} from '../../assets/data/adminConstants';
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
import { AnimatedPageWrapper } from '../common/Layout';
const CurrencyLogContent = ({ active }) => {
const { t } = useTranslation();
@@ -83,7 +84,7 @@ const CurrencyLogContent = ({ active }) => {
if(!active) return null;
return (
<>
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyLogSearchBar
searchParams={searchParams}
@@ -159,7 +160,7 @@ const CurrencyLogContent = ({ active }) => {
<TopButton />
</>
}
</>
</AnimatedPageWrapper>
);
};
export default CurrencyLogContent;

View File

@@ -0,0 +1,171 @@
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 { ItemLogSearchBar, useItemLogSearch } 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, STORAGE_GAME_LOG_ITEM_SEARCH,
} from '../../assets/data/adminConstants';
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
import { countDeltaType, itemTypeLarge } from '../../assets/data/options';
import { AnimatedPageWrapper } from '../common/Layout';
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
} = useItemLogSearch(token, 500);
useEffect(() => {
if(active) {
// 세션 스토리지에서 복사된 메일 데이터 가져오기
const paramsData = sessionStorage.getItem(STORAGE_GAME_LOG_ITEM_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_ITEM_SEARCH);
}
}
}, [active]);
const tableHeaders = useMemo(() => {
return [
// { id: 'logDay', label: '일자', width: '120px' },
{ id: 'logTime', label: '일시', width: '150px' },
{ 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: '120px' },
{ id: 'itemId', label: '아이템ID', width: '80px' },
{ id: 'itemName', label: '아이템명', width: '150px' },
{ id: 'itemTypeLarge', label: 'LargeType', width: '100px' },
{ id: 'itemTypeSmall', label: 'SmallType', width: '100px' },
{ id: 'countDeltaType', label: '증감유형', width: '80px' },
{ id: 'deltaCount', label: '수량', width: '80px' },
{ id: 'stackCount', label: '총량', width: '80px' },
];
}, []);
if(!active) return null;
return (
<AnimatedPageWrapper>
<FormWrapper>
<ItemLogSearchBar
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="GameItemDetailLogExport"
params={searchParams}
fileName={t('FILE_GAME_LOG_ITEM')}
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?.item_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>{item.itemId}</td>
<td>{item.itemName}</td>
<td>{itemTypeLarge.find(data => data.value === item.itemTypeLarge)?.name || item.itemTypeLarge}</td>
<td>{item.itemTypeSmall}</td>
<td>{countDeltaType.find(data => data.value === item.countDeltaType)?.name || item.countDeltaType}</td>
<td>{numberFormatter.formatCurrency(item.deltaCount)}</td>
<td>{numberFormatter.formatCurrency(item.stackCount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{dataList?.item_detail_list &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CurrencyLogContent;

View File

@@ -5,15 +5,31 @@ import { BtnWrapper, TableStyle } from '../../styles/Components';
import Button from '../../components/common/button/Button';
import Modal from '../../components/common/modal/Modal';
import { useEffect, useState, Fragment } from 'react';
import { questStatus } from '../../assets/data/options';
import { commonStatus } from '../../assets/data';
import { useModal } from '../../hooks/hook';
import { alertTypes } from '../../assets/data/types';
import { useAlert } from '../../context/AlertProvider';
const QuestDetailModal = ({ detailPop, handleClick, detailQuest, handleQuestComplete }) => {
const { showModal } = useAlert();
const QuestDetailModal = ({ detailPop, handleClick, detailQuest }) => {
const [detailList, setDetailList] = useState([])
useEffect(() => {
Array.isArray(detailQuest) && setDetailList(detailQuest)
Array.isArray(detailQuest.detailQuest) && setDetailList(detailQuest.detailQuest)
}, [detailQuest])
// const questlist = [{ taskNo: detailQuest.task_no, taskName: detailQuest.quest_name, counter: detailQuest.counter, state: detailQuest.status }];
const handleQuestCompleteConfirm = (data) => {
const params = {...data, quest_key: detailQuest.quest_key}
showModal('QUEST_TASK_COMPLETE_CONFIRM',{
type: alertTypes.confirm,
onConfirm: () => {
handleQuestComplete(params);
}
});
}
return (
<>
<Modal $view={detailPop} min="480px">
@@ -25,7 +41,8 @@ const QuestDetailModal = ({ detailPop, handleClick, detailQuest }) => {
<th width="80">Task No</th>
<th>Task Name</th>
<th width="120">Counter</th>
<th width="120">State</th>
<th width="120">상태</th>
<th width="120">완료처리</th>
</tr>
</thead>
<tbody>
@@ -36,7 +53,10 @@ const QuestDetailModal = ({ detailPop, handleClick, detailQuest }) => {
<td>{el.task_no}</td>
<td>{el.quest_name}</td>
<td>{el.counter}</td>
<td>{el.status}</td>
<td>{questStatus.find(data => data.value === el.status)?.name}</td>
<td>
{ el.status === commonStatus.running && <Button text="완료" theme="line" handleClick={() => handleQuestCompleteConfirm(el)} />}
</td>
</tr>
</Fragment>
);

View File

@@ -0,0 +1,146 @@
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import {
CircularProgressWrapper,
FormWrapper,
TableStyle,
TableWrapper,
} from '../../styles/Components';
import { useTranslation } from 'react-i18next';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { useUserCreateLogSearch, UserCreateLogSearchBar } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import Pagination from '../common/Pagination/Pagination';
import {
INITIAL_PAGE_LIMIT,STORAGE_GAME_LOG_USER_CREATE_SEARCH,
} from '../../assets/data/adminConstants';
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
import { AnimatedPageWrapper } from '../common/Layout';
const UserCreateLogContent = ({ active }) => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const [downloadState, setDownloadState] = useState({
loading: false,
progress: 0
});
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
handlePageChange,
updateSearchParams
} = useUserCreateLogSearch(token, 500);
useEffect(() => {
if(active) {
// 세션 스토리지에서 복사된 메일 데이터 가져오기
const paramsData = sessionStorage.getItem(STORAGE_GAME_LOG_USER_CREATE_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_USER_CREATE_SEARCH);
}
}
}, [active]);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '120px' },
{ id: 'accountId', label: 'account ID', width: '80px' },
{ id: 'userGuid', label: 'GUID', width: '200px' },
{ id: 'userNickname', label: '아바타명', width: '150px' },
{ id: 'createTime', label: '생성일시', width: '200px' },
];
}, []);
if(!active) return null;
return (
<AnimatedPageWrapper>
<FormWrapper>
<UserCreateLogSearchBar
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="GameUserCreateLogExport"
params={searchParams}
fileName={t('FILE_GAME_LOG_USER_CREATE')}
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?.user_create_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>{item.createdTime}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{dataList?.user_create_list &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default UserCreateLogContent;

View File

@@ -0,0 +1,159 @@
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import {
CircularProgressWrapper,
FormWrapper,
TableStyle,
TableWrapper,
} from '../../styles/Components';
import { useTranslation } from 'react-i18next';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { useUserLoginLogSearch, UserLoginLogSearchBar } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import Pagination from '../common/Pagination/Pagination';
import {
INITIAL_PAGE_LIMIT, STORAGE_GAME_LOG_USER_LOGIN_SEARCH,
} from '../../assets/data/adminConstants';
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
import { AnimatedPageWrapper } from '../common/Layout';
import { numberFormatter } from '../../utils';
const UserLoginLogContent = ({ 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
} = useUserLoginLogSearch(token, 500);
useEffect(() => {
if(active) {
// 세션 스토리지에서 복사된 메일 데이터 가져오기
const paramsData = sessionStorage.getItem(STORAGE_GAME_LOG_USER_LOGIN_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_USER_LOGIN_SEARCH);
}
}
}, [active]);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'accountId', label: 'account ID', width: '80px' },
{ id: 'userGuid', label: 'GUID', width: '180px' },
{ id: 'userNickname', label: '아바타명', width: '150px' },
{ id: 'tranId', label: '트랜잭션 ID', width: '200px' },
{ id: 'loginTime', label: '로그인시간', width: '150px' },
{ id: 'logoutTime', label: '로그아웃시간', width: '120px' },
{ id: 'serverType', label: '서버종류', width: '80px' },
{ id: 'languageType', label: '언어', width: '80px' },
{ id: 'playtime', label: '플레이시간(분)', width: '80px' },
];
}, []);
if(!active) return null;
return (
<AnimatedPageWrapper>
<FormWrapper>
<UserLoginLogSearchBar
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="GameUserLoginLogExport"
params={searchParams}
fileName={t('FILE_GAME_LOG_USER_LOGIN')}
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?.user_login_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>{item.tranId}</td>
<td>{item.loginTime}</td>
<td>{item.logoutTime}</td>
<td>{item.serverType}</td>
<td>{item.languageType}</td>
<td>{numberFormatter.formatSecondToMinuts(item.playtime)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{dataList?.user_login_list &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default UserLoginLogContent;

View File

@@ -1,18 +1,16 @@
import styled from 'styled-components';
import Button from '../common/button/Button';
import { InfoSubTitle, UserDefaultTable, UserInfoTable, UserTableWrapper } from '../../styles/ModuleComponents';
import { useTranslation } from 'react-i18next';
import { useRecoilValue } from 'recoil';
import { authList } from '../../store/authList';
import { useEffect, useState } from 'react';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { UserInventoryView, UserMyhomeView } from '../../apis';
import { UserMyhomeView } from '../../apis';
import { SelectInput } from '../../styles/Components';
const UserMyHomeInfo = ({ userInfo }) => {
const { t } = useTranslation();
const [dataList, setDataList] = useState();
const [loading, setLoading] = useState(true);
const [selectedHome, setSelectedHome] = useState('');
useEffect(() => {
if(userInfo && Object.keys(userInfo).length > 0) {
@@ -23,11 +21,18 @@ const UserMyHomeInfo = ({ userInfo }) => {
const fetchData = async () => {
const token = sessionStorage.getItem('token');
await UserMyhomeView(token, userInfo.guid).then(data => {
setDataList(data.myhome_info);
setDataList(data);
if (data.myhome_info && data.myhome_info.length > 0) {
setSelectedHome(data.myhome_info[0].myhome_guid);
}
setLoading(false);
});
};
const handleHomeChange = (e) => {
setSelectedHome(e.target.value);
};
return (
loading ? <TableSkeleton count={15}/> :
dataList &&
@@ -36,7 +41,13 @@ const UserMyHomeInfo = ({ userInfo }) => {
<tbody>
<tr>
<th>마이 홈명</th>
<td>{dataList.myhome_name}</td>
<SelectInput onChange={handleHomeChange} value={selectedHome}>
{dataList.myhome_info && dataList.myhome_info.map((data, index) => (
<option key={index} value={data.myhome_guid}>
{data.myhome_name}
</option>
))}
</SelectInput>
</tr>
</tbody>
</UserInfoTable>
@@ -51,15 +62,19 @@ const UserMyHomeInfo = ({ userInfo }) => {
</tr>
</thead>
<tbody>
{dataList.prop_list && dataList.prop_list.map((el, idx) => {
return (
<tr key={idx}>
<td>{idx + 1}</td>
<td>{el.item_id}</td>
<td>{el.item_name}</td>
</tr>
);
})}
{dataList.myhome_info.find(home => home.myhome_guid === selectedHome)?.prop_list?.map((el, idx) => (
<tr key={idx}>
<td>{idx + 1}</td>
<td>{el.item_id}</td>
<td>{el.item_name}</td>
</tr>
))}
{(!dataList.myhome_info.find(home => home.myhome_guid === selectedHome)?.prop_list ||
dataList.myhome_info.find(home => home.myhome_guid === selectedHome)?.prop_list.length === 0) && (
<tr>
<td colSpan="3" style={{textAlign: 'center'}}>{t('TABLE_DATA_NOT_FOUND')}</td>
</tr>
)}
</tbody>
</UserDefaultTable>
</UserTableWrapper>

View File

@@ -3,15 +3,22 @@ import { useState, useEffect, Fragment } from 'react';
import styled from 'styled-components';
import Button from '../../components/common/button/Button';
import QuestDetailModal from '../../components/DataManage/QuestDetailModal';
import { UserQuestView } from '../../apis/Users';
import { UserQuestTaskComplete, UserQuestView } from '../../apis/Users';
import { convertKTC } from '../../utils';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { useAlert } from '../../context/AlertProvider';
import { CaliumCharge } from '../../apis';
import { alertTypes } from '../../assets/data/types';
import { useLoading } from '../../context/LoadingProvider';
import { questCompleteStatusType } from '../../assets/data/options';
const UserQuestInfo = ({ userInfo }) => {
const [detailPop, setDetailPop] = useState('hidden');
const [dataList, setDataList] = useState({});
const [detailQuest, setDetailQuest] = useState({});
const [loading, setLoading] = useState(true);
const { showModal, showToast } = useAlert();
const { withLoading } = useLoading();
useEffect(() => {
if(userInfo && Object.keys(userInfo).length > 0) {
@@ -30,10 +37,30 @@ const UserQuestInfo = ({ userInfo }) => {
const handleClick = data => {
if (detailPop === 'hidden') {
setDetailPop('view');
setDetailQuest(data.detailQuest);
setDetailQuest(data);
} else setDetailPop('hidden');
};
const handleQuestComplete = async data => {
const token = sessionStorage.getItem('token');
await withLoading(async () => {
const params = {...data, guid: userInfo.guid};
return await UserQuestTaskComplete(token, params);
}).then(data => {
if (data.result === "SUCCESS") {
showToast('QUEST_TASK_COMPLETE', { type: alertTypes.success });
} else {
showToast(data.data.message, { type: alertTypes.error });
}
}).catch(error => {
showToast('API_FAIL', { type: alertTypes.error });
}).finally(() => {
handleClick();
fetchData();
});
};
return (
loading ? <TableSkeleton /> :
<>
@@ -59,7 +86,7 @@ const UserQuestInfo = ({ userInfo }) => {
<td>{el.quest_id}</td>
<td>{el.quest_name}</td>
<td>{el.quest_type}</td>
<td>{el.status}</td>
<td>{questCompleteStatusType.find(data => el.status === data.value).name || el.status}</td>
<td>{convertKTC(el.quest_assign_time, false)}</td>
<td>{convertKTC(el.quest_complete_time, false)}</td>
<td>
@@ -72,7 +99,7 @@ const UserQuestInfo = ({ userInfo }) => {
</tbody>
</QuestTable>
</UserTableWrapper>
<QuestDetailModal detailPop={detailPop} handleClick={handleClick} detailQuest={detailQuest} />
<QuestDetailModal detailPop={detailPop} handleClick={handleClick} detailQuest={detailQuest} handleQuestComplete={handleQuestComplete} />
</>
);
};

View File

@@ -16,6 +16,7 @@ import CircularProgress from '../common/CircularProgress';
import { useTranslation } from 'react-i18next';
import CurrencyIndexSearchBar from '../searchBar/CurrencyIndexSearchBar';
import { useNavigate } from 'react-router-dom';
import { AnimatedPageWrapper } from '../common/Layout';
const CreditContent = () => {
const { t } = useTranslation();
@@ -38,30 +39,47 @@ const CreditContent = () => {
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' },
// 기본 컬럼 (rowSpan=2)
{ id: 'logDay', label: '일자', width: '100px', rowSpan: 2 },
{ id: 'accountId', label: 'account ID', width: '80px', rowSpan: 2 },
{ id: 'userGuid', label: 'GUID', width: '200px', rowSpan: 2 },
{ id: 'userNickname', label: '아바타명', width: '150px', rowSpan: 2 },
// 획득량 그룹 헤더 (첫 번째 행에만 표시)
{ id: 'acquired', label: '획득', width: '400px', colSpan: 5, groupHeader: true },
// 획득량 컬럼 (두 번째 행에만 표시)
{ id: 'sapphireAcquired', label: '사파이어', width: '80px', groupRow: true },
{ id: 'goldAcquired', label: '골드', width: '80px', groupRow: true },
{ id: 'caliumAcquired', label: '칼리움', width: '80px', groupRow: true },
{ id: 'beamAcquired', label: 'BEAM', width: '80px', groupRow: true },
{ id: 'rubyAcquired', label: '루비', width: '80px', groupRow: true },
// 소모량 그룹 헤더 (첫 번째 행에만 표시)
{ id: 'consumed', label: '소모', width: '400px', colSpan: 5, groupHeader: true },
// 소모량 컬럼 (두 번째 행에만 표시)
{ id: 'sapphireConsumed', label: '사파이어', width: '80px', groupRow: true },
{ id: 'goldConsumed', label: '골드', width: '80px', groupRow: true },
{ id: 'caliumConsumed', label: '칼리움', width: '80px', groupRow: true },
{ id: 'beamConsumed', label: 'BEAM', width: '80px', groupRow: true },
{ id: 'rubyConsumed', label: '루비', width: '80px', groupRow: true },
// 계 컬럼 (rowSpan=2)
{ id: 'sapphireNet', label: '사파이어 계', width: '80px', rowSpan: 2 },
{ id: 'goldNet', label: '골드 계', width: '80px', rowSpan: 2 },
{ id: 'caliumNet', label: '칼리움 계', width: '80px', rowSpan: 2 },
{ id: 'beamNet', label: 'BEAM 계', width: '80px', rowSpan: 2 },
{ id: 'rubyNet', label: '루비 계', width: '80px', rowSpan: 2 },
// 기타 컬럼 (rowSpan=2)
{ id: 'totalCurrencies', label: '활동 수', width: '80px', rowSpan: 2 },
{ id: 'detail', label: '상세', width: '100px', rowSpan: 2 }
];
}, []);
const totals = useMemo(() => {
if (!dataList?.currency_list?.length) return null;
@@ -112,7 +130,7 @@ const CreditContent = () => {
}
return (
<>
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyIndexSearchBar
searchParams={searchParams}
@@ -150,30 +168,62 @@ const CreditContent = () => {
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => (
<th key={header.id} width={header.width}>{header.label}</th>
))}
{/* 첫 번째 행 - 기본 컬럼 + 그룹 헤더 + rowSpan=2 컬럼 */}
{tableHeaders.map(header => {
if (header.groupRow) return null; // 두 번째 행의 컬럼은 첫 번째 행에서 건너뜀
return (
<th
key={header.id}
width={header.width}
rowSpan={header.rowSpan}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
<tr>
{/* 두 번째 행 - 그룹 내 하위 컬럼만 */}
{tableHeaders.map(header => {
if (!header.groupRow) return null; // 첫 번째 행이나 rowSpan=2 컬럼은 두 번째 행에서 건너뜀
return (
<th key={header.id} width={header.width}>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{totals && (
<TotalRow>
<td colSpan="4">합계</td>
{/* 획득 그룹 합계 */}
<td>{numberFormatter.formatCurrency(totals.sapphireAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.sapphireConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.goldAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.goldConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.caliumAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.caliumConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.beamAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.beamConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.rubyAcquired)}</td>
{/* 소모 그룹 합계 */}
<td>{numberFormatter.formatCurrency(totals.sapphireConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.goldConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.caliumConsumed)}</td>
<td>{numberFormatter.formatCurrency(totals.beamConsumed)}</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>
@@ -181,25 +231,33 @@ const CreditContent = () => {
{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.sapphireConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.goldConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.caliumConsumed)}</td>
<td>{numberFormatter.formatCurrency(item.beamConsumed)}</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="상세보기"
@@ -214,7 +272,7 @@ const CreditContent = () => {
<TopButton />
</>
}
</>
</AnimatedPageWrapper>
);
};

View File

@@ -1,108 +1,76 @@
import { Fragment, useEffect, useState } from 'react';
import Button from '../../components/common/button/Button';
import React, { Fragment, useRef } from 'react';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { RetentionSearchBar } from '../../components/IndexManage/index';
import { RetentionIndexExport, RetentionIndexView } from '../../apis';
import { AnimatedPageWrapper } from '../common/Layout';
import { useRetentionSearch, RetentionSearchBar } from '../searchBar';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { ExcelDownButton } from '../common';
import { useTranslation } from 'react-i18next';
const RetentionContent = () => {
const { t } = useTranslation();
const tableRef = useRef(null);
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(new Date(d.setDate(d.getDate() - 1)).setHours(24, 0, 0, 0));
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [retentionData, setRetention] = useState(1);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useRetentionSearch(token);
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
const [excelBtn, setExcelBtn] = useState(true); //true 시 비활성화
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
// Retention 지표 데이터
const fetchData = async (startDate, endDate) => {
const startDateToLocal =
startDate.getFullYear() +
'-' +
(startDate.getMonth() + 1 < 9 ? '0' + (startDate.getMonth() + 1) : startDate.getMonth() + 1) +
'-' +
(startDate.getDate() < 9 ? '0' + startDate.getDate() : startDate.getDate());
const endDateToLocal =
endDate.getFullYear() +
'-' +
(endDate.getMonth() + 1 < 9 ? '0' + (endDate.getMonth() + 1) : endDate.getMonth() + 1) +
'-' +
(endDate.getDate() < 9 ? '0' + endDate.getDate() : endDate.getDate());
setDataList(await RetentionIndexView(token, startDateToLocal, endDateToLocal));
console.log(dataList);
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = (send_dt, end_dt) => {
fetchData(send_dt, end_dt);
setRetention(resultData.retention);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_Retention_Index.xlsx';
if(!excelBtn){
RetentionIndexExport(token, fileName, sendDate, finishDate);
}
};
return (
<>
<RetentionSearchBar setResultData={setResultData} resultData={resultData}
handleSearch={handleSearch} fetchData={fetchData} setRetention={setRetention} setExcelBtn={setExcelBtn} />
<AnimatedPageWrapper>
<RetentionSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
<TableInfo>
<ListOption>
<Button
theme={excelBtn === true ? "disable" : "line"}
text="엑셀 다운로드"
disabled={handleXlsxExport}
handleClick={handleXlsxExport} />
<ExcelDownButton tableRef={tableRef} fileName={t('FILE_INDEX_USER_RETENTION')} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead>
<tr>
{/* <th width="100">국가</th> */}
<th width="150">일자</th>
<th className="cell-nru">NRU</th>
{[...Array(Number(retentionData))].map((value, index) => {
return <th key={index}>{`D+${index + 1}`}</th>;
})}
</tr>
</thead>
<tbody>
{dataList.retention &&
dataList.retention.map(data => (
<tr className="cell-nru-th" key={data.date}>
<td>{data.date}</td>
{data['d-day'].map((day, index) => (
<td key={index}>{day.dif}</td>
))}
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<IndexTableWrap>
<TableStyle ref={tableRef}>
<caption></caption>
<thead>
<tr>
<th>일자</th>
<th>NRU</th>
<th>D+1</th>
<th>D+7</th>
<th>D+30</th>
</tr>
</thead>
<tbody>
{dataList?.map((data, index) => (
<Fragment key={index}>
<tr>
<td>{data.logDay}</td>
<td>{data.totalCreated}</td>
<td>{numberFormatter.formatPercent(data.d1_rate)}</td>
<td>{numberFormatter.formatPercent(data.d7_rate)}</td>
<td>{numberFormatter.formatPercent(data.d30_rate)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
}
</AnimatedPageWrapper>
);
};

View File

@@ -10,6 +10,7 @@ import Loading from '../common/Loading';
import { ExcelDownButton } from '../common';
import { useTranslation } from 'react-i18next';
import { formatStringDate } from '../../utils';
import { AnimatedPageWrapper } from '../common/Layout';
const UserContent = () => {
const token = sessionStorage.getItem('token');
@@ -24,20 +25,6 @@ const UserContent = () => {
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
// const [sendDate, setSendDate] = useState(START_DATE);
// const [finishDate, setFinishDate] = useState(END_DATE);
const headers = [
{key: 'date', label: '일자'},
{key: 'nru', label: 'NRU'},
{key: 'ugqCreate', label: '일자'},
{key: 'dglc', label: '일자'},
{key: 'dau', label: '일자'},
{key: 'mcu', label: '일자'},
{key: 'date', label: '일자'},
{key: 'date', label: '일자'},
]
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
@@ -54,8 +41,6 @@ const UserContent = () => {
setLoading(false);
});
// setSendDate(startDateToLocal);
// setFinishDate(endDateToLocal);
};
// 검색 함수
@@ -63,14 +48,8 @@ const UserContent = () => {
fetchData(send_dt, end_dt);
};
// 엑셀 다운로드
// const handleXlsxExport = () => {
// const fileName = 'Caliverse_User_Index.xlsx';
// userIndexExport(token, fileName, sendDate, finishDate);
// };
return (
<>
<AnimatedPageWrapper>
<DailyDashBoard />
<UserIndexSearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
@@ -125,8 +104,7 @@ const UserContent = () => {
</tbody>
</TableStyle>
</IndexTableWrap>
{loading && <Loading/>}
</>
</AnimatedPageWrapper>
);
};

View File

@@ -5,6 +5,7 @@ import { MenuImageDelete, MenuImageUpload } from '../../apis';
import { IMAGE_MAX_SIZE } from '../../assets/data/adminConstants';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { ImagePreview } from '../../styles/Components';
const ImageUploadBtn = ({ disabled,
onImageUpload,
@@ -220,14 +221,6 @@ const PreviewContainer = styled.div`
overflow: hidden;
`;
const ImagePreview = styled.img`
width: 100%;
height: 180px;
object-fit: contain;
border-radius: 4px 4px 0 0;
background-color: #f6f6f6;
`;
const PreviewInfo = styled.div`
display: flex;
justify-content: space-between;

View File

@@ -1,526 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { getOptionsArray } from '../../../utils';
import {
SelectInput,
SearchBarAlert
} from '../../../styles/Components';
import {
FormInput, FormInputSuffix, FormInputSuffixWrapper,
FormLabel,
FormRowGroup,
FormStatusBar,
FormStatusLabel,
FormStatusWarning,
} from '../../../styles/ModuleComponents';
import { CheckBox, SingleDatePicker, SingleTimePicker } from '../../common';
import Button from '../../common/button/Button';
import styled from 'styled-components';
import ImageUploadBtn from '../../ServiceManage/ImageUploadBtn';
const CaliForm = ({
config, // 폼 설정 JSON
mode, // 'create', 'update', 'view' 중 하나
initialData, // 초기 데이터
externalData, // 외부 데이터(옵션 등)
onSubmit, // 제출 핸들러
onCancel, // 취소 핸들러
className, // 추가 CSS 클래스
onFieldValidation, // 필드 유효성 검사 콜백
formRef // 폼 ref
}) => {
const { t } = useTranslation();
const [formData, setFormData] = useState({ ...(config?.initData || {}), ...(initialData || {}) });
const [errors, setErrors] = useState({});
const [isFormValid, setIsFormValid] = useState(false);
// 필드 변경 핸들러
const handleFieldChange = (fieldId, value) => {
setFormData(prev => ({
...prev,
[fieldId]: value
}));
};
// 날짜 변경 핸들러
const handleDateChange = (fieldId, date) => {
if (!date) return;
setFormData(prev => ({
...prev,
[fieldId]: date
}));
};
// 시간 변경 핸들러
const handleTimeChange = (fieldId, time) => {
if (!time) return;
const newDateTime = formData[fieldId] ? new Date(formData[fieldId]) : new Date();
newDateTime.setHours(time.getHours(), time.getMinutes(), 0, 0);
setFormData(prev => ({
...prev,
[fieldId]: newDateTime
}));
};
// 폼 유효성 검사
useEffect(() => {
const validateForm = () => {
const newErrors = {};
let isValid = true;
if (!config) return false;
// 필수 필드 검사
const requiredFields = config.fields
.filter(f =>
f.visibleOn.includes(mode) &&
f.validations?.includes("required")
)
.map(f => f.id);
requiredFields.forEach(fieldId => {
if (!formData[fieldId] && formData[fieldId] !== 0) {
newErrors[fieldId] = t('REQUIRED_FIELD');
isValid = false;
}
});
// 조건부 유효성 검사
if (config.validations && config.validations[mode]) {
for (const validation of config.validations[mode]) {
const conditionResult = evaluateCondition(validation.condition, {
...formData,
current_time: new Date().getTime()
});
if (conditionResult) {
// 전체 폼 검증 오류
newErrors._form = t(validation.message);
isValid = false;
}
}
}
setErrors(newErrors);
setIsFormValid(isValid);
if (onFieldValidation) {
onFieldValidation(isValid, newErrors);
}
return isValid;
};
validateForm();
}, [config, formData, mode, t, onFieldValidation]);
// 간단한 조건식 평가 함수
const evaluateCondition = (conditionStr, context) => {
try {
const fn = new Function(...Object.keys(context), `return ${conditionStr}`);
return fn(...Object.values(context));
} catch (e) {
console.error('Error evaluating condition:', e);
return false;
}
};
// 필드 렌더링
const renderField = (field) => {
const isEditable = field.editableOn.includes(mode);
const value = formData[field.id] !== undefined ? formData[field.id] : '';
const hasError = errors[field.id];
switch (field.type) {
case 'text':
return (
<div className="form-field">
<FormInput
type="text"
value={value}
onChange={e => handleFieldChange(field.id, e.target.value)}
disabled={!isEditable}
width={field.width}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'number':
return (
<div className="form-field">
<FormInput
type="number"
value={value}
onChange={e => handleFieldChange(field.id, Number(e.target.value))}
disabled={!isEditable}
width={field.width}
min={field.min}
max={field.max}
step={field.step || 1}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'select':
let options = [];
if (field.optionsKey) {
// 옵션 설정에서 가져오기
options = getOptionsArray(field.optionsKey);
} else if (field.dataSource && externalData) {
// 외부 데이터 소스 사용
const dataSource = externalData[field.dataSource] || [];
options = dataSource.map(item => ({
value: item[field.valueField],
label: field.displayFormat
? field.displayFormat.replace('{value}', item[field.valueField])
.replace('{display}', item[field.displayField])
: `${item[field.displayField]}(${item[field.valueField]})`
}));
} else if (field.options) {
options = field.options;
}
return (
<div className="form-field">
<SelectInput
value={value}
onChange={e => handleFieldChange(field.id, e.target.value)}
disabled={!isEditable}
width={field.width}
className={hasError ? 'error' : ''}
>
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.label}
</option>
))}
</SelectInput>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'datePicker':
return (
<div className="form-field">
<SingleDatePicker
label={field.label}
disabled={!isEditable}
dateLabel={field.dateLabel}
onDateChange={date => handleDateChange(field.id, date)}
selectedDate={value}
minDate={field.minDate}
maxDate={field.maxDate}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'timePicker':
return (
<div className="form-field">
<SingleTimePicker
label={field.label}
disabled={!isEditable}
selectedTime={value}
onTimeChange={time => handleTimeChange(field.id, time)}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'status':
let statusText = "";
if (field.optionsKey && formData[field.statusField]) {
const statusOptions = getOptionsArray(field.optionsKey);
const statusItem = statusOptions.find(item => item.value === formData[field.statusField]);
statusText = statusItem ? statusItem.name : "등록";
}
return (
<div className="form-field">
<FormStatusBar>
<FormStatusLabel>
{field.label}: {statusText}
</FormStatusLabel>
{mode === 'update' && field.warningMessage && (
<FormStatusWarning>
{t(field.warningMessage)}
</FormStatusWarning>
)}
</FormStatusBar>
</div>
);
case 'dateTimeRange':
return (
<div className="form-field">
<div className="date-time-range">
<SingleDatePicker
label={field.startDateLabel}
disabled={!isEditable}
dateLabel={field.startDateLabel}
onDateChange={date => handleDateChange(field.startDateField, date)}
selectedDate={formData[field.startDateField]}
/>
<SingleTimePicker
disabled={!isEditable}
selectedTime={formData[field.startDateField]}
onTimeChange={time => handleTimeChange(field.startDateField, time)}
/>
<SingleDatePicker
label={field.endDateLabel}
disabled={!isEditable}
dateLabel={field.endDateLabel}
onDateChange={date => handleDateChange(field.endDateField, date)}
selectedDate={formData[field.endDateField]}
/>
<SingleTimePicker
disabled={!isEditable}
selectedTime={formData[field.endDateField]}
onTimeChange={time => handleTimeChange(field.endDateField, time)}
/>
</div>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'imageUpload':
const imageLanguage = field.language;
const imageList = formData.image_list || [];
const imageData = imageList.find(img => img.language === imageLanguage) || { content: '' };
return (
<div className="form-field">
<LanguageWrapper>
<LanguageLabel>{imageLanguage}</LanguageLabel>
<ImageUploadBtn
onImageUpload={(file, fileName) => {
const updatedImageList = [...imageList];
const index = updatedImageList.findIndex(img => img.language === imageLanguage);
if (index !== -1) {
updatedImageList[index] = {
...updatedImageList[index],
content: fileName
};
} else {
updatedImageList.push({
language: imageLanguage,
content: fileName
});
}
handleFieldChange('image_list', updatedImageList);
}}
onFileDelete={() => {
const updatedImageList = [...imageList];
const index = updatedImageList.findIndex(img => img.language === imageLanguage);
if (index !== -1) {
updatedImageList[index] = {
...updatedImageList[index],
content: ''
};
handleFieldChange('image_list', updatedImageList);
}
}}
fileName={imageData.content}
disabled={!isEditable}
/>
</LanguageWrapper>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'checkbox':
return (
<div className="form-field">
<CheckBox
label={field.label}
id={field.id}
checked={formData[field.id] || false}
setData={e => handleFieldChange(field.id, e.target.checked)}
disabled={!isEditable}
/>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
case 'textWithSuffix':
const linkLanguage = field.suffix;
const linkList = formData.link_list || [];
const linkData = linkList.find(link => link.language === linkLanguage) || { content: '' };
return (
<div className="form-field">
{field.label && <FormLabel>{field.label}</FormLabel>}
<FormInputSuffixWrapper>
<FormInput
type="text"
value={linkData.content}
onChange={e => {
const updatedLinkList = [...linkList];
const index = updatedLinkList.findIndex(link => link.language === linkLanguage);
if (index !== -1) {
updatedLinkList[index] = {
...updatedLinkList[index],
content: e.target.value
};
} else {
updatedLinkList.push({
language: linkLanguage,
content: e.target.value
});
}
handleFieldChange('link_list', updatedLinkList);
}}
disabled={!isEditable}
width={field.width}
suffix="true"
/>
<FormInputSuffix>{linkLanguage}</FormInputSuffix>
</FormInputSuffixWrapper>
{hasError && <div className="field-error">{hasError}</div>}
</div>
);
default:
return null;
}
};
// 조건부 렌더링을 위한 필드 필터링
const getVisibleFields = () => {
if (!config) return [];
return config.fields.filter(field => {
if (!field.visibleOn.includes(mode)) return false;
// 조건부 표시 필드 처리
if (field.conditional) {
const { field: condField, operator, value } = field.conditional;
if (operator === "==" && formData[condField] !== value) return false;
if (operator === "!=" && formData[condField] === value) return false;
}
return true;
});
};
// 그리드 기반 필드 렌더링
const renderGridFields = () => {
if (!config) return null;
const visibleFields = getVisibleFields();
const { rows, columns } = config.grid;
// 그리드 레이아웃 생성
return (
<div className="form-grid" style={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: '10px' }}>
{visibleFields.map((field) => {
const { row, col, width } = field.position;
return (
<div
key={field.id}
className="form-cell"
style={{
gridRow: row + 1,
gridColumn: `${col + 1} / span ${width}`,
padding: '5px'
}}
>
<FormRowGroup>
<FormLabel>{field.label}{field.validations?.includes("required") && <span className="required">*</span>}</FormLabel>
{renderField(field)}
</FormRowGroup>
</div>
);
})}
</div>
);
};
// 버튼 렌더링
const renderButtons = () => {
if (!config || !config.actions || !config.actions[mode]) return null;
return (
<div className="form-actions">
{config.actions[mode].map(action => (
<Button
key={action.id}
text={action.label}
theme={action.theme}
handleClick={() => {
if (action.action === 'submit') {
if (isFormValid) {
onSubmit(formData);
}
} else if (action.action === 'close' || action.action === 'cancel') {
onCancel();
}
}}
disabled={action.action === 'submit' && !isFormValid}
/>
))}
</div>
);
};
if (!config) return <div>로딩 ...</div>;
return (
<div className={`json-config-form ${className || ''}`} ref={formRef}>
<div className="form-content">
{renderGridFields()}
{errors._form && (
<SearchBarAlert $marginTop="15px" $align="right">
{errors._form}
</SearchBarAlert>
)}
</div>
<div className="form-footer">
{renderButtons()}
</div>
</div>
);
};
export default CaliForm;
const LanguageWrapper = styled.div`
width: ${props => props.width || '100%'};
//margin-bottom: 20px;
padding-bottom: 20px;
padding-left: 90px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
`;
const LanguageLabel = styled.h4`
color: #444;
margin: 0 0 10px 20px;
font-size: 16px;
font-weight: 500;
`;

View File

@@ -1,5 +1,5 @@
import { NavLink, useNavigate } from 'react-router-dom';
import arrowIcon from '../../../assets/img/icon/icon-tab.png';
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
import { ConfigProvider, Menu, theme } from 'antd';
import styled from 'styled-components';
import { useRecoilValue } from 'recoil';
import { authList } from '../../../store/authList';
@@ -7,7 +7,6 @@ import Modal from '../modal/Modal';
import { BtnWrapper, ButtonClose, ModalText } from '../../../styles/Components';
import { useEffect, useState } from 'react';
import Button from '../button/Button';
import { useLocation } from 'react-router-dom';
import { AuthInfo } from '../../../apis';
import { getMenuConfig } from '../../../utils';
import { adminAuthLevel } from '../../../assets/data/types';
@@ -15,47 +14,55 @@ import { adminAuthLevel } from '../../../assets/data/types';
const Navi = () => {
const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList);
const menu = getMenuConfig(userInfo);
const [modalClose, setModalClose] = useState('hidden');
const [logoutModalClose, setLogoutModalClose] = useState('hidden');
const menuConfig = getMenuConfig(userInfo);
const location = useLocation();
const navigate = useNavigate();
const [modalClose, setModalClose] = useState('hidden');
const [logoutModalClose, setLogoutModalClose] = useState('hidden');
const [openKeys, setOpenKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
// 현재 경로에 따라 선택된 메뉴와 열린 서브메뉴 설정
useEffect(() => {
const path = location.pathname.split('/');
if (path.length > 1) {
// 첫 번째 경로(예: /usermanage)를 기반으로 openKeys 설정
setOpenKeys([path[1]]);
// 전체 경로(예: /usermanage/adminview)를 기반으로 selectedKeys 설정
if (path.length > 2) {
setSelectedKeys([`${path[1]}/${path[2]}`]);
} else {
setSelectedKeys([path[1]]);
}
}
}, [location.pathname]);
const handleToken = async () => {
const tokenStatus = await AuthInfo(token);
tokenStatus.message === '잘못된 타입의 토큰입니다.' && setLogoutModalClose('view');
if (tokenStatus.message === '잘못된 타입의 토큰입니다.') {
setLogoutModalClose('view');
}
};
useEffect(() => {
handleToken();
}, [token]);
const handleTopMenu = e => {
e.preventDefault();
e.target.classList.toggle('active');
// 메뉴 아이템 클릭 핸들러
const handleMenuClick = ({ key }) => {
handleToken();
};
const handleLink = e => {
let topActive = document.querySelectorAll('nav .active');
let currentTopMenu = e.target.closest('ul').previousSibling;
for (let i = 0; i < topActive.length; i++) {
if (topActive[i] !== currentTopMenu) {
topActive[i].classList.remove('active');
}
}
handleToken();
// 서브메뉴 열기/닫기 핸들러
const handleOpenChange = (keys) => {
setOpenKeys(keys);
};
// 등록 완료 모달
const handleModalClose = () => {
if (modalClose === 'hidden') {
setModalClose('view');
} else {
setModalClose('hidden');
}
setModalClose(modalClose === 'hidden' ? 'view' : 'hidden');
};
// 로그아웃 안내 모달
@@ -65,7 +72,6 @@ const Navi = () => {
} else {
setLogoutModalClose('hidden');
sessionStorage.removeItem('token');
navigate('/');
}
};
@@ -79,41 +85,56 @@ const Navi = () => {
default:
return submenu.authLevel === adminAuthLevel.NONE && userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === submenu.id);
}
}
};
const getMenuItems = () => {
return menuConfig
.filter(item => item.access)
.map(item => ({
key: item.link.substring(1),
label: item.title,
children: item.submenu.map(submenu => ({
key: `${item.link.substring(1)}/${submenu.link.split('/').pop()}`,
label: (
<MenuItemLink
to={isClickable(submenu) ? submenu.link : location.pathname}
$isclickable={isClickable(submenu) ? 'true' : 'false'}
onClick={(e) => {
if (!isClickable(submenu)) {
e.preventDefault();
handleModalClose();
}
}}
>
{submenu.title}
</MenuItemLink>
),
disabled: !isClickable(submenu)
}))
}));
};
return (
<>
<nav>
<ul>
{menu.map((item, idx) => {
return (
<li key={idx}>
{item.access && (
<TopMenu to={item.link} onClick={handleTopMenu}>
{item.title}
</TopMenu>
)}
<SubMenu>
{item.submenu && userInfo &&
item.submenu.map((submenu, idx) => {
return (
<SubMenuItem key={idx} $isclickable={isClickable(submenu) ? 'true' : 'false'}>
<NavLink
to={isClickable(submenu) ? submenu.link : location.pathname}
onClick={e => {
isClickable(submenu) ? handleLink(e) : handleModalClose();
}}>
{submenu.title}
</NavLink>
</SubMenuItem>
);
})}
</SubMenu>
</li>
);
})}
</ul>
</nav>
<StyledNavWrapper>
<ConfigProvider
theme={{
algorithm: theme.darkAlgorithm,
}}
>
<StyledMenu
theme="dark"
mode="inline"
openKeys={openKeys}
selectedKeys={selectedKeys}
onOpenChange={handleOpenChange}
onClick={handleMenuClick}
items={getMenuItems()}
multiple={true}
/>
</ConfigProvider>
</StyledNavWrapper>
{/* 접근 불가 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={modalClose}>
<BtnWrapper $justify="flex-end">
@@ -145,61 +166,15 @@ const Navi = () => {
export default Navi;
const TopMenu = styled(NavLink)`
padding: 16px 30px;
width: 100%;
text-align: left;
border-bottom: 1px solid #888;
position: relative;
color: #fff;
const StyledNavWrapper = styled.div`
`;
&:before {
content: '';
display: block;
width: 12px;
height: 12px;
position: absolute;
right: 30px;
top: 50%;
transform: translate(0, -50%);
background: url('${arrowIcon}') -12px 0 no-repeat;
}
&:hover,
const StyledMenu = styled(Menu)`
`;
const MenuItemLink = styled(NavLink)`
&.active {
background: #444;
font-weight: ${props => (props.$isclickable === 'false' ? 400 : 600)};
}
&.active ~ ul {
display: block;
}
&.active:before {
background: url('${arrowIcon}') 0 0 no-repeat;
}
`;
const SubMenu = styled.ul`
display: none;
`;
const SubMenuItem = styled.li`
background: #eee;
border-bottom: 1px solid #ccc;
color: #2c2c2c;
a {
width: 100%;
padding: 16px 30px;
color: ${props => (props.$isclickable === 'false' ? '#818181' : '#2c2c2c')};
text-align: left;
&:hover,
&.active {
color: ${props => (props.$isclickable === 'false' ? '#818181' : '#2c2c2c')};
font-weight: ${props => (props.$isclickable === 'false' ? 400 : 600)};
}
}
`;
const BackGround = styled.div`
background: #eee2;
width: 100%;
height: 100%;
z-index: 100;
`;
`;

View File

@@ -1,20 +1,24 @@
import { useState, useEffect } from 'react';
import UserIcon from '../../../assets/img/icon/icon-profile.png';
import { Layout, Avatar, Button as AntButton, Typography, Tooltip, Breadcrumb } from 'antd';
import { UserOutlined, LogoutOutlined, HomeOutlined } from '@ant-design/icons'
import styled from 'styled-components';
import Modal from '../modal/Modal';
import CloseIcon from '../../../assets/img/icon/icon-close.png';
import Button from '../../common/button/Button';
import { useRecoilState } from 'recoil';
import { Link, useNavigate } from 'react-router-dom';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { AuthLogout, AuthInfo } from '../../../apis';
import { BtnWrapper, ModalText } from '../../../styles/Components';
import { authList } from '../../../store/authList';
import { alertTypes } from '../../../assets/data/types';
import { useAlert } from '../../../context/AlertProvider';
import { menuConfig } from '../../../assets/data/menuConfig';
const { Header } = Layout;
const { Text } = Typography;
const Profile = () => {
const location = useLocation();
const { showModal } = useAlert();
const [infoData, setInfoData] = useRecoilState(authList);
const [errorModal, setErrorModal] = useState('hidden');
const navigate = useNavigate();
@@ -40,91 +44,123 @@ const Profile = () => {
// 필수값 입력 모달창
const handleErrorModal = () => {
if (errorModal === 'hidden') {
setErrorModal('view');
} else {
setErrorModal('hidden');
}
showModal('USER_LOGOUT_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleLogout()
});
};
// 카테고리별 첫 번째 아이템 링크 찾기
const getFirstItemLink = (categoryKey) => {
const category = menuConfig[categoryKey];
if (!category || !category.items) return `/${categoryKey}`;
// 첫 번째 visible 아이템 찾기
const firstVisibleItem = Object.entries(category.items)
.find(([_, item]) => item.view !== false);
if (!firstVisibleItem) return `/${categoryKey}`;
return `/${categoryKey}/${firstVisibleItem[0]}`;
};
const pathSnippets = location.pathname.split('/').filter(i => i);
const breadcrumbItems = [
{
title: <Link to="/"><HomeOutlined /></Link>,
}
];
if (pathSnippets.length > 0) {
// 첫 번째 경로 (메인 카테고리)
const mainCategory = pathSnippets[0];
if (menuConfig[mainCategory]) {
const firstItemLink = getFirstItemLink(mainCategory);
breadcrumbItems.push({
title: <Link to={firstItemLink}>{menuConfig[mainCategory].title}</Link>
});
// 두 번째 경로 (서브 카테고리)
if (pathSnippets.length > 1) {
const subCategory = pathSnippets[1];
if (menuConfig[mainCategory].items[subCategory]) {
breadcrumbItems.push({
title: menuConfig[mainCategory].items[subCategory].title
});
}
}
}
}
return (
<>
<ProfileWrapper>
<UserWrapper>{infoData.name && <Username>{infoData.name.length > 20 ? infoData.name.slice(0, 20) + '...' : infoData.name}</Username>}</UserWrapper>
<Link>
<LogoutBtn onClick={handleErrorModal}>로그아웃</LogoutBtn>
</Link>
</ProfileWrapper>
{/* 로그아웃 확인 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={errorModal}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleErrorModal} />
</BtnWrapper>
<ModalText $align="center">
로그아웃 하시겠습니까?
<br />
(로그아웃 저장되지 않은 값은 초기화 됩니다.)
</ModalText>
<BtnWrapper $gap="10px">
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleErrorModal} />
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleLogout} />
</BtnWrapper>
</Modal>
<StyledHeader>
<StyledBreadcrumb items={breadcrumbItems} />
<ProfileContainer>
<StyledAvatar
size={32}
icon={<UserOutlined />}
/>
{infoData.name &&
<StyledUsername>
{infoData.name.length > 20 ? infoData.name.slice(0, 20) + '...' : infoData.name}
</StyledUsername>
}
<Tooltip title="로그아웃">
<StyledLogoutButton
type="text"
icon={<LogoutOutlined />}
onClick={handleErrorModal}
/>
</Tooltip>
</ProfileContainer>
</StyledHeader>
</>
);
};
export default Profile;
const ProfileWrapper = styled.div`
background: #f6f6f6;
padding: 20px;
const StyledHeader = styled(Header)`
background: #f6f6f6;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
height: 64px;
`;
const StyledBreadcrumb = styled(Breadcrumb)`
font-size: 15px;
font-weight: 600;
`;
const ProfileContainer = styled.div`
display: flex;
flex-wrap: wrap;
gap: 30px;
word-break: break-all;
justify-content: flex-end;
align-items: center;
gap: 12px;
`;
const LogoutBtn = styled.button`
color: #2c2c2c;
line-height: 1;
border-bottom: 0.5px solid #2c2c2c;
font-size: 13px;
font-weight: 300;
border-radius: 0;
letter-spacing: 0;
width: max-content;
height: max-content;
const StyledAvatar = styled(Avatar)`
`;
const UserWrapper = styled.div`
padding-left: 35px;
position: relative;
font-size: 18px;
display: flex;
&:before {
background: url('${UserIcon}') 50% 50% no-repeat;
width: 24px;
height: 24px;
content: '';
display: block;
position: absolute;
left: 0;
top: 50%;
transform: translate(0, -50%);
}
const StyledUsername = styled(Text)`
font-weight: 600;
font-size: 18px;
color: rgba(0, 0, 0, 0.85);
`;
const Username = styled.div`
font-weight: 700;
padding-right: 3px;
`;
const StyledLogoutButton = styled(AntButton)`
color: rgba(0, 0, 0, 0.45);
transition: color 0.3s;
font-size: 18px;
&:hover {
color: #1677ff;
background: transparent;
}
const ButtonClose = styled.button`
width: 16px;
height: 16px;
background: url(${CloseIcon}) 50% 50% no-repeat;
`;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { motion } from 'framer-motion';
const pageVariants = {
initial: {
opacity: 0,
x: 20
},
animate: {
opacity: 1,
x: 0,
transition: {
duration: 0.3,
ease: "easeInOut"
}
},
exit: {
opacity: 0,
x: -20,
transition: {
duration: 0.2,
ease: "easeInOut"
}
}
};
const AnimatedPageWrapper = ({ children }) => {
return (
<motion.div
initial="initial"
animate="animate"
exit="exit"
variants={pageVariants}
style={{ width: '100%', height: '100%' }}
>
{children}
</motion.div>
);
};
export default AnimatedPageWrapper;

View File

@@ -2,6 +2,7 @@ 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';
import { AnimatedTabs } from '../index';
const { RangePicker } = DatePicker;
/**
@@ -52,7 +53,10 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
max,
format,
required,
showTime
showTime,
tabItems,
activeKey,
onTabChange
} = item;
// 현재 값 가져오기 (formData에서 또는 항목에서)
@@ -105,6 +109,8 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
return (
<DatePicker
{...commonProps}
allowClear={false}
showTime={showTime || false}
value={currentValue ? dayjs(currentValue) : null}
format={format || 'YYYY-MM-DD'}
onChange={(date) => onChange(key, date, handler)}
@@ -191,8 +197,8 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
case 'tab':
return <AnimatedTabs
items={tabItems}
activeKey={activeLanguage}
onChange={handleTabChange}
activeKey={activeKey}
onChange={onTabChange}
/>
case 'custom':
@@ -249,13 +255,15 @@ const StatusDisplay = ({ status }) => {
let color = '';
let text = '';
switch (status) {
const lowerStatus = typeof status === 'string' ? status.toLowerCase() : status;
switch (lowerStatus) {
case 'wait':
color = '#faad14';
color = '#FAAD14';
text = '대기';
break;
case 'running':
color = '#52c41a';
color = '#4287f5';
text = '진행중';
break;
case 'finish':
@@ -271,7 +279,7 @@ const StatusDisplay = ({ status }) => {
text = '삭제';
break;
default:
color = '#1890ff';
color = '#DEBB46';
text = status;
}

View File

@@ -1,5 +1,8 @@
import Layout from './Layout';
import LoginLayout from './LoginLayout';
import MainLayout from './MainLayout';
import AnimatedPageWrapper from './AnimatedPageWrapper';
import DetailGrid from './DetailGrid';
import DetailLayout from './DetailLayout';
export { Layout, LoginLayout, MainLayout };
export { Layout, LoginLayout, MainLayout, AnimatedPageWrapper, DetailGrid, DetailLayout };

View File

@@ -5,38 +5,72 @@ import { motion, AnimatePresence } from 'framer-motion';
// 통합된 애니메이션 탭 컴포넌트
const AnimatedTabs = ({ items, activeKey, onChange }) => {
// 각 항목의 children을 애니메이션 래퍼로 감싸기
const tabItems = items.map(item => ({
key: item.key,
label: item.label,
children: (
<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>
)
}));
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>
items={tabItems}
/>
);
};
// 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%;

View File

@@ -21,6 +21,7 @@ import CDivider from './CDivider';
import TopButton from './button/TopButton';
import AntButton from './button/AntButton';
import DetailLayout from './Layout/DetailLayout';
import AnimatedTabs from './control/AnimatedTabs';
import CaliTable from './Custom/CaliTable'
@@ -56,5 +57,6 @@ export { DateTimeInput,
FrontPagination,
DownloadProgress,
CaliTable,
DetailLayout
DetailLayout,
AnimatedTabs
};

View File

@@ -64,15 +64,15 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}
}, [modalType, content]);
useEffect(() => {
if(modalType === TYPE_REGISTRY && configData?.length > 0){
setResultData(prev => ({
...prev,
round_count: configData[0].default_round_count,
round_time: configData[0].round_time
}));
}
}, [modalType, configData]);
// useEffect(() => {
// if(modalType === TYPE_REGISTRY && configData?.length > 0){
// setResultData(prev => ({
// ...prev,
// round_count: configData[0].default_round_count,
// round_time: configData[0].round_time
// }));
// }
// }, [modalType, configData]);
useEffect(() => {
if (checkCondition()) {
@@ -126,6 +126,26 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}));
};
const handleEndTimeChange = (time) => {
if (!time) return;
const newDateTime = resultData.event_end_time
? new Date(resultData.event_end_time)
: new Date();
newDateTime.setHours(
time.getHours(),
time.getMinutes(),
0,
0
);
setResultData(prev => ({
...prev,
event_end_time: newDateTime
}));
};
// 종료 날짜 변경 핸들러
const handleEndDateChange = (date) => {
if (!date || !resultData.event_start_dt) return;
@@ -190,6 +210,18 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
return;
}
//최소 진행시간
if(resultData.event_operation_time < 10){
showToast('BATTLE_EVENT_MODAL_OPERATION_TIME_MIN_CHECK_WARNING', {type: alertTypes.warning});
return;
}
//최대 진행시간
if(resultData.repeat_type !== 'NONE' && resultData.event_operation_time > 1400){
showToast('BATTLE_EVENT_MODAL_OPERATION_TIME_MAX_CHECK_WARNING', {type: alertTypes.warning});
return;
}
// if(resultData.round_time === 0){
// const config = configData.find(data => data.id === resultData.config_id);
// setResultData({ ...resultData, round_time: config.round_time });
@@ -202,9 +234,15 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
break;
case "registConfirm":
const params = {
...resultData,
event_operation_time: resultData.event_operation_time * 60
};
if(isView('modify')){
await withLoading( async () => {
return await BattleEventModify(token, content?.id, resultData);
return await BattleEventModify(token, content?.id, params);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('UPDATE_COMPLETED', {type: alertTypes.success});
@@ -221,7 +259,7 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}
else{
await withLoading( async () => {
return await BattleEventSingleRegist(token, resultData);
return await BattleEventSingleRegist(token, params);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('REGIST_COMPLTE', {type: alertTypes.success});
@@ -265,6 +303,7 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
case "round":
case "hot":
case "mode":
case "operation_time":
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === battleEventStatusType.stop));
default:
return modalType === TYPE_MODIFY && (content?.status !== battleEventStatusType.stop);
@@ -302,12 +341,23 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
onDateChange={handleStartDateChange}
selectedDate={resultData?.event_start_dt}
/>
</FormRowGroup>
<FormRowGroup>
<SingleTimePicker
label="시작시간"
disabled={!isView('start_dt')}
selectedTime={resultData?.event_start_dt}
onTimeChange={handleStartTimeChange}
/>
<FormLabel>진행시간()</FormLabel>
<FormInput
type="number"
disabled={!isView('operation_time')}
width='100px'
min={10}
value={resultData?.event_operation_time}
onChange={e => setResultData({ ...resultData, event_operation_time: e.target.value })}
/>
</FormRowGroup>
<FormRowGroup>
<FormLabel>반복</FormLabel>
@@ -328,24 +378,24 @@ 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.round_count} onChange={e => setResultData({ ...resultData, round_count: e.target.value })} disabled={!isView('round')} width="100px">
{battleEventRoundCount.map((data, index) => (
<option key={index} value={data}>
{data}
</option>
))}
</SelectInput>
</FormRowGroup>
{/*<FormRowGroup>*/}
{/* <FormLabel>라운드 시간</FormLabel>*/}
{/* <SelectInput value={resultData.config_id} onChange={handleConfigChange} disabled={!isView('config')} width="200px">*/}
{/* {configData && configData?.map((data, index) => (*/}
{/* <option key={index} value={data.id}>*/}
{/* {data.desc}({data.id})*/}
{/* </option>*/}
{/* ))}*/}
{/* </SelectInput>*/}
{/* <FormLabel>라운드 수</FormLabel>*/}
{/* <SelectInput value={resultData.round_count} onChange={e => setResultData({ ...resultData, round_count: e.target.value })} disabled={!isView('round')} width="100px">*/}
{/* {battleEventRoundCount.map((data, index) => (*/}
{/* <option key={index} value={data}>*/}
{/* {data}*/}
{/* </option>*/}
{/* ))}*/}
{/* </SelectInput>*/}
{/*</FormRowGroup>*/}
<FormRowGroup>
{/*<FormLabel>배정 포드</FormLabel>*/}
{/*<SelectInput value={resultData.reward_group_id} onChange={e => setResultData({ ...resultData, reward_group_id: e.target.value })} disabled={!isView('reward')} width="200px">*/}
@@ -434,7 +484,8 @@ export const initData = {
hot_time: 1,
game_mode_id: 1,
event_start_dt: '',
event_end_dt: ''
event_end_dt: '',
event_operation_time: 10
}
export default BattleEventModal;

View File

@@ -1,38 +1,19 @@
import React, { useState, useEffect, Fragment } from 'react';
import styled, { css, keyframes } from 'styled-components';
import styled from 'styled-components';
import {
Title,
SelectInput,
BtnWrapper,
TextInput,
Label,
InputLabel,
Textarea,
SearchBarAlert,
ButtonGroupWrapper,
} from '../../styles/Components';
import Button from '../common/button/Button';
import { Title, ButtonGroupWrapper, } from '../../styles/Components';
import Modal from '../common/modal/Modal';
import { EventIsItem, EventModify, MenuBannerModify } from '../../apis';
import { 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 { authType, commonStatus } from '../../assets/data';
import { convertKTCDate } from '../../utils';
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';
import { alertTypes, languageNames } from '../../assets/data/types';
import { Image as AntImage } from 'antd';
import { AntButton, DetailLayout } from '../common';
function renderImageContent(imageData) {
if (!imageData) {
@@ -77,7 +58,6 @@ function renderImageContent(imageData) {
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();
@@ -94,17 +74,12 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
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);
// console.log(content);
const start_dt_KTC = convertKTCDate(content.start_dt);
const end_dt_KTC = convertKTCDate(content.end_dt);
@@ -131,13 +106,6 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
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) {
@@ -156,105 +124,9 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
})) : [];
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 = () => {
@@ -279,6 +151,7 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
case "submit":
if (!checkCondition()) return;
// console.log(resultData);
showModal('MENU_BANNER_UPDATE_SAVE', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('updateConfirm')
@@ -287,50 +160,43 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
case "updateConfirm":
withLoading( async () => {
return await MenuBannerModify(token, id, resultData);
}).then(result => {
if(result.result === 'ERROR'){
showToast(result.data.message, {
type: alertTypes.error
});
}else if(result.result === 'SUCCESS'){
showToast('UPDATE_COMPLETED', {type: alertTypes.success, duration: 4000});
}
}).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 반환)
// updateAuth가 없거나 FINISH 상태면 수정 버튼 숨김
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;
if (!updateAuth) return false;
if(content.status === commonStatus.wait){
return true;
}else if(content.status === commonStatus.running){
switch(fieldName){
case 'order_id':
case 'end_dt':
return true;
default:
return false;
}
}else{
return false;
}
}
@@ -371,12 +237,35 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
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'
type: 'date',
key: 'start_dt',
label: '시작일',
disabled: !isView('start_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '250px',
showTime: true
},
{
row: 2,
col: 2,
colSpan: 2,
type: 'date',
key: 'end_dt',
label: '종료일',
disabled: !isView('end_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '250px',
showTime: true
},
{
row: 3,
col: 0,
colSpan: 4,
type: 'tab',
key: 'languageTabs',
tabItems: tabItems,
activeKey: activeLanguage,
onTabChange: handleTabChange
},
]
}
@@ -393,76 +282,6 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
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="확인"
@@ -503,43 +322,6 @@ const initData = {
]
}
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;
@@ -590,22 +372,3 @@ const NoImagePlaceholder = styled.div`
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

@@ -186,17 +186,17 @@ const BattleEventSearchBar = ({ searchParams, onSearch, onReset, configData, rew
))}
</SelectInput>
</InputGroup>
<InputLabel>라운드 </InputLabel>
<InputGroup>
<SelectInput value={searchParams.roundCount} onChange={e => onSearch({ roundCount: e.target.value })}>
<option value='ALL'>전체</option>
{battleEventRoundCount.map((data, index) => (
<option key={index} value={data}>
{data}
</option>
))}
</SelectInput>
</InputGroup>
{/*<InputLabel>라운드 수</InputLabel>*/}
{/*<InputGroup>*/}
{/* <SelectInput value={searchParams.roundCount} onChange={e => onSearch({ roundCount: e.target.value })}>*/}
{/* <option value='ALL'>전체</option>*/}
{/* {battleEventRoundCount.map((data, index) => (*/}
{/* <option key={index} value={data}>*/}
{/* {data}*/}
{/* </option>*/}
{/* ))}*/}
{/* </SelectInput>*/}
{/*</InputGroup>*/}
</>
];

View File

@@ -1,8 +1,7 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { InputLabel } 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 { getCurrencyList } from '../../apis/Log';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';

View File

@@ -0,0 +1,241 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import {
amountDeltaType,
countDeltaType,
CurrencyType,
itemTypeLarge,
logAction,
userSearchType2,
} from '../../assets/data/options';
import {
getCurrencyItemList,
} from '../../apis/Log';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
export const useCurrencyItemLogSearch = (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 getCurrencyItemList(
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 CurrencyItemLogSearchBar = ({ 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 CurrencyItemLogSearchBar;

View File

@@ -0,0 +1,252 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import {
amountDeltaType,
countDeltaType,
CurrencyType,
itemTypeLarge,
logAction,
userSearchType2,
} from '../../assets/data/options';
import { getCurrencyDetailList, getItemDetailList } from '../../apis/Log';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
export const useItemLogSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
search_type: 'GUID',
search_data: '',
tran_id: '',
log_action: 'None',
item_large_type: 'None',
item_small_type: '',
count_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 getItemDetailList(
token,
params.search_type,
params.search_data,
params.tran_id,
params.log_action,
params.item_large_type,
params.item_small_type,
params.count_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',
item_large_type: 'None',
item_small_type: '',
count_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 ItemLogSearchBar = ({ 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)}
/>
</>,
<>
<InputLabel>LargeType</InputLabel>
<SelectInput value={searchParams.item_large_type} onChange={e => onSearch({ item_large_type: e.target.value }, false)} >
<option value="None">전체</option>
{itemTypeLarge.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
];
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>SmallType</InputLabel>
<TextInput
type="text"
placeholder='Small Type 입력'
value={searchParams.item_small_type}
width="150px"
onChange={e => onSearch({ item_small_type: e.target.value }, false)}
/>
</>,
<>
<InputLabel>증감유형</InputLabel>
<SelectInput value={searchParams.count_delta_type} onChange={e => onSearch({ count_delta_type: e.target.value }, false)} >
<option value="None">전체</option>
{countDeltaType.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 ItemLogSearchBar;

View File

@@ -1,166 +1,151 @@
import { useEffect, useState } from 'react';
import { styled } from 'styled-components';
import Button from '../common/button/Button';
import DatePickerComponent from '../common/Date/DatePickerComponent';
import { InputLabel } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { RetentionIndexView } from '../../apis';
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper, InputGroup, DatePickerWrapper, AlertText } from '../../styles/Components';
export const useRetentionSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const RetentionSearchBar = ({ resultData, setResultData, handleSearch, fetchData, setRetention, setExcelBtn }) => {
// 초기 날짜 세팅
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(new Date(d.setDate(d.getDate() - 1)).setHours(24, 0, 0, 0));
const [errorMessage, setErrorMessage] = useState('');
const [period, setPeriod] = useState(0);
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);
// resultData에 임의 날짜 넣어주기
useEffect(() => {
setResultData({
send_dt: START_DATE,
end_dt: '',
retention: 0,
});
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await RetentionIndexView(
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.retention);
return result.retention;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
// 발송 날짜 세팅 로직
const handleSelectedDate = data => {
const sendDate = new Date(data);
const resultSendData = new Date(sendDate.getFullYear(), sendDate.getMonth(), sendDate.getDate());
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);
const resultEndDate = new Date(resultSendData);
resultEndDate.setDate(resultEndDate.getDate() + Number(resultData.retention));
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
setResultData({ ...resultData, send_dt: resultSendData, end_dt: resultEndDate });
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 RetentionSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
// // 발송 날짜 세팅 로직
// const handleEndDate = data => {
// const endDate = new Date(data);
// const resultSendData = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
// setResultData({ ...resultData, end_dt: resultSendData });
// };
// Retention 세팅 로직
const handleRetention = e => {
const value = e.target.value;
const resultEndDate = new Date(resultData.send_dt);
resultEndDate.setDate(resultEndDate.getDate() + Number(value));
setResultData({ ...resultData, end_dt: resultEndDate, retention: value });
setPeriod(value);
};
//Retention 범위 선택 후 disable 처리 로직
const handleSearchBtn = e => {
e.preventDefault();
if(period == 0) {
setErrorMessage("필수값을 선택하세요.");
return false;
} else {
setErrorMessage("");
setExcelBtn(false); //활성화
handleSearch(resultData.send_dt, resultData.end_dt);
}
}
const handleReset = e => {
e.preventDefault();
setResultData({ send_dt: START_DATE, end_dt: '', retention: 0 });
setRetention(1);
setErrorMessage("");
setPeriod(1);
setExcelBtn(true); //비활성화
fetchData(START_DATE, END_DATE);
};
return (
const searchList = [
<>
<FormWrapper>
<SearchbarStyle>
<SearchItem>
<InputLabel>집계 기준일</InputLabel>
<InputGroup>
<DatePickerWrapper>
<DatePickerComponent
name="시작 일자" selectedDate={resultData.send_dt}
handleSelectedDate={data => handleSelectedDate(data)}
maxDate={new Date()} />
<span>~</span>
<DatePickerComponent
name="종료 일자"
selectedDate={resultData.end_dt}
maxDate={new Date()}
readOnly={true}
disabled={true}
type="retention" />
</DatePickerWrapper>
</InputGroup>
</SearchItem>
<SearchItem>
<InputLabel>Retention 범위</InputLabel>
<SelectInput
onChange={e => handleRetention(e)} value={resultData.retention}>
<option value={0}>선택</option>
<option value={1}>D+1</option>
<option value={7}>D+7</option>
<option value={30}>D+30</option>
</SelectInput>
</SearchItem>
{/* 기획 보류 */}
{/* <SearchItem>
<InputLabel>조회 국가</InputLabel>
<SelectInput>
<option value="">ALL</option>
<option value="">KR</option>
<option value="">EN</option>
<option value="">JP</option>
<option value="">TH</option>
</SelectInput>
</SearchItem> */}
<BtnWrapper $gap="8px">
<Button theme="reset" handleClick={handleReset} />
<Button
theme="search"
text="검색"
handleClick={handleSearchBtn}
/>
<AlertText>{errorMessage}</AlertText>
</BtnWrapper>
</SearchbarStyle>
</FormWrapper>
</>
);
<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 RetentionSearchBar;
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 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,175 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import { userSearchType2 } from '../../assets/data/options';
import { getUserCreateList } from '../../apis';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
export const useUserCreateLogSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
search_type: 'GUID',
search_data: '',
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
// const initialLoad = async () => {
// await fetchData(searchParams);
// };
//
// initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await getUserCreateList(
token,
params.search_type,
params.search_data,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result.data);
return result.data;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
search_type: 'GUID',
search_data: '',
start_dt: now,
end_dt: now,
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const UserCreateLogSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputGroup>
<SelectInput value={searchParams.search_type} onChange={e => onSearch({search_type: e.target.value }, false)}>
{userSearchType2.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
type="text"
placeholder={searchParams.search_type === 'GUID' ? 'GUID ID 입력' : searchParams.search_type === 'NICKNAME' ? '아바타명 입력' :'Account ID 입력'}
value={searchParams.search_data}
width="260px"
onChange={e => onSearch({ search_data: e.target.value }, false)}
/>
</InputGroup>
</>,
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default UserCreateLogSearchBar;

View File

@@ -0,0 +1,191 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import { userSearchType2 } from '../../assets/data/options';
import { getUserLoginDetailList } from '../../apis';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
export const useUserLoginLogSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
search_type: 'GUID',
search_data: '',
tran_id: '',
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 getUserLoginDetailList(
token,
params.search_type,
params.search_data,
params.tran_id,
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: '',
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 UserLoginLogSearchBar = ({ 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)}
/>
</>,
];
return <SearchBarLayout firstColumnData={searchList} secondColumnData={optionList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default UserLoginLogSearchBar;

View File

@@ -18,14 +18,18 @@ import PlayTimeSearchBar from './PlayTimeSearchBar';
import UserViewSearchBar from './UserViewSearchBar';
import AdminViewSearchBar from './AdminViewSearchBar';
import EventListSearchBar from './EventListSearchBar';
import RetentionSearchBar from './RetentionSearchBar';
import RetentionSearchBar, {useRetentionSearch} 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 ItemLogSearchBar, { useItemLogSearch } from './ItemLogSearchBar';
import CurrencyItemLogSearchBar, { useCurrencyItemLogSearch } from './CurrencyItemLogSearchBar';
import CurrencyIndexSearchBar, { useCurrencyIndexSearch } from './CurrencyIndexSearchBar';
import UserCreateLogSearchBar, { useUserCreateLogSearch } from './UserCreateLogSearchBar';
import UserLoginLogSearchBar, { useUserLoginLogSearch } from './UserLoginLogSearchBar';
import LandAuctionSearchBar from './LandAuctionSearchBar';
import CaliumRequestSearchBar from './CaliumRequestSearchBar';
@@ -51,6 +55,7 @@ export {
AdminViewSearchBar,
EventListSearchBar,
RetentionSearchBar,
useRetentionSearch,
UserBlockSearchBar,
UserIndexSearchBar,
ReportListSearchBar,
@@ -62,6 +67,14 @@ export {
LandAuctionSearchBar,
CaliumRequestSearchBar,
CurrencyIndexSearchBar,
useCurrencyIndexSearch
useCurrencyIndexSearch,
useItemLogSearch,
ItemLogSearchBar,
CurrencyItemLogSearchBar,
useCurrencyItemLogSearch,
UserCreateLogSearchBar,
useUserCreateLogSearch,
UserLoginLogSearchBar,
useUserLoginLogSearch,
};

View File

@@ -5,6 +5,7 @@ const resources = {
ko: {
translation: {
//메시지
USER_LOGOUT_CONFIRM: '로그아웃 하시겠습니까?\n(로그아웃 시 저장되지 않은 값은 초기화 됩니다.)',
NULL_MSG: '필수값을 입력해주세요.',
DATE_KTC: '* UTC+9 한국시간 기준으로 설정 (UTC+0 자동 반영처리)',
NOT_ITEM: '존재하지 않는 아이템코드입니다.',
@@ -53,9 +54,13 @@ const resources = {
EXCEL_EXPORT_LENGTH_LIMIT_WARNING: '엑셀 다운은 10만건 이하까지만 가능합니다.\r\n조건을 조정 후 다시 시도해주세요.',
DOWNLOAD_COMPLETE: '다운이 완료되었습니다.',
DOWNLOAD_FAIL: '다운이 실패하였습니다.',
DELETE_STATUS_ONLY_WAIT: '대기상태의 데이터만 삭제가 가능합니다.',
TABLE_DATA_NOT_FOUND: '데이터가 없습니다.',
//user
NICKNAME_CHANGES_CONFIRM: '닉네임을 변경하시겠습니까?',
NICKNAME_CHANGES_COMPLETE: '닉네임 변경이 완료되었습니다.',
QUEST_TASK_COMPLETE_CONFIRM: '퀘스트를 강제 완료 처리하시겠습니까?',
QUEST_TASK_COMPLETE: '퀘스트를 강제 완료 요청처리가 되었습니다.\n재 조회후 상태를 확인해주세요.\n(최대 3분정도 소요)',
//table
TABLE_ITEM_DELETE_TITLE: "선택 삭제",
TABLE_BUTTON_DETAIL_TITLE: "상세보기",
@@ -126,6 +131,8 @@ const resources = {
//전투시스템
BATTLE_EVENT_MODAL_START_DT_WARNING: "시작 시간은 현재 시간으로부터 10분 이후부터 가능합니다.",
BATTLE_EVENT_MODAL_TIME_CHECK_WARNING :"해당 시간에 속하는 이벤트가 존재합니다.",
BATTLE_EVENT_MODAL_OPERATION_TIME_MIN_CHECK_WARNING :"진행시간은 최소 10분 이상이여야 합니다.",
BATTLE_EVENT_MODAL_OPERATION_TIME_MAX_CHECK_WARNING :"진행시간은 최대 1400분까지 가능합니다.",
BATTLE_EVENT_REGIST_CONFIRM: "이벤트를 등록하시겠습니까?",
BATTLE_EVENT_UPDATE_CONFIRM: "이벤트를 수정하시겠습니까?",
BATTLE_EVENT_SELECT_DELETE: "선택된 이벤트를 삭제하시겠습니까?",
@@ -157,18 +164,24 @@ const resources = {
FILE_SIZE_OVER_ERROR: "파일의 사이즈가 5MB를 초과하였습니다.",
//파일명칭
FILE_INDEX_USER_CONTENT: 'Caliverse_User_Index.xlsx',
FILE_INDEX_USER_RETENTION: 'Caliverse_Retention.xlsx',
FILE_CALIUM_REQUEST: 'Caliverse_Calium_Request.xlsx',
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_GAME_LOG_USER_CREATE: 'Caliverse_Game_Log_User_Create',
FILE_GAME_LOG_USER_LOGIN: 'Caliverse_Game_Log_User_Login',
FILE_GAME_LOG_ITEM: 'Caliverse_Game_Log_Item',
FILE_GAME_LOG_CURRENCY_ITEM: 'Caliverse_Game_Log_Currecy_Item',
FILE_CURRENCY_INDEX: 'Caliverse_Currency_Index',
//서버 에러메시지
DYNAMODB_NOT_USER: '유저 정보를 확인해주세요.',
NICKNAME_EXIT_ERROR: '해당 닉네임이 존재합니다.',
NICKNAME_NUMBER_ERROR: '닉네임은 첫번째 글자에 숫자를 허용하지 않습니다.',
NICKNAME_SPECIALCHAR_ERROR: '닉네임은 특수문자를 사용할 수 없습니다.',
NICKNAME_LANGTH_ERROR: '닉네임은 최소 2글자에서 최대 12글자까지 허용 합니다.'
NICKNAME_LANGTH_ERROR: '닉네임은 최소 2글자에서 최대 12글자까지 허용 합니다.',
dynamoDB_connection_error: '운영DB 작업중 에러가 발생하였습니다.'
}
},
en: {

View File

@@ -1,5 +1,6 @@
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import {
Title,
TableStyle,
@@ -58,6 +59,13 @@ const BusinessLogView = () => {
updateSearchParams
} = useBusinessLogSearch(token, 500);
useEffect(()=>{
setDownloadState({
loading: false,
progress: 0
});
},[dataList]);
useEffect(() => {
// 세션 스토리지에서 복사된 메일 데이터 가져오기
const paramsData = sessionStorage.getItem(STORAGE_BUSINESS_LOG_SEARCH);
@@ -167,7 +175,7 @@ const BusinessLogView = () => {
// }
return (
<>
<AnimatedPageWrapper>
<Title>비즈니스 로그 조회</Title>
<FormWrapper>
<BusinessLogSearchBar
@@ -264,7 +272,7 @@ const BusinessLogView = () => {
<TopButton />
</>
}
</>
</AnimatedPageWrapper>
);
};

View File

@@ -1,128 +0,0 @@
import { styled } from 'styled-components';
import { Link } from 'react-router-dom';
import { Fragment, useState } from 'react';
import { Title, TableStyle, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
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';
import Modal from '../../components/common/modal/Modal';
import { useNavigate } from 'react-router-dom';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
const ContentsView = () => {
const navigate = useNavigate();
const userInfo = useRecoilValue(authList);
const [detailPop, setDetailPop] = useState('hidden');
const mokupData = [
{
landId: '2000515223',
ownerNick: '칼리버스F',
ownerId: '23d59e868ca342198f6a653d957914a5',
lockInDate: '2023-08-11 15:32:07',
landUrl: '0x5765eB84ab55369f430DdA0d0C2b443FB9372DB3',
},
];
const handleClick = () => {
if (detailPop === 'hidden') setDetailPop('view');
else setDetailPop('hidden');
};
return (
<>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === 13) ? (
<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>
<LandSearchBar />
<TableWrapper>
<TableStyle>
<thead>
<tr>
<th width="150">랜드 ID</th>
<th>소유자 아바타명</th>
<th>소유쟈 GUID</th>
<th width="200">Lock IN 처리 일자</th>
<th>랜드 URL</th>
<th width="150">상세보기</th>
</tr>
</thead>
<tbody>
{mokupData.map((data, index) => (
<Fragment key={index}>
<tr>
<td>{data.landId}</td>
<td>{data.ownerNick}</td>
<td>{data.ownerId}</td>
<td>{new Date(data.lockInDate).toLocaleString()}</td>
<td>
<LandLink to={data.landUrl}>{data.landUrl}</LandLink>
</td>
<td>
<Button theme="line" text="상세보기" handleClick={handleClick} />
</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<LandDetailModal detailPop={detailPop} handleClick={handleClick} />
</>
)}
</>
);
};
export default ContentsView;
const TableWrapper = styled.div`
overflow: auto;
border-top: 1px solid #000;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #666666;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
}
thead {
th {
position: sticky;
top: 0;
z-index: 10;
}
}
${TableStyle} {
min-width: 1000px;
th {
position: sticky;
top: 0;
}
}
`;
const LandLink = styled(Link)`
color: #61a2d0;
text-decoration: underline;
`;

View File

@@ -1,7 +1,6 @@
import { Fragment, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import Button from '../../components/common/button/Button';
import Pagination from '../../components/common/Pagination/Pagination';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import { registerLocale } from 'react-datepicker';
import { ko } from 'date-fns/esm/locale';
@@ -9,96 +8,21 @@ import 'react-datepicker/dist/react-datepicker.css';
import {
Title,
SelectInput,
TableStyle,
TableInfo,
ListCount,
ListOption,
TabScroll, TabItem, TabWrapper,
} from '../../styles/Components';
import { styled } from 'styled-components';
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';
import ItemLogContent from '../../components/DataManage/ItemLogContent';
import CurrencyItemLogContent from '../../components/DataManage/CurrencyItemLogContent';
import UserCreateLogContent from '../../components/DataManage/UserCreateLogContent';
import UserLoginLogContent from '../../components/DataManage/UserLoginLogContent';
registerLocale('ko', ko);
const ItemLogContent = () => {
const mokupData = [
{
date: '2023-08-05 12:11:32',
name: '칼리버스',
id: '16CD2ECD-4798-46CE-9B6B-F952CF11F196',
action: '획득',
route: '아이템 제작',
itemName: 'Item_name',
serialNumber: 'Serial_number',
itemCode: 'Item_code',
count: 1,
key: 'User_trade_key',
},
];
return (
<>
<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">액션</th>
<th width="150">획득/소진경로</th>
<th width="200">아이템명</th>
<th width="200">시리얼 넘버</th>
<th width="200">고유코드</th>
<th width="80">개수</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.action}</td>
<td>{data.route}</td>
<td>{data.itemName}e</td>
<td>{data.serialNumber}</td>
<td>{data.itemCode}</td>
<td>{data.count}</td>
<td>{data.key}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<Pagination />
</>
);
};
const GameLogView = () => {
const [activeTab, setActiveTab] = useState('CURRENCY');
@@ -109,7 +33,6 @@ const GameLogView = () => {
const searchData = JSON.parse(paramsData);
setActiveTab(searchData.tab);
console.log(searchData);
}
}, []);
@@ -119,7 +42,7 @@ const GameLogView = () => {
};
return (
<>
<AnimatedPageWrapper>
<Title>게임 로그 조회</Title>
<TabScroll>
<TabWrapper>
@@ -134,30 +57,13 @@ const GameLogView = () => {
})}
</TabWrapper>
</TabScroll>
{/*{activeTab === 'ITEM' && <ItemLogContent />}*/}
<CurrencyLogContent active={activeTab === 'CURRENCY'} />
{/*{activeTab === 'TRADE' && <TradeLogContent />}*/}
</>
<ItemLogContent active={activeTab === 'ITEM'} />
<CurrencyItemLogContent active={activeTab === 'CURRENCYITEM'} />
<UserCreateLogContent active={activeTab === 'USERCREATE'} />
<UserLoginLogContent active={activeTab === 'USERLOGIN'} />
</AnimatedPageWrapper>
);
};
export default withAuth(authType.gameLogRead)(GameLogView);
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: max-content;
}
`;

View File

@@ -1,6 +1,7 @@
import { styled } from 'styled-components';
import { Link } from 'react-router-dom';
import React, { Fragment, useRef, useState } from 'react';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import {
Title,
@@ -158,7 +159,7 @@ const LandInfoView = () => {
}
return (
<>
<AnimatedPageWrapper>
<Title>랜드 정보 조회</Title>
<FormWrapper>
<LandInfoSearchBar
@@ -240,7 +241,7 @@ const LandInfoView = () => {
content={detailData}
setDetailData={setDetailData}
/>
</>
</AnimatedPageWrapper>
);
};

View File

@@ -1,6 +1,7 @@
import { useState, Fragment } from 'react';
import { TabScroll, Title } from '../../styles/Components';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import styled from 'styled-components';
@@ -39,7 +40,7 @@ const UserView = () => {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.userSearchRead) ? (
<AuthModal />
) : (
<>
<AnimatedPageWrapper>
<Title>유저조회</Title>
<UserViewSearchBar setInfoView={setInfoView} resultData={resultData} handleTab={handleTab} setResultData={setResultData} />
<UserWrapper display={infoView}>
@@ -68,7 +69,7 @@ const UserView = () => {
{activeContent === '클레임' && <UserClaimInfo userInfo={resultData && resultData} />}
</UserTabInfo>
</UserWrapper>
</>
</AnimatedPageWrapper>
)}
</>
);
@@ -127,30 +128,3 @@ const UserTabInfo = styled.div`
background: #d9d9d9;
}
`;
const UserDefaultTable = styled.table`
border: 1px solid #e8eaec;
border-top: 1px solid #000;
font-size: 14px;
margin-bottom: 40px;
th {
background: #efefef;
font-weight: 700;
}
th,
td {
padding: 12px;
text-align: center;
border-left: 1px solid #e8eaec;
vertical-align: middle;
}
td {
background: #fff;
border-bottom: 1px solid #e8eaec;
word-break: break-all;
}
button {
height: 24px;
font-size: 13px;
}
`;

View File

@@ -1,5 +1,6 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import Button from '../../components/common/button/Button';
@@ -29,7 +30,7 @@ const EconomicIndex = () => {
setActiveTab(content);
};
return (
<>
<AnimatedPageWrapper>
<Title>경제 지표</Title>
<TabScroll>
<TabWrapper>
@@ -49,7 +50,7 @@ const EconomicIndex = () => {
{/*{activeTab === 'item' && <ItemContent />}*/}
{/*{activeTab === 'instance' && <InstanceContent />}*/}
{/*{activeTab === 'deco' && <DecoContent />}*/}
</>
</AnimatedPageWrapper>
);
};

View File

@@ -1,77 +1,50 @@
import { Fragment, useEffect, useState } from 'react';
import { useState } from 'react';
import { styled } from 'styled-components';
import { Link, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import Modal from '../../components/common/modal/Modal';
import Button from '../../components/common/button/Button';
import { Title, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
import { authList } from '../../store/authList';
import { userIndexView, userTotalIndex } from '../../apis';
import { Link } from 'react-router-dom';
import { AnimatedPageWrapper } from '../../components/common/Layout';
import { Title } from '../../styles/Components';
import { UserContent, SegmentContent, PlayTimeContent, RetentionContent, DailyActiveUserContent, DailyMedalContent } from '../../components/IndexManage/index';
import AuthModal from '../../components/common/modal/AuthModal';
import { authType } from '../../assets/data';
import { withAuth } from '../../hooks/hook';
import { TabUserIndexList } from '../../assets/data/options';
const UserIndex = () => {
const token = sessionStorage.getItem('token');
const navigate = useNavigate();
const userInfo = useRecoilValue(authList);
const [activeTab, setActiveTab] = useState('이용자 지표');
const [activeTab, setActiveTab] = useState('USER');
const handleTab = (e, content) => {
// e.preventDefault();
e.preventDefault();
setActiveTab(content);
};
const TabList = [
{ title: '이용자 지표' },
// { title: 'Retention' },
// { title: 'Segment' },
// { title: '플레이타임' },
// { title: 'DAU' },
// { title: '메달' },
];
return (
<>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.userIndicatorsRead) ? (
<AuthModal/>
) : (
<>
<Title>유저 지표</Title>
<TabWrapper>
{TabList.map((el, idx) => {
return (
<TabItem
key={idx}
$state={el.title === activeTab ? 'active' : 'unactive'}
onClick={(e) => handleTab(e, el.title)}
>
{el.title}
</TabItem>
);
})}
</TabWrapper>
<AnimatedPageWrapper>
<Title>유저 지표</Title>
<TabWrapper>
{TabUserIndexList.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 === 'DAU' && <DailyActiveUserContent />}*/}
{activeTab === '이용자 지표' && <UserContent />}
{activeTab === 'Retention' && <RetentionContent />}
{activeTab === 'Segment' && <SegmentContent />}
{activeTab === '플레이타임' && <PlayTimeContent />}
{/*{activeTab === '메달' && <DailyMedalContent />}*/}
</>
)}
</>
{/*{activeTab === 'DAU' && <DailyActiveUserContent />}*/}
{activeTab === 'USER' && <UserContent />}
{activeTab === 'RETENTION' && <RetentionContent />}
{activeTab === 'SEGMENT' && <SegmentContent />}
{activeTab === 'PLAYTIME' && <PlayTimeContent />}
{/*{activeTab === '메달' && <DailyMedalContent />}*/}
</AnimatedPageWrapper>
);
};
export default UserIndex;
export default withAuth(authType.userIndicatorsRead)(UserIndex);
const TabItem = styled(Link)`
display: inline-flex;

View File

@@ -30,7 +30,11 @@ import {
} from '../../components/common';
import { convertKTC, convertKTCDate, convertUTC, timeDiffMinute } from '../../utils';
import { BattleEventModal } from '../../components/ServiceManage';
import { INITIAL_PAGE_SIZE, INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import {
INITIAL_PAGE_SIZE,
INITIAL_PAGE_LIMIT,
BATTLE_EVENT_OPERATION_TIME_WAIT_SECONDS,
} from '../../assets/data/adminConstants';
import { useDataFetch, useModal, useTable, withAuth } from '../../hooks/hook';
import { StatusWapper, StatusLabel } from '../../styles/ModuleComponents';
import { battleEventStatus, battleRepeatType } from '../../assets/data/options';
@@ -44,6 +48,7 @@ import { useLoading } from '../../context/LoadingProvider';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import { historyTables } from '../../assets/data/data';
import { LogHistory } from '../../apis';
import { AnimatedPageWrapper } from '../../components/common/Layout';
const BattleEvent = () => {
const token = sessionStorage.getItem('token');
@@ -98,7 +103,7 @@ const BattleEvent = () => {
const endTime = (start_dt, operation_time) =>{
const startDate = new Date(start_dt);
startDate.setSeconds(startDate.getSeconds() + operation_time);
startDate.setSeconds(startDate.getSeconds() + operation_time + BATTLE_EVENT_OPERATION_TIME_WAIT_SECONDS);
return startDate;
}
@@ -251,7 +256,7 @@ const BattleEvent = () => {
}
return (
<>
<AnimatedPageWrapper>
<Title>전투시스템 타임 스케줄러</Title>
<FormWrapper>
<BattleEventSearchBar
@@ -295,16 +300,16 @@ const BattleEvent = () => {
<th width="90">그룹</th>
<th width="70">이벤트 ID</th>
<th width="200">이벤트명</th>
<th width="90">게임모드</th>
<th width="80">반복</th>
<th width="100">기간 시작일(KST)</th>
<th width="100">기간 종료일(KST)</th>
<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="70">라운드 </th>
{/*<th width="70">라운드 수</th>*/}
<th width="70">핫타임</th>
<th width="100">확인 / 수정</th>
<th width="150">히스토리</th>
@@ -321,6 +326,7 @@ const BattleEvent = () => {
<td>{battle.group_id}</td>
<td>{battle.id}</td>
<td>{battle.event_name}</td>
<td>{battle.game_mode_id}</td>
<StatusWapper>
<StatusLabel $status={battle.repeat_type}>
{battleRepeatType.find(data => data.value === battle.repeat_type).name}
@@ -335,10 +341,9 @@ const BattleEvent = () => {
{battleEventStatus.find(data => data.value === battle.status).name}
</StatusLabel>
</StatusWapper>
<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.round_count}</td>*/}
<td>{battle.hot_time}</td>
<td>
<Button theme="line" text="상세보기"
@@ -377,7 +382,7 @@ const BattleEvent = () => {
title="히스토리"
/>
</>
</AnimatedPageWrapper>
)
};

View File

@@ -36,6 +36,7 @@ import { useLoading } from '../../context/LoadingProvider';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import { historyTables } from '../../assets/data/data';
import { LogHistory } from '../../apis';
import { AnimatedPageWrapper } from '../../components/common/Layout';
const Board = () => {
const token = sessionStorage.getItem('token');
@@ -127,7 +128,7 @@ const Board = () => {
}
return (
<>
<AnimatedPageWrapper>
<Title>인게임 메시지</Title>
<TableInfo>
<ListOption>
@@ -217,7 +218,7 @@ const Board = () => {
title="히스토리"
/>
</TableWrapper>
</>
</AnimatedPageWrapper>
);
};

View File

@@ -35,6 +35,7 @@ import { alertTypes } from '../../assets/data/types';
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
import { historyTables } from '../../assets/data/data';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import {AnimatedPageWrapper} from '../../components/common/Layout';
const Event = () => {
const token = sessionStorage.getItem('token');
@@ -158,7 +159,7 @@ const Event = () => {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventRead) ? (
<AuthModal />
) : (
<>
<AnimatedPageWrapper>
<Title>출석 보상 이벤트 관리</Title>
<FormWrapper>
<CommonSearchBar
@@ -281,7 +282,7 @@ const Event = () => {
<ModalSubText $color={deleteDesc.length > 29 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({deleteDesc.length}/30)</ModalSubText>
</ModalInputItem>
</DynamicModal>
</>
</AnimatedPageWrapper>
)}
</>
);

View File

@@ -33,6 +33,7 @@ import { timeDiffMinute } from '../../utils';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes, currencyCodeTypes } from '../../assets/data/types';
import { useLoading } from '../../context/LoadingProvider';
import { AnimatedPageWrapper } from '../../components/common/Layout';
const EventRegist = () => {
const navigate = useNavigate();
@@ -275,7 +276,7 @@ const EventRegist = () => {
};
return (
<>
<AnimatedPageWrapper>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) ? (
<AuthModal/>
) : (
@@ -458,7 +459,7 @@ const EventRegist = () => {
</BtnWrapper>
</>
)}
</>
</AnimatedPageWrapper>
);
};

View File

@@ -14,6 +14,7 @@ import { useLoading } from '../../context/LoadingProvider';
import useEnhancedCommonSearch from '../../hooks/useEnhancedCommonSearch';
import CustomConfirmModal from '../../components/common/modal/CustomConfirmModal';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../../components/common/Layout';
const Items = () => {
const token = sessionStorage.getItem('token');
@@ -155,7 +156,7 @@ const Items = () => {
}
return (
<>
<AnimatedPageWrapper>
<Title>아이템 관리</Title>
<FormWrapper>
<CommonSearchBar
@@ -199,7 +200,7 @@ const Items = () => {
handleCancel={() => handleModalClose('delete')}
handleClose={() => handleModalClose('delete')}
/>
</>
</AnimatedPageWrapper>
);
};

View File

@@ -36,6 +36,7 @@ import { alertTypes } from '../../assets/data/types';
import { useAlert } from '../../context/AlertProvider';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import { historyTables } from '../../assets/data/data';
import { AnimatedPageWrapper } from '../../components/common/Layout';
const LandAuction = () => {
const token = sessionStorage.getItem('token');
@@ -161,7 +162,7 @@ const LandAuction = () => {
}
return (
<>
<AnimatedPageWrapper>
<Title>랜드 경매 관리</Title>
<FormWrapper>
<LandAuctionSearchBar
@@ -277,7 +278,7 @@ const LandAuction = () => {
title="히스토리"
/>
</>
</AnimatedPageWrapper>
)
};

View File

@@ -34,6 +34,7 @@ import { useLoading } from '../../context/LoadingProvider';
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import { historyTables } from '../../assets/data/data';
import { AnimatedPageWrapper } from '../../components/common/Layout';
const Mail = () => {
const token = sessionStorage.getItem('token');
@@ -134,7 +135,7 @@ const Mail = () => {
}
return (
<>
<AnimatedPageWrapper>
<Title>우편 조회 발송 관리</Title>
<FormWrapper>
<CommonSearchBar
@@ -238,7 +239,7 @@ const Mail = () => {
title="히스토리"
/>
</>
</AnimatedPageWrapper>
);
};

View File

@@ -15,6 +15,7 @@ import {
Textarea,
SearchBarAlert,
} from '../../styles/Components';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import IconDelete from '../../assets/img/icon/icon-delete.png';
import CloseIcon from '../../assets/img/icon/icon-close.png';
@@ -351,7 +352,7 @@ const MailRegist = () => {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.mailUpdate) ? (
<AuthModal />
) : (
<>
<AnimatedPageWrapper>
<Title>우편 등록</Title>
<RegistGroup>
@@ -637,7 +638,7 @@ const MailRegist = () => {
handleClick={() => handleSubmit('submit')}
/>
</BtnWrapper>
</>
</AnimatedPageWrapper>
)}
</>
);

View File

@@ -1,26 +1,28 @@
import { useState, Fragment, useRef } from 'react';
import React, { useState, Fragment, useRef } from 'react';
import 'react-datepicker/dist/react-datepicker.css';
import {
authType,
authType, commonStatus as CommonStatus,
} from '../../assets/data';
import { Title, FormWrapper} from '../../styles/Components';
import {
Pagination,
CaliTable, TableHeader,
} from '../../components/common';
import { convertKTC, timeDiffMinute } from '../../utils';
import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import { useModal, useTable, withAuth } from '../../hooks/hook';
import { MenuBannerDelete, MenuBannerDetailView } from '../../apis';
import { LogHistory, MenuBannerDelete, MenuBannerDetailView } from '../../apis';
import { useNavigate } from 'react-router-dom';
import tableInfo from '../../assets/data/pages/menuBannerTable.json'
import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage';
import { CommonSearchBar } 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';
import { historyTables } from '../../assets/data/data';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import { AnimatedPageWrapper } from '../../components/common/Layout';
const MenuBanner = () => {
const tableRef = useRef(null);
@@ -30,6 +32,7 @@ const MenuBanner = () => {
const token = sessionStorage.getItem('token');
const [detailData, setDetailData] = useState({});
const [historyData, setHistoryData] = useState({});
const {
modalState,
@@ -37,6 +40,7 @@ const MenuBanner = () => {
handleModalClose
} = useModal({
detail: 'hidden',
history: 'hidden',
});
const {
@@ -49,10 +53,6 @@ const MenuBanner = () => {
updateSearchParams,
loading,
configLoaded,
paginationType,
pagination,
goToNextPage,
goToPrevPage,
handlePageChange,
handlePageSizeChange
} = useEnhancedCommonSearch("menuBannerSearch");
@@ -65,6 +65,17 @@ const MenuBanner = () => {
const handleAction = async (action, item = null) => {
switch (action) {
case "history":
const params = {};
params.db_type = "MYSQL"
params.sql_id = item.id;
params.table_name = historyTables.menuBanner
await LogHistory(token, params).then(data => {
setHistoryData(data);
handleModalView('history');
});
break;
case "detail":
await MenuBannerDetailView(token, item.id).then(data => {
setDetailData(data.detail);
@@ -72,38 +83,22 @@ const MenuBanner = () => {
});
break;
case "delete":
const date_check = selectedRows.every(row => {
const timeDiff = timeDiffMinute(convertKTC(row.auction_start_dt), (new Date));
return timeDiff < 3;
});
if(date_check){
showToast('LAND_AUCTION_DELETE_DATE_WARNING', {type: alertTypes.warning});
return;
}
showModal('MENU_BANNER_SELECT_DELETE', {
type: alertTypes.confirm,
onConfirm: () => handleAction('deleteConfirm')
});
break;
case "deleteConfirm":
let list = [];
let isChecked = false;
const low = selectedRows[0];
selectedRows.map(data => {
// const row = dataList.list.find(row => row.id === Number(data.id));
// if(row.status !== commonStatus.wait) isChecked = true;
list.push({
id: data.id,
});
});
if(isChecked) {
showToast('LAND_AUCTION_WARNING_DELETE', {type: alertTypes.warning});
if(low.status !== CommonStatus.wait) {
showToast('DELETE_STATUS_ONLY_WAIT', {type: alertTypes.warning});
return;
}
await withLoading(async () => {
return await MenuBannerDelete(token, list);
return await MenuBannerDelete(token, low.id);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('DEL_COMPLETE', {type: alertTypes.success});
@@ -119,7 +114,6 @@ const MenuBanner = () => {
}).finally(() => {
handleSearch(updateSearchParams);
});
break;
default:
@@ -128,7 +122,7 @@ const MenuBanner = () => {
};
return (
<>
<AnimatedPageWrapper>
<Title>메뉴 배너 관리</Title>
{/* 조회조건 */}
@@ -183,13 +177,24 @@ const MenuBanner = () => {
{/* 상세 */}
<MenuBannerDetailModal
detailView={modalState.detailModal}
handleDetailView={() => handleModalClose('detail')}
handleDetailView={() => {
handleModalClose('detail');
handleSearch(updateSearchParams);
}}
content={detailData}
setDetailData={setDetailData}
/>
</>
<LogDetailModal
viewMode="changed"
detailView={modalState.historyModal}
handleDetailView={() => handleModalClose('history')}
changedData={historyData}
title="히스토리"
/>
</AnimatedPageWrapper>
)
};
export default withAuth(authType.battleEventRead)(MenuBanner);
export default withAuth(authType.menuBannerRead)(MenuBanner);

View File

@@ -4,12 +4,12 @@ import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import Button from '../../components/common/button/Button';
import Loading from '../../components/common/Loading';
import {
Title,
BtnWrapper,
SearchBarAlert,
} from '../../styles/Components';
import { AnimatedPageWrapper } from '../../components/common/Layout';
import { useNavigate } from 'react-router-dom';
import { MenuBannerSingleRegist } from '../../apis';
@@ -19,14 +19,14 @@ import {
FormInput, FormInputSuffix, FormInputSuffixWrapper, FormLabel, FormRowGroup,RegistGroup,
} from '../../styles/ModuleComponents';
import AuthModal from '../../components/common/modal/AuthModal';
import { authType, modalTypes } from '../../assets/data';
import { loadConfig, timeDiffMinute } from '../../utils';
import { authType } from '../../assets/data';
import { loadConfig } from '../../utils';
import { SingleDatePicker, SingleTimePicker } from '../../components/common';
import CheckBox from '../../components/common/input/CheckBox';
import ImageUploadBtn from '../../components/ServiceManage/ImageUploadBtn';
import CaliForm from '../../components/common/Custom/CaliForm';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { useLoading } from '../../context/LoadingProvider';
const MenuBannerRegist = () => {
const navigate = useNavigate();
@@ -34,8 +34,7 @@ const MenuBannerRegist = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const { showToast, showModal } = useAlert();
const [loading, setLoading] = useState(false); // 로딩 창
const {withLoading} = useLoading();
const [isNullValue, setIsNullValue] = useState(false); // 데이터 값 체크
const [alertMsg, setAlertMsg] = useState('');
@@ -45,7 +44,6 @@ const MenuBannerRegist = () => {
const [pageConfig, setPageConfig] = useState(null);
const [formData, setFormData] = useState({});
const [isFormValid, setIsFormValid] = useState(false);
useEffect(() => {
if(alertMsg){
@@ -69,19 +67,6 @@ const MenuBannerRegist = () => {
loadPageConfig();
}, []);
const handleFieldValidation = (isValid, errors) => {
setIsFormValid(isValid);
if (errors._form) {
setAlertMsg(t(errors._form));
}
};
// 폼 제출 핸들러
const handleFormSubmit = (data) => {
setFormData(data);
};
useEffect(() => {
if (checkCondition()) {
@@ -109,7 +94,7 @@ const MenuBannerRegist = () => {
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
showToast('DATE_START_DIFF_END_WARNING', {type: alertTypes.warning} );
return;
}
}
@@ -153,7 +138,7 @@ const MenuBannerRegist = () => {
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
showToast('DATE_START_DIFF_END_WARNING', {type: alertTypes.warning} );
return;
}
@@ -226,27 +211,43 @@ const MenuBannerRegist = () => {
navigate('/servicemanage/menubanner');
break;
case "registConfirm":
setLoading(true);
const result = await MenuBannerSingleRegist(token, resultData);
await withLoading(async () => {
return await MenuBannerSingleRegist(token, resultData);
}).then(result => {
// console.log(result);
if(result.result === 'ERROR'){
setResultData(prevData => ({
...prevData,
image_list: prevData.image_list.map(img => ({
...img,
content: ''
}))
}));
setLoading(false);
showToast('REGIST_COMPLTE', {
type: alertTypes.success,
duration: 4000,
});
navigate('/servicemanage/menubanner');
break;
case "warning":
setAlertMsg('');
showToast(result.data.message, {
type: alertTypes.error
});
}else if(result.result === 'SUCCESS'){
showToast('REGIST_COMPLTE', {
type: alertTypes.success,
duration: 4000,
});
navigate('/servicemanage/menubanner');
}
}).catch(error => {
showToast(error, {type: alertTypes.error} );
}).finally(() => {
})
break;
}
}
const checkCondition = () => {
return (
(resultData.start_dt.length !== 0) &&
(resultData.end_dt.length !== 0) &&
(resultData.start_dt && resultData.start_dt.length !== 0) &&
(resultData.end_dt && resultData.end_dt.length !== 0) &&
resultData.title !== '' &&
resultData.image_list.every(data => data.content !== '') &&
(resultData.is_link === false || (resultData.is_link === true && resultData.link_list.every(data => data.content !== '')))
@@ -258,7 +259,7 @@ const MenuBannerRegist = () => {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) ? (
<AuthModal/>
) : (
<>
<AnimatedPageWrapper>
<Title>메뉴배너 등록</Title>
<RegistGroup>
<FormRowGroup>
@@ -315,12 +316,11 @@ const MenuBannerRegist = () => {
/>
</FormRowGroup>
{resultData?.is_link &&
<>
<FormRowGroup>
<FormLabel> 링크</FormLabel>
<LanguageWrapper width="50%" >
{resultData.link_list.map((data, idx) => (
<FormInputSuffixWrapper>
<FormInputSuffixWrapper key={idx}>
<FormInput
type="text"
value={resultData?.link_list[idx].content}
@@ -336,7 +336,6 @@ const MenuBannerRegist = () => {
))}
</LanguageWrapper>
</FormRowGroup>
</>
}
</RegistGroup>
@@ -367,9 +366,7 @@ const MenuBannerRegist = () => {
})}
/>
</BtnWrapper>
{loading && <Loading/>}
</>
</AnimatedPageWrapper>
)}
</>
);
@@ -381,14 +378,14 @@ const initData = {
start_dt: '',
end_dt: '',
image_list: [
{ language: 'KO', content: '' },
{ language: 'EN', content: '' },
{ language: 'JA', content: '' },
{ language: 'Ko', content: '' },
{ language: 'En', content: '' },
{ language: 'Ja', content: '' },
],
link_list: [
{ language: 'KO', content: '' },
{ language: 'EN', content: '' },
{ language: 'JA', content: '' },
{ language: 'Ko', content: '' },
{ language: 'En', content: '' },
{ language: 'Ja', content: '' },
],
}

View File

@@ -1,433 +0,0 @@
import React, { useState, Fragment, useEffect } from 'react';
import styled from 'styled-components';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import Button from '../../components/common/button/Button';
import Loading from '../../components/common/Loading';
import {
Title,
BtnWrapper,
SearchBarAlert,
} from '../../styles/Components';
import { useNavigate } from 'react-router-dom';
import { MenuBannerSingleRegist } from '../../apis';
import { authList } from '../../store/authList';
import {
FormInput, FormInputSuffix, FormInputSuffixWrapper, FormLabel, FormRowGroup,RegistGroup,
} from '../../styles/ModuleComponents';
import AuthModal from '../../components/common/modal/AuthModal';
import { authType, modalTypes } from '../../assets/data';
import DynamicModal from '../../components/common/modal/DynamicModal';
import { loadConfig, timeDiffMinute } from '../../utils';
import { SingleDatePicker, SingleTimePicker } from '../../components/common';
import CheckBox from '../../components/common/input/CheckBox';
import ImageUploadBtn from '../../components/ServiceManage/ImageUploadBtn';
import { useModal } from '../../hooks/hook';
const MenuBannerRegist = () => {
const navigate = useNavigate();
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const [loading, setLoading] = useState(false); // 로딩 창
const {
modalState,
handleModalView,
handleModalClose
} = useModal({
cancel: 'hidden',
registConfirm: 'hidden',
registComplete: 'hidden'
});
const [isNullValue, setIsNullValue] = useState(false); // 데이터 값 체크
const [alertMsg, setAlertMsg] = useState('');
const [resultData, setResultData] = useState(initData); //데이터 정보
const [resetDateTime, setResetDateTime] = useState(false);
const [pageConfig, setPageConfig] = useState(null);
const [formData, setFormData] = useState({});
useEffect(() => {
const loadPageConfig = async () => {
try {
const config = await loadConfig('menuBannerRegist');
setPageConfig(config);
setFormData(config.initData);
} catch (error) {
console.error('Failed to load page configuration', error);
}
};
loadPageConfig();
}, []);
useEffect(() => {
if (checkCondition()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
useEffect(() => {
if (resetDateTime) {
setResetDateTime(false);
}
}, [resetDateTime]);
// 시작 날짜 변경 핸들러
const handleStartDateChange = (date) => {
if (!date) return;
const newDate = new Date(date);
if(resultData.end_dt){
const endDate = new Date(resultData.end_dt);
const startDay = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
return;
}
}
setResultData(prev => ({
...prev,
start_dt: newDate
}));
};
// 시작 시간 변경 핸들러
const handleStartTimeChange = (time) => {
if (!time) return;
const newDateTime = resultData.start_dt
? new Date(resultData.start_dt)
: new Date();
newDateTime.setHours(
time.getHours(),
time.getMinutes(),
0,
0
);
setResultData(prev => ({
...prev,
start_dt: newDateTime
}));
};
// 종료 날짜 변경 핸들러
const handleEndDateChange = (date) => {
if (!date || !resultData.start_dt) return;
const startDate = new Date(resultData.start_dt);
const endDate = new Date(date);
// 일자만 비교하기 위해 년/월/일만 추출
const startDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
return;
}
setResultData(prev => ({
...prev,
end_dt: endDate
}));
};
// 종료 시간 변경 핸들러
const handleEndTimeChange = (time) => {
if (!time) return;
const newDateTime = resultData.end_dt
? new Date(resultData.end_dt)
: new Date();
newDateTime.setHours(
time.getHours(),
time.getMinutes(),
0,
0
);
setResultData(prev => ({
...prev,
end_dt: newDateTime
}));
};
// 이미지 업로드
const handleImageUpload = (language, file, fileName) => {
const imageIndex = resultData.image_list.findIndex(img => img.language === language);
if (imageIndex !== -1) {
const updatedImageList = [...resultData.image_list];
updatedImageList[imageIndex] = {
...updatedImageList[imageIndex],
content: fileName,
};
setResultData({
...resultData,
image_list: updatedImageList
});
}
};
// 이미지 삭제
const handleImageDelete = (language) => {
const imageIndex = resultData.image_list.findIndex(img => img.language === language);
if (imageIndex !== -1) {
const updatedImageList = [...resultData.image_list];
updatedImageList[imageIndex] = {
...updatedImageList[imageIndex],
content: '',
};
setResultData({
...resultData,
image_list: updatedImageList
});
}
};
const handleSubmit = async (type, param = null) => {
switch (type) {
case "submit":
if (!checkCondition()) return;
const timeDiff = timeDiffMinute(resultData.start_dt, (new Date))
if(timeDiff < 60) {
setAlertMsg(t('EVENT_TIME_LIMIT_ADD'));
return;
}
handleModalView('registConfirm');
break;
case "cancel":
handleModalClose('cancel');
navigate('/servicemanage/menubanner');
break;
case "registConfirm":
setLoading(true);
const result = await MenuBannerSingleRegist(token, resultData);
setLoading(false);
handleModalClose('registConfirm');
handleModalView('registComplete');
break;
case "registComplete":
handleModalClose('registComplete');
navigate('/servicemanage/menubanner');
break;
case "warning":
setAlertMsg('');
break;
}
}
const checkCondition = () => {
return (
(resultData.start_dt.length !== 0) &&
(resultData.end_dt.length !== 0) &&
resultData.title !== '' &&
resultData.image_list.every(data => data.content !== '') &&
(resultData.is_link === false || (resultData.is_link === true && resultData.link_list.every(data => data.content !== '')))
);
};
return (
<>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) ? (
<AuthModal/>
) : (
<>
<Title>메뉴배너 등록</Title>
<RegistGroup>
<FormRowGroup>
<FormLabel>등록기간</FormLabel>
<SingleDatePicker
label="시작일자"
dateLabel="시작 일자"
onDateChange={handleStartDateChange}
selectedDate={resultData?.start_dt}
/>
<SingleTimePicker
selectedTime={resultData?.start_dt}
onTimeChange={handleStartTimeChange}
/>
<SingleDatePicker
label="종료일자"
dateLabel="종료 일자"
onDateChange={handleEndDateChange}
selectedDate={resultData?.end_dt}
/>
<SingleTimePicker
selectedTime={resultData?.end_dt}
onTimeChange={handleEndTimeChange}
/>
</FormRowGroup>
<FormRowGroup>
<FormLabel>배너 제목</FormLabel>
<FormInput
type="text"
width='50%'
value={resultData?.title}
onChange={e => setResultData({ ...resultData, title: e.target.value })}
/>
</FormRowGroup>
<FormLabel>이미지 첨부</FormLabel>
{resultData.image_list.map((data, idx) => (
<LanguageWrapper key={idx}>
<LanguageLabel>{data.language}</LanguageLabel>
<ImageUploadBtn
onImageUpload={(file, fileName) => handleImageUpload(data.language, file, fileName)}
onFileDelete={() => handleImageDelete(data.language)}
fileName={data.content}
setAlertMessage={setAlertMsg}
/>
</LanguageWrapper>
))}
<FormRowGroup>
<CheckBox
label="이미지 링크 여부"
id="reserve"
checked={resultData.is_link}
setData={e => setResultData({ ...resultData, is_link: e.target.checked })}
/>
</FormRowGroup>
{resultData?.is_link &&
<>
<FormRowGroup>
<FormLabel> 링크</FormLabel>
<LanguageWrapper width="50%" >
{resultData.link_list.map((data, idx) => (
<FormInputSuffixWrapper>
<FormInput
type="text"
value={resultData?.link_list[idx].content}
onChange={e => {
const updatedLinkList = [...resultData.link_list];
updatedLinkList[idx] = { ...updatedLinkList[idx], content: e.target.value };
setResultData({ ...resultData, link_list: updatedLinkList });
}}
suffix="true"
/>
<FormInputSuffix>{data.language}</FormInputSuffix>
</FormInputSuffixWrapper>
))}
</LanguageWrapper>
</FormRowGroup>
</>
}
</RegistGroup>
{isNullValue && (
<SearchBarAlert $align="right" $padding="0 0 15px">
{t('NULL_MSG')}
</SearchBarAlert>
)}
<BtnWrapper $justify="flex-end" $gap="10px">
<Button text="취소" theme="line" handleClick={() => handleModalView('cancel')} />
<Button
type="submit"
text="등록"
theme={checkCondition() ? 'primary' : 'disable'}
handleClick={() => handleSubmit('submit')}
/>
</BtnWrapper>
{/* 등록 모달 */}
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.registConfirmModal}
modalText={t('MENU_BANNER_REGIST_CONFIRM')}
handleSubmit={() => handleSubmit('registConfirm')}
handleCancel={() => handleModalClose('registConfirm')}
/>
{/* 완료 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={modalState.registCompleteModal}
modalText={t('REGIST_COMPLTE')}
handleSubmit={() => handleSubmit('registComplete')}
/>
{/* 취소 모달 */}
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.cancelModal}
modalText={t('MENU_BANNER_REGIST_CANCEL')}
handleCancel={() => handleModalClose('cancel')}
handleSubmit={() => handleSubmit('cancel')}
/>
{/* 경고 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={alertMsg ? 'view' : 'hidden'}
modalText={alertMsg}
handleSubmit={() => handleSubmit('warning')}
/>
{loading && <Loading/>}
</>
)}
</>
);
};
const initData = {
title: '',
is_link: false,
start_dt: '',
end_dt: '',
image_list: [
{ language: 'KO', content: '' },
{ language: 'EN', content: '' },
{ language: 'JA', content: '' },
],
link_list: [
{ language: 'KO', content: '' },
{ language: 'EN', content: '' },
{ language: 'JA', content: '' },
],
}
export default MenuBannerRegist;
const LanguageWrapper = styled.div`
width: ${props => props.width || '100%'};
//margin-bottom: 20px;
padding-bottom: 20px;
padding-left: 90px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
`;
const LanguageLabel = styled.h4`
color: #444;
margin: 0 0 10px 20px;
font-size: 16px;
font-weight: 500;
`;

View File

@@ -1,341 +0,0 @@
import { useState, Fragment, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import 'react-datepicker/dist/react-datepicker.css';
import { authList } from '../../store/authList';
import {
authType,
modalTypes,
landAuctionStatusType, opYNType,
} from '../../assets/data';
import { Title, FormWrapper, TableStyle, TableWrapper} from '../../styles/Components';
import {
CheckBox,
Button,
DynamicModal,
Pagination,
ViewTableInfo, CaliTable, TableHeader,
} from '../../components/common';
import { convertKTC, timeDiffMinute } from '../../utils';
import { INITIAL_PAGE_SIZE, INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import { useModal, useTable, withAuth } from '../../hooks/hook';
import { StatusWapper, StatusLabel } from '../../styles/ModuleComponents';
import { opMenuBannerStatus } from '../../assets/data/options';
import MenuBannerSearchBar, { useMenuBannerSearch } from '../../components/searchBar/MenuBannerSearchBar';
import { MenuBannerDelete, MenuBannerDetailView } from '../../apis';
import { useNavigate } from 'react-router-dom';
import MenuBannerModal from '../../components/modal/MenuBannerModal';
import tableInfo from '../../assets/data/pages/menuBannerTable.json'
const MenuBanner = () => {
const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const tableRef = useRef(null);
const navigate = useNavigate();
const [detailData, setDetailData] = useState({});
const {
modalState,
handleModalView,
handleModalClose
} = useModal({
detail: 'hidden',
deleteConfirm: 'hidden',
deleteComplete: 'hidden'
});
const [alertMsg, setAlertMsg] = useState('');
const [modalType, setModalType] = useState('regist');
const {
searchParams,
data: dataList,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
} = useMenuBannerSearch(token, INITIAL_PAGE_SIZE);
const {
selectedRows,
handleSelectRow,
isRowSelected
} = useTable(dataList?.event_list || [], {mode: 'single'});
const handleModalSubmit = async (type, param = null) => {
switch (type) {
case "regist":
setModalType('regist');
handleModalView('detail');
break;
case "detail":
await MenuBannerDetailView(token, param).then(data => {
setDetailData(data.event_detail);
setModalType('modify');
handleModalView('detail');
});
break;
case "delete":
const date_check = selectedRows.every(row => {
const timeDiff = timeDiffMinute(convertKTC(row.auction_start_dt), (new Date));
return timeDiff < 3;
});
if(date_check){
setAlertMsg(t('LAND_AUCTION_DELETE_DATE_WARNING'));
return;
}
if(selectedRows[0].status === landAuctionStatusType.auction_start || selectedRows[0].status === landAuctionStatusType.stl_end){
setAlertMsg(t('LAND_AUCTION_DELETE_STATUS_WARNING'));
return;
}
handleModalView('deleteConfirm');
break;
case "deleteConfirm":
let list = [];
let isChecked = false;
selectedRows.map(data => {
// const row = dataList.list.find(row => row.id === Number(data.id));
// if(row.status !== commonStatus.wait) isChecked = true;
list.push({
id: data.id,
});
});
if(isChecked) {
setAlertMsg(t('LAND_AUCTION_WARNING_DELETE'))
handleModalClose('deleteConfirm');
return;
}
await MenuBannerDelete(token, list).then(data => {
handleModalClose('deleteConfirm');
if(data.result === "SUCCESS") {
handleModalView('deleteComplete');
}else if(data.result === "ERROR_AUCTION_STATUS_IMPOSSIBLE"){
setAlertMsg(t('LAND_AUCTION_ERROR_DELETE_STATUS'));
}else{
setAlertMsg(t('DELETE_FAIL'));
}
}).catch(reason => {
setAlertMsg(t('API_FAIL'));
});
break;
case "deleteComplete":
handleModalClose('deleteComplete');
window.location.reload();
break;
case "warning":
setAlertMsg('')
break;
}
}
const handleAction = async (action, item = null) => {
switch (action) {
case "regist":
setModalType('regist');
handleModalView('detail');
break;
case "detail":
await MenuBannerDetailView(token, item).then(data => {
setDetailData(data.event_detail);
setModalType('modify');
handleModalView('detail');
});
break;
case "delete":
const date_check = selectedRows.every(row => {
const timeDiff = timeDiffMinute(convertKTC(row.auction_start_dt), (new Date));
return timeDiff < 3;
});
if(date_check){
setAlertMsg(t('LAND_AUCTION_DELETE_DATE_WARNING'));
return;
}
if(selectedRows[0].status === landAuctionStatusType.auction_start || selectedRows[0].status === landAuctionStatusType.stl_end){
setAlertMsg(t('LAND_AUCTION_DELETE_STATUS_WARNING'));
return;
}
handleModalView('deleteConfirm');
break;
case "deleteConfirm":
let list = [];
let isChecked = false;
selectedRows.map(data => {
// const row = dataList.list.find(row => row.id === Number(data.id));
// if(row.status !== commonStatus.wait) isChecked = true;
list.push({
id: data.id,
});
});
if(isChecked) {
setAlertMsg(t('LAND_AUCTION_WARNING_DELETE'))
handleModalClose('deleteConfirm');
return;
}
await MenuBannerDelete(token, list).then(data => {
handleModalClose('deleteConfirm');
if(data.result === "SUCCESS") {
handleModalView('deleteComplete');
}else if(data.result === "ERROR_AUCTION_STATUS_IMPOSSIBLE"){
setAlertMsg(t('LAND_AUCTION_ERROR_DELETE_STATUS'));
}else{
setAlertMsg(t('DELETE_FAIL'));
}
}).catch(reason => {
setAlertMsg(t('API_FAIL'));
});
break;
case "deleteComplete":
handleModalClose('deleteComplete');
window.location.reload();
break;
case "warning":
setAlertMsg('')
break;
default:
break;
}
};
return (
<>
<Title>메뉴 배너 관리</Title>
<FormWrapper>
<MenuBannerSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
{/*<ViewTableInfo total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>*/}
{/* {userInfo.auth_list?.some(auth => auth.id === authType.battleEventDelete) && (*/}
{/* <Button theme={selectedRows.length === 0 ? 'disable' : 'line'} text="선택 삭제" handleClick={() => handleModalSubmit('delete')} />*/}
{/* )}*/}
{/* {userInfo.auth_list?.some(auth => auth.id === authType.battleEventUpdate) && (*/}
{/* <Button*/}
{/* theme="primary"*/}
{/* text="이미지 등록"*/}
{/* type="button"*/}
{/* handleClick={e => {*/}
{/* e.preventDefault();*/}
{/* navigate('/servicemanage/menubanner/menubannerregist');*/}
{/* }}*/}
{/* />*/}
{/* )}*/}
{/*</ViewTableInfo>*/}
<TableHeader
config={tableInfo.header}
total={dataList?.total}
total_all={dataList?.total_all}
handleOrderBy={handleOrderByChange}
handlePageSize={handlePageSizeChange}
selectedRows={selectedRows}
onAction={handleAction}
navigate={navigate}
/>
<CaliTable
columns={tableInfo.columns}
data={dataList?.list}
selectedRows={selectedRows}
onSelectRow={handleSelectRow}
onAction={handleAction}
refProp={tableRef}
/>
{/*<TableWrapper>*/}
{/* <TableStyle ref={tableRef}>*/}
{/* <caption></caption>*/}
{/* <thead>*/}
{/* <tr>*/}
{/* <th width="40"></th>*/}
{/* <th width="70">번호</th>*/}
{/* <th width="80">등록 상태</th>*/}
{/* <th width="150">시작일(KST)</th>*/}
{/* <th width="150">종료일(KST)</th>*/}
{/* <th width="300">설명 제목</th>*/}
{/* <th width="90">링크여부</th>*/}
{/* <th width="100">상세보기</th>*/}
{/* <th width="150">히스토리</th>*/}
{/* </tr>*/}
{/* </thead>*/}
{/* <tbody>*/}
{/* {dataList?.list?.map(banner => (*/}
{/* <tr key={banner.row_num}>*/}
{/* <td>*/}
{/* <CheckBox name={'select'} id={banner.id}*/}
{/* setData={(e) => handleSelectRow(e, banner)}*/}
{/* checked={isRowSelected(banner.id)} />*/}
{/* </td>*/}
{/* <td>{banner.row_num}</td>*/}
{/* <StatusWapper>*/}
{/* <StatusLabel $status={banner.status}>*/}
{/* {opMenuBannerStatus.find(data => data.value === banner.status)?.name}*/}
{/* </StatusLabel>*/}
{/* </StatusWapper>*/}
{/* <td>{convertKTC(banner.start_dt)}</td>*/}
{/* <td>{convertKTC(banner.end_dt)}</td>*/}
{/* <td>{banner.title}</td>*/}
{/* <td>{opYNType.find(data => data.value === banner.is_link)?.name}</td>*/}
{/* <td>*/}
{/* <Button theme="line" text="상세보기"*/}
{/* handleClick={e => handleModalSubmit('detail', banner.id)} />*/}
{/* </td>*/}
{/* <td>{banner.update_by}</td>*/}
{/* </tr>*/}
{/* ))}*/}
{/* </tbody>*/}
{/* </TableStyle>*/}
{/*</TableWrapper>*/}
<Pagination postsPerPage={searchParams.pageSize} totalPosts={dataList?.total_all} setCurrentPage={handlePageChange} currentPage={searchParams.currentPage} pageLimit={INITIAL_PAGE_LIMIT} />
{/*상세*/}
<MenuBannerModal modalType={modalType} detailView={modalState.detailModal} handleDetailView={() => handleModalClose('detail')} content={detailData} setDetailData={setDetailData} />
{/*삭제 확인*/}
<DynamicModal
modalType={modalTypes.confirmOkCancel}
view={modalState.deleteConfirmModal}
handleCancel={() => handleModalClose('deleteConfirm')}
handleSubmit={() => handleModalSubmit('deleteConfirm')}
modalText={t('MENU_BANNER_SELECT_DELETE')}
/>
{/*삭제 완료*/}
<DynamicModal
modalType={modalTypes.completed}
view={modalState.deleteCompleteModal}
handleSubmit={() => handleModalSubmit('deleteComplete')}
modalText={t('DEL_COMPLETE')}
/>
{/* 경고 모달 */}
<DynamicModal
modalType={modalTypes.completed}
view={alertMsg ? 'view' : 'hidden'}
modalText={alertMsg}
handleSubmit={() => handleModalSubmit('warning')}
/>
</>
)
};
export default withAuth(authType.battleEventRead)(MenuBanner);

View File

@@ -1,8 +1,9 @@
import { Fragment, useEffect, useState } from 'react';
import styled from 'styled-components';
import { AnimatedPageWrapper } from '../../components/common/Layout';
import CheckBox from '../../components/common/input/CheckBox';
import styled from 'styled-components';
import { Title, FormWrapper, TableInfo, ListCount, ListOption, TableStyle, SelectInput, TableWrapper, ButtonClose, ModalText, BtnWrapper } from '../../styles/Components';
import Button from '../../components/common/button/Button';
@@ -242,7 +243,7 @@ const ReportList = () => {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.reportRead) ? (
<AuthModal/>
) : (
<>
<AnimatedPageWrapper>
<Title>신고내역 조회 답변</Title>
<ReportListSummary />
<FormWrapper>
@@ -331,7 +332,7 @@ const ReportList = () => {
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleConfirmeModalClose} />
</BtnWrapper>
</Modal>
</>
</AnimatedPageWrapper>
)}
</>
);

View File

@@ -1,6 +1,7 @@
import React, { useState, Fragment } from 'react';
import { Title, FormWrapper, TextInput } from '../../styles/Components';
import { useNavigate } from 'react-router-dom';
import { AnimatedPageWrapper } from '../../components/common/Layout';
import {
CommonSearchBar,
UserBlockDetailModal,
@@ -130,7 +131,7 @@ const UserBlock = () => {
};
return (
<>
<AnimatedPageWrapper>
<Title>이용자 제재 조회 등록</Title>
<FormWrapper>
<CommonSearchBar
@@ -209,7 +210,7 @@ const UserBlock = () => {
<ModalSubText $color={deleteDesc.length > 29 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({deleteDesc.length}/30)</ModalSubText>
</ModalInputItem>
</DynamicModal>
</>
</AnimatedPageWrapper>
);
};

View File

@@ -1,5 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import {AnimatedPageWrapper} from '../../components/common/Layout'
import Button from '../../components/common/button/Button';
import RadioInput from '../../components/common/input/Radio';
@@ -267,7 +269,7 @@ const UserBlockRegist = () => {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.blackListUpdate) ? (
<AuthModal/>
) : (
<>
<AnimatedPageWrapper>
<Title>이용자 제재 등록</Title>
<div>
@@ -563,7 +565,7 @@ const UserBlockRegist = () => {
/>
</BtnWrapper>
</BtnWrapper>
</>
</AnimatedPageWrapper>
)}
</>
);

View File

@@ -1,5 +1,6 @@
import { Fragment, useEffect, useState } from 'react';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import CheckBox from '../../components/common/input/CheckBox';
import { Title, FormWrapper, SelectInput, BtnWrapper, TableInfo, ListCount, ListOption, TableStyle, State, ButtonClose, ModalText } from '../../styles/Components';
import Button from '../../components/common/button/Button';
@@ -292,7 +293,7 @@ function AdminView() {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === 1) ? (
<AuthModal />
) : (
<>
<AnimatedPageWrapper>
<Title>운영자 조회</Title>
<FormWrapper action="" $flow="row">
<AdminViewSearchBar handleSearch={handleSearch} groupList={groupList} setResultData={setSearchData} setCurrentPage={setCurrentPage} />
@@ -492,7 +493,7 @@ function AdminView() {
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handlePasswordInitialize} />
</BtnWrapper>
</Modal>
</>
</AnimatedPageWrapper>
)}
</>
);

View File

@@ -1,5 +1,6 @@
import CheckBox from '../../components/common/input/CheckBox';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import Modal from '../../components/common/modal/Modal';
import { Title, FormWrapper, SelectInput, TableInfo, ListCount, ListOption, TableStyle, BtnWrapper, ButtonClose, ModalText } from '../../styles/Components';
import Button from '../../components/common/button/Button';
@@ -168,7 +169,7 @@ const AuthSetting = () => {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.authoritySettingRead) ? (
<AuthModal />
) : (
<>
<AnimatedPageWrapper>
<Title>권한 설정</Title>
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.authoritySettingUpdate) && (
<FormWrapper action="" $flow="row">
@@ -270,7 +271,7 @@ const AuthSetting = () => {
/>
</BtnWrapper>
</Modal>
</>
</AnimatedPageWrapper>
)}
</>
);

View File

@@ -2,6 +2,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../../components/common/Layout';
import { authList } from '../../store/authList';
import { authType, modalTypes } from '../../assets/data';
@@ -100,7 +101,7 @@ const AuthSettingUpdate = () => {
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.authoritySettingUpdate) ? (
<AuthModal />
) : (
<>
<AnimatedPageWrapper>
<Title>권한 설정</Title>
<FormWrapper $flow="column">
<TableStyle>
@@ -148,7 +149,7 @@ const AuthSettingUpdate = () => {
</BtnWrapper>
</FormWrapper>
</>
</AnimatedPageWrapper>
)}
</>
);

View File

@@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import 'react-datepicker/dist/react-datepicker.css';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import { authList } from '../../store/authList';
import {
authType,
@@ -136,7 +137,7 @@ const CaliumRequest = () => {
}
return (
<>
<AnimatedPageWrapper>
<Title>칼리움 사용 수량 요청</Title>
<FormWrapper>
<CommonSearchBar
@@ -250,7 +251,7 @@ const CaliumRequest = () => {
title="히스토리"
/>
</>
</AnimatedPageWrapper>
);
};

View File

@@ -1,6 +1,7 @@
import React, { useState, Fragment, useMemo } from 'react';
import Button from '../../components/common/button/Button';
import Loading from '../../components/common/Loading';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import {
Title,
@@ -139,7 +140,7 @@ const DataInitView = () => {
}
return (
<>
<AnimatedPageWrapper>
<Title>데이터 초기화</Title>
<MessageWrapper>
<FormRowGroup>
@@ -227,7 +228,7 @@ const DataInitView = () => {
handleCancel={() => handleModalClose('registConfirm')}
/>
<TopButton />
</>
</AnimatedPageWrapper>
);
};

View File

@@ -1,5 +1,6 @@
import React, { Fragment, useCallback, useState } from 'react';
import { AnimatedPageWrapper } from '../../components/common/Layout'
import { CommonSearchBar } from '../../components/ServiceManage';
import { Title, FormWrapper } from '../../styles/Components';
import { authType } from '../../assets/data';
@@ -51,7 +52,7 @@ const LogView = () => {
};
return (
<>
<AnimatedPageWrapper>
<Title>사용 이력 조회</Title>
{/* 조회조건 */}
@@ -99,7 +100,7 @@ const LogView = () => {
title="상세정보"
/>
</>
</AnimatedPageWrapper>
);
};

View File

@@ -27,7 +27,7 @@ export const Container = styled.div`
export const HeaderContainer = styled.div`
width: 280px;
flex: 0 0 280px;
background: #666666;
background: #141414;
min-height: 100vh;
align-self: stretch;
`;
@@ -719,7 +719,7 @@ export const TabScroll = styled.div`
export const TabItem = styled(Link)`
display: inline-flex;
width: 120px;
width: 150px;
height: 30px;
justify-content: center;
align-items: center;

View File

@@ -114,8 +114,49 @@ export const numberFormatter = {
console.error('Currency formatting error:', e);
return '0';
}
},
formatPercent: (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 valueToFormat = num / 100;
return new Intl.NumberFormat('ko-KR', {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: decimals
}).format(valueToFormat);
} catch (e) {
console.error('Currency formatting error:', e);
return '0%';
}
},
formatSecondToMinuts: (seconds) => {
if (seconds === null || seconds === undefined) return '0:00';
try {
const num = typeof seconds === 'string' ? parseFloat(seconds) : seconds;
if (isNaN(num)) return '0:00';
// 총 초를 분과 초로 변환
const minutes = Math.floor(num / 60);
const remainingSeconds = Math.floor(num % 60);
// 초가 10보다 작으면 앞에 0을 붙여 두 자리로 표시
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : remainingSeconds;
return `${minutes}:${formattedSeconds}`;
} catch (e) {
console.error('Seconds to minutes formatting error:', e);
return '0:00';
}
}
};
/**