Compare commits

...

40 Commits

Author SHA1 Message Date
760153c700 인스턴스 조회 추가 2025-11-28 16:40:32 +09:00
8dae810e3a 인스턴스 조회 추가 2025-11-28 16:40:18 +09:00
ac9bcdda8b 로그인정보 만료시 비밀번호 초기화 추가,
랭킹 강제 초기화 버튼 추가,
랭킹 시스템 조회 및 수정
2025-11-28 16:39:39 +09:00
3264e94093 로그인 비밀번호 초기화 추가 2025-10-27 18:54:45 +09:00
b801552839 제작 아이템 조회
랭킹 점수 관리
2025-09-16 16:43:58 +09:00
3169055646 칼리움 요청 날짜처리 변경
랭킹 스케줄 추가
2025-09-15 16:40:04 +09:00
5d2e1918d1 비즈니스로그조회 시간 추가
메타데이터 로드 추가
아이템 백과사전 추가
칼리움 요청 날짜처리 변경
2025-09-15 16:39:15 +09:00
4407fdc6b6 컴포넌트 관련 변경
이미지 업로드 한글명칭 불가 처리
유저 조회 부분 수정
2025-09-15 16:37:46 +09:00
b01c5cd410 아이템 조회 아이템ID 조건 추가 2025-09-15 16:25:12 +09:00
63b3704e89 히스토리 조회 관련 수정 2025-09-15 16:24:41 +09:00
f78a4912a6 event > rewardEvent 변경
월드이벤트(event) 추가
2025-09-15 16:23:48 +09:00
e25bcdc86e 선택 드랍다운 넓이 수정
아이템 백과사전 추가
2025-09-04 10:37:50 +09:00
5143b45610 모달 스크롤 추가
detailGrid 수정
전투이벤트, 랜드경매, 이벤트, 메일 상세 수정
랜드경매 예약종료일 제거
2025-08-09 09:50:14 +09:00
f4b629df52 경제지표 재화 보유
경제지표 아이템 보유
게임로그 스냅샷
히스토리 비즈니스로그 기준 변경
2025-08-04 17:40:37 +09:00
2ba8594e6b 칼리움 완료처리 파라미터 변경
우편 상세 확인시 페이지 처리
조회조건 변경시 페이지 초기화
랜드경매 메시지 제거
2025-07-28 14:13:01 +09:00
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
67c048a11d 메뉴배너 detailGrid 적용 2025-06-27 09:28:13 +09:00
f6a0319701 메뉴배너 detailGrid 적용 2025-06-27 09:28:06 +09:00
0368bf77e7 antBUtton 생성
topButton 이미지 변경
엑셀 버튼 조정
tab 컨트론 생성
detailGrid, layout 생성
modal motion 적용
2025-06-27 09:25:41 +09:00
b2b579ead1 전투시스템 스케줄러 game mode 방식 변경 2025-06-19 18:54:15 +09:00
495243a1a3 경제지표 재화 추가
게임 로그 재화지표 export api 추가
경제지표 재화 상세 > 재화 로그 페이지 이동 처리
2025-06-16 15:45:36 +09:00
7993f37e26 qa 주소 변경 2025-06-16 15:43:20 +09:00
93a7c087e1 style, utils 수정 2025-06-12 14:16:46 +09:00
38fa323db6 Log currency 관련 API 호출 추가
components 정리에 따른 호출 위치 수정
excelExportButton에 functionName 추가
게임 로그조회 수정
2025-06-12 14:16:26 +09:00
6f9f0307ac component 정리
currencyLogSearchBar 생성
currencyLogCOntent 생성
excelExportButton api호출 방식 수정
2025-06-12 14:08:11 +09:00
dc7934d906 비즈니스 로그 조회 및 파일 다운 수정 2025-06-04 15:19:08 +09:00
216 changed files with 17431 additions and 7665 deletions

2
.gitignore vendored
View File

