유저 지표 잔존율 생성
This commit is contained in:
@@ -62,10 +62,14 @@ export const userIndexExport = async (token, filename, sendDate, endDate) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Retention
|
// Retention
|
||||||
export const RetentionIndexView = async (token, start_dt, end_dt) => {
|
export const RetentionIndexView = async (token, startDate, endDate, order, size, currentPage) => {
|
||||||
try {
|
try {
|
||||||
const res = await Axios.get(`/api/v1/indicators/retention/list?start_dt=${start_dt}&end_dt=${end_dt}`, {
|
const res = await Axios.get(`/api/v1/indicators/retention/list?start_dt=${startDate}&end_dt=${endDate}
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ export const TabEconomicIndexList = [
|
|||||||
// { value: 'instance', name: '인스턴스' },
|
// { value: 'instance', name: '인스턴스' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const TabUserIndexList = [
|
||||||
|
{ value: 'USER', name: '이용자 지표' },
|
||||||
|
{ value: 'RETENTION', name: '잔존율' },
|
||||||
|
// { value: 'SEGMENT', name: 'Segment' },
|
||||||
|
// { value: 'PLAYTIME', name: '플레이타임' },
|
||||||
|
];
|
||||||
|
|
||||||
export const mailSendType = [
|
export const mailSendType = [
|
||||||
{ value: 'ALL', name: '전체' },
|
{ value: 'ALL', name: '전체' },
|
||||||
{ value: 'RESERVE_SEND', name: '예약 발송' },
|
{ value: 'RESERVE_SEND', name: '예약 발송' },
|
||||||
|
|||||||
@@ -1,108 +1,76 @@
|
|||||||
import { Fragment, useEffect, useState } from 'react';
|
import React, { Fragment, useRef } from 'react';
|
||||||
|
|
||||||
import Button from '../../components/common/button/Button';
|
|
||||||
|
|
||||||
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
|
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
|
||||||
import { RetentionSearchBar } from '../../components/IndexManage/index';
|
import { AnimatedPageWrapper } from '../common/Layout';
|
||||||
import { RetentionIndexExport, RetentionIndexView } from '../../apis';
|
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 RetentionContent = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tableRef = useRef(null);
|
||||||
const token = sessionStorage.getItem('token');
|
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 {
|
||||||
const [resultData, setResultData] = useState([]);
|
searchParams,
|
||||||
const [retentionData, setRetention] = useState(1);
|
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 (
|
return (
|
||||||
<>
|
<AnimatedPageWrapper>
|
||||||
<RetentionSearchBar setResultData={setResultData} resultData={resultData}
|
<RetentionSearchBar
|
||||||
handleSearch={handleSearch} fetchData={fetchData} setRetention={setRetention} setExcelBtn={setExcelBtn} />
|
searchParams={searchParams}
|
||||||
|
onSearch={(newParams, executeSearch = true) => {
|
||||||
|
if (executeSearch) {
|
||||||
|
handleSearch(newParams);
|
||||||
|
} else {
|
||||||
|
updateSearchParams(newParams);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
<TableInfo>
|
<TableInfo>
|
||||||
<ListOption>
|
<ListOption>
|
||||||
<Button
|
<ExcelDownButton tableRef={tableRef} fileName={t('FILE_INDEX_USER_RETENTION')} />
|
||||||
theme={excelBtn === true ? "disable" : "line"}
|
|
||||||
text="엑셀 다운로드"
|
|
||||||
disabled={handleXlsxExport}
|
|
||||||
handleClick={handleXlsxExport} />
|
|
||||||
</ListOption>
|
</ListOption>
|
||||||
</TableInfo>
|
</TableInfo>
|
||||||
<IndexTableWrap>
|
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
|
||||||
<TableStyle>
|
<IndexTableWrap>
|
||||||
<caption></caption>
|
<TableStyle ref={tableRef}>
|
||||||
<thead>
|
<caption></caption>
|
||||||
<tr>
|
<thead>
|
||||||
{/* <th width="100">국가</th> */}
|
<tr>
|
||||||
<th width="150">일자</th>
|
<th>일자</th>
|
||||||
<th className="cell-nru">NRU</th>
|
<th>NRU</th>
|
||||||
{[...Array(Number(retentionData))].map((value, index) => {
|
<th>D+1</th>
|
||||||
return <th key={index}>{`D+${index + 1}`}</th>;
|
<th>D+7</th>
|
||||||
})}
|
<th>D+30</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{dataList.retention &&
|
{dataList?.map((data, index) => (
|
||||||
dataList.retention.map(data => (
|
<Fragment key={index}>
|
||||||
<tr className="cell-nru-th" key={data.date}>
|
<tr>
|
||||||
<td>{data.date}</td>
|
<td>{data.logDay}</td>
|
||||||
{data['d-day'].map((day, index) => (
|
<td>{data.totalCreated}</td>
|
||||||
<td key={index}>{day.dif}</td>
|
<td>{numberFormatter.formatPercent(data.d1_rate)}</td>
|
||||||
))}
|
<td>{numberFormatter.formatPercent(data.d7_rate)}</td>
|
||||||
</tr>
|
<td>{numberFormatter.formatPercent(data.d30_rate)}</td>
|
||||||
))}
|
</tr>
|
||||||
</tbody>
|
</Fragment>
|
||||||
</TableStyle>
|
))}
|
||||||
</IndexTableWrap>
|
</tbody>
|
||||||
</>
|
</TableStyle>
|
||||||
|
</IndexTableWrap>
|
||||||
|
}
|
||||||
|
</AnimatedPageWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
|
import { InputLabel } from '../../styles/Components';
|
||||||
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { userSearchType2 } from '../../assets/data/options';
|
import { getCurrencyList } from '../../apis/Log';
|
||||||
import { getCurrencyDetailList, getCurrencyList } from '../../apis/Log';
|
|
||||||
import { useAlert } from '../../context/AlertProvider';
|
import { useAlert } from '../../context/AlertProvider';
|
||||||
import { alertTypes } from '../../assets/data/types';
|
import { alertTypes } from '../../assets/data/types';
|
||||||
|
|
||||||
|
|||||||
@@ -1,166 +1,151 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { InputLabel } from '../../styles/Components';
|
||||||
import { styled } from 'styled-components';
|
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
|
||||||
import Button from '../common/button/Button';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import DatePickerComponent from '../common/Date/DatePickerComponent';
|
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 }) => {
|
const [searchParams, setSearchParams] = useState({
|
||||||
// 초기 날짜 세팅
|
start_dt: (() => {
|
||||||
let d = new Date();
|
const date = new Date();
|
||||||
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
|
date.setDate(date.getDate() - 1);
|
||||||
const END_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(24, 0, 0, 0));
|
return date;
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
})(),
|
||||||
const [period, setPeriod] = useState(0);
|
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(() => {
|
useEffect(() => {
|
||||||
setResultData({
|
const initialLoad = async () => {
|
||||||
send_dt: START_DATE,
|
await fetchData(searchParams);
|
||||||
end_dt: '',
|
};
|
||||||
retention: 0,
|
|
||||||
});
|
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 handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
|
||||||
const handleSelectedDate = data => {
|
const updatedParams = {
|
||||||
const sendDate = new Date(data);
|
...searchParams,
|
||||||
const resultSendData = new Date(sendDate.getFullYear(), sendDate.getMonth(), sendDate.getDate());
|
...newParams,
|
||||||
|
page_no: newParams.page_no || 1 // Reset to first page on new search
|
||||||
|
};
|
||||||
|
updateSearchParams(updatedParams);
|
||||||
|
|
||||||
const resultEndDate = new Date(resultSendData);
|
if (executeSearch) {
|
||||||
resultEndDate.setDate(resultEndDate.getDate() + Number(resultData.retention));
|
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 searchList = [
|
||||||
// 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 (
|
|
||||||
<>
|
<>
|
||||||
<FormWrapper>
|
<InputLabel>일자</InputLabel>
|
||||||
<SearchbarStyle>
|
<SearchPeriod
|
||||||
<SearchItem>
|
startDate={searchParams.start_dt}
|
||||||
<InputLabel>집계 기준일</InputLabel>
|
handleStartDate={date => onSearch({ start_dt: date }, false)}
|
||||||
<InputGroup>
|
endDate={searchParams.end_dt}
|
||||||
<DatePickerWrapper>
|
handleEndDate={date => onSearch({ end_dt: date }, false)}
|
||||||
<DatePickerComponent
|
/>
|
||||||
name="시작 일자" selectedDate={resultData.send_dt}
|
</>,
|
||||||
handleSelectedDate={data => handleSelectedDate(data)}
|
];
|
||||||
maxDate={new Date()} />
|
|
||||||
<span>~</span>
|
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RetentionSearchBar;
|
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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import PlayTimeSearchBar from './PlayTimeSearchBar';
|
|||||||
import UserViewSearchBar from './UserViewSearchBar';
|
import UserViewSearchBar from './UserViewSearchBar';
|
||||||
import AdminViewSearchBar from './AdminViewSearchBar';
|
import AdminViewSearchBar from './AdminViewSearchBar';
|
||||||
import EventListSearchBar from './EventListSearchBar';
|
import EventListSearchBar from './EventListSearchBar';
|
||||||
import RetentionSearchBar from './RetentionSearchBar';
|
import RetentionSearchBar, {useRetentionSearch} from './RetentionSearchBar';
|
||||||
import UserBlockSearchBar from './UserBlockSearchBar';
|
import UserBlockSearchBar from './UserBlockSearchBar';
|
||||||
import UserIndexSearchBar from './UserIndexSearchBar';
|
import UserIndexSearchBar from './UserIndexSearchBar';
|
||||||
import ReportListSearchBar from './ReportListSearchBar';
|
import ReportListSearchBar from './ReportListSearchBar';
|
||||||
@@ -53,6 +53,7 @@ export {
|
|||||||
AdminViewSearchBar,
|
AdminViewSearchBar,
|
||||||
EventListSearchBar,
|
EventListSearchBar,
|
||||||
RetentionSearchBar,
|
RetentionSearchBar,
|
||||||
|
useRetentionSearch,
|
||||||
UserBlockSearchBar,
|
UserBlockSearchBar,
|
||||||
UserIndexSearchBar,
|
UserIndexSearchBar,
|
||||||
ReportListSearchBar,
|
ReportListSearchBar,
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ const resources = {
|
|||||||
FILE_SIZE_OVER_ERROR: "파일의 사이즈가 5MB를 초과하였습니다.",
|
FILE_SIZE_OVER_ERROR: "파일의 사이즈가 5MB를 초과하였습니다.",
|
||||||
//파일명칭
|
//파일명칭
|
||||||
FILE_INDEX_USER_CONTENT: 'Caliverse_User_Index.xlsx',
|
FILE_INDEX_USER_CONTENT: 'Caliverse_User_Index.xlsx',
|
||||||
|
FILE_INDEX_USER_RETENTION: 'Caliverse_Retention.xlsx',
|
||||||
FILE_CALIUM_REQUEST: 'Caliverse_Calium_Request.xlsx',
|
FILE_CALIUM_REQUEST: 'Caliverse_Calium_Request.xlsx',
|
||||||
FILE_LAND_AUCTION: 'Caliverse_Land_Auction.xlsx',
|
FILE_LAND_AUCTION: 'Caliverse_Land_Auction.xlsx',
|
||||||
FILE_BUSINESS_LOG: 'Caliverse_Log.xlsx',
|
FILE_BUSINESS_LOG: 'Caliverse_Log.xlsx',
|
||||||
|
|||||||
@@ -72,22 +72,3 @@ const GameLogView = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default withAuth(authType.gameLogRead)(GameLogView);
|
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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,65 +1,42 @@
|
|||||||
import { Fragment, useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { styled } from 'styled-components';
|
import { styled } from 'styled-components';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { AnimatedPageWrapper } from '../../components/common/Layout';
|
import { AnimatedPageWrapper } from '../../components/common/Layout';
|
||||||
|
import { Title } from '../../styles/Components';
|
||||||
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 { UserContent, SegmentContent, PlayTimeContent, RetentionContent, DailyActiveUserContent, DailyMedalContent } from '../../components/IndexManage/index';
|
import { UserContent, SegmentContent, PlayTimeContent, RetentionContent, DailyActiveUserContent, DailyMedalContent } from '../../components/IndexManage/index';
|
||||||
import AuthModal from '../../components/common/modal/AuthModal';
|
|
||||||
import { authType } from '../../assets/data';
|
import { authType } from '../../assets/data';
|
||||||
import { withAuth } from '../../hooks/hook';
|
import { withAuth } from '../../hooks/hook';
|
||||||
|
import { TabUserIndexList } from '../../assets/data/options';
|
||||||
|
|
||||||
const UserIndex = () => {
|
const UserIndex = () => {
|
||||||
const token = sessionStorage.getItem('token');
|
const [activeTab, setActiveTab] = useState('USER');
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const userInfo = useRecoilValue(authList);
|
|
||||||
const [activeTab, setActiveTab] = useState('이용자 지표');
|
|
||||||
|
|
||||||
const handleTab = (e, content) => {
|
const handleTab = (e, content) => {
|
||||||
// e.preventDefault();
|
e.preventDefault();
|
||||||
setActiveTab(content);
|
setActiveTab(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TabList = [
|
|
||||||
{ title: '이용자 지표' },
|
|
||||||
// { title: 'Retention' },
|
|
||||||
// { title: 'Segment' },
|
|
||||||
// { title: '플레이타임' },
|
|
||||||
// { title: 'DAU' },
|
|
||||||
// { title: '메달' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPageWrapper>
|
<AnimatedPageWrapper>
|
||||||
<Title>유저 지표</Title>
|
<Title>유저 지표</Title>
|
||||||
<TabWrapper>
|
<TabWrapper>
|
||||||
{TabList.map((el, idx) => {
|
{TabUserIndexList.map((el, idx) => {
|
||||||
return (
|
return (
|
||||||
<TabItem
|
<li key={idx}>
|
||||||
key={idx}
|
<TabItem $state={activeTab === el.value ? 'active' : 'none'} onClick={e => handleTab(e, el.value)}>
|
||||||
$state={el.title === activeTab ? 'active' : 'unactive'}
|
{el.name}
|
||||||
onClick={(e) => handleTab(e, el.title)}
|
</TabItem>
|
||||||
>
|
</li>
|
||||||
{el.title}
|
)
|
||||||
</TabItem>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</TabWrapper>
|
</TabWrapper>
|
||||||
|
|
||||||
{/*{activeTab === 'DAU' && <DailyActiveUserContent />}*/}
|
{/*{activeTab === 'DAU' && <DailyActiveUserContent />}*/}
|
||||||
{activeTab === '이용자 지표' && <UserContent />}
|
{activeTab === 'USER' && <UserContent />}
|
||||||
{activeTab === 'Retention' && <RetentionContent />}
|
{activeTab === 'RETENTION' && <RetentionContent />}
|
||||||
{activeTab === 'Segment' && <SegmentContent />}
|
{activeTab === 'SEGMENT' && <SegmentContent />}
|
||||||
{activeTab === '플레이타임' && <PlayTimeContent />}
|
{activeTab === 'PLAYTIME' && <PlayTimeContent />}
|
||||||
{/*{activeTab === '메달' && <DailyMedalContent />}*/}
|
{/*{activeTab === '메달' && <DailyMedalContent />}*/}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,26 @@ export const numberFormatter = {
|
|||||||
console.error('Currency formatting error:', e);
|
console.error('Currency formatting error:', e);
|
||||||
return '0';
|
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%';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user