게임로그 아이템
게임로그 재화(아이템) 추가
This commit is contained in:
@@ -127,4 +127,80 @@ 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}¤cy_type=${currencyType}&amount_delta_type=${amountDeltaType}&start_dt=${startDate}&end_dt=${endDate}
|
||||
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,7 @@ 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 LOG_ACTION_FAIL_CALIUM_ECHO = 'FailCaliumEchoSystem';
|
||||
export const BATTLE_EVENT_OPERATION_TIME_WAIT_SECONDS = 300;
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ export const languageType = [
|
||||
|
||||
export const TabGameLogList = [
|
||||
{ value: 'CURRENCY', name: '재화 로그' },
|
||||
// { value: 'ITEM', name: '아이템 로그' },
|
||||
{ value: 'ITEM', name: '아이템 로그' },
|
||||
{ value: 'CURRENCYITEM', name: '재화(아이템) 로그' },
|
||||
// { value: 'TRADE', name: '거래 로그' },
|
||||
];
|
||||
|
||||
@@ -95,6 +96,12 @@ export const landAuctionStatus = [
|
||||
{ value: 'FAIL', name: '실패' },
|
||||
];
|
||||
|
||||
export const questStatus = [
|
||||
{ value: 'WAIT', name: '미완료' },
|
||||
{ value: 'COMPLETE', name: '완료' },
|
||||
{ value: 'RUNNING', name: '진행중' },
|
||||
];
|
||||
|
||||
export const currencyItemCode = [
|
||||
{ value: '19010001', name: '골드' },
|
||||
{ value: '19010002', name: '사파이어' },
|
||||
@@ -225,6 +232,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: '대기' },
|
||||
|
||||
167
src/components/DataManage/CurrencyItemLogContent.js
Normal file
167
src/components/DataManage/CurrencyItemLogContent.js
Normal file
@@ -0,0 +1,167 @@
|
||||
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';
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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 />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CurrencyItemLogContent;
|
||||
170
src/components/DataManage/ItemLogContent.js
Normal file
170
src/components/DataManage/ItemLogContent.js
Normal file
@@ -0,0 +1,170 @@
|
||||
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';
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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 />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CurrencyLogContent;
|
||||
@@ -145,7 +145,6 @@ const ProfileContainer = styled.div`
|
||||
`;
|
||||
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
background-color: #1677ff;
|
||||
`;
|
||||
|
||||
const StyledUsername = styled(Text)`
|
||||
|
||||
241
src/components/searchBar/CurrencyItemLogSearchBar.js
Normal file
241
src/components/searchBar/CurrencyItemLogSearchBar.js
Normal 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;
|
||||
252
src/components/searchBar/ItemLogSearchBar.js
Normal file
252
src/components/searchBar/ItemLogSearchBar.js
Normal 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;
|
||||
@@ -25,6 +25,8 @@ 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 LandAuctionSearchBar from './LandAuctionSearchBar';
|
||||
import CaliumRequestSearchBar from './CaliumRequestSearchBar';
|
||||
@@ -62,6 +64,10 @@ export {
|
||||
LandAuctionSearchBar,
|
||||
CaliumRequestSearchBar,
|
||||
CurrencyIndexSearchBar,
|
||||
useCurrencyIndexSearch
|
||||
useCurrencyIndexSearch,
|
||||
useItemLogSearch,
|
||||
ItemLogSearchBar,
|
||||
CurrencyItemLogSearchBar,
|
||||
useCurrencyItemLogSearch,
|
||||
};
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ 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: "선택된 이벤트를 삭제하시겠습니까?",
|
||||
@@ -165,6 +166,8 @@ const resources = {
|
||||
FILE_BUSINESS_LOG: 'Caliverse_Log.xlsx',
|
||||
FILE_BATTLE_EVENT: 'Caliverse_Battle_Event.xlsx',
|
||||
FILE_GAME_LOG_CURRENCY: 'Caliverse_Game_Log_Currency',
|
||||
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: '유저 정보를 확인해주세요.',
|
||||
|
||||
@@ -24,82 +24,11 @@ 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';
|
||||
|
||||
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');
|
||||
|
||||
@@ -135,9 +64,9 @@ const GameLogView = () => {
|
||||
})}
|
||||
</TabWrapper>
|
||||
</TabScroll>
|
||||
{/*{activeTab === 'ITEM' && <ItemLogContent />}*/}
|
||||
<CurrencyLogContent active={activeTab === 'CURRENCY'} />
|
||||
{/*{activeTab === 'TRADE' && <TradeLogContent />}*/}
|
||||
<ItemLogContent active={activeTab === 'ITEM'} />
|
||||
<CurrencyItemLogContent active={activeTab === 'CURRENCYITEM'} />
|
||||
</AnimatedPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user