@@ -32,3 +32,5 @@ yarn-error.log*
/.idea/misc.xml
/.idea/modules.xml
/.idea/vcs.xml
/.idea/git_toolbox_blame.xml
/.idea/git_toolbox_prj.xml

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;
@@ -11,11 +15,16 @@ server {
# api reverse proxy
location /api/ {
proxy_pass http://172.40.129.180:23450;
proxy_pass http://172.24.128.231:23450;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
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

@@ -3,13 +3,17 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@hookform/resolvers": "^3.2.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"antd": "^5.26.1",
"axios": "^1.4.0",
"date-fns": "^2.30.0",
"dayjs": "^1.11.13",
"dotenv-cli": "^7.4.2",
"framer-motion": "^12.19.1",
"i18next": "^23.15.1",
"lodash": "^4.17.21",
"react": "^18.2.0",

View File

@@ -13,11 +13,21 @@ import {
LogView,
} from './pages/UserManage';
import { EconomicIndex, UserIndex } from './pages/IndexManage';
import { LandInfoView, CryptView, GameLogView, UserView, BusinessLogView, } from './pages/DataManage';
import {
LandInfoView,
GameLogView,
UserView,
BusinessLogView,
MetaItemView,
RankManage,
MetaCraftingView,
RankInfoView,
MetaInstanceView
} from './pages/DataManage';
import {
Board,
Event,
EventRegist,
RewardEvent,
RewardEventRegist,
Items,
Mail,
MailRegist,
@@ -26,7 +36,8 @@ import {
UserBlockRegist,
LandAuction,
BattleEvent,
MenuBanner, MenuBannerRegist,
MenuBanner, MenuBannerRegist, Ranking,
Event
} from './pages/ServiceManage';
const RouteInfo = () => {
@@ -59,8 +70,12 @@ const RouteInfo = () => {
<Route path="userview" element={<UserView />} />
<Route path="landview" element={<LandInfoView />} />
<Route path="gamelogview" element={<GameLogView />} />
<Route path="cryptview" element={<CryptView />} />
<Route path="businesslogview" element={<BusinessLogView />} />
<Route path="itemdictionary" element={<MetaItemView />} />
<Route path="craftdictionary" element={<MetaCraftingView />} />
<Route path="rankmanage" element={<RankManage />} />
<Route path="rankview" element={<RankInfoView />} />
<Route path="instancedictionary" element={<MetaInstanceView />} />
</Route>
<Route path="/servicemanage">
<Route path="board" element={<Board />} />
@@ -70,12 +85,14 @@ const RouteInfo = () => {
<Route path="userblock/userblockregist" element={<UserBlockRegist />} />
<Route path="reportlist" element={<ReportList />} />
<Route path="items" element={<Items />} />
<Route path="event" element={<Event />} />
<Route path="event/eventregist" element={<EventRegist />} />
<Route path="rewardevent" element={<RewardEvent />} />
<Route path="rewardevent/eventregist" element={<RewardEventRegist />} />
<Route path="landauction" element={<LandAuction />} />
<Route path="battleevent" element={<BattleEvent />} />
<Route path="menubanner" element={<MenuBanner />} />
<Route path="menubanner/menubannerregist" element={<MenuBannerRegist />} />
<Route path="ranking" element={<Ranking />} />
<Route path="event" element={<Event />} />
</Route>
</Route>
</Routes>

View File

@@ -1,79 +0,0 @@
import { Route, Routes } from 'react-router-dom';
import { Layout, LoginLayout, MainLayout } from './components/common/Layout';
import LoginBg from './assets/img/login-bg.png';
import { Login } from './pages/Login';
import LoginFail from './pages/LoginFail';
import { AccountEdit, AccountRegist, PasswordReset } from './pages/Account';
import Main from './pages/Main';
import {
AdminView,
AuthSetting,
AuthSettingUpdate,
CaliumRequest,
CaliumRequestRegist,
LogView,
} from './pages/UserManage';
import { EconomicIndex, UserIndex } from './pages/IndexManage';
import { ContentsView, CryptView, GameLogView, UserView } from './pages/DataManage';
import {
Board,
Event,
EventRegist,
Items,
Mail,
MailRegist,
ReportList,
UserBlock,
UserBlockRegist,
WhiteList,
} from './pages/ServiceManage';
const RouteInfo = () => {
return (
<Routes>
<Route element={<LoginLayout $bgimg={LoginBg} $padding="50px" />}>
<Route path="/" element={<Login />} />
<Route path="/fail" element={<LoginFail />} />
<Route path="/account/regist" element={<AccountRegist />} />
<Route path="/account/pwdreset" element={<PasswordReset />} />
<Route path="/account/edit" element={<AccountEdit />} />
</Route>
<Route element={<MainLayout />}>
<Route path="/main" element={<Main />} />
</Route>
<Route element={<Layout />}>
<Route path="/usermanage/">
<Route path="adminview" element={<AdminView />} />
<Route path="logview" element={<LogView />} />
<Route path="authsetting" element={<AuthSetting />} />
<Route path="authsetting/:id" element={<AuthSettingUpdate />} />
<Route path="caliumrequest" element={<CaliumRequest />} />
</Route>
<Route path="/indexmanage">
<Route path="userindex" element={<UserIndex />} />
<Route path="economicindex" element={<EconomicIndex />} />
</Route>
<Route path="/datamanage">
<Route path="userview" element={<UserView />} />
<Route path="contentsview" element={<ContentsView />} />
<Route path="gamelogview" element={<GameLogView />} />
<Route path="cryptview" element={<CryptView />} />
</Route>
<Route path="/servicemanage">
<Route path="board" element={<Board />} />
<Route path="whitelist" element={<WhiteList />} />
<Route path="mail" element={<Mail />} />
<Route path="mail/mailregist" element={<MailRegist />} />
<Route path="userblock" element={<UserBlock />} />
<Route path="userblock/userblockregist" element={<UserBlockRegist />} />
<Route path="reportlist" element={<ReportList />} />
<Route path="items" element={<Items />} />
<Route path="event" element={<Event />} />
<Route path="event/eventregist" element={<EventRegist />} />
</Route>
</Route>
</Routes>
)
}
export default RouteInfo;

View File

@@ -85,10 +85,10 @@ export const AdminDeleteUser = async (token, params) => {
}
};
export const AdminChangePw = async (token, params) => {
export const AdminChangePw = async ( params) => {
try {
const res = await Axios.post('/api/v1/admin/init-password', params, {
headers: { Authorization: `Bearer ${token}` },
headers: { },
});
return res.data;

View File

@@ -129,4 +129,21 @@ export const BattleRewardView = async (token) => {
throw new Error('BattleRewardView Error', e);
}
}
};
export const GameModeView = async (token) => {
try {
const res = await Axios.get(
`/api/v1/battle/game-mode/list`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res.data.data.game_mode_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('GameModeView Error', e);
}
}
};

132
src/apis/Dictionary.js Normal file
View File

@@ -0,0 +1,132 @@
//운영 정보 관리 - 백과사전 api 연결
import { Axios, responseFileDownload } from '../utils';
// 아이템 백과사전 조회
export const getItemDictionaryList = async (token, searchType, searchData, largeType, smallType, brand, gender, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/dictionary/item/list?search_type=${searchType}&search_data=${searchData}
&large_type=${largeType}&small_type=${smallType}&brand=${brand}&gender=${gender}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('getItemDictionaryList API error:', error);
throw error;
}
};
export const ItemDictionaryExport = async (token, params) => {
try {
await Axios.get(`/api/v1/dictionary/item/excel-export?search_type=${params.search_type}&search_data=${params.search_data}
&large_type=${params.large_type}&small_type=${params.small_type}&brand=${params.brand}&gender=${params.gender}
&lang=${params.lang}&task_id=${params.taskId}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob'
}).then(response => {
responseFileDownload(response, {
defaultFileName: 'itemDictionary'
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('ItemDictionaryExport Error', e);
}
}
};
export const getCraftingDictionaryList = async (token, searchType, searchData, smallType, recipeType, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/dictionary/craft/list?search_type=${searchType}&search_data=${searchData}
&small_type=${smallType}&recipe_type=${recipeType}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('getCraftingDictionaryList API error:', error);
throw error;
}
};
export const CraftingDictionaryExport = async (token, params) => {
try {
await Axios.get(`/api/v1/dictionary/craft/excel-export?search_type=${params.search_type}&search_data=${params.search_data}
&small_type=${params.small_type}&recipe_type=${params.recipe_type}
&lang=${params.lang}&task_id=${params.taskId}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob'
}).then(response => {
responseFileDownload(response, {
defaultFileName: 'craftingDictionary'
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('CraftingDictionaryExport Error', e);
}
}
};
export const getInstanceDictionaryList = async (token, searchType, searchData, contentsType, accessType, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/dictionary/instance/list?search_type=${searchType}&search_data=${searchData}
&contents_type=${contentsType}&access_type=${accessType}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('getInstanceDictionaryList API error:', error);
throw error;
}
};
export const InstanceDictionaryExport = async (token, params) => {
try {
await Axios.get(`/api/v1/dictionary/instance/excel-export?search_type=${params.search_type}&search_data=${params.search_data}
&contents_type=${params.contents_type}&access_type=${params.access_type}
&lang=${params.lang}&task_id=${params.taskId}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob'
}).then(response => {
responseFileDownload(response, {
defaultFileName: 'instanceDictionary'
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('InstanceDictionaryExport Error', e);
}
}
};
export const BrandView = async (token) => {
try {
const res = await Axios.get(
`/api/v1/dictionary/brand/list`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res.data.data.brand_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('BrandView Error', e);
}
}
};

View File

@@ -1,13 +1,13 @@
//운영서비스 관리 - 이벤트 api 연결
//운영서비스 관리 - 통합 이벤트 api 연결
import { Axios } from '../utils';
// 이벤트 리스트 조회
export const EventView = async (token, title, content, status, startDate, endDate, order, size, currentPage) => {
export const EventView = async (token, searchData, status, startDate, endDate, order, size, currentPage) => {
try {
const res = await Axios.get(
`/api/v1/event/list?title=${title}&content=${content}&status=${status}&start_dt=${startDate}&end_dt=${endDate}&orderby=${order}&page_no=${currentPage}
&page_size=${size}`,
`/api/v1/world-event/list?search_data=${searchData}&status=${status}&start_dt=${startDate}&end_dt=${endDate}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`,
{
headers: { Authorization: `Bearer ${token}` },
},
@@ -24,11 +24,11 @@ export const EventView = async (token, title, content, status, startDate, endDat
// 이벤트 상세보기
export const EventDetailView = async (token, id) => {
try {
const res = await Axios.get(`/api/v1/event/detail/${id}`, {
const res = await Axios.get(`/api/v1/world-event/detail/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data.detail;
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('EventDetailView Error', e);
@@ -39,11 +39,11 @@ export const EventDetailView = async (token, id) => {
// 이벤트 등록
export const EventSingleRegist = async (token, params) => {
try {
const res = await Axios.post(`/api/v1/event`, params, {
const res = await Axios.post(`/api/v1/world-event`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res;
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('EventSingleRegist Error', e);
@@ -51,10 +51,10 @@ export const EventSingleRegist = async (token, params) => {
}
};
// 우편 수정
// 이벤트 수정
export const EventModify = async (token, id, params) => {
try {
const res = await Axios.put(`/api/v1/event/${id}`, params, {
const res = await Axios.put(`/api/v1/world-event/${id}`, params, {
headers: { Authorization: `Bearer ${token}` },
});
@@ -66,15 +66,14 @@ export const EventModify = async (token, id, params) => {
}
};
// 우편 삭제
export const EventDelete = async (token, params, id) => {
// 이벤트 삭제
export const EventDelete = async (token, id) => {
try {
const res = await Axios.delete(`/api/v1/event/delete`, {
headers: { Authorization: `Bearer ${token}` },
data: { list: params },
const res = await Axios.delete(`/api/v1/world-event/delete?id=${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
return res.data.data.list;
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('EventDelete Error', e);
@@ -82,17 +81,20 @@ export const EventDelete = async (token, params, id) => {
}
};
// 이벤트 우편 아이템 확인
export const EventIsItem = async (token, params) => {
// 이벤트 메타데이터 조회
export const EventActionView = async (token) => {
try {
const res = await Axios.post(`/api/v1/event/item`, params, {
headers: { Authorization: `Bearer ${token}` },
});
const res = await Axios.get(
`/api/v1/dictionary/event-action/list`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res;
return res.data.data.event_action_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('EventIsItem Error', e);
throw new Error('EventActionView Error', e);
}
}
};

View File

@@ -12,7 +12,7 @@ export const LogViewList = async (token, searchType, searchKey, historyType, sta
},
);
return res.data.data;
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('LogViewList Error', e);

View File

@@ -36,6 +36,19 @@ export const userTotalIndex = async token => {
}
};
export const dashboardCaliumIndex = async token => {
try {
const res = await Axios.get(`/api/v1/indicators/dashboard/calium/converter`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('dashboardCaliumIndex', e);
}
}
};
// 유저 지표 다운로드
export const userIndexExport = async (token, filename, sendDate, endDate) => {
try {
@@ -62,10 +75,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;
@@ -183,10 +200,10 @@ export const PlaytimeIndexExport = async (token, filename, sendDate, endDate) =>
// 2. 경제 지표
// 재화 조회 (currency)
export const CurrencyIndexView = async (token, start_dt, end_dt, currency_type) => {
// 재화 획득 조회
export const CurrencyAcquireIndexView = async (token, start_dt, end_dt, currencyType, deltaType) => {
try {
const res = await Axios.get(`/api/v1/indicators/currency/use?start_dt=${start_dt}&end_dt=${end_dt}&currency_type=${currency_type}`, {
const res = await Axios.get(`/api/v1/indicators/currency/list?start_dt=${start_dt}&end_dt=${end_dt}&currency_type=${currencyType}&delta_type=${deltaType}`, {
headers: { Authorization: `Bearer ${token}` },
});
@@ -198,75 +215,10 @@ export const CurrencyIndexView = async (token, start_dt, end_dt, currency_type)
}
};
// 재화 지표 다운로드
export const CurrencyIndexExport = async (token, filename, sendDate, endDate, currencyType) => {
try {
await Axios.get(`/api/v1/indicators/currency/excel-down?file=${filename}&start_dt=${sendDate}&end_dt=${endDate}&currency_type=${currencyType}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('CurrencyIndexExport Error', e);
}
}
};
// VBP
export const VbpIndexView = async (token, start_dt, end_dt) => {
try {
const res = await Axios.get(`/api/v1/indicators/currency/vbp?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('VbpIndexView Error', e);
}
}
};
// VBP 다운로드
export const VBPIndexExport = async (token, filename, sendDate, endDate) => {
try {
await Axios.get(`/api/v1/indicators/currency/excel-down?file=${filename}&start_dt=${sendDate}&end_dt=${endDate}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('VBPIndexExport Error', e);
}
}
};
// Item
export const ItemIndexView = async (token, start_dt, end_dt) => {
export const ItemIndexView = async (token, start_dt, end_dt, itemId, deltaType) => {
try {
const res = await Axios.get(`/api/v1/indicators/currency/item?start_dt=${start_dt}&end_dt=${end_dt}`, {
const res = await Axios.get(`/api/v1/indicators/item/list?start_dt=${start_dt}&end_dt=${end_dt}&item_id=${itemId}&delta_type=${deltaType}`, {
headers: { Authorization: `Bearer ${token}` },
});
@@ -278,27 +230,17 @@ export const ItemIndexView = async (token, start_dt, end_dt) => {
}
};
// Item 다운로드
export const ItemIndexExport = async (token, filename, sendDate, endDate) => {
// Assets
export const AssetsIndexView = async (token, start_dt, end_dt, itemId, deltaType) => {
try {
await Axios.get(`/api/v1/indicators/currency/excel-down?file=${filename}&start_dt=${sendDate}&end_dt=${endDate}`, {
const res = await Axios.get(`/api/v1/indicators/assets/list?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('ItemIndexExport Error', e);
throw new Error('AssetsIndexView Error', e);
}
}
};
@@ -320,137 +262,4 @@ export const InstanceIndexView = async (token, data, start_dt, end_dt) => {
throw new Error('InstanceIndexView Error', e);
}
}
};
// Instance 다운로드
export const InstanceIndexExport = async (token, filename, data, sendDate, endDate) => {
try {
await Axios.get(
`/api/v1/indicators/currency/excel-down?file=${filename}&search_key=${data ? data : ''}
&start_dt=${sendDate}&end_dt=${endDate}`,
{
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
},
).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('InstanceIndexExport Error', e);
}
}
};
// Clothes
export const ClothesIndexView = async (token, data, start_dt, end_dt) => {
try {
const res = await Axios.get(`/api/v1/indicators/currency/clothes?search_key=${data ? data : ''}&start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('ClothesIndexView Error', e);
}
}
};
// Clothes 다운로드
export const ClothesIndexExport = async (token, filename, data, sendDate, endDate) => {
try {
await Axios.get(
`/api/v1/indicators/currency/excel-down?file=${filename}&search_key=${data ? data : ''}
&start_dt=${sendDate}&end_dt=${endDate}`,
{
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
},
).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('ClothesIndexExport Error', e);
}
}
};
// DAU
export const DailyActiveUserView = async (token, start_dt, end_dt) => {
try {
const res = await Axios.get(`/api/v1/indicators/dau/list?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data.dau_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('DailyActiveUserView Error', e);
}
}
};
// DAU 다운로드
export const DailyActiveUserExport = async (token, filename, sendDate, endDate) => {
try {
await Axios.get(`/api/v1/indicators/dau/excel-down?file=${filename}&start_dt=${sendDate}&end_dt=${endDate}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
}).then(response => {
const href = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
} catch (e) {
if (e instanceof Error) {
throw new Error('PlaytimeIndexExport Error', e);
}
}
};
// Daily Medal
export const DailyMedalView = async (token, start_dt, end_dt) => {
try {
const res = await Axios.get(`/api/v1/indicators/daily-medal/list?start_dt=${start_dt}&end_dt=${end_dt}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data.daily_medal_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('DailyMedalView Error', e);
}
}
};

View File

@@ -1,12 +1,13 @@
//운영 정보 관리 - 로그 api 연결
import { Axios } from '../utils';
import { Axios, responseFileDownload } from '../utils';
// 비즈니스 로그 조회
export const BusinessLogList = async (token, params) => {
try {
const res = await Axios.post(`/api/v1/log/generic/list`, params, {
headers: { Authorization: `Bearer ${token}` },
timeout: 600000
});
return res.data;
@@ -15,4 +16,298 @@ export const BusinessLogList = async (token, params) => {
throw new Error('BusinessLogList Error', e);
}
}
};
export const BusinessLogExport = async (token, params) => {
try {
await Axios.post(`/api/v1/log/generic/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob'
}).then(response => {
responseFileDownload(response, {
defaultFileName: 'businessLog'
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('BusinessLogExport Error', e);
}
}
};
export const getExcelProgress = async (token, taskId) => {
try {
const response = await Axios.get(`/api/v1/log/progress/${taskId}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.data) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
console.error('Progress API error:', error);
throw error;
}
};
export const getCurrencyList = async (token, startDate, endDate, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/log/currency/list?start_dt=${startDate}&end_dt=${endDate}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('getCurrencyList API error:', error);
throw error;
}
};
export const GameCurrencyLogExport = async (token, params, fileName) => {
try {
await Axios.post(`/api/v1/log/currency/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
timeout: 300000
}).then(response => {
responseFileDownload(response, {
defaultFileName: fileName
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('GameCurrencyLogExport Error', e);
}
}
};
export const getCurrencyDetailList = async (token, searchType, searchData, tranId, logAction, currencyType, amountDeltaType, startDate, endDate, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/log/currency/detail/list?search_type=${searchType}&search_data=${searchData}&tran_id=${tranId}
&log_action=${logAction}&currency_type=${currencyType}&amount_delta_type=${amountDeltaType}&start_dt=${startDate}&end_dt=${endDate}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('getCurrencyDetailList API error:', error);
throw error;
}
};
export const GameCurrencyDetailLogExport = async (token, params, fileName) => {
try {
await Axios.post(`/api/v1/log/currency/detail/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
timeout: 300000
}).then(response => {
responseFileDownload(response, {
defaultFileName: fileName
});
});
} catch (e) {
if (e instanceof Error) {
throw new Error('GameCurrencyDetailLogExport Error', e);
}
}
};
export const getItemDetailList = async (token, searchType, searchData, itemId, 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}&item_id=${itemId}
&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 {
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 {
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 {
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 {
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);
}
}
};
export const getUserSnapshotList = async (token, searchType, searchData, startDate, endDate, order, size, currentPage) => {
try {
const response = await Axios.get(`/api/v1/log/user/snapshot/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('getUserSnapshotList API error:', error);
throw error;
}
};
export const GameUserSnapshotLogExport = async (token, params, fileName) => {
try {
await Axios.post(`/api/v1/log/user/snapshot/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('GameUserSnapshotLogExport 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

@@ -1,21 +0,0 @@
//AI api 연결
import { Axios } from '../utils';
export const AnalyzeAI = async (token, params) => {
try {
const res = await Axios.post('/api/v1/ai/analyze', params, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('analyzeAI Error', e);
}
}
};

217
src/apis/Rank.js Normal file
View File

@@ -0,0 +1,217 @@
//운영서비스 관리 - 랭킹 스케줄 api 연결
import { Axios } from '../utils';
// 리스트 조회
export const RankingScheduleView = async (token, title, content, status, startDate, endDate, order, size, currentPage) => {
try {
const res = await Axios.get(
`/api/v1/rank/schedule/list?title=${title}&content=${content}&status=${status}&start_dt=${startDate}&end_dt=${endDate}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingScheduleView Error', e);
}
}
};
export const RankingScheduleSimpleView = async (token) => {
try {
const res = await Axios.get(
`/api/v1/rank/schedule/simple-list`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res.data.data.list;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingScheduleSimpleView Error', e);
}
}
};
export const RankingSnapshotView = async (token, guid) => {
try {
const res = await Axios.get(
`/api/v1/rank/snapshot/list?guid=${guid}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res.data.data.ranking_snapshot_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingSnapshotView Error', e);
}
}
};
// 전투시스템 상세보기
export const RankingScheduleDetailView = async (token, id) => {
try {
const res = await Axios.get(`/api/v1/rank/schedule/detail/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingScheduleDetailView Error', e);
}
}
};
export const RankerListView = async (token, guid, snapshot) => {
try {
const res = await Axios.get(`/api/v1/rank/ranker/list?guid=${guid}&snapshot=${snapshot}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankerListView Error', e);
}
}
};
// 랭킹스케줄 등록
export const RankingScheduleSingleRegist = async (token, params) => {
try {
const res = await Axios.post(`/api/v1/rank/schedule`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingScheduleSingleRegist Error', e);
}
}
};
// 랭킹스케줄 수정
export const RankingScheduleModify = async (token, id, params) => {
try {
const res = await Axios.put(`/api/v1/rank/schedule/${id}`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingScheduleModify Error', e);
}
}
};
// 랭킹스케줄 삭제
export const RankingScheduleDelete = async (token, id) => {
try {
const res = await Axios.delete(`/api/v1/rank/schedule/delete?id=${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingScheduleDelete Error', e);
}
}
};
export const RankingDataView = async (token) => {
try {
const res = await Axios.get(
`/api/v1/dictionary/ranking/list`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res.data.data.ranking_list;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingDataView Error', e);
}
}
};
export const RankingInfoView = async (token, guid) => {
try {
const res = await Axios.get(`/api/v1/rank/info?guid=${guid}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingInfoView Error', e);
}
}
};
export const RankerInfoModify = async (token, params) => {
try {
const res = await Axios.put(`/api/v1/rank/info`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankerInfoModify Error', e);
}
}
};
export const RankingUpdate = async (token, guid) => {
try {
const res = await Axios.put(`/api/v1/rank/ranking/${guid}`, {},{
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingUpdate Error', e);
}
}
};
export const RankingInit = async (token, guid) => {
try {
const res = await Axios.put(`/api/v1/rank/ranking/init/${guid}`, {},{
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingInit Error', e);
}
}
};
export const RankingSnapshot = async (token, guid) => {
try {
const res = await Axios.put(`/api/v1/rank/ranking/snapshot/${guid}`, {},{
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RankingSnapshot Error', e);
}
}
};

98
src/apis/RewardEvent.js Normal file
View File

@@ -0,0 +1,98 @@
//운영서비스 관리 - 이벤트 api 연결
import { Axios } from '../utils';
// 이벤트 리스트 조회
export const RewardEventView = async (token, title, content, status, startDate, endDate, order, size, currentPage) => {
try {
const res = await Axios.get(
`/api/v1/event/list?title=${title}&content=${content}&status=${status}&start_dt=${startDate}&end_dt=${endDate}
&orderby=${order}&page_no=${currentPage}&page_size=${size}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
return res.data.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RewardEventView Error', e);
}
}
};
// 이벤트 상세보기
export const RewardEventDetailView = async (token, id) => {
try {
const res = await Axios.get(`/api/v1/event/detail/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data.data.detail;
} catch (e) {
if (e instanceof Error) {
throw new Error('RewardEventDetailView Error', e);
}
}
};
// 이벤트 등록
export const RewardEventSingleRegist = async (token, params) => {
try {
const res = await Axios.post(`/api/v1/event`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res;
} catch (e) {
if (e instanceof Error) {
throw new Error('RewardEventSingleRegist Error', e);
}
}
};
// 우편 수정
export const RewardEventModify = async (token, id, params) => {
try {
const res = await Axios.put(`/api/v1/event/${id}`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('RewardEventModify Error', e);
}
}
};
// 우편 삭제
export const RewardEventDelete = async (token, params, id) => {
try {
const res = await Axios.delete(`/api/v1/event/delete`, {
headers: { Authorization: `Bearer ${token}` },
data: { list: params },
});
return res.data.data.list;
} catch (e) {
if (e instanceof Error) {
throw new Error('RewardEventDelete Error', e);
}
}
};
// 이벤트 우편 아이템 확인
export const EventIsItem = async (token, params) => {
try {
const res = await Axios.post(`/api/v1/event/item`, params, {
headers: { Authorization: `Bearer ${token}` },
});
return res;
} catch (e) {
if (e instanceof Error) {
throw new Error('EventIsItem Error', e);
}
}
};

View File

@@ -12,7 +12,7 @@ export const UserView = async (token, searchType, searchKey) => {
{ headers: { Authorization: `Bearer ${token}` } },
);
return res.data.data.result;
return res.data;
} catch (e) {
if (e instanceof Error) {
throw new Error('UserView Error', e);
@@ -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

@@ -12,11 +12,16 @@ export * from './BlackList';
export * from './Users';
export * from './Indicators';
export * from './Item';
export * from './Event';
export * from './RewardEvent';
export * from './Calium';
export * from './Land';
export * from './Menu';
export * from './OpenAI';
// export * from './OpenAI';
export * from './Log';
export * from './Data';
export * from './Dictionary';
export * from './Rank';
export * from './Event';
const apiModules = {};
const allApis = {};

View File

@@ -10,6 +10,11 @@ export const AUCTION_MIN_MINUTE_TIME = 15; // 15분
export const IMAGE_MAX_SIZE = 5242880;
export const STORAGE_MAIL_COPY = 'copyMailData';
export const STORAGE_BUSINESS_LOG_SEARCH = 'businessLogSearchParam';
export const STORAGE_GAME_LOG_CURRENCY_SEARCH = 'gameLogCurrencySearchParam';
export const 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

@@ -0,0 +1,18 @@
{
"baseUrl": "/api/v1/world-event",
"endpoints": {
"EventView": {
"method": "GET",
"url": "/list",
"dataPath": "data",
"paramFormat": "query"
},
"EventDetailView": {
"method": "GET",
"url": "/detail/:id",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["id"]
}
}
}

View File

@@ -4,7 +4,7 @@
"LogViewList": {
"method": "GET",
"url": "/list",
"dataPath": "data.data",
"dataPath": "data",
"paramFormat": "query"
},
"LogviewDetail": {

View File

@@ -1,9 +1,17 @@
import itemAPI from './itemAPI.json';
import menuBannerAPI from './menuBannerAPI.json';
import historyAPI from './historyAPI.json';
import eventAPI from './eventAPI.json';
import rankingAPI from './rankingAPI.json';
import metaCraftingAPI from './metaCraftingAPI.json';
import metaInstanceAPI from './metaInstanceAPI.json';
export {
itemAPI,
menuBannerAPI,
historyAPI
historyAPI,
eventAPI,
rankingAPI,
metaCraftingAPI,
metaInstanceAPI
};

View File

@@ -0,0 +1,11 @@
{
"baseUrl": "/api/v1/dictionary/craft",
"endpoints": {
"getCraftingDictionaryList": {
"method": "GET",
"url": "/list",
"dataPath": "data",
"paramFormat": "query"
}
}
}

View File

@@ -0,0 +1,11 @@
{
"baseUrl": "/api/v1/dictionary/instance",
"endpoints": {
"getInstanceDictionaryList": {
"method": "GET",
"url": "/list",
"dataPath": "data",
"paramFormat": "query"
}
}
}

View File

@@ -0,0 +1,18 @@
{
"baseUrl": "/api/v1/rank/schedule",
"endpoints": {
"RankingScheduleView": {
"method": "GET",
"url": "/list",
"dataPath": "data",
"paramFormat": "query"
},
"RankingScheduleDetailView": {
"method": "GET",
"url": "/detail/:id",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["id"]
}
}
}

View File

@@ -1,131 +0,0 @@
{
"baseUrl": "/api/v1/users",
"endpoints": {
"UserView": {
"method": "GET",
"url": "/api/v1/users/find-users",
"dataPath": "data.data.result",
"paramFormat": "query",
"paramMapping": ["search_type", "search_key"]
},
"UserInfoView": {
"method": "GET",
"url": "/api/v1/users/basicinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserChangeNickName": {
"method": "PUT",
"url": "/api/v1/users/change-nickname",
"dataPath": null,
"paramFormat": "body",
"paramMapping": ["guid", "nickname"]
},
"UserChangeAdminLevel": {
"method": "PUT",
"url": "/api/v1/users/change-level",
"dataPath": null,
"paramFormat": "body",
"paramMapping": ["guid", "level"]
},
"UserKick": {
"method": "PUT",
"url": "/api/v1/users/user-kick",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["guid"]
},
"UserAvatarView": {
"method": "GET",
"url": "/api/v1/users/avatarinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserClothView": {
"method": "GET",
"url": "/api/v1/users/clothinfo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserToolView": {
"method": "GET",
"url": "/api/v1/users/toolslot",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserInventoryView": {
"method": "GET",
"url": "/api/v1/users/inventory",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserInventoryItemDelete": {
"method": "DELETE",
"url": "/api/v1/users/inventory/delete/item",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["guid", "inventory_id"]
},
"UserTattooView": {
"method": "GET",
"url": "/api/v1/users/tattoo",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserQuestView": {
"method": "GET",
"url": "/api/v1/users/quest",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserFriendListView": {
"method": "GET",
"url": "/api/v1/users/friendlist",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
},
"UserMailView": {
"method": "POST",
"url": "/api/v1/users/mail",
"dataPath": "data.data",
"paramFormat": "body",
"paramMapping": ["guid", "page", "limit"]
},
"UserMailDelete": {
"method": "DELETE",
"url": "/api/v1/users/mail/delete",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["mail_id"]
},
"UserMailItemDelete": {
"method": "DELETE",
"url": "/api/v1/users/mail/delete/item",
"dataPath": "data",
"paramFormat": "body",
"paramMapping": ["mail_id", "item_id"]
},
"UserMailDetailView": {
"method": "GET",
"url": "/api/v1/users/mail/:id",
"dataPath": "data.data",
"paramFormat": "path",
"paramMapping": ["id"]
},
"UserMyhomeView": {
"method": "GET",
"url": "/api/v1/users/myhome",
"dataPath": "data.data",
"paramFormat": "query",
"paramMapping": ["guid"]
}
}
}

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: {
@@ -123,7 +123,7 @@ export const STATUS_STYLES = {
},
};
export const logFieldLabels = {
export const FieldLabels = {
// DynamoDB 필드
'attribFieldName': '속성 명',
'pk': '파티션 키',
@@ -153,15 +153,27 @@ export const logFieldLabels = {
'create_by': '생성자',
'status': '상태',
'deleted': '삭제 여부',
'guid': 'GUID',
'meta_id': '메타 ID',
'start_dt': '시작일',
'end_dt': '종료일',
'base_dt': '기준일',
'title': '제목',
// 이벤트 필드 관련
'eventId': '이벤트 ID',
'eventName': '이벤트 명',
'event_name': '이벤트 명',
'repeatType': '반복 타입',
'eventOperationTime': '운영 시간(초)',
'eventStartDt': '시작 시간',
'eventEndDt': '종료 시간',
'repeat_type': '반복 타입',
'eventOperationTime': '진행 시간(분)',
'event_operation_time': '진행 시간(분)',
'eventStartDt': '이벤트 시작일',
'event_start_dt': '이벤트 시작일',
'eventEndDt': '이벤트 종료일',
'event_end_dt': '이벤트 종료일',
'roundTime': '라운드 시간(초)',
'round_time': '라운드 시간(초)',
'roundCount': '라운드 수',
'hotTime': '핫타임',
'configId': '설정 ID',
@@ -181,15 +193,54 @@ export const logFieldLabels = {
'ffa_hot_time': 'FFA 핫타임',
'round_count': '라운드 수',
//dictionary
'max_count': '최대 보유 가능 수량',
'stack_max_count': '최대 스택 가능 수량',
'expire_type': '아이템 만료 타입',
'expire_start_dt': '만료 시작 시간',
'expire_end_dt': '만료 종료 시간',
'expire_time_sec': '만료 시간 연장 여부',
'user_tradable': '유저 간 거래 가능 여부',
'system_tradable': '상점에서 판매 가능 여부',
'throwable': '버리기 가능 여부',
'cart_buy': '상점에서 구매 가능 여부',
'rarity': '희귀도',
'default_attrib': '기본 속성',
'attrib_random_group': '랜덤 그룹',
'item_set': '아이템 세트',
'buff': '아이템 사용 시 획득 버프',
'dress_slot_type': '착용 부위',
'product_link': '제품 URL',
'prop_small_type': '제작 아이템 그룹',
'gacha_group_id': '랜덤박스 그룹 ID',
'ugq_action': 'UGQ 사용 가능 여부',
'linked_land': '연결된 랜드 ID',
//스케줄
'refresh_interval': '새로고침 주기',
'initialization_interval': '초기화 주기',
'snapshot_interval': '스냅샷 주기',
'event_action_id': '이벤트 액션 그룹',
'global_event_action_id': '이벤트 액션 그룹',
'personal_event_action_id': '개인제작 액션 그룹',
'max_point': '기여도 목표점수',
//메뉴
'image_list': '이미지 목록',
'is_link': '링크 여부',
'order_id': '정렬'
};
export const historyTables = {
userBlock: 'black_list',
landAuction: 'land_auction',
landOwnerChange: 'land_ownership_changes',
event: 'event',
rewardEvent: 'event',
mail: 'mail',
notice: 'notice',
battleEvent: 'battle_event',
caliumRequest: 'calium_request',
menuBanner: 'menu_banner',
event: 'world_event',
rankingSchedule: 'ranking_schedule',
}

View File

@@ -1,4 +1,4 @@
export {authType, ivenTabType, modalTypes, TabList, tattooSlot, commonStatus, ViewTitleCountType, landAuctionStatusType} from './types'
export {authType, ivenTabType, modalTypes, tattooSlot, commonStatus, ViewTitleCountType, landAuctionStatusType} from './types'
export {
mailSendType,
mailType,
@@ -7,7 +7,7 @@ export {
adminLevelType,
logOption,
eventStatus,
currencyType,
currencyItemCode,
blockStatus,
blockSanctions,
blockPeriod,
@@ -27,5 +27,7 @@ export {
opYNType,
opUserSessionType,
opMailType,
amountDeltaType,
TabUserList
} from './options'
export {benItems, MinuteList, HourList, caliumRequestInitData, STATUS_STYLES, months, PAGE_SIZE_OPTIONS, ORDER_OPTIONS} from './data'

View File

@@ -100,15 +100,7 @@ export const menuConfig = {
permissions: {
read: authType.gameLogRead
},
view: false,
authLevel: adminAuthLevel.NONE
},
cryptview: {
title: '크립토 조회',
permissions: {
read: authType.cryptoRead
},
view: false,
view: true,
authLevel: adminAuthLevel.NONE
},
businesslogview: {
@@ -118,6 +110,47 @@ export const menuConfig = {
},
view: true,
authLevel: adminAuthLevel.NONE
},
itemdictionary: {
title: '아이템 백과사전 조회',
permissions: {
read: authType.itemDictionaryRead
},
view: true,
authLevel: adminAuthLevel.NONE
},
craftdictionary: {
title: '제작 아이템 조회',
permissions: {
read: authType.craftingDictionaryRead
},
view: true,
authLevel: adminAuthLevel.NONE
},
instancedictionary: {
title: '인스턴스 조회',
permissions: {
read: authType.instanceDictionaryRead
},
view: true,
authLevel: adminAuthLevel.NONE
},
rankmanage: {
title: '랭킹 점수 관리',
permissions: {
read: authType.rankManagerRead,
update: authType.rankManagerUpdate,
},
view: true,
authLevel: adminAuthLevel.NONE
},
rankview: {
title: '랭킹 시스템 조회',
permissions: {
read: authType.rankInfoRead,
},
view: true,
authLevel: adminAuthLevel.NONE
}
}
},
@@ -154,17 +187,17 @@ export const menuConfig = {
view: true,
authLevel: adminAuthLevel.NONE
},
reportlist: {
title: '신고내역',
permissions: {
read: authType.reportRead,
update: authType.reportUpdate,
delete: authType.reportDelete
},
view: true,
authLevel: adminAuthLevel.NONE
},
event: {
// reportlist: {
// title: '신고내역',
// permissions: {
// read: authType.reportRead,
// update: authType.reportUpdate,
// delete: authType.reportDelete
// },
// view: true,
// authLevel: adminAuthLevel.NONE
// },
rewardevent: {
title: '보상 이벤트 관리',
permissions: {
read: authType.eventRead,
@@ -204,16 +237,36 @@ export const menuConfig = {
view: true,
authLevel: adminAuthLevel.NONE
},
// menubanner: {
// title: '메뉴 배너 관리',
// permissions: {
// read: authType.menuBannerRead,
// update: authType.menuBannerUpdate,
// delete: authType.menuBannerDelete
// },
// view: true,
// authLevel: adminAuthLevel.NONE
// },
menubanner: {
title: '메뉴 배너 관리',
permissions: {
read: authType.menuBannerRead,
update: authType.menuBannerUpdate,
delete: authType.menuBannerDelete
},
view: true,
authLevel: adminAuthLevel.NONE
},
ranking: {
title: '랭킹 스케줄러',
permissions: {
read: authType.rankingRead,
update: authType.rankingUpdate,
delete: authType.rankingDelete
},
view: true,
authLevel: adminAuthLevel.NONE
},
event: {
title: '통합 이벤트 관리',
permissions: {
read: authType.worldEventRead,
update: authType.worldEventUpdate,
delete: authType.worldEventDelete
},
view: true,
authLevel: adminAuthLevel.NONE
},
}
}
};

View File

@@ -4,6 +4,52 @@ export const languageType = [
{ value: 'JA', name: '일본어' },
];
export const TabUserList = [
{ value: 'BASIC', name: '기본정보' },
{ value: 'AVATAR', name: '아바타' },
{ value: 'CLOTH', name: '의상' },
{ value: 'TOOL', name: '도구' },
{ value: 'INVENTORY', name: '인벤토리' },
{ value: 'MAIL', name: '우편' },
{ value: 'MYHOME', name: '마이홈' },
{ value: 'FRIEND', name: '친구목록' },
{ value: 'TATTOO', name: '타투' },
{ value: 'QUEST', name: '퀘스트' }
// { value: 'CLAIM', name: '클레임' }
];
export const TabGameLogList = [
{ value: 'CURRENCY', name: '재화 로그' },
{ value: 'ITEM', name: '아이템 로그' },
{ value: 'CURRENCYITEM', name: '재화(아이템) 로그' },
{ value: 'USERCREATE', name: '유저생성 로그' },
{ value: 'USERLOGIN', name: '유저로그인 로그' },
{ value: 'SNAPSHOT', name: '스냅샷 로그' },
];
export const TabEconomicIndexList = [
{ value: 'CURRENCY_ACQUIRE', name: '재화 획득' },
{ value: 'CURRENCY_CONSUME', name: '재화 소모' },
{ value: 'ITEM_ACQUIRE', name: '아이템 획득' },
{ value: 'ITEM_CONSUME', name: '아이템 소모' },
{ value: 'CURRENCY_ASSETS', name: '재화 보유' },
{ value: 'ITEM_ASSETS', name: '아이템 보유' },
];
export const TabUserIndexList = [
{ value: 'USER', name: '이용자 지표' },
{ value: 'RETENTION', name: '잔존율' },
{ value: 'CURRENCY', name: '재화' },
// { value: 'SEGMENT', name: 'Segment' },
// { value: 'PLAYTIME', name: '플레이타임' },
];
export const TabRankManageList = [
{ value: 'RANK', name: '랭킹 보드' },
// { value: 'RUN_RACE', name: '점프 러너 랭킹 보드' },
// { value: 'BATTLE_OBJECT', name: '컴뱃 존 랭킹 보드' }
];
export const mailSendType = [
{ value: 'ALL', name: '전체' },
{ value: 'RESERVE_SEND', name: '예약 발송' },
@@ -81,7 +127,18 @@ export const landAuctionStatus = [
{ value: 'FAIL', name: '실패' },
];
export const currencyType = [
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: '사파이어' },
{ value: '19010005', name: '루비' },
@@ -183,6 +240,17 @@ export const landSearchType = [
{ value: 'NAME', name: '랜드명' },
];
export const itemSearchType = [
{ value: 'ID', name: '아이템ID' },
{ value: 'NAME', name: '아이템명' },
];
export const instanceSearchType = [
{ value: 'ID', name: '인스턴스 ID' },
{ value: 'NAME', name: '인스턴스명' },
{ value: 'BUILDING', name: '빌딩 ID' },
];
export const blockType = [
{ value: '', name: '선택' },
{ value: 'Access_Restrictions', name: '접근 제한' },
@@ -205,6 +273,30 @@ export const CurrencyType = [
{value: 'Ruby', name: '루비' }
]
export const amountDeltaType = [
{value: 'Acquire', name: '획득' },
{value: 'Consume', name: '소모' },
{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: '대기' },
@@ -320,6 +412,35 @@ export const opItemRestore = [
{ value: 'IMPOSSIBLE', name: '불가능' },
];
export const opPropSmallType = [
{ value: 'ALL', name: '전체' },
{ value: 'FURNITURE', name: 'FURNITURE' },
{ value: 'COOKING', name: 'COOKING' },
{ value: 'CLOTHES', name: 'CLOTHES' },
];
export const opPropRecipeType = [
{ value: 'ALL', name: '전체' },
{ value: 'Basic', name: '기본' },
{ value: 'Add', name: '등록 필요' },
];
export const opInstanceContentsType = [
{ value: 'ALL', name: '전체' },
{ value: 'Concert', name: 'Concert' },
{ value: 'Movie', name: 'Movie' },
{ value: 'Meeting', name: 'Meeting' },
{ value: 'MyHome', name: 'MyHome' },
{ value: 'Normal', name: 'Normal' },
];
export const opInstanceAccessType = [
{ value: 'ALL', name: '전체' },
{ value: 'Public', name: 'Public' },
{ value: 'Item', name: 'Item' },
{ value: 'Belong', name: 'Belong' },
];
export const opEquipType = [
{ value: 0, name: '미장착' },
{ value: 1, name: '의상장착' },
@@ -327,7 +448,7 @@ export const opEquipType = [
{ value: 3, name: '타투장착' },
]
export const opItemType = [
export const opItemLargeType = [
{ value: 'TOOL', name: '도구' },
{ value: 'EXPENDABLE', name: '소모품' },
{ value: 'TICKET', name: '티켓' },
@@ -339,6 +460,95 @@ export const opItemType = [
{ value: 'CURRENCY', name: '재화' },
{ value: 'PRODUCT', name: '제품' },
{ value: 'BEAUTY', name: '뷰티' },
{ value: 'SET_BOX', name: '세트 박스' },
]
export const opItemSmallType = [
{ value: 'HANDMIRROR', name: '손거울' },
{ value: 'LIGHTSTICK', name: '응원봉' },
{ value: 'FIRECRACKER', name: '폭죽총' },
{ value: 'LIGHTSABER', name: '광선검' },
{ value: 'REGISTER_ITEM_SOCIAL_ACTION', name: '소셜액션 등록형 아이템' },
{ value: 'REGISTER_ITEM_INTERIOR', name: '인테리어 등록형 아이템' },
{ value: 'SAIYAN_AURA', name: '초사이어인 오라' },
{ value: 'TICKET', name: '소모형 티켓' },
{ value: 'RANDOMBOX', name: '골드(재화) 가챠 랜덤 박스' },
{ value: 'SHIRT', name: '상의' },
{ value: 'DRESS', name: '드레스' },
{ value: 'OUTER', name: '겉옷' },
{ value: 'PANTS', name: '하의' },
{ value: 'GLOVES', name: '장갑' },
{ value: 'RING', name: '반지' },
{ value: 'BRACELET', name: '팔찌' },
{ value: 'BAG', name: '가방(크로스백)' },
{ value: 'BACKPACK', name: '가방(백팩)' },
{ value: 'CAP', name: '모자' },
{ value: 'MASK', name: '가면' },
{ value: 'GLASSES', name: '안경' },
{ value: 'EARRING', name: '귀걸이' },
{ value: 'NECKLACE', name: '목걸이' },
{ value: 'SHOES', name: '신발' },
{ value: 'SOCKS', name: '양말' },
{ value: 'ANKLET', name: '발찌' },
{ value: 'OFFICECHAIR', name: '의자' },
{ value: 'WALLMOUNTTV', name: '벽걸이TV' },
{ value: 'OUTDOORCHAIR', name: '야외용의자' },
{ value: 'TV', name: 'TV' },
{ value: 'VIGNETTE', name: '소품' },
{ value: 'COOKWARE', name: '조리도구' },
{ value: 'KITCHEN_TOOL', name: '부엌도구' },
{ value: 'LAPTOP', name: '노트북' },
{ value: 'OUTDOOR_GOODS', name: '캠핑도구' },
{ value: 'BED', name: '침대' },
{ value: 'DECO', name: '소형장식품' },
{ value: 'FURNITURE', name: '(러그)가구' },
{ value: 'MUSIC', name: '악기/음향' },
{ value: 'SHELF_S', name: '소형 선반(TV다이)' },
{ value: 'SHELF_L', name: '대형 선반(금고,책장)' },
{ value: 'SOFA_SINGLE', name: '1인용 소파' },
{ value: 'SOFA_COUCH', name: '카우치 소파' },
{ value: 'LIGHT_CEILING', name: '천정 조명' },
{ value: 'LIGHT_FLOOR', name: '스탠드 조명' },
{ value: 'LIGHT_TABLE', name: '탁상 조명' },
{ value: 'LIGHT_PENDENT', name: '팬던트 조명' },
{ value: 'TABLE_S', name: '소형 테이블' },
{ value: 'TABLE_L', name: '대형 테이블' },
{ value: 'TABLE_LIVINGROOM', name: '거실 테이블' },
{ value: 'TABLE_OFFICE', name: '사무용 테이블' },
{ value: 'LEISURE_APPLIANCE', name: '스포츠/여가' },
{ value: 'INDUCTION', name: '인덕션' },
{ value: 'MICROWAVE', name: '전자레인지' },
{ value: 'LARGE_APPLIANCE', name: '대형가전' },
{ value: 'COSMETIC', name: '화장품' },
{ value: 'CHEST', name: '앞면' },
{ value: 'LEFT_ARM', name: '왼팔' },
{ value: 'RIGHT_ARM', name: '오른팔' },
{ value: 'BACK', name: '후면' },
{ value: 'LEFT_LEG', name: '왼다리' },
{ value: 'RIGHT_LEG', name: '오른다리' },
{ value: 'CARTRIDGE', name: '속성 카트리지' },
{ value: 'BUFF_DRINK', name: '드링크(물약) 아이템' },
{ value: 'INTERPHONE', name: '인터폰' },
{ value: 'MEGAPHONE', name: '확성기' },
{ value: 'CURRENCY', name: 'CURRENCY' },
{ value: 'NFTLAND', name: 'NFTLAND' },
{ value: 'SUMMONSTONE', name: '소환석' },
{ value: 'GOLD', name: '골드' },
{ value: 'SAPPHIRE', name: '사파이어' },
{ value: 'CALIUM', name: '칼리움' },
{ value: 'BEAM', name: '빔' },
{ value: 'RUBY', name: '루비' },
{ value: 'LIGHT_LIMITED', name: '언리얼 라이트 사용 조명' },
{ value: 'SPEAKER', name: '재생 기능성 스피커' },
{ value: 'SETBOX', name: '세트박스' },
{ value: 'DRESS_SHOES', name: '드레스+신발' },
{ value: 'SHOULDERBAG', name: '숄더백' },
{ value: 'RECIPE', name: '레시피' }
]
export const opGender = [
{ value: 'MALE', name: '남성' },
{ value: 'FEMALE', name: '여성' },
]
export const opHistoryType = [
@@ -410,6 +620,74 @@ export const opDBType = [
{ value: 'MySql', name: 'MySql'},
]
export const opLogCategory = [
{ value: 'SCHEDULER', name: '스케줄러'},
{ value: 'DYNAMODB', name: 'DynamoDB'},
{ value: 'MARIADB', name: 'MariaDB'},
{ value: 'MESSAGE_QUEUE', name: '메시지큐'},
{ value: 'REDIS', name: 'Redis'},
{ value: 'S3', name: 'S3'},
{ value: 'BATCH_JOB', name: '배치잡'},
]
export const opLogAction = [
{ value: 'KICK_USER', name: '유저킥' },
{ value: 'ADMIN_LEVEL', name: 'GM 레벨' },
{ value: 'NICKNAME_CHANGE', name: '아바타명 변경' },
{ value: 'MAIL_ITEM', name: '메일 아이템' },
{ value: 'QUEST_TASK', name: '퀘스트 Task' },
{ value: 'SCHEDULE_CLEANUP', name: '스케줄 캐시정리' },
{ value: 'SCHEDULE_DATA_INIT', name: '스케줄 데이터 초기화' },
{ value: 'SCHEDULE_LAND_OWNER_CHANGE', name: '스케줄 랜드 소유자 변경' },
{ value: 'SCHEDULE_BLACK_LIST', name: '스케줄 이용자 제재' },
{ value: 'SCHEDULE_NOTICE', name: '스케줄 인게임메시지' },
{ value: 'SCHEDULE_MAIL', name: '스케줄 우편' },
{ value: 'SCHEDULE_EVENT', name: '스케줄 이벤트' },
{ value: 'SCHEDULE_BATTLE_EVENT', name: '스케줄 전투 이벤트' },
{ value: 'SCHEDULE_LAND_AUCTION', name: '스케줄 랜드 경매' },
{ value: 'BANNER', name: '메뉴 배너' },
{ value: 'BATTLE_EVENT', name: '전투 이벤트' },
{ value: 'BUILDING', name: '빌딩' },
{ value: 'LAND_OWNER_CHANGE', name: '랜드 소유자 변경' },
{ value: 'LAND_AUCTION', name: '랜드 경매' },
{ value: 'GROUP', name: '그룹' },
{ value: 'ADMIN', name: '운영자' },
{ value: 'ADMIN_GROUP', name: '운영자 그룹' },
{ value: 'ADMIN_DELETE', name: '운영자 삭제' },
{ value: 'AUTH_ADMIN', name: '운영자 권한' },
{ value: 'PASSWORD_INIT', name: '비밀번호 초기화' },
{ value: 'PASSWORD_CHANGE', name: '비밀번호 변경' },
{ value: 'BLACK_LIST', name: '이용자 제재' },
{ value: 'CALIUM_REQUEST', name: '칼리움 요청' },
{ value: 'EVENT', name: '이벤트' },
{ value: 'MAIL', name: '우편' },
{ value: 'NOTICE', name: '인게임메시지' },
{ value: 'DATA_INIT', name: '데이터 초기화' },
{ value: 'DATA', name: '데이터' },
{ value: 'USER', name: '사용자' },
{ value: 'ITEM', name: '아이템' }
]
export const opCommonStatus = [
{ value: 'SUCCESS', name: '성공' },
{ value: 'FAIL', name: '실패' },
{ value: 'WAIT', name: '대기' },
{ value: 'END', name: '종료' },
{ value: 'RUNNING', name: '진행중' },
]
export const opRankingType = [
{ value: 'PIONEER', name: '개척자' },
{ value: 'RUNNER1', name: '점프러너 - 맵1' },
{ value: 'RUNNER2', name: '점프러너 - 맵2' },
{ value: 'RUNNER3', name: '점프러너 - 맵3' },
{ value: 'RUNNER4', name: '점프러너 - 맵4' },
{ value: 'BATTLE_FFA', name: '컴뱃존 - FFA' },
{ value: 'BATTLE_TEAM', name: '컴뱃존 - TEAM' },
{ value: 'EVENT_CONTRIBUTION', name: '월드 이벤트 - 기여도' },
{ value: 'EVENT_CRAFT', name: '월드 이벤트 - 개인 제작' },
]
// export const logAction = [
// { value: "None", name: "ALL" },
// { value: "AIChatDeleteCharacter", name: "NPC 삭제" },
@@ -706,6 +984,7 @@ export const opDBType = [
export const logAction = [
{ value: "None", name: "전체" },
{ value: "AdminToolQuestTaskForceComplete", name: "AdminToolQuestTaskForceComplete" },
{ value: "AIChatDeleteCharacter", name: "AIChatDeleteCharacter" },
{ value: "AIChatDeleteUser", name: "AIChatDeleteUser" },
{ value: "AIChatGetCharacter", name: "AIChatGetCharacter" },
@@ -740,12 +1019,12 @@ export const logAction = [
{ value: "BeaconShopUpdateDailyCount", name: "BeaconShopUpdateDailyCount" },
{ value: "BeaconShopDeleteRecord", name: "BeaconShopDeleteRecord" },
{ value: "BeaconShopDeactiveItems", name: "BeaconShopDeactiveItems" },
{ value: "BrokerApiAdmin", name: "BrokerApiAdmin" },
// { value: "BrokerApiAdmin", name: "BrokerApiAdmin" },
{ value: "BrokerApiPlanetAuth", name: "BrokerApiPlanetAuth" },
{ value: "BrokerApiUserExchangeOrderCompleted", name: "BrokerApiUserExchangeOrderCompleted" },
{ value: "BrokerApiUserExchangeOrderCreated", name: "BrokerApiUserExchangeOrderCreated" },
{ value: "BrokerApiUserSystemMailSend", name: "BrokerApiUserSystemMailSend" },
{ value: "BrokerApiUserEchoSystemRequest", name: "BrokerApiUserEchoSystemRequest" },
// { value: "BrokerApiUserSystemMailSend", name: "BrokerApiUserSystemMailSend" },
// { value: "BrokerApiUserEchoSystemRequest", name: "BrokerApiUserEchoSystemRequest" },
{ value: "BrokerApiUserLogin", name: "BrokerApiUserLogin" },
{ value: "BuffAdd", name: "BuffAdd" },
{ value: "BuffDelete", name: "BuffDelete" },
@@ -795,6 +1074,7 @@ export const logAction = [
{ value: "ClaimReward", name: "ClaimReward" },
{ value: "ConvertCalium", name: "ConvertCalium" },
{ value: "ConvertExchangeCalium", name: "ConvertExchangeCalium" },
{ value: "ContentsMove", name: "ContentsMove" },
{ value: "CraftFinish", name: "CraftFinish" },
{ value: "CraftHelp", name: "CraftHelp" },
{ value: "CraftRecipeRegister", name: "CraftRecipeRegister" },
@@ -821,6 +1101,12 @@ export const logAction = [
{ value: "FriendAdd", name: "FriendAdd" },
{ value: "FriendDelete", name: "FriendDelete" },
{ value: "GainLandProfit", name: "GainLandProfit" },
{ value: "GameModeObjectInteraction", name: "GameModeObjectInteraction" },
{ value: "GameModeObjectStateUpdate", name: "GameModeObjectStateUpdate" },
{ value: "GameModeUserDead", name: "GameModeUserDead" },
{ value: "GameModeUserRespawn", name: "GameModeUserRespawn" },
{ value: "GameModePenalty", name: "GameModePenalty" },
{ value: "GameModeAddMatchCount", name: "GameModeAddMatchCount" },
{ value: "InviteParty", name: "InviteParty" },
{ value: "ItemBuy", name: "ItemBuy" },
{ value: "ItemDestroy", name: "ItemDestroy" },
@@ -851,8 +1137,17 @@ export const logAction = [
{ value: "MailRead", name: "MailRead" },
{ value: "MailSend", name: "MailSend" },
{ value: "MailTaken", name: "MailTaken" },
{ value: "MatchReserve", name: "MatchReserve" },
{ value: "MatchCancel", name: "MatchCancel" },
{ value: "MatchResult", name: "MatchResult" },
{ value: "MatchRoomUserJoin", name: "MatchRoomUserJoin" },
{ value: "MatchRoomUserQuit", name: "MatchRoomUserQuit" },
{ value: "MatchRoomCreate", name: "MatchRoomCreate" },
{ value: "MatchRoomUpdate", name: "MatchRoomUpdate" },
{ value: "MatchRoomDestroy", name: "MatchRoomDestroy" },
{ value: "ModifyLandInfo", name: "ModifyLandInfo" },
{ value: "MoneyChange", name: "MoneyChange" },
{ value: "MoveToBeacon", name: "MoveToBeacon" },
{ value: "ProductGive", name: "ProductGive" },
{ value: "ProductOpenFailed", name: "ProductOpenFailed" },
{ value: "ProductOpenSuccess", name: "ProductOpenSuccess" },
@@ -867,6 +1162,10 @@ export const logAction = [
{ value: "QuestMailSend", name: "QuestMailSend" },
{ value: "QuestMainTask", name: "QuestMainTask" },
{ value: "QuestTaskUpdate", name: "QuestTaskUpdate" },
{ value: "RankingStart", name: "RankingStart" },
{ value: "RankingFinish", name: "RankingFinish" },
{ value: "RankingScoreUpdate", name: "RankingScoreUpdate" },
{ value: "RankingEventActionScore", name: "RankingEventActionScore" },
{ value: "RefuseFriendRequest", name: "RefuseFriendRequest" },
{ value: "RenameFriendFolder", name: "RenameFriendFolder" },
{ value: "RenameMyhome", name: "RenameMyhome" },
@@ -876,6 +1175,11 @@ export const logAction = [
{ value: "ReplySummonParty", name: "ReplySummonParty" },
{ value: "ReservationEnterToServer", name: "ReservationEnterToServer" },
{ value: "RewardProp", name: "RewardProp" },
{ value: "RunRaceFinishReward", name: "RunRaceFinishReward" },
{ value: "RunRaceRespawnReward", name: "RunRaceRespawnReward" },
{ value: "RunRaceUnFinishReward", name: "RunRaceUnFinishReward" },
{ value: "RunRaceCheckPointAbusing", name: "RunRaceCheckPointAbusing" },
{ value: "RunRaceResultSummary", name: "RunRaceResultSummary" },
{ value: "SaveMyhome", name: "SaveMyhome" },
{ value: "SeasonPassBuyCharged", name: "SeasonPassBuyCharged" },
{ value: "SeasonPassStartNew", name: "SeasonPassStartNew" },
@@ -925,6 +1229,7 @@ export const logAction = [
{ value: "UpdateGameOption", name: "UpdateGameOption" },
{ value: "UpdateLanguage", name: "UpdateLanguage" },
{ value: "UpdateUgcNpcLike", name: "UpdateUgcNpcLike" },
{ value: "UpdateGameModePlayerRegulation", name: "UpdateGameModePlayerRegulation" },
{ value: "UserBlock", name: "UserBlock" },
{ value: "UserBlockCancel", name: "UserBlockCancel" },
{ value: "UserCreate", name: "UserCreate" },
@@ -932,6 +1237,7 @@ export const logAction = [
{ value: "UserLogout", name: "UserLogout" },
{ value: "UserLogoutSnapShot", name: "UserLogoutSnapShot" },
{ value: "UserReport", name: "UserReport" },
{ value: "WorldEventActionScore", name: "WorldEventActionScore" },
{ value: "Warp", name: "Warp" },
{ value: "igmApiLogin", name: "igmApiLogin" }
];
@@ -972,11 +1278,15 @@ export const logDomain = [
{ value: "Cart", name: "Cart" },
{ value: "Currency", name: "Currency" },
{ value: "CustomDefineUi", name: "CustomDefineUi" },
{ value: "EventActionScore", name: "EventActionScore" },
{ value: "EscapePosition", name: "EscapePosition" },
{ value: "Friend", name: "Friend" },
{ value: "Farming", name: "Farming" },
{ value: "FarmingReward", name: "FarmingReward" },
{ value: "GameLogInOut", name: "GameLogInOut" },
{ value: "GameObjectInteraction", name: "GameObjectInteraction" },
{ value: "GameModePenalty", name: "GameModePenalty" },
{ value: "GameModePlayRegulation", name: "GameModePlayRegulation" },
{ value: "Item", name: "Item" },
{ value: "IgmApi", name: "IgmApi" },
{ value: "MyHome", name: "MyHome" },
@@ -988,6 +1298,9 @@ export const logDomain = [
{ value: "Mail", name: "Mail" },
{ value: "MailStoragePeriodExpired", name: "MailStoragePeriodExpired" },
{ value: "MailProfile", name: "MailProfile" },
{ value: "MatchUser", name: "MatchUser" },
{ value: "MatchServerUser", name: "MatchServerUser" },
{ value: "MatchRoom", name: "MatchRoom" },
{ value: "Party", name: "Party" },
{ value: "PartyMember", name: "PartyMember" },
{ value: "PartyVote", name: "PartyVote" },
@@ -1002,9 +1315,16 @@ export const logDomain = [
{ value: "QuestMain", name: "QuestMain" },
{ value: "QuestUgq", name: "QuestUgq" },
{ value: "QuestMail", name: "QuestMail" },
{ value: "Ranking", name: "Ranking" },
{ value: "Ranker", name: "Ranker" },
{ value: "RenewalShopProducts", name: "RenewalShopProducts" },
{ value: "Rental", name: "Rental" },
{ value: "RewardProp", name: "RewardProp" },
{ value: "RunRaceFinishReward", name: "RunRaceFinishReward" },
{ value: "RunRaceRespawnReward", name: "RunRaceRespawnReward" },
{ value: "RunRaceUnFinishReward", name: "RunRaceUnFinishReward" },
{ value: "RunRaceCheckPointAbusing", name: "RunRaceCheckPointAbusing" },
{ value: "RunRaceRewardSummary", name: "RunRaceRewardSummary" },
{ value: "Stage", name: "Stage" },
{ value: "SocialAction", name: "SocialAction" },
{ value: "SeasonPass", name: "SeasonPass" },

View File

@@ -0,0 +1,96 @@
{
"initialSearchParams": {
"searchType": "GUID",
"searchData": "",
"logAction": "None",
"logDomain": "BASE",
"tran_id": "",
"startDate": "",
"endDate": "",
"orderBy": "DESC",
"pageSize": 500,
"currentPage": 1
},
"searchFields": [
{
"type": "select",
"id": "searchType",
"optionsRef": "userSearchType2",
"col": 1,
"required": true
},
{
"type": "text",
"id": "searchData",
"placeholder": "대상 입력",
"width": "300px",
"col": 1,
"required": true
},
{
"type": "period",
"startDateId": "startDate",
"endDateId": "endDate",
"label": "조회 일자",
"col": 1
},
{
"type": "text",
"id": "searchContent",
"label": "우편 내용",
"placeholder": "우편 내용(공백으로 구분)",
"width": "300px",
"col": 1
},
{
"type": "select",
"id": "sendType",
"label": "발송 방식",
"optionsRef": "mailSendType",
"col": 2
},
{
"type": "select",
"id": "status",
"label": "발송 상태",
"optionsRef": "mailSendStatus",
"col": 2
},
{
"type": "select",
"id": "mailType",
"label": "우편 타입",
"optionsRef": "mailType",
"col": 2
},
{
"type": "select",
"id": "receiveType",
"label": "수신 대상",
"optionsRef": "mailReceiveType",
"col": 2
}
],
"apiInfo": {
"functionName": "MailView",
"loadOnMount": true,
"paramsMapping": [
"searchTitle",
"searchContent",
"sendType",
"status",
"mailType",
"receiveType",
{"param": "startDate", "transform": "toISOString"},
{"param": "endDate", "transform": "toISOString"},
"orderBy",
"pageSize",
"currentPage"
],
"pageField": "currentPage",
"pageSizeField": "pageSize",
"orderField": "orderBy"
}
}

View File

@@ -1,7 +1,6 @@
{
"initialSearchParams": {
"searchTitle": "",
"searchContent": "",
"searchData": "",
"status": "ALL",
"startDate": "",
"endDate": "",
@@ -13,51 +12,37 @@
"searchFields": [
{
"type": "text",
"id": "searchTitle",
"label": "우편 제목",
"id": "searchData",
"label": "제목",
"placeholder": "제목 입력",
"width": "300px",
"col": 1
},
{
"type": "period",
"startDateId": "startDate",
"endDateId": "endDate",
"label": "조회 일자",
"col": 1
},
{
"type": "text",
"id": "searchContent",
"label": "우편 내용",
"placeholder": "우편 내용(공백으로 구분)",
"width": "300px",
"col": 1
},
{
"type": "select",
"id": "status",
"label": "이벤트 상태",
"optionsRef": "eventStatus",
"col": 2
"label": "상태",
"optionsRef": "opMenuBannerStatus",
"col": 1
},
{
"type": "period",
"startDateId": "startDate",
"endDateId": "endDate",
"label": "기간",
"col": 1
}
],
"apiInfo": {
"functionName": "EventView",
"endpointName": "EventView",
"loadOnMount": true,
"paramsMapping": [
"searchTitle",
"searchContent",
"status",
"paramTransforms": [
{"param": "startDate", "transform": "toISOString"},
{"param": "endDate", "transform": "toISOString"},
"orderBy",
"pageSize",
"currentPage"
{"param": "endDate", "transform": "toISOString"}
],
"pageField": "currentPage",
"pageSizeField": "pageSize",
"pageField": "page_no",
"pageSizeField": "page_size",
"orderField": "orderBy"
}
}

View File

@@ -0,0 +1,119 @@
{
"id": "eventTable",
"selection": {
"type": "single",
"idField": "id"
},
"header": {
"countType": "total",
"orderType": "desc",
"pageType": "default",
"buttons": [
{
"id": "delete",
"text": "선택 삭제",
"theme": "line",
"disableWhen": "noSelection",
"requiredAuth": "worldEventDelete",
"action": "delete"
},
{
"id": "register",
"text": "통합 이벤트 등록",
"theme": "primary",
"requiredAuth": "worldEventUpdate",
"action": "regist"
}
]
},
"columns": [
{
"id": "checkbox",
"type": "checkbox",
"width": "40px",
"title": ""
},
{
"id": "row_num",
"type": "text",
"width": "70px",
"title": "번호"
},
{
"id": "status",
"type": "status",
"width": "120px",
"title": "상태",
"option_name": "opCommonStatus"
},
{
"id": "title",
"type": "text",
"title": "제목",
"width": "150px"
},
{
"id": "personal_event_action_id",
"type": "text",
"width": "150px",
"title": "개인제작 이벤트 모드"
},
{
"id": "global_event_action_id",
"type": "text",
"width": "150px",
"title": "기여도 이벤트 모드"
},
{
"id": "max_point",
"type": "text",
"width": "150px",
"title": "기여도 목표점수"
},
{
"id": "start_dt",
"type": "date",
"width": "220px",
"title": "시작일(KST)",
"format": {
"type": "function",
"name": "convertKTC"
}
},
{
"id": "end_dt",
"type": "date",
"width": "220px",
"title": "종료일(KST)",
"format": {
"type": "function",
"name": "convertKTC"
}
},
{
"id": "detail",
"type": "button",
"width": "120px",
"title": "상세보기",
"text": "상세보기",
"action": {
"type": "modal",
"target": "detailModal",
"dataParam": {
"id": "id"
}
}
},
{
"id": "history",
"type": "button",
"width": "120px",
"title": "히스토리",
"text": "히스토리"
}
],
"sort": {
"defaultColumn": "row_num",
"defaultDirection": "desc"
}
}

View File

@@ -3,8 +3,8 @@
"searchType": "ID",
"searchData": "",
"historyType": "",
"startDate": "",
"endDate": "",
"startDate": "today",
"endDate": "today",
"orderBy": "DESC",
"pageSize": 50,
"currentPage": 1
@@ -37,7 +37,10 @@
"startDateId": "startDate",
"endDateId": "endDate",
"label": "기간",
"col": 2
"col": 2,
"width": "500px",
"format": "YYYY-MM-DD HH:mm:ss",
"showTime": true
}
],

View File

@@ -12,7 +12,7 @@
},
"columns": [
{
"id": "timestamp",
"id": "logTime",
"type": "date",
"width": "200px",
"title": "일시(KST)",
@@ -22,25 +22,38 @@
}
},
{
"id": "dbType",
"id": "category",
"type": "option",
"width": "100px",
"title": "DB타입",
"option_name": "opDBType"
"title": "로그종류",
"option_name": "opLogCategory"
},
{
"id": "historyType",
"id": "action",
"type": "option",
"width": "150px",
"title": "이력종류",
"option_name": "opHistoryType"
"title": "로그액션",
"option_name": "opLogAction"
},
{
"id": "userId",
"id": "status",
"type": "option",
"width": "100px",
"title": "상태",
"option_name": "opCommonStatus"
},
{
"id": "worker",
"type": "text",
"width": "100px",
"title": "작업자"
},
{
"id": "message",
"type": "text",
"width": "100px",
"title": "비고"
},
{
"id": "detail",
"type": "button",

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

@@ -0,0 +1,49 @@
{
"initialSearchParams": {
"search_type": "ID",
"search_data": "",
"small_type": "ALL",
"recipe_type": "ALL",
"orderBy": "DESC",
"pageSize": 50,
"currentPage": 1
},
"searchFields": [
{
"type": "select",
"id": "search_type",
"optionsRef": "itemSearchType",
"col": 1
},
{
"type": "text",
"id": "search_data",
"placeholder": "아이템 입력",
"width": "300px",
"col": 1
},
{
"type": "select",
"id": "small_type",
"label": "제작 분류",
"optionsRef": "opPropSmallType",
"col": 1
},
{
"type": "select",
"id": "recipe_type",
"label": "레시피 필요 여부",
"optionsRef": "opPropRecipeType",
"col": 1
}
],
"apiInfo": {
"endpointName": "getCraftingDictionaryList",
"loadOnMount": true,
"pageField": "page_no",
"pageSizeField": "page_size",
"orderField": "orderBy"
}
}

View File

@@ -0,0 +1,49 @@
{
"initialSearchParams": {
"search_type": "ID",
"search_data": "",
"contents_type": "ALL",
"access_type": "ALL",
"orderBy": "DESC",
"pageSize": 50,
"currentPage": 1
},
"searchFields": [
{
"type": "select",
"id": "search_type",
"optionsRef": "instanceSearchType",
"col": 1
},
{
"type": "text",
"id": "search_data",
"placeholder": "인스턴스 입력",
"width": "300px",
"col": 1
},
{
"type": "select",
"id": "contents_type",
"label": "컨텐츠 타입",
"optionsRef": "opInstanceContentsType",
"col": 1
},
{
"type": "select",
"id": "access_type",
"label": "입장 방식",
"optionsRef": "opInstanceAccessType",
"col": 1
}
],
"apiInfo": {
"endpointName": "getInstanceDictionaryList",
"loadOnMount": true,
"pageField": "page_no",
"pageSizeField": "page_size",
"orderField": "orderBy"
}
}

View File

@@ -0,0 +1,48 @@
{
"initialSearchParams": {
"searchData": "",
"status": "ALL",
"startDate": "",
"endDate": "",
"orderBy": "DESC",
"pageSize": 50,
"currentPage": 1
},
"searchFields": [
{
"type": "text",
"id": "searchData",
"label": "제목",
"placeholder": "제목 입력",
"width": "300px",
"col": 1
},
{
"type": "select",
"id": "status",
"label": "상태",
"optionsRef": "opMenuBannerStatus",
"col": 1
},
{
"type": "period",
"startDateId": "startDate",
"endDateId": "endDate",
"label": "기간",
"col": 1
}
],
"apiInfo": {
"endpointName": "RankingScheduleView",
"loadOnMount": true,
"paramTransforms": [
{"param": "startDate", "transform": "toISOString"},
{"param": "endDate", "transform": "toISOString"}
],
"pageField": "page_no",
"pageSizeField": "page_size",
"orderField": "orderBy"
}
}

View File

@@ -0,0 +1,164 @@
{
"id": "rankingTable",
"selection": {
"type": "single",
"idField": "id"
},
"header": {
"countType": "total",
"orderType": "desc",
"pageType": "default",
"buttons": [
{
"id": "update",
"text": "강제 새로고침",
"theme": "line",
"disableWhen": "noSelection",
"requiredAuth": "rankingUpdate",
"action": "update"
},
{
"id": "init",
"text": "강제 초기화",
"theme": "line",
"disableWhen": "noSelection",
"requiredAuth": "rankingUpdate",
"action": "rankingInit"
},
{
"id": "snapshot",
"text": "강제 스냅샷",
"theme": "line",
"disableWhen": "noSelection",
"requiredAuth": "rankingUpdate",
"action": "rankingSnapshot"
},
{
"id": "delete",
"text": "선택 삭제",
"theme": "line",
"disableWhen": "noSelection",
"requiredAuth": "rankingDelete",
"action": "delete"
},
{
"id": "register",
"text": "랭킹 스케줄 등록",
"theme": "primary",
"requiredAuth": "rankingUpdate",
"action": "regist"
}
]
},
"columns": [
{
"id": "checkbox",
"type": "checkbox",
"width": "40px",
"title": ""
},
{
"id": "row_num",
"type": "text",
"width": "70px",
"title": "번호"
},
{
"id": "status",
"type": "status",
"width": "100px",
"title": "상태",
"option_name": "opCommonStatus"
},
{
"id": "title",
"type": "text",
"title": "제목"
},
{
"id": "meta_id",
"type": "text",
"title": "랭킹모드",
"width": "100px"
},
{
"id": "event_action_id",
"type": "text",
"title": "이벤트 액션 그룹",
"width": "150px"
},
{
"id": "start_dt",
"type": "date",
"width": "200px",
"title": "시작일(KST)",
"format": {
"type": "function",
"name": "convertKTC"
}
},
{
"id": "end_dt",
"type": "date",
"width": "200px",
"title": "종료일(KST)",
"format": {
"type": "function",
"name": "convertKTC"
}
},
{
"id": "base_dt",
"type": "date",
"width": "200px",
"title": "기준일(KST)",
"format": {
"type": "function",
"name": "convertKTC"
}
},
{
"id": "refresh_interval",
"type": "text",
"width": "120px",
"title": "새로고침 주기"
},
{
"id": "initialization_interval",
"type": "text",
"width": "120px",
"title": "초기화 주기"
},
{
"id": "snapshot_interval",
"type": "text",
"width": "120px",
"title": "스냅샷 주기"
},
{
"id": "detail",
"type": "button",
"width": "120px",
"title": "상세보기",
"text": "상세보기",
"action": {
"type": "modal",
"target": "detailModal",
"dataParam": {
"id": "id"
}
}
},
{
"id": "history",
"type": "button",
"width": "120px",
"title": "히스토리",
"text": "히스토리"
}
],
"sort": {
"defaultColumn": "row_num",
"defaultDirection": "desc"
}
}

View File

@@ -0,0 +1,63 @@
{
"initialSearchParams": {
"searchTitle": "",
"searchContent": "",
"status": "ALL",
"startDate": "",
"endDate": "",
"orderBy": "DESC",
"pageSize": 50,
"currentPage": 1
},
"searchFields": [
{
"type": "text",
"id": "searchTitle",
"label": "우편 제목",
"placeholder": "제목 입력",
"width": "300px",
"col": 1
},
{
"type": "period",
"startDateId": "startDate",
"endDateId": "endDate",
"label": "조회 일자",
"col": 1
},
{
"type": "text",
"id": "searchContent",
"label": "우편 내용",
"placeholder": "우편 내용(공백으로 구분)",
"width": "300px",
"col": 1
},
{
"type": "select",
"id": "status",
"label": "이벤트 상태",
"optionsRef": "eventStatus",
"col": 2
}
],
"apiInfo": {
"functionName": "RewardEventView",
"loadOnMount": true,
"paramsMapping": [
"searchTitle",
"searchContent",
"status",
{"param": "startDate", "transform": "toISOString"},
{"param": "endDate", "transform": "toISOString"},
"orderBy",
"pageSize",
"currentPage"
],
"pageField": "currentPage",
"pageSizeField": "pageSize",
"orderField": "orderBy"
}
}

View File

@@ -52,6 +52,18 @@ export const authType = {
menuBannerRead: 50,
menuBannerUpdate: 51,
menuBannerDelete: 52,
itemDictionaryRead: 53,
rankManagerRead: 54,
rankManagerUpdate: 55,
rankingRead: 56,
rankingUpdate: 57,
rankingDelete: 58,
worldEventRead: 59,
worldEventUpdate: 60,
worldEventDelete: 61,
craftingDictionaryRead: 62,
rankInfoRead: 63,
instanceDictionaryRead: 64,
levelReader: 999,
@@ -75,20 +87,6 @@ export const adminAuthLevel = {
DEVELOPER: "Developer",
}
export const TabList = [
{ title: '기본정보' },
{ title: '아바타' },
{ title: '의상' },
{ title: '도구' },
{ title: '인벤토리' },
{ title: '우편' },
{ title: '마이홈' },
{ title: '친구목록' },
{ title: '타투' },
{ title: '퀘스트' },
// { title: '클레임' },
];
export const ivenTabType = {
CLOTH: "cloth",
PROP: "prop",
@@ -158,4 +156,10 @@ export const currencyCodeTypes = {
sapphire: "19010002",
ruby: "19010005",
calium: "19010003"
}
}
export const languageNames = {
'Ko': '한국어',
'En': '영어',
'Ja': '일본어',
};

View File

@@ -1,198 +0,0 @@
import { styled } from 'styled-components';
import { useState } from 'react';
import Button from '../../components/common/button/Button';
import DatePicker, { registerLocale } from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { getMonth, getYear } from 'date-fns';
import range from 'lodash/range';
import { TextInput, SelectInput, DatePickerWrapper, InputLabel, BtnWrapper } from '../../styles/Components';
const GoodsLogSearchBar = () => {
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [selectData, setSelectData] = useState('default');
const years = range(1990, getYear(new Date()) + 1, 1);
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
const handleChange = e => {
setSelectData(e.target.value);
};
return (
<>
<SearchbarStyle2>
<SearchRow>
<SearchItem>
<InputLabel>조회 기간</InputLabel>
<DatePickerWrapper>
<InputGroup>
<DatePicker
selected={startDate}
onChange={date => setStartDate(date)}
className="datepicker"
placeholderText="검색기간 선택"
calendarClassName="calendar"
dateFormat="yyyy - MM - dd"
locale="ko"
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
<div className="calendar-top">
<button
className="btn-prev"
onClick={e => {
e.preventDefault();
decreaseMonth();
}}
disabled={prevMonthButtonDisabled}></button>
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
.
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<button
className="btn-next"
onClick={e => {
e.preventDefault();
increaseMonth();
}}
disabled={nextMonthButtonDisabled}></button>
</div>
)}
/>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
</InputGroup>
<span>~</span>
<InputGroup>
<DatePicker
selected={endDate}
onChange={date => setEndDate(date)}
className="datepicker"
placeholderText="검색기간 선택"
calendarClassName="calendar"
dateFormat="yyyy - MM - dd"
minDate = {startDate}
locale="ko"
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
<div className="calendar-top">
<button
className="btn-prev"
onClick={e => {
e.preventDefault();
decreaseMonth();
}}
disabled={prevMonthButtonDisabled}></button>
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
.
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<button
className="btn-next"
onClick={e => {
e.preventDefault();
increaseMonth();
}}
disabled={nextMonthButtonDisabled}></button>
</div>
)}
/>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
</InputGroup>
</DatePickerWrapper>
</SearchItem>
</SearchRow>
<SearchRow>
<SearchItem>
<InputLabel>조회 대상</InputLabel>
<TextInput type="text" placeholder="조회 대상 유저의 GUID를 입력하세요." width="600px" />
</SearchItem>
<BtnWrapper $gap="8px">
<Button theme="reset" />
<Button theme="gray" text="검색" />
</BtnWrapper>
</SearchRow>
</SearchbarStyle2>
</>
);
};
export default GoodsLogSearchBar;
const SearchbarStyle = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 20px 0;
font-size: 14px;
padding: 20px;
border-top: 1px solid #000;
border-bottom: 1px solid #000;
margin-bottom: 40px;
`;
const SearchbarStyle2 = styled(SearchbarStyle)`
flex-flow: column;
gap: 20px;
`;
const SearchRow = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px 0;
`;
const InputGroup = styled.div`
display: flex;
align-items: center;
gap: 5px;
`;
const SearchItem = styled.div`
display: flex;
align-items: center;
gap: 20px;
margin-right: 50px;
${TextInput}, ${SelectInput} {
height: 35px;
}
${TextInput} {
padding: 0 10px;
max-width: 400px;
}
`;

View File

@@ -0,0 +1,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

@@ -0,0 +1,166 @@
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import {
CircularProgressWrapper,
FormWrapper,
TableStyle,
TableWrapper,
} from '../../styles/Components';
import { amountDeltaType, CurrencyType } from '../../assets/data';
import { useTranslation } from 'react-i18next';
import { numberFormatter } from '../../utils';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { CurrencyLogSearchBar, useCurrencyLogSearch } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import Pagination from '../common/Pagination/Pagination';
import {
INITIAL_PAGE_LIMIT,
STORAGE_BUSINESS_LOG_SEARCH,
STORAGE_GAME_LOG_CURRENCY_SEARCH,
} from '../../assets/data/adminConstants';
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
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
} = useCurrencyLogSearch(token, 500);
useEffect(() => {
if(active) {
// 세션 스토리지에서 복사된 메일 데이터 가져오기
const paramsData = sessionStorage.getItem(STORAGE_GAME_LOG_CURRENCY_SEARCH);
if (paramsData) {
const searchData = JSON.parse(paramsData);
handleSearch({
start_dt: new Date(searchData.start_dt),
end_dt: new Date(searchData.end_dt),
search_data: searchData.guid
});
// 사용 후 세션 스토리지 데이터 삭제
sessionStorage.removeItem(STORAGE_GAME_LOG_CURRENCY_SEARCH);
}
}
}, [active]);
const tableHeaders = useMemo(() => {
return [
// { id: 'logDay', label: '일자', width: '120px' },
{ id: 'logTime', label: '일시', width: '120px' },
{ id: 'accountId', label: 'account ID', width: '80px' },
{ id: 'userGuid', label: 'GUID', width: '200px' },
{ id: 'userNickname', label: '아바타명', width: '150px' },
{ id: 'tranId', label: '트랜잭션 ID', width: '200px' },
{ id: 'action', label: '액션', width: '150px' },
{ id: 'currencyType', label: '재화종류', width: '120px' },
{ id: 'amountDeltaType', label: '증감유형', width: '120px' },
{ id: 'deltaAmount', label: '수량', width: '120px' },
// { id: 'deltaAmount', label: '수량원본', width: '120px' },
{ id: 'currencyAmount', label: '잔량', width: '120px' },
];
}, []);
if(!active) return null;
return (
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyLogSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo orderType="asc" pageType="B" total={dataList?.total} total_all={dataList?.total_all}>
<ExcelExportButton
functionName="GameCurrencyDetailLogExport"
params={searchParams}
fileName={t('FILE_GAME_LOG_CURRENCY')}
onLoadingChange={setDownloadState}
dataSize={dataList?.total_all}
/>
{downloadState.loading && (
<CircularProgressWrapper>
<CircularProgress
progress={downloadState.progress}
size={36}
progressColor="#4A90E2"
/>
</CircularProgressWrapper>
)}
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => (
<th key={header.id} width={header.width}>{header.label}</th>
))}
</tr>
</thead>
<tbody>
{dataList?.currency_detail_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logTime}</td>
<td>{item.accountId}</td>
<td>{item.userGuid}</td>
<td>{item.userNickname}</td>
<td>{item.tranId}</td>
<td>{item.action}</td>
<td>{CurrencyType.find(data => data.value === item.currencyType)?.name}</td>
<td>{amountDeltaType.find(data => data.value === item.amountDeltaType)?.name}</td>
<td>{numberFormatter.formatCurrency(item.deltaAmount)}</td>
{/*<td>{item.deltaAmount}</td>*/}
<td>{numberFormatter.formatCurrency(item.currencyAmount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{dataList?.currency_detail_list &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
<TopButton />
</>
}
</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

@@ -1,219 +0,0 @@
import { useState } from 'react';
import { styled } from 'styled-components';
import RadioInput from '../common/input/Radio';
import Button from '../common/button/Button';
import DatePicker, { registerLocale } from 'react-datepicker';
import { ko } from 'date-fns/esm/locale';
import 'react-datepicker/dist/react-datepicker.css';
import { getMonth, getYear } from 'date-fns';
import range from 'lodash/range';
import { TextInput, SelectInput, DatePickerWrapper, InputLabel, BtnWrapper } from '../../styles/Components';
const ItemLogSearchBar = () => {
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [selectData, setSelectData] = useState('default');
const years = range(1990, getYear(new Date()) + 1, 1);
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
const handleChange = e => {
setSelectData(e.target.value);
};
return (
<>
<SearchbarStyle2>
<SearchRow>
<RadioGroup>
<RadioInput label="기본 조회" id="single" name="receiver" value="default" fontWeight="600" checked={selectData === 'default'} handleChange={handleChange} />
<RadioInput label="아이템 소유자 추적" id="multi" name="receiver" value="item" fontWeight="600" checked={selectData === 'item'} handleChange={handleChange} />
</RadioGroup>
</SearchRow>
<SearchRow>
<SearchItem>
<InputLabel>조회 기간</InputLabel>
<DatePickerWrapper>
<InputGroup>
<DatePicker
selected={startDate}
onChange={date => setStartDate(date)}
className="datepicker"
placeholderText="검색기간 선택"
calendarClassName="calendar"
dateFormat="yyyy - MM - dd"
locale="ko"
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
<div className="calendar-top">
<button
className="btn-prev"
onClick={e => {
e.preventDefault();
decreaseMonth();
}}
disabled={prevMonthButtonDisabled}></button>
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
.
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<button
className="btn-next"
onClick={e => {
e.preventDefault();
increaseMonth();
}}
disabled={nextMonthButtonDisabled}></button>
</div>
)}
/>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
</InputGroup>
<span>~</span>
<InputGroup>
<DatePicker
selected={endDate}
onChange={date => setEndDate(date)}
className="datepicker"
placeholderText="검색기간 선택"
calendarClassName="calendar"
dateFormat="yyyy - MM - dd"
minDate = {startDate}
locale="ko"
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
<div className="calendar-top">
<button
className="btn-prev"
onClick={e => {
e.preventDefault();
decreaseMonth();
}}
disabled={prevMonthButtonDisabled}></button>
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
.
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<button
className="btn-next"
onClick={e => {
e.preventDefault();
increaseMonth();
}}
disabled={nextMonthButtonDisabled}></button>
</div>
)}
/>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
</InputGroup>
</DatePickerWrapper>
</SearchItem>
</SearchRow>
<SearchRow>
{selectData === 'default' ? (
<SearchItem>
<InputLabel>조회 대상</InputLabel>
<TextInput type="text" placeholder="조회 대상 유저의 GUID를 입력하세요." width="600px" />
</SearchItem>
) : (
<SearchItem>
<InputLabel>아이템 ID</InputLabel>
<TextInput type="text" placeholder="아이템의 GUID를 입력하세요." width="600px" />
</SearchItem>
)}
<BtnWrapper $gap="8px">
<Button theme="reset" />
<Button theme="gray" text="검색" />
</BtnWrapper>
</SearchRow>
</SearchbarStyle2>
</>
);
};
export default ItemLogSearchBar;
const SearchbarStyle = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 20px 0;
font-size: 14px;
padding: 20px;
border-top: 1px solid #000;
border-bottom: 1px solid #000;
margin-bottom: 40px;
`;
const SearchbarStyle2 = styled(SearchbarStyle)`
flex-flow: column;
gap: 20px;
`;
const SearchRow = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px 0;
`;
const InputGroup = styled.div`
display: flex;
align-items: center;
gap: 5px;
`;
const RadioGroup = styled(InputGroup)`
gap: 30px;
height: 35px;
`;
const SearchItem = styled.div`
display: flex;
align-items: center;
gap: 20px;
margin-right: 50px;
${TextInput}, ${SelectInput} {
height: 35px;
}
${TextInput} {
padding: 0 10px;
max-width: 400px;
}
`;

View File

@@ -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,99 @@
import styled from 'styled-components';
import { useState, useEffect } from 'react';
import { UserToolView } from '../../apis/Users';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
const RankPioneerInfo = ({ userInfo }) => {
const [dataList, setDataList] = useState();
const [rowData, setRowData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, []);
useEffect(() => {
if(dataList && dataList.slot_list)
setRowData([
{ title: 'GUID', itemNo: dataList.slot_list.slot1?.tool_id, itemName: dataList?.slot_list.slot1?.tool_name },
{ title: '아바타명', itemNo: dataList.slot_list.slot2?.tool_id, itemName: dataList?.slot_list.slot2?.tool_name },
{ title: '점수', itemNo: dataList.slot_list.slot3?.tool_id, itemName: dataList?.slot_list.slot3?.tool_name },
])
}, [dataList])
const fetchData = async () => {
const token = sessionStorage.getItem('token');
await UserToolView(token, userInfo.guid).then(data => {
setDataList(data);
setLoading(false);
});
};
return (
loading ? <TableSkeleton width='30%' count={4} /> :
<>
<ToolWrapper>
<UserInfoTable $maxwidth="570px">
<colgroup>
<col width="120" />
<col width="30%" />
<col width="70%" />
</colgroup>
<tbody>
{rowData && rowData.map((el, idx) => {
return (
<tr key={idx}>
<th>{el.title}</th>
<td>{el.itemNo}</td>
<td>{el.itemName}</td>
</tr>
);
})}
</tbody>
</UserInfoTable>
</ToolWrapper>
</>
);
};
export default RankPioneerInfo;
const UserInfoTable = styled.table`
width: 100%;
max-width: ${props => props.$maxwidth || 'auto'};
font-size: 13px;
border-radius: 15px;
overflow: hidden;
tr:first-child {
th,
td {
border-top: 0;
}
}
th,
td {
height: 36px;
vertical-align: middle;
border-top: 1px solid #d9d9d9;
}
th {
width: 120px;
background: #888;
color: #fff;
font-weight: 700;
}
td {
background: #fff;
padding: 0 20px;
}
`;
const ToolWrapper = styled.div`
${UserInfoTable} {
td {
border-left: 1px solid #d9d9d9;
}
}
`;

View File

@@ -0,0 +1,84 @@
import React, { useRef } from 'react';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { RankInfoSearchBar, useRankInfoSearch } from '../searchBar';
import { useDataFetch } from '../../hooks/hook';
import { RankingScheduleSimpleView } from '../../apis';
import { FormWrapper, TableStyle, TableWrapper } from '../../styles/Components';
import { ExcelDownButton, ViewTableInfo } from '../common';
import { useTranslation } from 'react-i18next';
import { formatTimeFromSeconds } from '../../utils';
const RankingSnapshotInfo = () => {
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const { t } = useTranslation();
const {
config,
loading: dataLoading,
searchParams,
data: dataList,
handleSearch,
handleReset,
updateSearchParams,
snapshotData
} = useRankInfoSearch(token);
const {
data: rankingScheduleData
} = useDataFetch(() => RankingScheduleSimpleView(token), [token]);
return (
<>
<FormWrapper>
<RankInfoSearchBar
config={config}
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
scheduleData={rankingScheduleData}
snapshotData={snapshotData}
/>
</FormWrapper>
<ViewTableInfo>
<ExcelDownButton tableRef={tableRef} fileName={t('FILE_RANKING_SNAPSHOT')} />
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<TableWrapper>
<TableStyle ref={tableRef}>
<caption></caption>
<thead>
<tr>
<th width="80">순위</th>
<th width="120">account ID</th>
<th width="200">GUID</th>
<th width="150">아바타명</th>
<th width="80">점수</th>
</tr>
</thead>
<tbody>
{dataList?.ranker_List?.map((rank, index) => (
<tr key={index}>
<td>{rank.rank}</td>
<td>{rank.account_id}</td>
<td>{rank.user_guid}</td>
<td>{rank.nickname}</td>
<td>{rank.score_type === "Time" ? formatTimeFromSeconds(rank.score) : rank.score}</td>
</tr>
))}
</tbody>
</TableStyle>
</TableWrapper>
}
</>
);
};
export default RankingSnapshotInfo;

View File

@@ -1,198 +0,0 @@
import { styled } from 'styled-components';
import { useState } from 'react';
import Button from '../../components/common/button/Button';
import DatePicker, { registerLocale } from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { getMonth, getYear } from 'date-fns';
import range from 'lodash/range';
import { TextInput, SelectInput, DatePickerWrapper, InputLabel, BtnWrapper } from '../../styles/Components';
const TradeLogSerchBar = () => {
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [selectData, setSelectData] = useState('default');
const years = range(1990, getYear(new Date()) + 1, 1);
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
const handleChange = e => {
setSelectData(e.target.value);
};
return (
<>
<SearchbarStyle2>
<SearchRow>
<SearchItem>
<InputLabel>조회 기간</InputLabel>
<DatePickerWrapper>
<InputGroup>
<DatePicker
selected={startDate}
onChange={date => setStartDate(date)}
className="datepicker"
placeholderText="검색기간 선택"
calendarClassName="calendar"
dateFormat="yyyy - MM - dd"
locale="ko"
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
<div className="calendar-top">
<button
className="btn-prev"
onClick={e => {
e.preventDefault();
decreaseMonth();
}}
disabled={prevMonthButtonDisabled}></button>
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
.
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<button
className="btn-next"
onClick={e => {
e.preventDefault();
increaseMonth();
}}
disabled={nextMonthButtonDisabled}></button>
</div>
)}
/>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
</InputGroup>
<span>~</span>
<InputGroup>
<DatePicker
selected={endDate}
onChange={date => setEndDate(date)}
className="datepicker"
placeholderText="검색기간 선택"
calendarClassName="calendar"
dateFormat="yyyy - MM - dd"
minDate = {startDate}
locale="ko"
renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
<div className="calendar-top">
<button
className="btn-prev"
onClick={e => {
e.preventDefault();
decreaseMonth();
}}
disabled={prevMonthButtonDisabled}></button>
<select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
.
<select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<button
className="btn-next"
onClick={e => {
e.preventDefault();
increaseMonth();
}}
disabled={nextMonthButtonDisabled}></button>
</div>
)}
/>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
<SelectInput>
<option value="">00</option>
<option value="">01</option>
</SelectInput>
</InputGroup>
</DatePickerWrapper>
</SearchItem>
</SearchRow>
<SearchRow>
<SearchItem>
<InputLabel>조회 대상</InputLabel>
<TextInput type="text" placeholder="조회 대상 유저의 GUID를 입력하세요." width="600px" />
</SearchItem>
<BtnWrapper $gap="8px">
<Button theme="reset" />
<Button theme="gray" text="검색" />
</BtnWrapper>
</SearchRow>
</SearchbarStyle2>
</>
);
};
export default TradeLogSerchBar;
const SearchbarStyle = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 20px 0;
font-size: 14px;
padding: 20px;
border-top: 1px solid #000;
border-bottom: 1px solid #000;
margin-bottom: 40px;
`;
const SearchbarStyle2 = styled(SearchbarStyle)`
flex-flow: column;
gap: 20px;
`;
const SearchRow = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px 0;
`;
const InputGroup = styled.div`
display: flex;
align-items: center;
gap: 5px;
`;
const SearchItem = styled.div`
display: flex;
align-items: center;
gap: 20px;
margin-right: 50px;
${TextInput}, ${SelectInput} {
height: 35px;
}
${TextInput} {
padding: 0 10px;
max-width: 400px;
}
`;

View File

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

@@ -93,12 +93,6 @@ const UserInfoTable = styled.table`
font-size: 13px;
border-radius: 15px;
overflow: hidden;
tr:first-child {
th,
td {
border-top: 0;
}
}
th,
td {
height: 36px;

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

@@ -0,0 +1,201 @@
import React, { useCallback, useEffect, useState } from 'react';
import NicknameChangeModal from '../../components/DataManage/NicknameChangeModal';
import { UserInfoView } from '../../apis/Users';
import { authType } from '../../assets/data';
import { useTranslation } from 'react-i18next';
import { useRecoilValue } from 'recoil';
import { authList } from '../../store/authList';
import { UserDefault } from '../../styles/ModuleComponents';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { UserInfoSkeleton } from '../Skeleton/UserInfoSkeleton';
import { useModal } from '../../hooks/hook';
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import { alertTypes } from '../../assets/data/types';
import { Button, Col, Descriptions, InputNumber, Row, Space, Table } from 'antd';
import { RankerInfoModify, RankingInfoView } from '../../apis';
import { InputItem, TextInput } from '../../styles/Components';
import CustomConfirmModal from '../common/modal/CustomConfirmModal';
import InputConfirmModal from '../common/modal/InputConfirmModal';
import { opRankingType } from '../../assets/data/options';
import { formatTimeFromSeconds } from '../../utils';
const UserRankInfo = ({ userInfo }) => {
const { t } = useTranslation();
const authInfo = useRecoilValue(authList);
const token = sessionStorage.getItem('token');
const {showModal, showToast} = useAlert();
const {withLoading} = useLoading();
const {
modalState,
handleModalView,
handleModalClose
} = useModal({
valueChange: 'hidden'
});
const [dataList, setDataList] = useState({});
const [loading, setLoading] = useState(true);
const [authUpdate, setAuthUpdate] = useState(false);
const [selectRow, setSelectRow] = useState();
const [updateValue, setUpdateValue] = useState();
const [comment, setComment] = useState();
useEffect(() => {
setAuthUpdate(authInfo?.auth_list?.some(auth => auth.id === authType.rankManagerUpdate));
}, [authInfo]);
useEffect(() => {
if(userInfo && Object.keys(userInfo).length > 0) {
fetchData();
}
}, [userInfo]);
const fetchData = async () => {
setLoading(true);
await RankingInfoView(token, userInfo.guid).then(data => {
setDataList(data.user_ranking_info);
}).catch(error => {
showToast(error, {type: alertTypes.error});
}).finally(() => {
setLoading(false);
});
};
const findAndFormatScore = (rankingType) => {
if (!dataList?.rankingItems) return '';
const item = dataList.rankingItems.find(item => item.rankingType === rankingType);
if (!item || item.score === null || item.score === undefined) return '';
// scoreType이 "Time"이면 시:분:초 형식으로 변환 (초 단위로 가정)
if (item.scoreType === "Time") {
return formatTimeFromSeconds(item.score);
}
// 그 외의 경우는 숫자 그대로 반환
return item.score.toString();
};
const columns = [
{
dataIndex: 'label',
width: 200,
onCell: () => ({
style: {
backgroundColor: '#f0f0f0',
fontWeight: 'bold'
}
})
},
{
dataIndex: 'content',
width: 300,
},
{
dataIndex: 'action',
},
];
const generateTableData = () => {
if (!dataList) return [];
const baseData = [
{
key: 'guid',
label: 'GUID',
content: userInfo?.guid || '',
action: null
},
{
key: 'nickname',
label: '아바타명',
content: dataList?.nickname || userInfo?.nickname || '',
action: null
}
];
// opRankingType을 기반으로 동적 생성
const rankingData = opRankingType.map(rankType => {
const formattedScore = findAndFormatScore(rankType.value);
const hasScore = formattedScore !== '';
return {
key: rankType.value.toLowerCase(),
label: rankType.name,
content: formattedScore,
action: authUpdate && hasScore ? (
<Button
type="primary"
onClick={() => handleSubmit('valueChange', rankType.value)}
>
수정
</Button>
) : null
};
});
return [...baseData, ...rankingData];
};
const data = generateTableData();
const handleSubmit = async (type, param = null) => {
let params = {};
switch (type) {
case "valueChange":
setSelectRow(param);
const comment = dataList.rankingItems.find(item => item.rankingType === param)?.scoreType === 'Time' ? '초단위로 입력해주세요.' : '점수';
setComment(comment);
handleModalView('valueChange');
break;
case "valueChangeConfirm":
const item = dataList.rankingItems.find(item => item.rankingType === selectRow);
params.guid = item.guid;
params.score = updateValue;
params.user_guid = userInfo.guid;
await withLoading(async () => {
return await RankerInfoModify(token, params);
}).then(data => {
showToast('UPDATE_COMPLETED', {type: alertTypes.success});
fetchData();
}).catch(error => {
showToast(error, {type: alertTypes.error});
}).finally(() => {
handleModalClose('valueChange');
});
break;
}
}
return (
loading ? <TableSkeleton width='30%' count={5} /> :
<>
<div>
<UserDefault>
<Table
columns={columns}
dataSource={data}
pagination={false}
showHeader={false}
bordered
/>
</UserDefault>
</div>
<InputConfirmModal
inputType='number'
view={modalState.valueChangeModal}
handleSubmit={() => handleSubmit('valueChangeConfirm')}
handleCancel={() => handleModalClose('valueChange')}
handleClose={() => handleModalClose('valueChange')}
value={updateValue}
setValue={setUpdateValue}
inputText={t('UPDATE_VALUE_COMMENT',{comment:comment})}
/>
</>
);
};
export default UserRankInfo;

View File

@@ -1,57 +0,0 @@
import { styled } from 'styled-components';
import Button from '../../components/common/button/Button';
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper } from '../../styles/Components';
const UserSearchBar = () => {
return (
<>
<FormWrapper>
<SearchbarStyle>
<SearchItem>
<InputLabel>유저 조회</InputLabel>
<TextInput type="text" width="300px" placeholder="조회 대상 유저의 GUID를 입력하세요." />
</SearchItem>
<BtnWrapper $gap="8px">
<Button theme="reset" />
<Button
theme="primary"
text="검색"
handleClick={e => {
e.preventDefault();
}}
/>
</BtnWrapper>
</SearchbarStyle>
</FormWrapper>
</>
);
};
export default UserSearchBar;
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,153 @@
import React, { Fragment, useMemo, useRef, useState } from 'react';
import {
CircularProgressWrapper,
FormWrapper,
TableStyle,
TableWrapper,
} from '../../styles/Components';
import { useTranslation } from 'react-i18next';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { UserSnapshotLogSearchBar, useUserSnapshotLogSearch } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import Pagination from '../common/Pagination/Pagination';
import {
INITIAL_PAGE_LIMIT,
} 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 UserSnapshotLogContent = ({ 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
} = useUserSnapshotLogSearch(token, 500);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '80px' },
{ id: 'accountId', label: 'account ID', width: '80px' },
{ id: 'userGuid', label: 'GUID', width: '180px' },
{ id: 'userNickname', label: '아바타명', width: '150px' },
{ id: 'gold', label: '골드', width: '80px' },
{ id: 'sapphire', label: '사파이어', width: '80px' },
{ id: 'calium', label: '칼리움', width: '80px' },
{ id: 'ruby', label: '루비', width: '80px' },
{ id: 'item_13080002', label: '퀘스트 메달', width: '80px' },
{ id: 'item_13080004', label: '보급품 메달', width: '80px' },
{ id: 'item_13080005', label: '제작 메달', width: '80px' },
{ id: 'item_13080006', label: '에테론 315 포드', width: '80px' },
{ id: 'item_13080007', label: '에테론 316 포드', width: '80px' },
{ id: 'item_13080008', label: '에테론 317 포드', width: '80px' },
{ id: 'item_13080009', label: '에테론 318 포드', width: '80px' },
{ id: 'item_11570001', label: '강화잉크', width: '80px' },
{ id: 'item_11570002', label: '연성잉크', width: '80px' },
{ id: 'lastLogoutTime', label: '마지막 로그아웃 일자', width: '150px' },
];
}, []);
if(!active) return null;
return (
<AnimatedPageWrapper>
<FormWrapper>
<UserSnapshotLogSearchBar
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="GameUserSnapshotLogExport"
params={searchParams}
fileName={t('FILE_GAME_LOG_USER_SNAPSHOT')}
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?.snapshot_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.gold)}</td>
<td>{numberFormatter.formatCurrency(item.sapphire)}</td>
<td>{numberFormatter.formatCurrency(item.calium)}</td>
<td>{numberFormatter.formatCurrency(item.ruby)}</td>
<td>{item.item_13080002}</td>
<td>{item.item_13080004}</td>
<td>{item.item_13080005}</td>
<td>{item.item_13080006}</td>
<td>{item.item_13080007}</td>
<td>{item.item_13080008}</td>
<td>{item.item_13080009}</td>
<td>{item.item_11570001}</td>
<td>{item.item_11570002}</td>
<td>{item.lastLogoutTime}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{dataList?.snapshot_list &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default UserSnapshotLogContent;

View File

@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
import { UserTattooView } from '../../apis/Users';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
const UserTatttooInfo = ({ userInfo }) => {
const UserTattooInfo = ({ userInfo }) => {
const [dataList, setDataList] = useState();
const [loading, setLoading] = useState(true);
@@ -62,7 +62,7 @@ const UserTatttooInfo = ({ userInfo }) => {
);
};
export default UserTatttooInfo;
export default UserTattooInfo;
const UserDefaultTable = styled.table`
border: 1px solid #e8eaec;

View File

@@ -0,0 +1,24 @@
export { default as UserDefaultInfo } from './UserDefaultInfo';
export { default as UserAvatarInfo } from './UserAvatarInfo';
export { default as UserDressInfo } from './UserDressInfo';
export { default as UserToolInfo } from './UserToolInfo';
export { default as UserInventoryInfo } from './UserInventoryInfo';
export { default as UserMailInfo } from './UserMailInfo';
export { default as UserMyHomeInfo } from './UserMyHomeInfo';
export { default as UserFriendInfo } from './UserFriendInfo';
export { default as UserTattooInfo } from './UserTattooInfo';
export { default as UserQuestInfo } from './UserQuestInfo';
export { default as UserClaimInfo } from './UserClaimInfo';
export { default as CurrencyLogContent } from './CurrencyLogContent';
export { default as ItemLogContent } from './ItemLogContent';
export { default as CurrencyItemLogContent } from './CurrencyItemLogContent';
export { default as UserLoginLogContent } from './UserLoginLogContent';
export { default as UserCreateLogContent } from './UserCreateLogContent';
export { default as UserSnapshotLogContent } from './UserSnapshotLogContent';
export { default as RankPioneerInfo } from './RankPioneerInfo';
export { default as LandDetailModal } from './LandDetailModal';
export { default as MailDetailModal } from './MailDetailModal';
export { default as QuestDetailModal } from './QuestDetailModal';
export { default as NicknameChangeModal } from './NicknameChangeModal';
export { default as UserRankInfo } from './UserRankInfo';
export { default as RankingSnapshotInfo } from './RankingSnapshotInfo';

View File

@@ -1,312 +1,279 @@
import { useEffect, useState } from 'react';
import { styled } from 'styled-components';
import Button from '../../components/common/button/Button';
import React, { Fragment, useMemo, useRef, useState } from 'react';
import { CurrencyIndexExport, CurrencyIndexView } from '../../apis';
import { SelectInput, TableStyle, TableInfo, ListOption, InputLabel } from '../../styles/Components';
import {
TableStyle,
FormWrapper,
TableWrapper, CircularProgressWrapper, TotalRow,
} from '../../styles/Components';
import CreditSeacrhBar from '../../components/IndexManage/CreditSearchBar';
import { uniqBy } from 'lodash';
import { sumBy } from 'lodash';
import { useCurrencyIndexSearch } from '../searchBar';
import { Button, TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import {STORAGE_GAME_LOG_CURRENCY_SEARCH, } from '../../assets/data/adminConstants';
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
import { useTranslation } from 'react-i18next';
import CurrencyUserIndexSearchBar from '../searchBar/CurrencyUserIndexSearchBar';
import { useNavigate } from 'react-router-dom';
import { AnimatedPageWrapper } from '../common/Layout';
const CreditContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const CURRENCY_LIST = [
{ "key": "Gold", "name": "골드" },
{ "key": "Sapphire", "name": "사파이어" },
{ "key": "Calium", "name": "칼리움" },
{ "key": "Onyxium", "name": "오닉시움" }
];
const navigate = useNavigate();
const tableRef = useRef(null);
const [downloadState, setDownloadState] = useState({
loading: false,
progress: 0
});
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
const [currencyType, setCurrencyType] = useState('Gold');
const [currencyText, setCurrencyText] = useState('골드');
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useCurrencyIndexSearch(token);
const [dataList, setDataList] = useState([]);
const [routeData, setRouteData] = useState([]);
const tableHeaders = useMemo(() => {
return [
// 기본 컬럼 (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 }
];
}, []);
useEffect(() => {
fetchData(sendDate, finishDate, currencyType);
}, [currencyType]);
const totals = useMemo(() => {
if (!dataList?.currency_list?.length) return null;
const fetchData = async (startDate, endDate) => {
const newStartDate = new Date(startDate);
const newEndDate = new Date(endDate);
return dataList.currency_list.reduce((acc, item) => {
return {
sapphireAcquired: (acc.sapphireAcquired || 0) + (item.sapphireAcquired || 0),
sapphireConsumed: (acc.sapphireConsumed || 0) + (item.sapphireConsumed || 0),
goldAcquired: (acc.goldAcquired || 0) + (item.goldAcquired || 0),
goldConsumed: (acc.goldConsumed || 0) + (item.goldConsumed || 0),
caliumAcquired: (acc.caliumAcquired || 0) + (item.caliumAcquired || 0),
caliumConsumed: (acc.caliumConsumed || 0) + (item.caliumConsumed || 0),
beamAcquired: (acc.beamAcquired || 0) + (item.beamAcquired || 0),
beamConsumed: (acc.beamConsumed || 0) + (item.beamConsumed || 0),
rubyAcquired: (acc.rubyAcquired || 0) + (item.rubyAcquired || 0),
rubyConsumed: (acc.rubyConsumed || 0) + (item.rubyConsumed || 0),
sapphireNet: (acc.sapphireNet || 0) + (item.sapphireNet || 0),
goldNet: (acc.goldNet || 0) + (item.goldNet || 0),
caliumNet: (acc.caliumNet || 0) + (item.caliumNet || 0),
beamNet: (acc.beamNet || 0) + (item.beamNet || 0),
rubyNet: (acc.rubyNet || 0) + (item.rubyNet || 0),
totalCurrencies: (acc.totalCurrencies || 0) + (item.totalCurrencies || 0),
};
}, {});
}, [dataList?.currency_list]);
const startDateToLocal =
newStartDate.getFullYear() +
'-' +
(newStartDate.getMonth() + 1 < 9 ? '0' + (newStartDate.getMonth() + 1) : newStartDate.getMonth() + 1) +
'-' +
(newStartDate.getDate() < 9 ? '0' + newStartDate.getDate() : newStartDate.getDate());
const handleModalSubmit = async (type, param = null) => {
switch (type) {
case "detail":
const params = {
tab: "CURRENCY",
start_dt: (() => {
const date = new Date(param.logDay);
return date;
})(),
end_dt: (() => {
const date = new Date(param.logDay);
date.setDate(date.getDate() + 1);
return date;
})(),
guid: param.userGuid
};
const endDateToLocal =
newEndDate.getFullYear() +
'-' +
(newEndDate.getMonth() + 1 < 9 ? '0' + (newEndDate.getMonth() + 1) : newEndDate.getMonth() + 1) +
'-' +
(newEndDate.getDate() < 9 ? '0' + newEndDate.getDate() : newEndDate.getDate());
setDataList(await CurrencyIndexView(token, startDateToLocal, endDateToLocal, currencyType));
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
setRoutArray(await CurrencyIndexView(token, startDateToLocal, endDateToLocal, currencyType));
};
const handleCurrencyChange = e => {
let value = e.target.value;
setCurrencyType(value);
CURRENCY_LIST.filter(data => data.key === value).map(data => {
setCurrencyText(data.name);
});
};
//route data
const setRoutArray = async (data) => {
let routeArray = [];
let routeAcqArr = [];
let routeConArr = [];
//생산량 route
data.list && data.list[0].daily_data.filter(routeData => routeData.delta_type === 'ACQUIRE').map(routeData => {
routeData.data.map(routeData => {
if(!routeAcqArr.includes(routeData.route) ){
routeAcqArr.push(routeData.route);
}
})
});
routeArray.ACQUIRE = routeAcqArr;
//소진량 route
data.list && data.list[0].daily_data.filter(routeData => routeData.delta_type === 'CONSUME').map(routeData => {
routeData.data.map(routeData => {
if(!routeConArr.includes(routeData.route) ){
routeConArr.push(routeData.route);
}
})
});
routeArray.CONSUME = routeConArr;
setRouteData(routeArray);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_Credit_Index.xlsx';
CurrencyIndexExport(token, fileName, sendDate, finishDate, currencyType);
};
// 복사한 데이터를 세션 스토리지에 저장
sessionStorage.setItem(STORAGE_GAME_LOG_CURRENCY_SEARCH, JSON.stringify(params));
navigate('/datamanage/gamelogview');
break;
}
}
return (
<>
<CreditSeacrhBar fetchData={fetchData} />
<TableInfo2>
<SelectInput onChange={handleCurrencyChange}>
{CURRENCY_LIST.map((item, index) => (
<option value={item.key} key={index}>
{item.name}
</option>
))}
</SelectInput>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo2>
<TableWrapper>
<EconomicTable>
<thead>
<tr>
<th colSpan="2" className="text-center" width="300">
Product
</th>
{dataList.list && uniqBy(dataList.list, 'date').map(data =>
<th width="160" key={data.date}>{data.date}</th>
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyUserIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ExcelExportButton
functionName="GameCurrencyLogExport"
params={searchParams}
fileName={t('FILE_CURRENCY_INDEX')}
onLoadingChange={setDownloadState}
dataSize={dataList?.length}
/>
{downloadState.loading && (
<CircularProgressWrapper>
<CircularProgress
progress={downloadState.progress}
size={36}
progressColor="#4A90E2"
/>
</CircularProgressWrapper>
)}
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{/* 첫 번째 행 - 기본 컬럼 + 그룹 헤더 + 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.goldAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.caliumAcquired)}</td>
<td>{numberFormatter.formatCurrency(totals.beamAcquired)}</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>
)}
</tr>
</thead>
<tbody>
<tr>
<TableTitle colSpan="2">(Total) {currencyText} 생산량</TableTitle>
{dataList.list &&
dataList.list.map((data) =>
(data.total).filter(totalData => data.date === totalData.date && totalData.delta_type === 'ACQUIRE')
.map((totalData, index) => (
<TableData key={index}
$state={totalData.dif !== "" && totalData.dif !== "Infinity" ? "danger" : ""}>
{totalData.quantity}
{
totalData.dif !== "" && totalData.dif !== "Infinity"
? (<span>({totalData.dif})</span>)
: ("")
}
</TableData>
)
))
}
</tr>
<tr>
<TableTitle colSpan="2">(Total) {currencyText} 소진량</TableTitle>
{dataList.list &&
dataList.list.map((data) =>
(data.total).filter(totalData => data.date === totalData.date && totalData.delta_type === 'CONSUME')
.map((totalData, index) => (
<TableData key={index}
$state={totalData.dif !== "" && totalData.dif !== "Infinity" ? "danger" : ""}>
{totalData.quantity}
{
totalData.dif !== "" && totalData.dif !== "Infinity"
? (<span>({totalData.dif})</span>)
: ("")
}
</TableData>
)
))
}
</tr>
<tr>
<TableTitle colSpan="2">(Total) {currencyText} 보유량</TableTitle>
{dataList.list &&
dataList.list.map((data, index) => (
<TableData key={index}>
{sumBy(
data.total.filter(totalData => data.date === totalData.date && totalData.delta_type === 'ACQUIRE'),
'quantity',
) -
sumBy(
data.total.filter(totalData => data.date === totalData.date && totalData.delta_type === 'CONSUME'),
'quantity',
)}
</TableData>
))
}
</tr>
{/* 획득 GET */}
{
routeData.ACQUIRE && routeData.ACQUIRE.map((route, index) => (
<tr key={index}>
<TableTitle>GET</TableTitle>
<TableTitle>{route}</TableTitle>
{dataList.list &&
dataList.list.map((data) =>
data.daily_data.filter(dailyData => data.date === dailyData.date && dailyData.delta_type === 'ACQUIRE')
.map(dailyData => (dailyData.data).filter(routeData => data.date === routeData.date && routeData.route === route)
.map((routeData, i) => (
<TableData key={i} data={routeData.date}>{routeData.quantity}</TableData>
)))
)
}
</tr>
))
}
{/* 소진 USE CONSUME */}
{
routeData.CONSUME && routeData.CONSUME.map((route, index) => (
<tr key={index}>
<TableTitle>USE</TableTitle>
<TableTitle>{route}</TableTitle>
{dataList.list &&
dataList.list.map((data) =>
data.daily_data.filter(dailyData => data.date === dailyData.date && dailyData.delta_type === 'CONSUME')
.map(dailyData => (dailyData.data).filter(routeData => data.date === routeData.date && routeData.route === route)
.map((routeData, i) => (
<TableData key={i} data={routeData.date}>{routeData.quantity}</TableData>
)))
)
}
</tr>
))
}
</tbody>
</EconomicTable>
</TableWrapper>
</>
{dataList?.currency_list?.map((item, index) => (
<Fragment key={index}>
<tr>
{/* 기본 정보 */}
<td>{item.logDay}</td>
<td>{item.accountId}</td>
<td>{item.userGuid}</td>
<td>{item.userNickname}</td>
{/* 획득 그룹 */}
<td>{numberFormatter.formatCurrency(item.sapphireAcquired)}</td>
<td>{numberFormatter.formatCurrency(item.goldAcquired)}</td>
<td>{numberFormatter.formatCurrency(item.caliumAcquired)}</td>
<td>{numberFormatter.formatCurrency(item.beamAcquired)}</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="상세보기"
handleClick={e => handleModalSubmit('detail', item)} />
</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CreditContent;
const TableWrapper = styled.div`
width: 100%;
min-width: 680px;
overflow: auto;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #666666;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
}
${TableStyle} {
width: 100%;
min-width: 900px;
th {
&.cell-nru {
background: #f0f0f0;
border-left: 1px solid #aaa;
border-right: 1px solid #aaa;
}
}
td {
&.blank {
background: #f9f9f9;
}
&.cell-nru {
background: #fafafa;
border-left: 1px solid #aaa;
border-right: 1px solid #aaa;
}
}
}
`;
const TableInfo2 = styled(TableInfo)`
justify-content: space-between;
${InputLabel} {
font-size: 12px;
}
${SelectInput} {
width: auto;
min-width: 100px;
height: 24px;
}
`;
const TableDate = styled.th`
color: ${props => (props.$state === 'danger' ? '#d60000' : '#2c2c2c')};
`;
const TableData = styled.td`
background: ${props => (props.$state === 'danger' ? '#d60000' : props.$state === 'blank' ? '#F9F9F9' : 'transparent')};
color: ${props => (props.$state === 'danger' ? '#fff' : '#2c2c2c')};
`;
const perData = styled.span`
display: ${props => (props.$view === 'hidden' ? 'none' : 'block')};
`;
const TableTitle = styled.td`
text-align: center;
`;
const EconomicTable = styled(TableStyle)`
${TableData} {
text-align: left;
}
tbody {
tr:nth-child(1),
tr:nth-child(2),
tr:nth-child(3) {
background: #f5fcff;
}
}
`;

View File

@@ -0,0 +1,126 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption,
} from '../../styles/Components';
import { useCurrencyAcquireIndexSearch, CurrencyAcquireIndexSearchBar } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
const CurrencyAcquireContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useCurrencyAcquireIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'mail', label: '우편', width: '80px' },
{ id: 'ShopSell', label: '상점 판매', width: '80px' },
{ id: 'ShopPurchase', label: '상점 구매', width: '80px' },
{ id: 'seasonPass', label: '시즌 패스', width: '80px' },
{ id: 'claim', label: '클레임', width: '80px' },
{ id: 'quest', label: '퀘스트', width: '80px' },
{ id: 'ugq', label: 'UGQ', width: '80px' },
{ id: 'battleObject', label: '보급품 상자', width: '80px' },
{ id: 'randomBox', label: '랜덤박스', width: '80px' },
{ id: 'landRent', label: '랜드 임대', width: '80px' },
{ id: 'caliumExchange', label: '칼리움 교환소', width: '80px' },
{ id: 'caliumConverter', label: '칼리움 컨버터', width: '80px' },
{ id: 'beaconShop', label: '비컨 상점', width: '80px' },
{ id: 'etc', label: '기타', width: '80px' },
{ id: 'summary', label: '합계', width: '80px' },
];
}, []);
return (
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyAcquireIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_CURRENCY_ACQUIRE')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.currency_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.MailTaken)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopSell)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopPurchase)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.SeasonPassTakeReward)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ClaimReward)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.QuestMainReward || 0) + (item.actionSummary.QuestTaskUpdate || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.UgqAbort)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.RewardProp)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemRandomBoxUse)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.GainLandProfit)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ConvertExchangeCalium)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ConvertCalium)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconSell || 0) + (item.actionSummary.BeaconShopReceivePaymentForSales || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.MoneyChange)}</td>
<td>{numberFormatter.formatCurrency(item.totalDeltaAmount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CurrencyAcquireContent;

View File

@@ -0,0 +1,109 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption
} from '../../styles/Components';
import {
AssetsIndexSearchBar, useAssetsIndexSearch,
} from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
import DailyDashBoard from './DailyCaliumDashBoard';
const CurrencyAssetsContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useAssetsIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'userCount', label: '유저수', width: '100px' },
{ id: 'gold', label: '골드', width: '100px' },
{ id: 'sapphire', label: '사파이어', width: '100px' },
{ id: 'calium', label: '칼리움', width: '100px' },
{ id: 'ruby', label: '루비', width: '100px' }
];
}, []);
return (
<AnimatedPageWrapper>
<DailyDashBoard />
<FormWrapper>
<AssetsIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_ASSETS_CURRENCY')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.assets_list?.map((data, index) => (
<Fragment key={index}>
<tr>
<td>{data.logDay}</td>
<td>{numberFormatter.formatCurrency(data.userCount)}</td>
<td>{numberFormatter.formatCurrency(data.gold)}</td>
<td>{numberFormatter.formatCurrency(data.sapphire)}</td>
<td>{numberFormatter.formatCurrency(data.calium)}</td>
<td>{numberFormatter.formatCurrency(data.ruby)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CurrencyAssetsContent;

View File

@@ -0,0 +1,128 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption,
} from '../../styles/Components';
import {
useCurrencyConsumeIndexSearch, CurrencyConsumeIndexSearchBar,
} from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
const CurrencyConsumeContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useCurrencyConsumeIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'itemBuy', label: '아이템 구매', width: '80px' },
{ id: 'ShopPurchase', label: '상점 구매', width: '80px' },
{ id: 'ShopRePurchase', label: '상점 재구매', width: '80px' },
{ id: 'beaconShop', label: '비컨상점', width: '80px' },
{ id: 'beacon', label: '비컨', width: '80px' },
{ id: 'taxi', label: '택시', width: '80px' },
{ id: 'farming', label: '파밍', width: '80px' },
{ id: 'seasonPass', label: '시즌 패스', width: '80px' },
{ id: 'caliumExchange', label: '칼리움 교환소', width: '80px' },
{ id: 'caliumConverter', label: '칼리움 컨버터', width: '80px' },
{ id: 'rent', label: '랜드 렌탈', width: '80px' },
{ id: 'landAuction', label: '랜드 경매', width: '80px' },
{ id: 'ugq', label: 'UGQ', width: '80px' },
{ id: 'etc', label: '기타', width: '80px' },
{ id: 'summary', label: '합계', width: '80px' },
];
}, []);
return (
<AnimatedPageWrapper>
<FormWrapper>
<CurrencyConsumeIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_CURRENCY_CONSUME')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.currency_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemBuy)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopPurchase)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopRePurchase)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconShopRegisterItem || 0) + (item.actionSummary.BeaconShopPurchaseItem || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconCreate || 0) + (item.actionSummary.BeaconEdit || 0) + (item.actionSummary.BeaconAppearanceCustomize || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.TaxiMove)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.FarmingStart)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.SeasonPassBuyCharged)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ConvertExchangeCalium)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ConvertCalium)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.RentFloor)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.LandAuctionBid)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.UgqAssign)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.MoneyChange ||0) + (item.actionSummary.RenewalShopProducts ||0) + (item.actionSummary.CharacterAppearanceCustomize ||0))}</td>
<td>{numberFormatter.formatCurrency(item.totalDeltaAmount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default CurrencyConsumeContent;

View File

@@ -1,110 +0,0 @@
import { useEffect, useState } from 'react';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { DailySearchBar } from '../../components/IndexManage/index';
import { DailyActiveUserExport, DailyActiveUserView } from '../../apis';
const PlayTimeContent = () => {
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
// DAU 데이터
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());
// await DailyActiveUserView(token, startDateToLocal, endDateToLocal).then(data => {
// console.log(data);
// setDataList(data);
// });
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = (send_dt, end_dt) => {
fetchData(send_dt, end_dt);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_Dau.xlsx';
DailyActiveUserExport(token, fileName, sendDate, finishDate);
};
return (
<>
<DailySearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th rowSpan="1" width="45">
일자
</th>
<th colSpan="1" width="30">
DAU
</th>
{/*<th colSpan="1" width="30">*/}
{/* DALC*/}
{/*</th>*/}
<th colSpan="1" width="30">
DGLC
</th>
{/*<th colSpan="1" width="30">*/}
{/* MaxAU*/}
{/*</th>*/}
</tr>
</thead>
<tbody>
{dataList && (dataList || []).map((data, index) => (
<tr key={index}>
<td>{data.date}</td>
<td>{data.dau}</td>
{/*<td>{data.dalc}</td>*/}
<td>{data.dglc}</td>
{/*<td>{data.maxAu}</td>*/}
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
);
};
export default PlayTimeContent;

View File

@@ -0,0 +1,141 @@
import { useEffect, useState } from 'react';
import { dashboardCaliumIndex } from '../../apis';
import { styled } from 'styled-components';
import TitleArrow from '../../assets/img/icon/icon-title.png';
const DailyDashBoard = () => {
const [boardState, setBoardState] = useState('active');
const [totalData, setTotalData] = useState([]);
const handleBoard = () => {
if (boardState === 'active') {
setBoardState('inactive');
} else {
setBoardState('active');
}
};
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const token = sessionStorage.getItem('token');
await dashboardCaliumIndex(token).then(data => {
setTotalData(data.dashboard_calium);
});
};
const dashboardItems = [
{
title: '컨버터 칼리움 보유량',
value: totalData.total_calium || 0
},
{
title: '컨버터 변환 효율',
value: `${totalData.converter_rate || 0}%`
},
{
title: '인플레이션 가중치',
value: `${totalData.inflation_rate || 0}%`
},
{
title: '칼리움 누적 총량',
value: totalData.cumulative_calium || 0
}
];
return (
<DailyBoardWrapper>
{totalData &&
<DailyBoard>
<BoardTitle onClick={handleBoard} $state={boardState}>
Daily Dashboard
</BoardTitle>
<BoardInfo $state={boardState}>
<BoxWrapper>
{dashboardItems?.map((item, index) => (
<InfoItem key={index}>
<InfoTitle>{item.title}</InfoTitle>
<InfoValue>
{item.value}
</InfoValue>
</InfoItem>
))}
</BoxWrapper>
</BoardInfo>
</DailyBoard>
}
</DailyBoardWrapper>
);
};
export default DailyDashBoard;
const DailyBoardWrapper = styled.div`
padding-top: 20px;
border-top: 1px solid #000;
`;
const DailyBoard = styled.div`
background: #f6f6f6;
border-radius: 10px;
margin-bottom: 20px;
`;
const BoardTitle = styled.div`
font-size: 24px;
font-weight: 600;
line-height: 52px;
padding: 0 10px;
cursor: pointer;
&:after {
content: '';
display: inline-block;
width: 11px;
height: 52px;
margin-left: 10px;
background: url(${TitleArrow}) 50% 50% no-repeat;
position: absolute;
transform: ${props => (props.$state === 'active' ? 'rotate(0)' : 'rotate(180deg)')};
}
`;
const BoardInfo = styled.div`
padding: 20px;
border-top: 1px solid #d9d9d9;
display: ${props => (props.$state === 'active' ? 'block' : 'none')};
`;
const BoxWrapper = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px;
`;
const InfoItem = styled.div`
width: 18%;
background: #fff;
padding: 15px 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
border-radius: 15px;
`;
const InfoTitle = styled.div`
font-size: 20px;
font-weight: 600;
`;
const InfoValue = styled.div`
display: inline-flex;
flex-wrap: wrap;
margin: 5px 0;
gap: 5px 0;
align-items: center;
font-size: 18px;
font-weight: 600;
`;

View File

@@ -26,7 +26,6 @@ const DailyDashBoard = ({ content }) => {
const fetchData = async () => {
const token = sessionStorage.getItem('token');
await userTotalIndex(token).then(data => {
console.log(data);
setTotalData(data.dashboard);
});
};

View File

@@ -1,111 +0,0 @@
import { Fragment, useEffect, useState } from 'react';
import Button from '../common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { DailySearchBar } from './index';
import { DailyMedalView } from '../../apis';
const DailyMedalContent = () => {
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
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 DailyMedalView(token, startDateToLocal, endDateToLocal));
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = (send_dt, end_dt) => {
fetchData(send_dt, end_dt);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_Daily_Medal.xlsx';
//DailyActiveUserExport(token, fileName, sendDate, finishDate);
};
return (
<>
<DailySearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead >
<tr>
<th rowSpan="1" width="20">
일자
</th>
<th colSpan="1" width="30">
UserID
</th>
<th colSpan="1" width="30">
닉네임
</th>
<th colSpan="1" width="30">
Item ID
</th>
<th colSpan="1" width="30">
획득량
</th>
</tr>
</thead>
<tbody>
{(dataList || []).map((data, index) => (
<tr key={index}>
<td>{data.date}</td>
<td>{data.dau}</td>
<td>{data.dalc}</td>
<td>{data.dglc}</td>
<td>{data.maxAu}</td>
{Array.from({ length: 24 }, (_, i) => (
<td key={i}>{data['h' + i]}</td>
))}
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
);
};
export default DailyMedalContent;

View File

@@ -1,7 +1,7 @@
import { styled } from 'styled-components';
import { useEffect, useState } from 'react';
import DecoSearchBar from '../../components/IndexManage/DecoSearchBar';
import DecoSearchBar from '../searchBar/DecoSearchBar';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption } from '../../styles/Components';

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import { TableStyle, TableInfo, ListOption } from '../../styles/Components';
import Button from '../../components/common/button/Button';
import InstanceSearchBar from '../../components/IndexManage/InstanceSearchBar';
import InstanceSearchBar from '../searchBar/InstanceSearchBar';
import { InstanceIndexExport, InstanceIndexView } from '../../apis';
const InstanceContent = () => {

View File

@@ -0,0 +1,152 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption, TextInput, TableInfoContent, Notice,
} from '../../styles/Components';
import { ItemAcquireIndexSearchBar, useItemAcquireIndexSearch } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
const ItemAcquireContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useItemAcquireIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'mail', label: '우편', width: '80px' },
{ id: 'shopPurchase', label: '상점 구매', width: '80px' },
{ id: 'shopRePurchase', label: '상점 재구매', width: '80px' },
{ id: 'itemBuy', label: '아이템 구매', width: '80px' },
{ id: 'itemUse', label: '아이템 사용', width: '80px' },
{ id: 'seasonPass', label: '시즌 패스', width: '80px' },
{ id: 'claim', label: '클레임', width: '80px' },
{ id: 'quest', label: '퀘스트', width: '80px' },
{ id: 'ugq', label: 'UGQ', width: '80px' },
{ id: 'battleObject', label: '배틀맵', width: '80px' },
{ id: 'runRace', label: '런레이스', width: '80px' },
{ id: 'prop', label: '보급품 상자', width: '80px' },
{ id: 'randomBox', label: '랜덤박스', width: '80px' },
{ id: 'beacon', label: '비컨', width: '80px' },
{ id: 'beaconShop', label: '비컨 상점', width: '80px' },
{ id: 'myHome', label: '마이홈', width: '80px' },
{ id: 'craft', label: '크래프트', width: '80px' },
{ id: 'etc', label: '기타', width: '80px' },
{ id: 'summary', label: '합계', width: '80px' },
];
}, []);
return (
<AnimatedPageWrapper>
<FormWrapper>
<ItemAcquireIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
{dataList?.item_list && dataList.item_list.length > 0 &&
<TableInfoContent>
<TextInput
type="text"
value={dataList.item_list[0].itemId}
width="100px"
readOnly
/>
<TextInput
type="text"
value={dataList.item_list[0].itemName}
width="150px"
readOnly
/>
<Notice $color='#F15F5F'>* 확인되지 않은 액션이 있을 있습니다</Notice>
</TableInfoContent>
}
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_ITEM_ACQUIRE')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.item_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.MailTaken)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopPurchase)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopRePurchase)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemBuy)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemUse)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.SeasonPassTakeReward)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ClaimReward)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.QuestMainReward || 0) + (item.actionSummary.QuestTaskUpdate || 0) + (item.actionSummary.QuestMainTask || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.UgqAbort)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BattleRoundStateUpdate || 0) + (item.actionSummary.BattlePodCombatOccupyReward || 0) + (item.actionSummary.BattleObjectInteraction || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.RunRaceFinishReward || 0) + (item.actionSummary.RunRaceRespawnReward || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.RewardProp)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemRandomBoxUse)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconCreate || 0) + (item.actionSummary.BeaconEdit || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconShopPurchaseItem || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.DeleteMyhome || 0) + (item.actionSummary.SaveMyhome || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.CraftFinish || 0) + (item.actionSummary.CraftStop || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.CheatCommandItem || 0) + (item.actionSummary.CharacterAppearanceUpdate || 0)
+ (item.actionSummary.ItemTattooLevelUp || 0) + (item.actionSummary.UserCreate || 0) + (item.actionSummary.JoinInstance || 0))}</td>
<td>{numberFormatter.formatCurrency(item.totalDeltaCount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default ItemAcquireContent;

View File

@@ -0,0 +1,119 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption
} from '../../styles/Components';
import {
AssetsIndexSearchBar, useAssetsIndexSearch,
} from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
import DailyDashBoard from './DailyCaliumDashBoard';
const ItemAssetsContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useAssetsIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'userCount', label: '유저수', width: '100px' },
{ id: 'item_13080002', label: '퀘스트 메달', width: '100px' },
{ id: 'item_13080004', label: '보급품 메달', width: '100px' },
{ id: 'item_13080005', label: '제작 메달', width: '100px' },
{ id: 'item_13080006', label: '에테론 315 포드', width: '100px' },
{ id: 'item_13080007', label: '에테론 316 포드', width: '100px' },
{ id: 'item_13080008', label: '에테론 317 포드', width: '100px' },
{ id: 'item_13080009', label: '에테론 318 포드', width: '100px' },
{ id: 'item_11570001', label: '강화 잉크', width: '100px' },
{ id: 'item_11570002', label: '연성 잉크', width: '100px' }
];
}, []);
return (
<AnimatedPageWrapper>
<DailyDashBoard />
<FormWrapper>
<AssetsIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_ASSETS_ITEM')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.assets_list?.map((data, index) => (
<Fragment key={index}>
<tr>
<td>{data.logDay}</td>
<td>{numberFormatter.formatCurrency(data.userCount)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080002)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080004)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080005)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080006)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080007)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080008)}</td>
<td>{numberFormatter.formatCurrency(data.item_13080009)}</td>
<td>{numberFormatter.formatCurrency(data.item_11570001)}</td>
<td>{numberFormatter.formatCurrency(data.item_11570002)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default ItemAssetsContent;

View File

@@ -0,0 +1,143 @@
import React, { Fragment, useMemo, useRef } from 'react';
import {
TableStyle,
FormWrapper,
TableWrapper, ListOption, TableInfoContent, TextInput, Label, Notice,
} from '../../styles/Components';
import {
ItemAcquireIndexSearchBar,
ItemConsumeIndexSearchBar,
useItemAcquireIndexSearch,
useItemConsumeIndexSearch,
} from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { numberFormatter } from '../../utils';
import { useTranslation } from 'react-i18next';
import { AnimatedPageWrapper } from '../common/Layout';
import CSVDownloadButton from '../common/button/CsvDownButton';
import styled from 'styled-components';
const ItemConsumeContent = () => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
updateSearchParams
} = useItemConsumeIndexSearch(token);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '100px' },
{ id: 'shopSell', label: '상점 판매', width: '80px' },
{ id: 'itemUse', label: '아이템 사용', width: '80px' },
{ id: 'beaconShop', label: '비컨상점', width: '80px' },
{ id: 'beacon', label: '비컨', width: '80px' },
{ id: 'quest', label: '퀘스트', width: '80px' },
{ id: 'ugq', label: 'UGQ', width: '80px' },
{ id: 'randomBox', label: '랜덤박스', width: '80px' },
{ id: 'myHome', label: '마이홈', width: '80px' },
{ id: 'craft', label: '크래프트', width: '80px' },
{ id: 'etc', label: '기타', width: '80px' },
{ id: 'summary', label: '합계', width: '80px' },
];
}, []);
return (
<AnimatedPageWrapper>
<FormWrapper>
<ItemConsumeIndexSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo >
{dataList?.item_list && dataList.item_list.length > 0 &&
<TableInfoContent>
<TextInput
type="text"
value={dataList.item_list[0].itemId}
width="100px"
readOnly
/>
<TextInput
type="text"
value={dataList.item_list[0].itemName}
width="300px"
readOnly
/>
<Notice $color='#F15F5F'>* 확인되지 않은 액션이 있을 있습니다</Notice>
</TableInfoContent>
}
<ListOption>
<CSVDownloadButton tableRef={tableRef} fileName={t('FILE_INDEX_ITEM_CONSUME')} />
</ListOption>
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => {
return (
<th
key={header.id}
width={header.width}
colSpan={header.colSpan}
>
{header.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{dataList?.item_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ShopSell)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemUse)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconShopRegisterItem || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.BeaconEdit || 0) + (item.actionSummary.BeaconCreate || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.QuestTaskUpdate || 0))}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.UgqAbort)}</td>
<td>{numberFormatter.formatCurrency(item.actionSummary.ItemRandomBoxUse)}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.SaveMyhome || 0))}</td>
<td>{numberFormatter.formatCurrency((item.actionSummary.CraftStart || 0))}</td>
<td>{numberFormatter.formatCurrency(
(item.actionSummary.SummonParty || 0) + (item.actionSummary.ItemDestroy || 0) + (item.actionSummary.CreatePartyInstance || 0) + (item.actionSummary.ItemTattooChangeAttribute || 0)
+ (item.actionSummary.CheatCommandItem || 0) + (item.actionSummary.ItemDestoryByExpiration || 0) + (item.actionSummary.ItemDestroyByUser || 0) + (item.actionSummary.ItemTattooLevelUp || 0)
)}</td>
<td>{numberFormatter.formatCurrency(item.totalDeltaCount)}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default ItemConsumeContent;

View File

@@ -5,7 +5,7 @@ import Button from '../../components/common/button/Button';
import { SelectInput, TableStyle, TableInfo, ListOption, InputLabel, InputGroup } from '../../styles/Components';
import ItemSearchBar from '../../components/IndexManage/ItemSearchBar';
import ItemSearchBar from '../searchBar/ItemSearchBar';
import { ItemIndexExport, ItemIndexView } from '../../apis';
const ItemContent = () => {

View File

@@ -1,112 +0,0 @@
import { Fragment, useEffect, useState } from 'react';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { PlayTimeSearchBar } from '../../components/IndexManage/index';
import { PlaytimeIndexExport, PlaytimeIndexView } from '../../apis';
const PlayTimeContent = () => {
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
// 이용자 지표 데이터
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 PlaytimeIndexView(token, startDateToLocal, endDateToLocal));
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = (send_dt, end_dt) => {
fetchData(send_dt, end_dt);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_PlayTime_Index.xlsx';
PlaytimeIndexExport(token, fileName, sendDate, finishDate);
};
return (
<>
<PlayTimeSearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th rowSpan="2" width="140">
일자
</th>
<th colSpan="4" width="520">
유저수
</th>
<th rowSpan="2" width="160">
누적 플레이타임()
</th>
<th rowSpan="2" width="160">
1인당 평균 플레이타임()
</th>
</tr>
<tr>
<th>30 이내</th>
<th>30 ~ 1시간</th>
<th>1시간 ~ 3시간</th>
<th>3시간 이상</th>
</tr>
</thead>
<tbody>
{dataList.playtime &&
dataList.playtime.map(time => (
<tr key={time.date}>
<td>{time.date}</td>
{time.user_cnt.map((cnt, index) => (
<td className="text-left" key={index}>
{cnt}
</td>
))}
<td className="text-left">{time.total_time}</td>
<td className="text-left">{time.average_time}</td>
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
);
};
export default PlayTimeContent;

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 { useTranslation } from 'react-i18next';
import CSVDownloadButton from '../common/button/CsvDownButton';
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} />
<CSVDownloadButton 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

@@ -1,166 +0,0 @@
import { useEffect, useState } from 'react';
import { styled } from 'styled-components';
import Button from '../../components/common/button/Button';
import DatePickerComponent from '../common/Date/DatePickerComponent';
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper, InputGroup, DatePickerWrapper, AlertText } from '../../styles/Components';
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);
// resultData에 임의 날짜 넣어주기
useEffect(() => {
setResultData({
send_dt: START_DATE,
end_dt: '',
retention: 0,
});
}, []);
// 발송 날짜 세팅 로직
const handleSelectedDate = data => {
const sendDate = new Date(data);
const resultSendData = new Date(sendDate.getFullYear(), sendDate.getMonth(), sendDate.getDate());
const resultEndDate = new Date(resultSendData);
resultEndDate.setDate(resultEndDate.getDate() + Number(resultData.retention));
setResultData({ ...resultData, send_dt: resultSendData, end_dt: resultEndDate });
};
// // 발송 날짜 세팅 로직
// 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>
<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>
</>
);
};
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

@@ -1,90 +0,0 @@
import { Fragment, useEffect, useState } from 'react';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { SegmentSearchBar } from '../../components/IndexManage/index';
import { SegmentIndexExport, SegmentIndexView } from '../../apis';
const SegmentContent = () => {
const token = sessionStorage.getItem('token');
const END_DATE = new Date();
const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = useState([]);
const [finishDate, setFinishDate] = useState(END_DATE);
useEffect(() => {
fetchData(END_DATE);
}, []);
// Retention 지표 데이터
const fetchData = async endDate => {
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 SegmentIndexView(token, endDateToLocal));
setFinishDate(endDateToLocal);
};
// 검색 함수
const handleSearch = end_dt => {
fetchData(end_dt);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_Segment_Index.xlsx';
SegmentIndexExport(token, fileName, finishDate);
};
return (
<>
<SegmentSearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<IndexTableWrap>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th colSpan="1" width="200">
{dataList && dataList.start_dt} ~ {dataList && dataList.end_dt}
</th>
<th colSpan="2" width="400">
KIP
</th>
</tr>
<tr>
{/* <th>국가</th> */}
<th>세그먼트 분류</th>
<th>AU</th>
<th>AU Percentage by User Segment (%)</th>
</tr>
</thead>
<tbody>
{dataList && dataList.segment &&
dataList.segment.map((segment, index) => (
<tr key={index}>
{/* <td rowSpan="6">TH</td> */}
<td>{segment.type}</td>
<td>{segment.au}</td>
<td>{segment.dif}</td>
</tr>
))}
</tbody>
</TableStyle>
</IndexTableWrap>
</>
);
};
export default SegmentContent;

View File

@@ -1,15 +1,14 @@
import { Fragment, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import Button from '../../components/common/button/Button';
import { TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { DailyDashBoard } from '../../components/IndexManage/index';
import { Title, TableStyle, TableInfo, ListOption, IndexTableWrap } from '../../styles/Components';
import { UserIndexSearchBar, DailyDashBoard } from '../../components/IndexManage/index';
import { userIndexView, userIndexExport } from '../../apis';
import Loading from '../common/Loading';
import { userIndexView } from '../../apis';
import { ExcelDownButton } from '../common';
import { useTranslation } from 'react-i18next';
import { formatStringDate } from '../../utils';
import { AnimatedPageWrapper } from '../common/Layout';
import { UserIndexSearchBar } from '../searchBar';
const UserContent = () => {
const token = sessionStorage.getItem('token');
@@ -24,20 +23,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 +39,6 @@ const UserContent = () => {
setLoading(false);
});
// setSendDate(startDateToLocal);
// setFinishDate(endDateToLocal);
};
// 검색 함수
@@ -63,14 +46,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 +102,7 @@ const UserContent = () => {
</tbody>
</TableStyle>
</IndexTableWrap>
{loading && <Loading/>}
</>
</AnimatedPageWrapper>
);
};

View File

@@ -1,219 +0,0 @@
import { styled } from 'styled-components';
import { useState, useEffect } from 'react';
import { TableStyle, TableInfo, ListOption } from '../../styles/Components';
import Button from '../../components/common/button/Button';
import VBPSearchBar from '../../components/IndexManage/VBPSearchBar';
import { VBPIndexExport, VbpIndexView } from '../../apis';
const VBPContent = () => {
const token = sessionStorage.getItem('token');
let d = new Date();
const START_DATE = new Date(new Date(d.setDate(d.getDate() - 1)).setHours(0, 0, 0, 0));
const END_DATE = new Date();
const [sendDate, setSendDate] = useState(START_DATE);
const [finishDate, setFinishDate] = useState(END_DATE);
const [dataList, setDataList] = useState([]);
useEffect(() => {
fetchData(START_DATE, END_DATE);
}, []);
// console.log(dataList);
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 VbpIndexView(token, startDateToLocal, endDateToLocal));
setSendDate(startDateToLocal);
setFinishDate(endDateToLocal);
};
// 엑셀 다운로드
const handleXlsxExport = () => {
const fileName = 'Caliverse_VBP_Index.xlsx';
VBPIndexExport(token, fileName, sendDate, finishDate);
};
return (
<>
<VBPSearchBar fetchData={fetchData} />
<TableInfo>
<ListOption>
<Button theme="line" text="엑셀 다운로드" handleClick={handleXlsxExport} />
</ListOption>
</TableInfo>
<TableWrapper>
<EconomicTable>
<thead>
<tr>
<th colSpan="2" className="text-center" width="300">
Product
</th>
<th width="160">2023-08-07</th>
<th width="160">2023-08-08</th>
<th width="160">2023-08-09</th>
<th width="160">2023-08-10</th>
<th width="160">2023-08-11</th>
<th width="160">2023-08-12</th>
</tr>
</thead>
<tbody>
<tr>
<TableTitle colSpan="2">(Total) VBP 생산량</TableTitle>
<TableData>500000</TableData>
<TableData>500000</TableData>
<TableData>500000</TableData>
<TableData>500000</TableData>
<TableData>500000</TableData>
<TableData>500000</TableData>
</tr>
<tr>
<TableTitle colSpan="2">(Total) VBP 소진량</TableTitle>
<TableData>490000</TableData>
<TableData>490000</TableData>
<TableData>490000</TableData>
<TableData>490000</TableData>
<TableData>490000</TableData>
<TableData>490000</TableData>
</tr>
<tr>
<TableTitle colSpan="2">(Total) VBP 보유량</TableTitle>
<TableData>3.2M</TableData>
<TableData>3.3M</TableData>
<TableData>3.3M</TableData>
<TableData>3.4M</TableData>
<TableData>3.5M</TableData>
<TableData>3.5M</TableData>
</tr>
<tr>
<TableTitle rowSpan="2">GET</TableTitle>
<TableTitle>퀘스트 보상</TableTitle>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
</tr>
<tr>
<TableTitle>시즌패스 보상</TableTitle>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
</tr>
<tr>
<TableTitle rowSpan="2">USE</TableTitle>
<TableTitle>퀘스트 보상</TableTitle>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
</tr>
<tr>
<TableTitle>시즌패스 보상</TableTitle>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
<TableData>150000</TableData>
</tr>
{/* {mokupData.map((data, index) => (
<Fragment key={index}>
<tr>
<td>{data.date}</td>
<td>{data.name}</td>
<td>{data.trader}</td>
<td>{data.id}</td>
<td>{data.key}</td>
</tr>
</Fragment>
))} */}
</tbody>
</EconomicTable>
</TableWrapper>
</>
);
};
export default VBPContent;
const TableWrapper = styled.div`
width: 100%;
min-width: 680px;
overflow: auto;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #666666;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
}
${TableStyle} {
width: 100%;
min-width: 900px;
th {
&.cell-nru {
background: #f0f0f0;
border-left: 1px solid #aaa;
border-right: 1px solid #aaa;
}
}
td {
&.blank {
background: #f9f9f9;
}
&.cell-nru {
background: #fafafa;
border-left: 1px solid #aaa;
border-right: 1px solid #aaa;
}
}
}
`;
const TableData = styled.td`
background: ${props => (props.$state === 'danger' ? '#d60000' : props.$state === 'blank' ? '#F9F9F9' : 'transparent')};
color: ${props => (props.$state === 'danger' ? '#fff' : '#2c2c2c')};
`;
const TableTitle = styled.td`
text-align: center;
`;
const EconomicTable = styled(TableStyle)`
${TableData} {
text-align: left;
}
tbody {
tr:nth-child(1),
tr:nth-child(2),
tr:nth-child(3) {
background: #f5fcff;
}
}
`;

View File

@@ -1,27 +1,17 @@
import UserIndexSearchBar from "./UserIndexSearchBar";
import RetentionSearchBar from "./RetentionSearchBar";
import SegmentSearchBar from "./SegmentSearchBar";
import DailyDashBoard from "./DailyDashBoard";
import PlayTimeSearchBar from "./PlayTimeSearchBar";
import UserContent from "./UserContent";
import PlayTimeContent from "./PlayTimeContent";
import RetentionContent from "./RetentionContent";
import SegmentContent from "./SegmentContent";
import DailyActiveUserContent from "./DailyActiveUserContent";
import DailyMedalContent from "./DailyMedalContent";
import DailySearchBar from "./DailySearchBar";
import CurrencyConsumeContent from "./CurrencyConsumeContent";
import CurrencyAcquireContent from "./CurrencyAcquireContent";
import ItemAcquireContent from "./ItemAcquireContent";
import ItemConsumeContent from "./ItemConsumeContent";
export {
UserIndexSearchBar,
RetentionSearchBar,
SegmentSearchBar,
DailyDashBoard,
PlayTimeSearchBar,
DailyDashBoard,
UserContent,
SegmentContent,
RetentionContent,
PlayTimeContent,
DailySearchBar,
DailyActiveUserContent,
DailyMedalContent,
CurrencyConsumeContent,
CurrencyAcquireContent,
ItemAcquireContent,
ItemConsumeContent
};

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,
@@ -31,6 +32,18 @@ const ImageUploadBtn = ({ disabled,
if (!file) return;
const koreanRegex = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/;
if (koreanRegex.test(file.name)) {
showToast('FILE_KOREAN_NAME_WARNING', {
type: alertTypes.warning
});
if (document.querySelector('#fileinput')) {
document.querySelector('#fileinput').value = '';
}
onFileDelete();
return;
}
// 이미지 파일 확장자 체크
const fileExt = file.name.split('.').pop().toLowerCase();
if (fileExt !== 'png' && fileExt !== 'jpg' && fileExt !== 'jpeg') {
@@ -220,14 +233,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,27 +1,28 @@
import BoardInfoModal from './modal/BoardInfoModal';
import BoardRegistModal from './modal/BoardRegistModal';
import MailDetailModal from './modal/MailDetailModal';
import LandAuctionModal from './modal/LandAuctionModal'
import BattleEventModal from './modal/BattleEventModal'
import ReportListAnswerModal from './modal/ReportListAnswerModal';
import ReportListDetailModal from './modal/ReportListDetailModal';
import UserBlockDetailModal from './modal/UserBlockDetailModal';
import OwnerChangeModal from './modal/OwnerChangeModal';
import BoardInfoModal from '../modal/BoardInfoModal';
import BoardRegistModal from '../modal/BoardRegistModal';
import MailDetailModal from '../modal/MailDetailModal';
import LandAuctionModal from '../modal/LandAuctionModal'
import BattleEventModal from '../modal/BattleEventModal'
import ReportListAnswerModal from '../modal/ReportListAnswerModal';
import ReportListDetailModal from '../modal/ReportListDetailModal';
import UserBlockDetailModal from '../modal/UserBlockDetailModal';
import OwnerChangeModal from '../modal/OwnerChangeModal';
//searchbar
import SearchFilter from './searchBar/SearchFilter';
import ReportListSearchBar from './searchBar/ReportListSearchBar';
import UserBlockSearchBar from './searchBar/UserBlockSearchBar';
import EventListSearchBar from './searchBar/EventListSearchBar';
import LandAuctionSearchBar from './searchBar/LandAuctionSearchBar'
import MailListSearchBar from './searchBar/MailListSearchBar';
import LandInfoSearchBar from './searchBar/LandInfoSearchBar';
import BusinessLogSearchBar from './searchBar/BusinessLogSearchBar';
import DataInitSearchBar from './searchBar/DataInitSearchBar';
import LogViewSearchBar from './searchBar/LogViewSearchBar';
import AdminViewSearchBar from './searchBar/AdminViewSearchBar';
import CaliumRequestSearchBar from './searchBar/CaliumRequestSearchBar';
import SearchFilter from '../searchBar/SearchFilter';
import ReportListSearchBar from '../searchBar/ReportListSearchBar';
import UserBlockSearchBar from '../searchBar/UserBlockSearchBar';
import EventListSearchBar from '../searchBar/EventListSearchBar';
import LandAuctionSearchBar from '../searchBar/LandAuctionSearchBar'
import MailListSearchBar from '../searchBar/MailListSearchBar';
import LandInfoSearchBar from '../searchBar/LandInfoSearchBar';
import BusinessLogSearchBar from '../searchBar/BusinessLogSearchBar';
import DataInitSearchBar from '../searchBar/DataInitSearchBar';
import LogViewSearchBar from '../searchBar/LogViewSearchBar';
import AdminViewSearchBar from '../searchBar/AdminViewSearchBar';
import CaliumRequestSearchBar from '../searchBar/CaliumRequestSearchBar';
import CurrencyLogSearchBar from '../searchBar/CurrencyLogSearchBar';
import CommonSearchBar from './searchBar/CommonSearchBar';
import CommonSearchBar from '../searchBar/CommonSearchBar';
import useCommonSearch from '../../hooks/useCommonSearch';
//etc
@@ -35,6 +36,7 @@ export {
MailListSearchBar,
LandInfoSearchBar,
BusinessLogSearchBar,
CurrencyLogSearchBar,
DataInitSearchBar,
LogViewSearchBar,
AdminViewSearchBar,

View File

@@ -1,523 +0,0 @@
import { useState, useEffect, Fragment } from 'react';
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, Textarea, SearchBarAlert } from '../../../styles/Components';
import Button from '../../common/button/Button';
import Modal from '../../common/modal/Modal';
import { EventIsItem, EventModify } from '../../../apis';
import { authList } from '../../../store/authList';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import { authType, benItems, commonStatus, currencyType } from '../../../assets/data';
import {
AppendRegistBox, AppendRegistTable, AreaBtnClose,
BtnDelete, DetailInputItem, DetailInputRow,
DetailModalWrapper, RegistGroup, DetailRegistInfo, DetailState,
Item, ItemList, LangArea
} from '../../../styles/ModuleComponents';
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../../utils';
import DateTimeInput from '../../common/input/DateTimeInput';
import { useLoading } from '../../../context/LoadingProvider';
import { useAlert } from '../../../context/AlertProvider';
import { alertTypes } from '../../../assets/data/types';
const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => {
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const {withLoading} = useLoading();
const {showModal, showToast} = useAlert();
const id = content && content.id;
const updateAuth = userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventUpdate);
const [time, setTime] = useState({
start_hour: '00',
start_min: '00',
end_hour: '00',
end_min: '00',
}); //시간 정보
const [item, setItem] = useState('');
const [itemCount, setItemCount] = useState('');
const [resource, setResource] = useState('19010001');
const [resourceCount, setResourceCount] = useState('');
const [resultData, setResultData] = useState({});
const [isNullValue, setIsNullValue] = useState(false);
// 과거 판단
const [isPast, setIsPast] = useState(false);
const [isChanged, setIsChanged] = useState(false);
const [btnValidation, setBtnValidation] = useState(false);
const [isReadOnly, setIsReadOnly] = useState(false);
const [itemCheckMsg, setItemCheckMsg] = useState('');
useEffect(() => {
if(content){
const start_dt_KTC = convertKTCDate(content.start_dt)
const end_dt_KTC = convertKTCDate(content.end_dt)
setResultData({
start_dt: start_dt_KTC,
end_dt: end_dt_KTC,
event_type: content.event_type,
mail_list: content.mail_list,
item_list: content.item_list,
});
setTime({ ...time,
start_hour: String(start_dt_KTC.getHours()).padStart(2, '0'),
start_min: String(start_dt_KTC.getMinutes()).padStart(2, '0'),
end_hour: String(end_dt_KTC.getHours()).padStart(2, '0'),
end_min: String(end_dt_KTC.getMinutes()).padStart(2, '0')
});
start_dt_KTC < (new Date) ? setIsPast(true) : setIsPast(false);
content.mail_list.length === 1 && setBtnValidation(true);
}
setItem('');
}, [content]);
useEffect(() => {
if(!updateAuth || isPast){
setIsReadOnly(true);
}else{
setIsReadOnly(false);
}
}, [updateAuth, isPast]);
useEffect(() => {
if (conditionCheck()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
useEffect(() => {
setItemCheckMsg('');
}, [item]);
// 아이템 수량 숫자 체크
const handleItemCount = e => {
if (e.target.value === '0' || e.target.value === '-0') {
setItemCount('1');
e.target.value = '1';
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
setItemCount(plusNum);
} else {
setItemCount(e.target.value);
}
};
// 아이템 추가
const handleItemList = async () => {
if(benItems.includes(item)){
showToast('MAIL_ITEM_ADD_BEN', {type: alertTypes.warning});
return;
}
if(item.length === 0 || itemCount.length === 0) return;
const result = await EventIsItem(token, {item: item});
if(result.data.result === "ERROR"){
setItemCheckMsg(t('NOT_ITEM'));
return;
}
const itemIndex = resultData.item_list.findIndex((data) => data.item === item);
if (itemIndex !== -1) {
setItemCheckMsg(t('MAIL_ITEM_ADD_DUPL'));
return;
}
const newItem = { item: item, item_cnt: itemCount, item_name: result.data.data.item_info.item_name };
resultData.item_list.push(newItem);
setIsChanged(true);
setItem('');
setItemCount('');
};
// 아이템 삭제
const onItemRemove = id => {
let filterList = resultData.item_list && resultData.item_list.filter(item => item !== resultData.item_list[id]);
setIsChanged(true);
setResultData({ ...resultData, item_list: filterList });
};
// 자원 수량 숫자 체크
const handleResourceCount = e => {
if (e.target.value === '0' || e.target.value === '-0') {
setResourceCount('1');
e.target.value = '1';
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
setResourceCount(plusNum);
} else {
setResourceCount(e.target.value);
}
};
// 자원 추가
const handleResourceList = (e) => {
if(resource.length === 0 || resourceCount.length === 0) return;
const itemIndex = resultData.item_list.findIndex(
(item) => item.item === resource
);
if (itemIndex !== -1) {
const item_cnt = resultData.item_list[itemIndex].item_cnt;
resultData.item_list[itemIndex].item_cnt = Number(item_cnt) + Number(resourceCount);
} else {
const name = currencyType.find(well => well.value === resource).name;
const newItem = { item: resource, item_cnt: resourceCount, item_name: name };
resultData.item_list.push(newItem);
}
setIsChanged(true);
setResource('')
setResourceCount('');
};
// 입력창 삭제
const onLangDelete = language => {
let filterList = resultData.mail_list && resultData.mail_list.filter(el => el.language !== language);
if (filterList.length === 1) setBtnValidation(true);
setIsChanged(true);
setResultData({ ...resultData, mail_list: filterList });
};
// 날짜 처리
const handleDateChange = (data, type) => {
const date = new Date(data);
setResultData({
...resultData,
[`${type}_dt`]: combineDateTime(date, time[`${type}_hour`], time[`${type}_min`]),
});
setIsChanged(true);
};
// 시간 처리
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`]),
});
setIsChanged(true);
};
// 확인 버튼 후 다 초기화
const handleReset = () => {
setBtnValidation(false);
setIsNullValue(false);
setIsChanged(false);
};
const conditionCheck = () => {
return (
content && content.mail_list.every(data => data.content !== '' && data.title !== '') &&
isChanged
);
};
const handleSubmit = async (type, param = null) => {
switch (type) {
case "submit":
if (!conditionCheck()) return;
showModal('MAIL_UPDATE_SAVE', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('updateConfirm')
});
break;
case "updateConfirm":
const timeDiff = timeDiffMinute(resultData.start_dt, (new Date))
// 이벤트 시작 30분전이나 이미 SystemMail이 add된 상태에서는 수정할 수 없다.
if(content.add_flag || timeDiff <= 30){
showToast('EVENT_TIME_LIMIT_UPDATE', {type: alertTypes.warning});
return;
}
withLoading( async () => {
return await EventModify(token, id, resultData);
}).catch(error => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
showToast('UPDATE_COMPLETED', {type: alertTypes.success});
handleDetailView();
});
break;
}
}
const detailState = (status) => {
switch (status) {
case commonStatus.wait:
return <DetailState>대기</DetailState>;
case commonStatus.running:
return <DetailState>진행중</DetailState>;
case commonStatus.finish:
return <DetailState result={commonStatus.finish}>완료</DetailState>;
case commonStatus.fail:
return <DetailState result={commonStatus.fail}>실패</DetailState>;
case commonStatus.delete:
return <DetailState result={commonStatus.delete}>삭제</DetailState>;
default:
return null;
}
};
return (
<>
<Modal min="960px" $view={detailView}>
<Title $align="center">이벤트 상세 정보</Title>
{content &&
<DetailRegistInfo>
<span>등록자 : {content.create_by}</span>
<span>등록일 : {convertKTC(content.create_dt, false)}</span>
{typeof content.update_by !== 'undefined' && (
<>
<span>수정자 : {content.update_by}</span>
<span>수정일 : {convertKTC(content.update_dt, false)}</span>
</>
)}
</DetailRegistInfo>
}
<DetailModalWrapper>
{content &&
<RegistGroup>
<DetailInputRow>
<DateTimeInput
title="이벤트 기간"
dateName="시작 일자"
selectedDate={convertKTCDate(content.start_dt)}
handleSelectedDate={data => handleDateChange(data, 'start')}
onChange={e => handleTimeChange(e, 'start')}
/>
<DateTimeInput
dateName="종료 일자"
selectedDate={convertKTCDate(content.end_dt)}
handleSelectedDate={data => handleDateChange(data, 'end')}
onChange={e => handleTimeChange(e, 'end')}
/>
</DetailInputRow>
<DetailInputRow>
<DetailInputItem>
<InputLabel>이벤트 상태</InputLabel>
<div>{detailState(content.status)}</div>
</DetailInputItem>
{content.status === commonStatus.delete &&
<DetailInputItem>
<InputLabel>삭제 사유</InputLabel>
<div>{content.delete_desc}</div>
</DetailInputItem>
}
</DetailInputRow>
</RegistGroup>
}
{resultData.mail_list &&
resultData.mail_list.map(data => {
return (
<Fragment key={data.language}>
<AppendRegistBox>
<LangArea>
언어 : {data.language}
{btnValidation === false && !isReadOnly ? (
<AreaBtnClose
onClick={e => {
e.preventDefault();
onLangDelete(data.language);
}}
/>
) : (
<AreaBtnClose opacity="10%" />
)}
</LangArea>
<AppendRegistTable>
<tbody>
<tr>
<th width="120">
<Label>제목</Label>
</th>
<td>
<DetailInputItem>
<TextInput
placeholder="우편 제목 입력"
maxLength="30"
id={data.language}
value={data.title}
readOnly={isReadOnly}
onChange={e => {
let list = [...resultData.mail_list];
let findIndex = resultData.mail_list && resultData.mail_list.findIndex(item => item.language === e.target.id);
list[findIndex].title = e.target.value.trimStart();
setResultData({ ...resultData, mail_list: list });
setIsChanged(true);
}}
/>
</DetailInputItem>
</td>
</tr>
<tr>
<th>
<Label>내용</Label>
</th>
<td>
<Textarea
value={data.content}
readOnly={isReadOnly}
id={data.language}
onChange={e => {
if (e.target.value.length > 2000) {
return;
}
let list = [...resultData.mail_list];
let findIndex = resultData.mail_list && resultData.mail_list.findIndex(item => item.language === e.target.id);
list[findIndex].content = e.target.value.trimStart();
setResultData({ ...resultData, mail_list: list });
setIsChanged(true);
}}
/>
</td>
</tr>
</tbody>
</AppendRegistTable>
</AppendRegistBox>
</Fragment>
);
})}
<AppendRegistBox>
<AppendRegistTable>
<tbody>
<tr>
<th width="120">
<Label>아이템 첨부</Label>
</th>
<td>
<DetailInputItem>
<TextInput
placeholder="Item Meta id 입력"
value={item}
onChange={e => {
let list = [];
list = e.target.value.trimStart();
setItem(list);
}}
disabled={isReadOnly}
/>
<TextInput
placeholder="수량"
value={itemCount}
type="number"
onChange={e => handleItemCount(e)}
width="90px"
disabled={isReadOnly}
/>
<Button
text="추가"
theme={itemCount.length === 0 || item.length === 0 ? 'disable' : 'search'}
handleClick={handleItemList}
/>
{itemCheckMsg && <SearchBarAlert>{itemCheckMsg}</SearchBarAlert>}
</DetailInputItem>
</td>
</tr>
<tr>
<th width="120">
<Label>자원 첨부</Label>
</th>
<td>
<DetailInputItem>
<SelectInput onChange={e => setResource(e.target.value)} value={resource} disabled={isReadOnly}>
{currencyType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
placeholder="수량"
type="number"
value={resourceCount}
disabled={isReadOnly}
onChange={e => handleResourceCount(e)}
width="200px"
/>
<Button
text="추가"
theme={resourceCount.length === 0 || resource.length === 0 ? 'disable' : 'search'}
handleClick={handleResourceList}
width="100px"
height="35px"
errorMessage={isReadOnly} />
</DetailInputItem>
<div>
{resultData.item_list && (
<ItemList>
{resultData.item_list.map((data, index) => {
return (
<Item key={index}>
<span>
{data.item_name}[{data.item}] ({data.item_cnt})
</span>
{!isReadOnly && <BtnDelete onClick={() => onItemRemove(index)}></BtnDelete>}
</Item>
);
})}
</ItemList>
)}
</div>
</td>
</tr>
</tbody>
</AppendRegistTable>
</AppendRegistBox>
</DetailModalWrapper>
<BtnWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
<Button
text="확인"
theme="line"
name="확인버튼"
handleClick={() => {
handleDetailView();
handleReset();
setDetailData('');
}}
/>
{!isReadOnly && (
<Button
type="submit"
text="수정"
id="수정버튼"
theme={conditionCheck() ? 'primary' : 'disable'}
handleClick={() => handleSubmit('submit')}
/>
)}
</BtnWrapper>
</Modal>
</>
);
};
export default EventDetailModal;

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,79 +1,29 @@
import React from 'react';
import DatePickerComponent from './DatePickerComponent';
import { DatePickerWrapper } from '../../../styles/Components';
import {
FormRowGroup,
FormLabel,
DateContainer,
DateTimeWrapper,
DateTimeGroup,
} from '../../../styles/ModuleComponents';
import { useTranslation } from 'react-i18next';
import { DatePicker } from 'antd';
import dayjs from 'dayjs';
const { RangePicker } = DatePicker;
const DateRangePicker = ({
label,
startDate,
endDate,
onStartDateChange,
onEndDateChange,
pastDate = new Date(),
disabled,
startLabel = '시작 일자',
endLabel = '종료 일자',
setAlert,
value,
onChange,
format,
showTime = true,
size = 'middle',
...props
}) => {
const { t } = useTranslation();
const handleStartDate = (date) => {
const newDate = new Date(date);
onStartDateChange(newDate);
};
const handleEndDate = (date) => {
let newDate = new Date(date);
if (startDate && newDate < startDate) {
setAlert(t('DATE_START_DIFF_END'));
newDate = new Date(startDate);
}
onEndDateChange(newDate);
};
return (
<FormRowGroup>
<FormLabel>{label}</FormLabel>
<DateTimeWrapper>
<DateTimeGroup>
<DateContainer>
<DatePickerWrapper>
<DatePickerComponent
name={startLabel}
handleSelectedDate={handleStartDate}
selectedDate={startDate}
pastDate={pastDate}
disabled={disabled}
/>
</DatePickerWrapper>
</DateContainer>
</DateTimeGroup>
<DateTimeGroup>
<DateContainer>
<DatePickerWrapper>
<DatePickerComponent
name={endLabel}
handleSelectedDate={handleEndDate}
selectedDate={endDate}
pastDate={pastDate}
disabled={disabled}
/>
</DatePickerWrapper>
</DateContainer>
</DateTimeGroup>
</DateTimeWrapper>
</FormRowGroup>
<RangePicker
showTime={showTime}
value={value ? [dayjs(value[0]), dayjs(value[1])] : [null, null]}
format={format || 'YYYY-MM-DD HH:mm:ss'}
onChange={onChange}
placeholder={['시작 일시', '종료 일시']}
size={size}
allowClear={false}
{...props}
/>
);
};
export default DateRangePicker;

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

@@ -0,0 +1,205 @@
import { NavLink, useNavigate } from 'react-router-dom';
import arrowIcon from '../../../assets/img/icon/icon-tab.png';
import styled from 'styled-components';
import { useRecoilValue } from 'recoil';
import { authList } from '../../../store/authList';
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';
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 location = useLocation();
const navigate = useNavigate();
const handleToken = async () => {
const tokenStatus = await AuthInfo(token);
tokenStatus.message === '잘못된 타입의 토큰입니다.' && setLogoutModalClose('view');
};
useEffect(() => {
handleToken();
}, [token]);
const handleTopMenu = e => {
e.preventDefault();
e.target.classList.toggle('active');
};
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 handleModalClose = () => {
if (modalClose === 'hidden') {
setModalClose('view');
} else {
setModalClose('hidden');
}
};
// 로그아웃 안내 모달
const handleConfirmClose = () => {
if (logoutModalClose === 'hidden') {
setLogoutModalClose('view');
} else {
setLogoutModalClose('hidden');
sessionStorage.removeItem('token');
navigate('/');
}
};
const isClickable = (submenu) => {
switch (userInfo.auth_level_type) {
case adminAuthLevel.DEVELOPER:
case adminAuthLevel.READER:
case adminAuthLevel.MASTER:
return true;
default:
return submenu.authLevel === adminAuthLevel.NONE && userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === submenu.id);
}
}
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>
{/* 접근 불가 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={modalClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleModalClose} />
</BtnWrapper>
<ModalText $align="center">
해당 메뉴에 대한 조회 권한이 없습니다.
<br />
권한 등급을 변경 다시 이용해주세요.
</ModalText>
<BtnWrapper $gap="10px">
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleModalClose} />
</BtnWrapper>
</Modal>
{/* 로그아웃 안내 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={logoutModalClose}>
<BtnWrapper $justify="flex-end">
<ButtonClose onClick={handleConfirmClose} />
</BtnWrapper>
<ModalText $align="center">로그아웃 되었습니다.</ModalText>
<BtnWrapper $gap="10px">
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleConfirmClose} />
</BtnWrapper>
</Modal>
</>
);
};
export default Navi;
const TopMenu = styled(NavLink)`
padding: 16px 30px;
width: 100%;
text-align: left;
border-bottom: 1px solid #888;
position: relative;
color: #fff;
&: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,
&.active {
background: #444;
}
&.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

@@ -0,0 +1,370 @@
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;
const { TextArea } = Input;
/**
* 위치 지정 가능한 그리드 형태 상세 정보 표시 컴포넌트
* @param {Array} items - 표시할 항목 배열 (row, col, rowSpan, colSpan 속성 추가)
* @param {Object} formData - 폼 데이터 객체
* @param {Function} onChange - 값 변경 시 호출할 함수
* @param {boolean} disabled - 전체 비활성화 여부
* @param {number} columns - 그리드의 총 컬럼 수 (기본값: 4)
*/
const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }) => {
// 항목을 행과 열 위치별로 그룹화
const positionedItems = {};
// 각 항목의 위치 및 span 정보 처리
items.forEach(item => {
const rowIndex = item.row || 0;
const colIndex = item.col || 0;
if (!positionedItems[rowIndex]) {
positionedItems[rowIndex] = {};
}
positionedItems[rowIndex][colIndex] = {
...item,
rowSpan: item.rowSpan || 1,
colSpan: item.colSpan || 1
};
});
// 행 번호 목록 (정렬)
const rows = Object.keys(positionedItems).map(Number).sort((a, b) => a - b);
// 항목에 따른 컴포넌트 렌더링 함수
const renderComponent = (item) => {
const {
type,
key,
keys,
label,
value,
options,
placeholder,
disabled: itemDisabled,
width,
handler,
min,
max,
step,
format,
required,
showTime,
tabItems,
activeKey,
onTabChange,
maxLength,
rows: textareaRows
} = item;
// 현재 값 가져오기 (formData에서 또는 항목에서)
const currentValue = formData[key] !== undefined ? formData[key] : value;
// 컴포넌트 공통 속성
const commonProps = {
id: key,
disabled: disabled || itemDisabled,
style: width ? { width, fontSize: '15px' } : { width: '100%', fontSize: '15px' }
};
// 항목 타입에 따른 컴포넌트 렌더링
switch (type) {
case 'text':
return <Input
{...commonProps}
value={currentValue}
onChange={(e) => onChange(key, e.target.value, handler)}
placeholder={placeholder || `${label} 입력`}
/>;
case 'number':
return <InputNumber
{...commonProps}
value={currentValue}
min={min}
max={max}
step={step || 1}
onChange={(value) => onChange(key, value, handler)}
placeholder={placeholder || `${label} 입력`}
/>;
case 'display':
return <Input
{...commonProps}
value={currentValue || ''}
readOnly
style={{
...commonProps.style,
backgroundColor: '#f5f5f5',
cursor: 'default'
}}
placeholder={placeholder || ''}
/>;
case 'textarea':
return <TextArea
{...commonProps}
value={currentValue || ''}
onChange={(e) => onChange(key, e.target.value, handler)}
placeholder={placeholder}
maxLength={maxLength}
rows={textareaRows || 4}
showCount={!!maxLength}
/>;
case 'select':
return (
<Select
{...commonProps}
value={currentValue}
onChange={(value) => onChange(key, value, handler)}
placeholder={placeholder || `${label} 선택`}
popupMatchSelectWidth={false}
>
{options && options.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.name}
</Select.Option>
))}
</Select>
);
case 'date':
return (
<DatePicker
{...commonProps}
allowClear={false}
showTime={showTime || false}
value={currentValue ? dayjs(currentValue) : null}
format={format || 'YYYY-MM-DD'}
onChange={(date) => onChange(key, date, handler)}
placeholder={placeholder || `${label} 선택`}
/>
);
case 'dateRange':
return (
<RangePicker
{...commonProps}
showTime={showTime !== false}
value={keys ? [
formData[keys.start] ? dayjs(formData[keys.start]) : null,
formData[keys.end] ? dayjs(formData[keys.end]) : null
] : (currentValue ? [
currentValue.start ? dayjs(currentValue.start) : null,
currentValue.end ? dayjs(currentValue.end) : null
] : null)}
format={format || 'YYYY-MM-DD HH:mm:ss'}
onChange={(dates) => {
if (dates && dates.length === 2) {
// 두 개의 별도 필드에 각각 업데이트
if (item.keys) {
// 두 개의 onChange를 순차적으로 호출하는 대신
// 한 번에 두 필드를 모두 업데이트하는 방식으로 변경
const updatedData = {
...formData,
[item.keys.start]: dates[0],
[item.keys.end]: dates[1]
};
// handler가 있으면 handler 실행, 없으면 직접 onChange 호출
if (handler) {
handler(dates, key, updatedData);
} else {
// onChange를 통해 전체 업데이트된 데이터를 전달
onChange('dateRange_update', updatedData, null);
}
} else {
// 기존 방식 지원 (하위 호환성)
onChange(key, {
start: dates[0],
end: dates[1]
}, handler);
}
} else {
// 두 필드 모두 비우기
if (item.keys) {
const updatedData = {
...formData,
[item.keys.start]: null,
[item.keys.end]: null
};
if (handler) {
handler(null, key, updatedData);
} else {
onChange('dateRange_update', updatedData, null);
}
} else {
onChange(key, { start: null, end: null }, handler);
}
}
}}
placeholder={[
item.startLabel || '시작 일시',
item.endLabel || '종료 일시'
]}
/>
);
case 'time':
return (
<TimePicker
{...commonProps}
value={currentValue ? dayjs(currentValue, 'HH:mm') : null}
format={format || 'HH:mm'}
onChange={(time) => onChange(key, time, handler)}
placeholder={placeholder || `${label} 선택`}
/>
);
case 'switch':
return (
<Switch
checked={currentValue}
onChange={(checked) => onChange(key, checked, handler)}
/>
);
case 'checkbox':
return (
<Checkbox
checked={currentValue}
disabled={disabled || itemDisabled}
onChange={(e) => onChange(key, e.target.checked, handler)}
>
{item.checkboxLabel}
</Checkbox>
);
case 'status':
return <StatusDisplay status={currentValue} />;
case 'tab':
return <AnimatedTabs
items={tabItems}
activeKey={activeKey}
onChange={onTabChange}
/>
case 'custom':
return item.render ? item.render(formData, onChange) : null;
case 'label':
default:
return <div style={{
padding: '4px 11px',
minHeight: '32px',
lineHeight: '24px',
fontSize: '15px',
color: currentValue ? '#000' : '#bfbfbf'
}}>
{currentValue || placeholder || ''}
</div>;
}
};
// 각 셀의 폭 계산 (Ant Design의 24-컬럼 시스템 기준)
const colWidth = 24 / columns;
return (
<GridContainer>
<Form layout="horizontal">
{rows.map(rowIndex => {
const rowItems = positionedItems[rowIndex];
const cols = Object.keys(rowItems).map(Number).sort((a, b) => a - b);
return (
<Row key={`row-${rowIndex}`} gutter={[16, 16]}>
{cols.map(colIndex => {
const item = rowItems[colIndex];
const itemColSpan = Math.min(item.colSpan * colWidth, 24);
return (
<Col
key={`${item.key}-${rowIndex}-${colIndex}`}
span={itemColSpan}
xs={24}
sm={itemColSpan}
>
<Form.Item
label={item.label}
required={item.required}
tooltip={item.tooltip}
>
{renderComponent(item)}
</Form.Item>
</Col>
);
})}
</Row>
);
})}
</Form>
</GridContainer>
);
};
// 상태 표시 컴포넌트
const StatusDisplay = ({ status }) => {
let color = '';
let text = '';
const lowerStatus = typeof status === 'string' ? status.toLowerCase() : status;
switch (lowerStatus) {
case 'wait':
color = '#FAAD14';
text = '대기';
break;
case 'running':
color = '#4287f5';
text = '진행중';
break;
case 'finish':
color = '#d9d9d9';
text = '만료';
break;
case 'fail':
color = '#ff4d4f';
text = '실패';
break;
case 'delete':
color = '#ff4d4f';
text = '삭제';
break;
default:
color = '#DEBB46';
text = status;
}
return (
<StatusTag color={color}>
{text}
</StatusTag>
);
};
const GridContainer = styled.div`
width: 100%;
padding: 16px 0;
font-size: 15px;
`;
const StatusTag = styled.div`
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
background-color: ${props => props.color};
color: white;
font-size: 15px;
font-weight: 500;
`;
export default DetailGrid;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Card, Descriptions } from 'antd';
import { getFieldLabel } from '../../../utils';
const InfoCard = ({
title,
data,
keyPrefix = 'item',
size = 'small',
column = 1,
bordered = true,
type = 'inner'
}) => {
if (!data ||
typeof data !== 'object' ||
Object.keys(data).length === 0) {
return null;
}
const items = Object.entries(data).map(([key, value]) => ({
key: `${keyPrefix}-${key}`,
label: getFieldLabel(key),
children: (() => {
if (value === null || value === undefined || value === '') {
return '-';
}
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
return value.join(', ');
}
return JSON.stringify(value, null, 2);
}
return String(value);
})()
}));
return (
<Card
size={size}
title={title}
type={type}
style={{ marginBottom: 16 }}
>
<Descriptions
bordered={bordered}
column={column}
size={size}
items={items}
/>
</Card>
);
};
export default InfoCard;

View File

@@ -0,0 +1,139 @@
import React from 'react';
import styled from 'styled-components';
import { Card } from 'antd';
import DetailGrid from './DetailGrid';
/**
* Ant Design 방식의 DetailModalWrapper 컴포넌트
* @param {Array} itemGroups - 표시할 항목 그룹 배열
* @param {Object} formData - 폼 데이터 객체
* @param {Function} onChange - 값 변경 시 호출할 함수
* @param {boolean} disabled - 전체 비활성화 여부
* @param {number} columnCount - 한 행에 표시할 컬럼 수 (기본값: 4)
* @param {ReactNode} children - 추가 컨텐츠
*/
const DetailLayout = ({
itemGroups,
formData,
onChange,
disabled = false,
columnCount = 4,
children
}) => {
// 값 변경 핸들러
const handleChange = (key, value, handler) => {
let updatedFormData = { ...formData };
// dateRange 전용 업데이트 처리
if (key === 'dateRange_update') {
updatedFormData = value; // value가 이미 완전히 업데이트된 객체
}
// 키가 점 표기법이면 중첩 객체 업데이트
else if (key.includes('.')) {
const [parentKey, childKey] = key.split('.');
updatedFormData = {
...formData,
[parentKey]: {
...formData[parentKey],
[childKey]: value
}
};
} else {
// 일반 키는 직접 업데이트
updatedFormData = {
...formData,
[key]: value
};
}
// 핸들러가 있으면 핸들러 실행 (업데이트된 데이터를 전달)
if (handler) {
handler(value, key, updatedFormData);
} else {
// 핸들러가 없으면 직접 onChange 호출
onChange(updatedFormData);
}
};
return (
<DetailWrapper>
{itemGroups.map((group, index) => (
<Card
key={`group-${index}`}
title={group.title}
style={{ marginBottom: 16 }}
>
<DetailGrid
items={group.items}
formData={formData}
onChange={handleChange}
disabled={disabled || group.disabled}
columnCount={group.columnCount || columnCount}
/>
</Card>
))}
{children}
</DetailWrapper>
);
};
const DetailWrapper = styled.div`
width: 100%;
max-width: 1200px;
margin: 0 auto;
`;
export default DetailLayout;
//예시
// const itemGroupsExample = [
// {
// title: '기본 정보',
// items: [
// {
// row: 0,
// col: 0,
// colSpan: 2,
// type: 'text',
// key: 'title',
// label: '제목',
// required: true,
// disabled: !isView('title'),
// width: '300px',
// },
// {
// row: 0,
// col: 2,
// colSpan: 2,
// type: 'number',
// key: 'order_id',
// label: '순서',
// required: true,
// disabled: !isView('order_id'),
// width: '200px',
// min: 1,
// },
// {
// row: 1,
// col: 0,
// colSpan: 2,
// type: 'status',
// key: 'status',
// label: '상태',
// value: resultData.status,
// },
// {
// row: 1,
// col: 2,
// colSpan: 2,
// type: 'switch',
// key: 'is_link',
// label: '링크 사용',
// disabled: !isView('is_link'),
// },
// ]
// },
// {
// title: ''
// }
// ];

View File

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

View File

@@ -26,7 +26,7 @@ const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction,
</SearchRow>
)}
{isSearch &&
<SearchRow>
<SearchRow direction={direction}>
<BtnWrapper $gap="8px">
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
<Button theme="reset" handleClick={onReset} type="button" />

Some files were not shown because too many files have changed in this diff Show More