Compare commits

..

26 Commits

Author SHA1 Message Date
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
156 changed files with 13482 additions and 6122 deletions

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,10 @@ server {
listen [::]:8080; listen [::]:8080;
server_name localhost; server_name localhost;
client_max_body_size 100M;
client_body_timeout 300s;
client_header_timeout 300s;
location / { location / {
root /usr/share/nginx/admintool; root /usr/share/nginx/admintool;
index index.html index.htm; index index.html index.htm;
@@ -16,6 +20,11 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 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; 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

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

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

@@ -0,0 +1,95 @@
//운영 정보 관리 - 백과사전 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 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'; 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 { try {
const res = await Axios.get( 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} `/api/v1/world-event/list?search_data=${searchData}&status=${status}&start_dt=${startDate}&end_dt=${endDate}
&page_size=${size}`, &orderby=${order}&page_no=${currentPage}&page_size=${size}`,
{ {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}, },
@@ -24,11 +24,11 @@ export const EventView = async (token, title, content, status, startDate, endDat
// 이벤트 상세보기 // 이벤트 상세보기
export const EventDetailView = async (token, id) => { export const EventDetailView = async (token, id) => {
try { 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}` }, headers: { Authorization: `Bearer ${token}` },
}); });
return res.data.data.detail; return res.data.data;
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
throw new Error('EventDetailView Error', e); throw new Error('EventDetailView Error', e);
@@ -39,11 +39,11 @@ export const EventDetailView = async (token, id) => {
// 이벤트 등록 // 이벤트 등록
export const EventSingleRegist = async (token, params) => { export const EventSingleRegist = async (token, params) => {
try { try {
const res = await Axios.post(`/api/v1/event`, params, { const res = await Axios.post(`/api/v1/world-event`, params, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
return res; return res.data;
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
throw new Error('EventSingleRegist Error', e); throw new Error('EventSingleRegist Error', e);
@@ -51,10 +51,10 @@ export const EventSingleRegist = async (token, params) => {
} }
}; };
// 우편 수정 // 이벤트 수정
export const EventModify = async (token, id, params) => { export const EventModify = async (token, id, params) => {
try { 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}` }, 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 { try {
const res = await Axios.delete(`/api/v1/event/delete`, { const res = await Axios.delete(`/api/v1/world-event/delete?id=${id}`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` }
data: { list: params },
}); });
return res.data.data.list; return res.data;
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
throw new Error('EventDelete Error', e); 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 { try {
const res = await Axios.post(`/api/v1/event/item`, params, { const res = await Axios.get(
`/api/v1/dictionary/event-action/list`,
{
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); },
);
return res; return res.data.data.event_action_list;
} catch (e) { } catch (e) {
if (e instanceof Error) { 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) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
throw new Error('LogViewList Error', e); 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) => { export const userIndexExport = async (token, filename, sendDate, endDate) => {
try { try {
@@ -62,10 +75,14 @@ export const userIndexExport = async (token, filename, sendDate, endDate) => {
}; };
// Retention // Retention
export const RetentionIndexView = async (token, start_dt, end_dt) => { export const RetentionIndexView = async (token, startDate, endDate, order, size, currentPage) => {
try { try {
const res = await Axios.get(`/api/v1/indicators/retention/list?start_dt=${start_dt}&end_dt=${end_dt}`, { const res = await Axios.get(`/api/v1/indicators/retention/list?start_dt=${startDate}&end_dt=${endDate}
headers: { Authorization: `Bearer ${token}` }, &orderby=${order}&page_no=${currentPage}&page_size=${size}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
}); });
return res.data.data; return res.data.data;
@@ -183,10 +200,10 @@ export const PlaytimeIndexExport = async (token, filename, sendDate, endDate) =>
// 2. 경제 지표 // 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 { 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}` }, 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 // Item
export const ItemIndexView = async (token, start_dt, end_dt) => { export const ItemIndexView = async (token, start_dt, end_dt, itemId, deltaType) => {
try { 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}` }, headers: { Authorization: `Bearer ${token}` },
}); });
@@ -278,27 +230,17 @@ export const ItemIndexView = async (token, start_dt, end_dt) => {
} }
}; };
// Item 다운로드 // Assets
export const ItemIndexExport = async (token, filename, sendDate, endDate) => { export const AssetsIndexView = async (token, start_dt, end_dt, itemId, deltaType) => {
try { 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}` }, 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) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
throw new Error('ItemIndexExport Error', e); throw new Error('AssetsIndexView Error', e);
} }
} }
}; };
@@ -321,136 +263,3 @@ export const InstanceIndexView = async (token, data, start_dt, end_dt) => {
} }
} }
}; };
// 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

@@ -111,7 +111,6 @@ export const getCurrencyDetailList = async (token, searchType, searchData, tranI
export const GameCurrencyDetailLogExport = async (token, params, fileName) => { export const GameCurrencyDetailLogExport = async (token, params, fileName) => {
try { try {
console.log(params);
await Axios.post(`/api/v1/log/currency/detail/excel-export`, params, { await Axios.post(`/api/v1/log/currency/detail/excel-export`, params, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
responseType: 'blob', responseType: 'blob',
@@ -128,3 +127,187 @@ export const GameCurrencyDetailLogExport = async (token, params, fileName) => {
} }
} }
}; };
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 { try {
const res = await Axios.delete(`/api/v1/menu/banner/delete`, { const res = await Axios.delete(`/api/v1/menu/banner/delete?id=${id}`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` }
data: { list: params },
}); });
return res.data; return res.data;

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

@@ -0,0 +1,99 @@
//운영서비스 관리 - 랭킹 스케줄 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 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 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);
}
}
};

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

View File

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

View File

@@ -11,6 +11,10 @@ export const IMAGE_MAX_SIZE = 5242880;
export const STORAGE_MAIL_COPY = 'copyMailData'; export const STORAGE_MAIL_COPY = 'copyMailData';
export const STORAGE_BUSINESS_LOG_SEARCH = 'businessLogSearchParam'; export const STORAGE_BUSINESS_LOG_SEARCH = 'businessLogSearchParam';
export const STORAGE_GAME_LOG_CURRENCY_SEARCH = 'gameLogCurrencySearchParam'; 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 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 }; 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": { "LogViewList": {
"method": "GET", "method": "GET",
"url": "/list", "url": "/list",
"dataPath": "data.data", "dataPath": "data",
"paramFormat": "query" "paramFormat": "query"
}, },
"LogviewDetail": { "LogviewDetail": {

View File

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

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,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' color: 'white'
}, },
WAIT: { WAIT: {
background: '#DEBB46', background: '#FAAD14',
color: 'black' color: 'black'
}, },
FAIL: { FAIL: {
background: '#D33B27', background: '#ff4d4f',
color: 'white' color: 'white'
}, },
FINISH: { FINISH: {
@@ -78,11 +78,11 @@ export const STATUS_STYLES = {
color: 'black' color: 'black'
}, },
REJECT: { REJECT: {
background: '#D33B27', background: '#ff4d4f',
color: 'white' color: 'white'
}, },
CANCEL: { CANCEL: {
background: '#D33B27', background: '#ff4d4f',
color: 'white' color: 'white'
}, },
RESV_START: { RESV_START: {
@@ -106,7 +106,7 @@ export const STATUS_STYLES = {
color: 'white' color: 'white'
}, },
REGISTER: { REGISTER: {
background: '#DEBB46', background: '#FAAD14',
color: 'black' color: 'black'
}, },
STOP: { STOP: {
@@ -123,7 +123,7 @@ export const STATUS_STYLES = {
}, },
}; };
export const logFieldLabels = { export const FieldLabels = {
// DynamoDB 필드 // DynamoDB 필드
'attribFieldName': '속성 명', 'attribFieldName': '속성 명',
'pk': '파티션 키', 'pk': '파티션 키',
@@ -181,15 +181,40 @@ export const logFieldLabels = {
'ffa_hot_time': 'FFA 핫타임', 'ffa_hot_time': 'FFA 핫타임',
'round_count': '라운드 수', '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',
}; };
export const historyTables = { export const historyTables = {
userBlock: 'black_list', userBlock: 'black_list',
landAuction: 'land_auction', landAuction: 'land_auction',
landOwnerChange: 'land_ownership_changes', landOwnerChange: 'land_ownership_changes',
event: 'event', rewardEvent: 'event',
mail: 'mail', mail: 'mail',
notice: 'notice', notice: 'notice',
battleEvent: 'battle_event', battleEvent: 'battle_event',
caliumRequest: 'calium_request', caliumRequest: 'calium_request',
menuBanner: 'menu_banner',
event: 'world_event',
rankingSchedule: 'ranking_schedule',
} }

View File

@@ -1,4 +1,4 @@
export {authType, ivenTabType, modalTypes, TabUserList, tattooSlot, commonStatus, ViewTitleCountType, landAuctionStatusType} from './types' export {authType, ivenTabType, modalTypes, tattooSlot, commonStatus, ViewTitleCountType, landAuctionStatusType} from './types'
export { export {
mailSendType, mailSendType,
mailType, mailType,
@@ -27,6 +27,7 @@ export {
opYNType, opYNType,
opUserSessionType, opUserSessionType,
opMailType, opMailType,
amountDeltaType amountDeltaType,
TabUserList
} from './options' } from './options'
export {benItems, MinuteList, HourList, caliumRequestInitData, STATUS_STYLES, months, PAGE_SIZE_OPTIONS, ORDER_OPTIONS} from './data' export {benItems, MinuteList, HourList, caliumRequestInitData, STATUS_STYLES, months, PAGE_SIZE_OPTIONS, ORDER_OPTIONS} from './data'

View File

@@ -103,14 +103,6 @@ export const menuConfig = {
view: false, view: false,
authLevel: adminAuthLevel.NONE authLevel: adminAuthLevel.NONE
}, },
cryptview: {
title: '크립토 조회',
permissions: {
read: authType.cryptoRead
},
view: false,
authLevel: adminAuthLevel.NONE
},
businesslogview: { businesslogview: {
title: '비즈니스 로그 조회', title: '비즈니스 로그 조회',
permissions: { permissions: {
@@ -118,6 +110,30 @@ export const menuConfig = {
}, },
view: true, view: true,
authLevel: adminAuthLevel.NONE 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
},
rankmanage: {
title: '랭킹 점수 관리',
permissions: {
read: authType.rankManagerRead
},
view: true,
authLevel: adminAuthLevel.NONE
} }
} }
}, },
@@ -154,17 +170,17 @@ export const menuConfig = {
view: true, view: true,
authLevel: adminAuthLevel.NONE authLevel: adminAuthLevel.NONE
}, },
reportlist: { // reportlist: {
title: '신고내역', // title: '신고내역',
permissions: { // permissions: {
read: authType.reportRead, // read: authType.reportRead,
update: authType.reportUpdate, // update: authType.reportUpdate,
delete: authType.reportDelete // delete: authType.reportDelete
}, // },
view: true, // view: true,
authLevel: adminAuthLevel.NONE // authLevel: adminAuthLevel.NONE
}, // },
event: { rewardevent: {
title: '보상 이벤트 관리', title: '보상 이벤트 관리',
permissions: { permissions: {
read: authType.eventRead, read: authType.eventRead,
@@ -214,6 +230,26 @@ export const menuConfig = {
view: true, view: true,
authLevel: adminAuthLevel.NONE 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,18 +4,50 @@ export const languageType = [
{ value: 'JA', name: '일본어' }, { 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 = [ export const TabGameLogList = [
{ value: 'CURRENCY', name: '재화 로그' }, { value: 'CURRENCY', name: '재화 로그' },
// { value: 'ITEM', name: '아이템 로그' }, { value: 'ITEM', name: '아이템 로그' },
// { value: 'TRADE', name: '거래 로그' }, { value: 'CURRENCYITEM', name: '재화(아이템) 로그' },
{ value: 'USERCREATE', name: '유저생성 로그' },
{ value: 'USERLOGIN', name: '유저로그인 로그' },
{ value: 'SNAPSHOT', name: '스냅샷 로그' },
]; ];
export const TabEconomicIndexList = [ export const TabEconomicIndexList = [
{ value: 'CURRENCY', name: '재화(유저)' }, { value: 'CURRENCY_ACQUIRE', name: '재화 획득' },
// { value: 'ITEM', name: '아이템' }, { value: 'CURRENCY_CONSUME', name: '재화 소모' },
// { value: 'VBP', name: 'VBP' }, { value: 'ITEM_ACQUIRE', name: '아이템 획득' },
// { value: 'deco', name: '의상/타투' }, { value: 'ITEM_CONSUME', name: '아이템 소모' },
// { value: 'instance', 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: 'PIONEER', name: '개척자 랭킹 보드' },
{ value: 'RUN_RACE', name: '점프 러너 랭킹 보드' },
{ value: 'BATTLE_OBJECT', name: '컴뱃 존 랭킹 보드' }
]; ];
export const mailSendType = [ export const mailSendType = [
@@ -95,6 +127,17 @@ export const landAuctionStatus = [
{ value: 'FAIL', name: '실패' }, { value: 'FAIL', name: '실패' },
]; ];
export const questStatus = [
{ value: 'WAIT', name: '미완료' },
{ value: 'COMPLETE', name: '완료' },
{ value: 'RUNNING', name: '진행중' },
];
export const questCompleteStatusType = [
{ value: 0, name: '미완료' },
{ value: 1, name: '완료' }
]
export const currencyItemCode = [ export const currencyItemCode = [
{ value: '19010001', name: '골드' }, { value: '19010001', name: '골드' },
{ value: '19010002', name: '사파이어' }, { value: '19010002', name: '사파이어' },
@@ -197,6 +240,11 @@ export const landSearchType = [
{ value: 'NAME', name: '랜드명' }, { value: 'NAME', name: '랜드명' },
]; ];
export const itemSearchType = [
{ value: 'ID', name: '아이템ID' },
{ value: 'NAME', name: '아이템명' },
];
export const blockType = [ export const blockType = [
{ value: '', name: '선택' }, { value: '', name: '선택' },
{ value: 'Access_Restrictions', name: '접근 제한' }, { value: 'Access_Restrictions', name: '접근 제한' },
@@ -225,6 +273,24 @@ export const amountDeltaType = [
{value: 'None', 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 = [ export const battleEventStatus = [
{ value: 'ALL', name: '전체' }, { value: 'ALL', name: '전체' },
{ value: 'WAIT', name: '대기' }, { value: 'WAIT', name: '대기' },
@@ -340,6 +406,19 @@ export const opItemRestore = [
{ value: 'IMPOSSIBLE', name: '불가능' }, { 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 opEquipType = [ export const opEquipType = [
{ value: 0, name: '미장착' }, { value: 0, name: '미장착' },
{ value: 1, name: '의상장착' }, { value: 1, name: '의상장착' },
@@ -347,7 +426,7 @@ export const opEquipType = [
{ value: 3, name: '타투장착' }, { value: 3, name: '타투장착' },
] ]
export const opItemType = [ export const opItemLargeType = [
{ value: 'TOOL', name: '도구' }, { value: 'TOOL', name: '도구' },
{ value: 'EXPENDABLE', name: '소모품' }, { value: 'EXPENDABLE', name: '소모품' },
{ value: 'TICKET', name: '티켓' }, { value: 'TICKET', name: '티켓' },
@@ -359,6 +438,95 @@ export const opItemType = [
{ value: 'CURRENCY', name: '재화' }, { value: 'CURRENCY', name: '재화' },
{ value: 'PRODUCT', name: '제품' }, { value: 'PRODUCT', name: '제품' },
{ value: 'BEAUTY', 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 = [ export const opHistoryType = [
@@ -430,6 +598,62 @@ export const opDBType = [
{ value: 'MySql', name: 'MySql'}, { 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 logAction = [ // export const logAction = [
// { value: "None", name: "ALL" }, // { value: "None", name: "ALL" },
// { value: "AIChatDeleteCharacter", name: "NPC 삭제" }, // { value: "AIChatDeleteCharacter", name: "NPC 삭제" },
@@ -726,6 +950,7 @@ export const opDBType = [
export const logAction = [ export const logAction = [
{ value: "None", name: "전체" }, { value: "None", name: "전체" },
{ value: "AdminToolQuestTaskForceComplete", name: "AdminToolQuestTaskForceComplete" },
{ value: "AIChatDeleteCharacter", name: "AIChatDeleteCharacter" }, { value: "AIChatDeleteCharacter", name: "AIChatDeleteCharacter" },
{ value: "AIChatDeleteUser", name: "AIChatDeleteUser" }, { value: "AIChatDeleteUser", name: "AIChatDeleteUser" },
{ value: "AIChatGetCharacter", name: "AIChatGetCharacter" }, { value: "AIChatGetCharacter", name: "AIChatGetCharacter" },
@@ -760,12 +985,12 @@ export const logAction = [
{ value: "BeaconShopUpdateDailyCount", name: "BeaconShopUpdateDailyCount" }, { value: "BeaconShopUpdateDailyCount", name: "BeaconShopUpdateDailyCount" },
{ value: "BeaconShopDeleteRecord", name: "BeaconShopDeleteRecord" }, { value: "BeaconShopDeleteRecord", name: "BeaconShopDeleteRecord" },
{ value: "BeaconShopDeactiveItems", name: "BeaconShopDeactiveItems" }, { value: "BeaconShopDeactiveItems", name: "BeaconShopDeactiveItems" },
{ value: "BrokerApiAdmin", name: "BrokerApiAdmin" }, // { value: "BrokerApiAdmin", name: "BrokerApiAdmin" },
{ value: "BrokerApiPlanetAuth", name: "BrokerApiPlanetAuth" }, { value: "BrokerApiPlanetAuth", name: "BrokerApiPlanetAuth" },
{ value: "BrokerApiUserExchangeOrderCompleted", name: "BrokerApiUserExchangeOrderCompleted" }, { value: "BrokerApiUserExchangeOrderCompleted", name: "BrokerApiUserExchangeOrderCompleted" },
{ value: "BrokerApiUserExchangeOrderCreated", name: "BrokerApiUserExchangeOrderCreated" }, { value: "BrokerApiUserExchangeOrderCreated", name: "BrokerApiUserExchangeOrderCreated" },
{ value: "BrokerApiUserSystemMailSend", name: "BrokerApiUserSystemMailSend" }, // { value: "BrokerApiUserSystemMailSend", name: "BrokerApiUserSystemMailSend" },
{ value: "BrokerApiUserEchoSystemRequest", name: "BrokerApiUserEchoSystemRequest" }, // { value: "BrokerApiUserEchoSystemRequest", name: "BrokerApiUserEchoSystemRequest" },
{ value: "BrokerApiUserLogin", name: "BrokerApiUserLogin" }, { value: "BrokerApiUserLogin", name: "BrokerApiUserLogin" },
{ value: "BuffAdd", name: "BuffAdd" }, { value: "BuffAdd", name: "BuffAdd" },
{ value: "BuffDelete", name: "BuffDelete" }, { value: "BuffDelete", name: "BuffDelete" },
@@ -815,6 +1040,7 @@ export const logAction = [
{ value: "ClaimReward", name: "ClaimReward" }, { value: "ClaimReward", name: "ClaimReward" },
{ value: "ConvertCalium", name: "ConvertCalium" }, { value: "ConvertCalium", name: "ConvertCalium" },
{ value: "ConvertExchangeCalium", name: "ConvertExchangeCalium" }, { value: "ConvertExchangeCalium", name: "ConvertExchangeCalium" },
{ value: "ContentsMove", name: "ContentsMove" },
{ value: "CraftFinish", name: "CraftFinish" }, { value: "CraftFinish", name: "CraftFinish" },
{ value: "CraftHelp", name: "CraftHelp" }, { value: "CraftHelp", name: "CraftHelp" },
{ value: "CraftRecipeRegister", name: "CraftRecipeRegister" }, { value: "CraftRecipeRegister", name: "CraftRecipeRegister" },
@@ -841,6 +1067,12 @@ export const logAction = [
{ value: "FriendAdd", name: "FriendAdd" }, { value: "FriendAdd", name: "FriendAdd" },
{ value: "FriendDelete", name: "FriendDelete" }, { value: "FriendDelete", name: "FriendDelete" },
{ value: "GainLandProfit", name: "GainLandProfit" }, { 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: "InviteParty", name: "InviteParty" },
{ value: "ItemBuy", name: "ItemBuy" }, { value: "ItemBuy", name: "ItemBuy" },
{ value: "ItemDestroy", name: "ItemDestroy" }, { value: "ItemDestroy", name: "ItemDestroy" },
@@ -871,8 +1103,17 @@ export const logAction = [
{ value: "MailRead", name: "MailRead" }, { value: "MailRead", name: "MailRead" },
{ value: "MailSend", name: "MailSend" }, { value: "MailSend", name: "MailSend" },
{ value: "MailTaken", name: "MailTaken" }, { 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: "ModifyLandInfo", name: "ModifyLandInfo" },
{ value: "MoneyChange", name: "MoneyChange" }, { value: "MoneyChange", name: "MoneyChange" },
{ value: "MoveToBeacon", name: "MoveToBeacon" },
{ value: "ProductGive", name: "ProductGive" }, { value: "ProductGive", name: "ProductGive" },
{ value: "ProductOpenFailed", name: "ProductOpenFailed" }, { value: "ProductOpenFailed", name: "ProductOpenFailed" },
{ value: "ProductOpenSuccess", name: "ProductOpenSuccess" }, { value: "ProductOpenSuccess", name: "ProductOpenSuccess" },
@@ -896,6 +1137,11 @@ export const logAction = [
{ value: "ReplySummonParty", name: "ReplySummonParty" }, { value: "ReplySummonParty", name: "ReplySummonParty" },
{ value: "ReservationEnterToServer", name: "ReservationEnterToServer" }, { value: "ReservationEnterToServer", name: "ReservationEnterToServer" },
{ value: "RewardProp", name: "RewardProp" }, { 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: "SaveMyhome", name: "SaveMyhome" },
{ value: "SeasonPassBuyCharged", name: "SeasonPassBuyCharged" }, { value: "SeasonPassBuyCharged", name: "SeasonPassBuyCharged" },
{ value: "SeasonPassStartNew", name: "SeasonPassStartNew" }, { value: "SeasonPassStartNew", name: "SeasonPassStartNew" },
@@ -945,6 +1191,7 @@ export const logAction = [
{ value: "UpdateGameOption", name: "UpdateGameOption" }, { value: "UpdateGameOption", name: "UpdateGameOption" },
{ value: "UpdateLanguage", name: "UpdateLanguage" }, { value: "UpdateLanguage", name: "UpdateLanguage" },
{ value: "UpdateUgcNpcLike", name: "UpdateUgcNpcLike" }, { value: "UpdateUgcNpcLike", name: "UpdateUgcNpcLike" },
{ value: "UpdateGameModePlayerRegulation", name: "UpdateGameModePlayerRegulation" },
{ value: "UserBlock", name: "UserBlock" }, { value: "UserBlock", name: "UserBlock" },
{ value: "UserBlockCancel", name: "UserBlockCancel" }, { value: "UserBlockCancel", name: "UserBlockCancel" },
{ value: "UserCreate", name: "UserCreate" }, { value: "UserCreate", name: "UserCreate" },
@@ -997,6 +1244,9 @@ export const logDomain = [
{ value: "Farming", name: "Farming" }, { value: "Farming", name: "Farming" },
{ value: "FarmingReward", name: "FarmingReward" }, { value: "FarmingReward", name: "FarmingReward" },
{ value: "GameLogInOut", name: "GameLogInOut" }, { value: "GameLogInOut", name: "GameLogInOut" },
{ value: "GameObjectInteraction", name: "GameObjectInteraction" },
{ value: "GameModePenalty", name: "GameModePenalty" },
{ value: "GameModePlayRegulation", name: "GameModePlayRegulation" },
{ value: "Item", name: "Item" }, { value: "Item", name: "Item" },
{ value: "IgmApi", name: "IgmApi" }, { value: "IgmApi", name: "IgmApi" },
{ value: "MyHome", name: "MyHome" }, { value: "MyHome", name: "MyHome" },
@@ -1008,6 +1258,9 @@ export const logDomain = [
{ value: "Mail", name: "Mail" }, { value: "Mail", name: "Mail" },
{ value: "MailStoragePeriodExpired", name: "MailStoragePeriodExpired" }, { value: "MailStoragePeriodExpired", name: "MailStoragePeriodExpired" },
{ value: "MailProfile", name: "MailProfile" }, { value: "MailProfile", name: "MailProfile" },
{ value: "MatchUser", name: "MatchUser" },
{ value: "MatchServerUser", name: "MatchServerUser" },
{ value: "MatchRoom", name: "MatchRoom" },
{ value: "Party", name: "Party" }, { value: "Party", name: "Party" },
{ value: "PartyMember", name: "PartyMember" }, { value: "PartyMember", name: "PartyMember" },
{ value: "PartyVote", name: "PartyVote" }, { value: "PartyVote", name: "PartyVote" },
@@ -1025,6 +1278,11 @@ export const logDomain = [
{ value: "RenewalShopProducts", name: "RenewalShopProducts" }, { value: "RenewalShopProducts", name: "RenewalShopProducts" },
{ value: "Rental", name: "Rental" }, { value: "Rental", name: "Rental" },
{ value: "RewardProp", name: "RewardProp" }, { 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: "Stage", name: "Stage" },
{ value: "SocialAction", name: "SocialAction" }, { value: "SocialAction", name: "SocialAction" },
{ value: "SeasonPass", name: "SeasonPass" }, { value: "SeasonPass", name: "SeasonPass" },

View File

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

View File

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

View File

@@ -14,14 +14,14 @@
"text": "선택 삭제", "text": "선택 삭제",
"theme": "line", "theme": "line",
"disableWhen": "noSelection", "disableWhen": "noSelection",
"requiredAuth": "battleEventDelete", "requiredAuth": "menuBannerDelete",
"action": "delete" "action": "delete"
}, },
{ {
"id": "register", "id": "register",
"text": "이미지 등록", "text": "이미지 등록",
"theme": "primary", "theme": "primary",
"requiredAuth": "battleEventUpdate", "requiredAuth": "menuBannerUpdate",
"action": "navigate", "action": "navigate",
"navigateTo": "/servicemanage/menubanner/menubannerregist" "navigateTo": "/servicemanage/menubanner/menubannerregist"
} }
@@ -40,6 +40,12 @@
"width": "70px", "width": "70px",
"title": "번호" "title": "번호"
}, },
{
"id": "order_id",
"type": "text",
"width": "70px",
"title": "순서"
},
{ {
"id": "status", "id": "status",
"type": "status", "type": "status",
@@ -94,10 +100,11 @@
} }
}, },
{ {
"id": "update_by", "id": "history",
"type": "text", "type": "button",
"width": "150px", "width": "120px",
"title": "히스토리" "title": "히스토리",
"text": "히스토리"
} }
], ],
"sort": { "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,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,140 @@
{
"id": "rankingTable",
"selection": {
"type": "single",
"idField": "id"
},
"header": {
"countType": "total",
"orderType": "desc",
"pageType": "default",
"buttons": [
{
"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,16 @@ export const authType = {
menuBannerRead: 50, menuBannerRead: 50,
menuBannerUpdate: 51, menuBannerUpdate: 51,
menuBannerDelete: 52, menuBannerDelete: 52,
itemDictionaryRead: 53,
rankManagerRead: 54,
rankManagerUpdate: 55,
rankingRead: 56,
rankingUpdate: 57,
rankingDelete: 58,
worldEventRead: 59,
worldEventUpdate: 60,
worldEventDelete: 61,
craftingDictionaryRead: 62,
levelReader: 999, levelReader: 999,
@@ -75,20 +85,6 @@ export const adminAuthLevel = {
DEVELOPER: "Developer", DEVELOPER: "Developer",
} }
export const TabUserList = [
{ title: '기본정보' },
{ title: '아바타' },
{ title: '의상' },
{ title: '도구' },
{ title: '인벤토리' },
{ title: '우편' },
{ title: '마이홈' },
{ title: '친구목록' },
{ title: '타투' },
{ title: '퀘스트' },
// { title: '클레임' },
];
export const ivenTabType = { export const ivenTabType = {
CLOTH: "cloth", CLOTH: "cloth",
PROP: "prop", PROP: "prop",
@@ -161,7 +157,7 @@ export const currencyCodeTypes = {
} }
export const languageNames = { export const languageNames = {
'KO': '한국어', 'Ko': '한국어',
'EN': '영어', 'En': '영어',
'JA': '일본어', 'Ja': '일본어',
}; };

View File

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

View File

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

View File

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

View File

@@ -5,15 +5,31 @@ import { BtnWrapper, TableStyle } from '../../styles/Components';
import Button from '../../components/common/button/Button'; import Button from '../../components/common/button/Button';
import Modal from '../../components/common/modal/Modal'; import Modal from '../../components/common/modal/Modal';
import { useEffect, useState, Fragment } from 'react'; 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([]) const [detailList, setDetailList] = useState([])
useEffect(() => { useEffect(() => {
Array.isArray(detailQuest) && setDetailList(detailQuest) Array.isArray(detailQuest.detailQuest) && setDetailList(detailQuest.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 ( return (
<> <>
<Modal $view={detailPop} min="480px"> <Modal $view={detailPop} min="480px">
@@ -25,7 +41,8 @@ const QuestDetailModal = ({ detailPop, handleClick, detailQuest }) => {
<th width="80">Task No</th> <th width="80">Task No</th>
<th>Task Name</th> <th>Task Name</th>
<th width="120">Counter</th> <th width="120">Counter</th>
<th width="120">State</th> <th width="120">상태</th>
<th width="120">완료처리</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -36,7 +53,10 @@ const QuestDetailModal = ({ detailPop, handleClick, detailQuest }) => {
<td>{el.task_no}</td> <td>{el.task_no}</td>
<td>{el.quest_name}</td> <td>{el.quest_name}</td>
<td>{el.counter}</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> </tr>
</Fragment> </Fragment>
); );

View File

@@ -0,0 +1,146 @@
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import {
CircularProgressWrapper,
FormWrapper,
TableStyle,
TableWrapper,
} from '../../styles/Components';
import { useTranslation } from 'react-i18next';
import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { useUserCreateLogSearch, UserCreateLogSearchBar } from '../searchBar';
import { TopButton, ViewTableInfo } from '../common';
import Pagination from '../common/Pagination/Pagination';
import {
INITIAL_PAGE_LIMIT,STORAGE_GAME_LOG_USER_CREATE_SEARCH,
} from '../../assets/data/adminConstants';
import ExcelExportButton from '../common/button/ExcelExportButton';
import CircularProgress from '../common/CircularProgress';
import { AnimatedPageWrapper } from '../common/Layout';
const UserCreateLogContent = ({ active }) => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const tableRef = useRef(null);
const [downloadState, setDownloadState] = useState({
loading: false,
progress: 0
});
const {
searchParams,
loading: dataLoading,
data: dataList,
handleSearch,
handleReset,
handlePageChange,
updateSearchParams
} = useUserCreateLogSearch(token, 500);
useEffect(() => {
if(active) {
// 세션 스토리지에서 복사된 메일 데이터 가져오기
const paramsData = sessionStorage.getItem(STORAGE_GAME_LOG_USER_CREATE_SEARCH);
if (paramsData) {
const searchData = JSON.parse(paramsData);
handleSearch({
start_dt: new Date(searchData.start_dt),
end_dt: new Date(searchData.end_dt),
search_data: searchData.guid
});
// 사용 후 세션 스토리지 데이터 삭제
sessionStorage.removeItem(STORAGE_GAME_LOG_USER_CREATE_SEARCH);
}
}
}, [active]);
const tableHeaders = useMemo(() => {
return [
{ id: 'logDay', label: '일자', width: '120px' },
{ id: 'accountId', label: 'account ID', width: '80px' },
{ id: 'userGuid', label: 'GUID', width: '200px' },
{ id: 'userNickname', label: '아바타명', width: '150px' },
{ id: 'createTime', label: '생성일시', width: '200px' },
];
}, []);
if(!active) return null;
return (
<AnimatedPageWrapper>
<FormWrapper>
<UserCreateLogSearchBar
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo orderType="asc" pageType="B" total={dataList?.total} total_all={dataList?.total_all}>
<ExcelExportButton
functionName="GameUserCreateLogExport"
params={searchParams}
fileName={t('FILE_GAME_LOG_USER_CREATE')}
onLoadingChange={setDownloadState}
dataSize={dataList?.total_all}
/>
{downloadState.loading && (
<CircularProgressWrapper>
<CircularProgress
progress={downloadState.progress}
size={36}
progressColor="#4A90E2"
/>
</CircularProgressWrapper>
)}
</ViewTableInfo>
{dataLoading ? <TableSkeleton width='100%' count={15} /> :
<>
<TableWrapper>
<TableStyle ref={tableRef}>
<thead>
<tr>
{tableHeaders.map(header => (
<th key={header.id} width={header.width}>{header.label}</th>
))}
</tr>
</thead>
<tbody>
{dataList?.user_create_list?.map((item, index) => (
<Fragment key={index}>
<tr>
<td>{item.logDay}</td>
<td>{item.accountId}</td>
<td>{item.userGuid}</td>
<td>{item.userNickname}</td>
<td>{item.createdTime}</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
{dataList?.user_create_list &&
<Pagination
postsPerPage={searchParams.page_size}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams.page_no}
pageLimit={INITIAL_PAGE_LIMIT}
/>
}
<TopButton />
</>
}
</AnimatedPageWrapper>
);
};
export default UserCreateLogContent;

View File

@@ -93,12 +93,6 @@ const UserInfoTable = styled.table`
font-size: 13px; font-size: 13px;
border-radius: 15px; border-radius: 15px;
overflow: hidden; overflow: hidden;
tr:first-child {
th,
td {
border-top: 0;
}
}
th, th,
td { td {
height: 36px; 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 { InfoSubTitle, UserDefaultTable, UserInfoTable, UserTableWrapper } from '../../styles/ModuleComponents';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRecoilValue } from 'recoil';
import { authList } from '../../store/authList';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { TableSkeleton } from '../Skeleton/TableSkeleton'; import { TableSkeleton } from '../Skeleton/TableSkeleton';
import { UserInventoryView, UserMyhomeView } from '../../apis'; import { UserMyhomeView } from '../../apis';
import { SelectInput } from '../../styles/Components';
const UserMyHomeInfo = ({ userInfo }) => { const UserMyHomeInfo = ({ userInfo }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dataList, setDataList] = useState(); const [dataList, setDataList] = useState();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedHome, setSelectedHome] = useState('');
useEffect(() => { useEffect(() => {
if(userInfo && Object.keys(userInfo).length > 0) { if(userInfo && Object.keys(userInfo).length > 0) {
@@ -23,11 +21,18 @@ const UserMyHomeInfo = ({ userInfo }) => {
const fetchData = async () => { const fetchData = async () => {
const token = sessionStorage.getItem('token'); const token = sessionStorage.getItem('token');
await UserMyhomeView(token, userInfo.guid).then(data => { 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); setLoading(false);
}); });
}; };
const handleHomeChange = (e) => {
setSelectedHome(e.target.value);
};
return ( return (
loading ? <TableSkeleton count={15}/> : loading ? <TableSkeleton count={15}/> :
dataList && dataList &&
@@ -36,7 +41,13 @@ const UserMyHomeInfo = ({ userInfo }) => {
<tbody> <tbody>
<tr> <tr>
<th>마이 홈명</th> <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> </tr>
</tbody> </tbody>
</UserInfoTable> </UserInfoTable>
@@ -51,15 +62,19 @@ const UserMyHomeInfo = ({ userInfo }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{dataList.prop_list && dataList.prop_list.map((el, idx) => { {dataList.myhome_info.find(home => home.myhome_guid === selectedHome)?.prop_list?.map((el, idx) => (
return (
<tr key={idx}> <tr key={idx}>
<td>{idx + 1}</td> <td>{idx + 1}</td>
<td>{el.item_id}</td> <td>{el.item_id}</td>
<td>{el.item_name}</td> <td>{el.item_name}</td>
</tr> </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> </tbody>
</UserDefaultTable> </UserDefaultTable>
</UserTableWrapper> </UserTableWrapper>

View File

@@ -3,15 +3,22 @@ import { useState, useEffect, Fragment } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import Button from '../../components/common/button/Button'; import Button from '../../components/common/button/Button';
import QuestDetailModal from '../../components/DataManage/QuestDetailModal'; import QuestDetailModal from '../../components/DataManage/QuestDetailModal';
import { UserQuestView } from '../../apis/Users'; import { UserQuestTaskComplete, UserQuestView } from '../../apis/Users';
import { convertKTC } from '../../utils'; import { convertKTC } from '../../utils';
import { TableSkeleton } from '../Skeleton/TableSkeleton'; 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 UserQuestInfo = ({ userInfo }) => {
const [detailPop, setDetailPop] = useState('hidden'); const [detailPop, setDetailPop] = useState('hidden');
const [dataList, setDataList] = useState({}); const [dataList, setDataList] = useState({});
const [detailQuest, setDetailQuest] = useState({}); const [detailQuest, setDetailQuest] = useState({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { showModal, showToast } = useAlert();
const { withLoading } = useLoading();
useEffect(() => { useEffect(() => {
if(userInfo && Object.keys(userInfo).length > 0) { if(userInfo && Object.keys(userInfo).length > 0) {
@@ -30,10 +37,30 @@ const UserQuestInfo = ({ userInfo }) => {
const handleClick = data => { const handleClick = data => {
if (detailPop === 'hidden') { if (detailPop === 'hidden') {
setDetailPop('view'); setDetailPop('view');
setDetailQuest(data.detailQuest); setDetailQuest(data);
} else setDetailPop('hidden'); } 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 ( return (
loading ? <TableSkeleton /> : loading ? <TableSkeleton /> :
<> <>
@@ -59,7 +86,7 @@ const UserQuestInfo = ({ userInfo }) => {
<td>{el.quest_id}</td> <td>{el.quest_id}</td>
<td>{el.quest_name}</td> <td>{el.quest_name}</td>
<td>{el.quest_type}</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_assign_time, false)}</td>
<td>{convertKTC(el.quest_complete_time, false)}</td> <td>{convertKTC(el.quest_complete_time, false)}</td>
<td> <td>
@@ -72,7 +99,7 @@ const UserQuestInfo = ({ userInfo }) => {
</tbody> </tbody>
</QuestTable> </QuestTable>
</UserTableWrapper> </UserTableWrapper>
<QuestDetailModal detailPop={detailPop} handleClick={handleClick} detailQuest={detailQuest} /> <QuestDetailModal detailPop={detailPop} handleClick={handleClick} detailQuest={detailQuest} handleQuestComplete={handleQuestComplete} />
</> </>
); );
}; };

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

View File

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

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 fetchData = async () => {
const token = sessionStorage.getItem('token'); const token = sessionStorage.getItem('token');
await userTotalIndex(token).then(data => { await userTotalIndex(token).then(data => {
console.log(data);
setTotalData(data.dashboard); 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

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

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

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 { userIndexView } from '../../apis';
import { UserIndexSearchBar, DailyDashBoard } from '../../components/IndexManage/index';
import { userIndexView, userIndexExport } from '../../apis';
import Loading from '../common/Loading';
import { ExcelDownButton } from '../common'; import { ExcelDownButton } from '../common';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { formatStringDate } from '../../utils'; import { formatStringDate } from '../../utils';
import { AnimatedPageWrapper } from '../common/Layout';
import { UserIndexSearchBar } from '../searchBar';
const UserContent = () => { const UserContent = () => {
const token = sessionStorage.getItem('token'); const token = sessionStorage.getItem('token');
@@ -24,20 +23,6 @@ const UserContent = () => {
const [dataList, setDataList] = useState([]); const [dataList, setDataList] = useState([]);
const [resultData, setResultData] = 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(() => { useEffect(() => {
fetchData(START_DATE, END_DATE); fetchData(START_DATE, END_DATE);
}, []); }, []);
@@ -54,8 +39,6 @@ const UserContent = () => {
setLoading(false); setLoading(false);
}); });
// setSendDate(startDateToLocal);
// setFinishDate(endDateToLocal);
}; };
// 검색 함수 // 검색 함수
@@ -63,14 +46,8 @@ const UserContent = () => {
fetchData(send_dt, end_dt); fetchData(send_dt, end_dt);
}; };
// 엑셀 다운로드
// const handleXlsxExport = () => {
// const fileName = 'Caliverse_User_Index.xlsx';
// userIndexExport(token, fileName, sendDate, finishDate);
// };
return ( return (
<> <AnimatedPageWrapper>
<DailyDashBoard /> <DailyDashBoard />
<UserIndexSearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} /> <UserIndexSearchBar setResultData={setResultData} resultData={resultData} handleSearch={handleSearch} fetchData={fetchData} />
<TableInfo> <TableInfo>
@@ -125,8 +102,7 @@ const UserContent = () => {
</tbody> </tbody>
</TableStyle> </TableStyle>
</IndexTableWrap> </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 '../searchBar/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 "../searchBar/UserIndexSearchBar";
import RetentionSearchBar from "../searchBar/RetentionSearchBar";
import SegmentSearchBar from "../searchBar/SegmentSearchBar";
import DailyDashBoard from "./DailyDashBoard"; import DailyDashBoard from "./DailyDashBoard";
import PlayTimeSearchBar from "../searchBar/PlayTimeSearchBar";
import UserContent from "./UserContent"; import UserContent from "./UserContent";
import PlayTimeContent from "./PlayTimeContent";
import RetentionContent from "./RetentionContent"; import RetentionContent from "./RetentionContent";
import SegmentContent from "./SegmentContent"; import CurrencyConsumeContent from "./CurrencyConsumeContent";
import DailyActiveUserContent from "./DailyActiveUserContent"; import CurrencyAcquireContent from "./CurrencyAcquireContent";
import DailyMedalContent from "./DailyMedalContent"; import ItemAcquireContent from "./ItemAcquireContent";
import DailySearchBar from "../searchBar/DailySearchBar"; import ItemConsumeContent from "./ItemConsumeContent";
export { export {
UserIndexSearchBar,
RetentionSearchBar,
SegmentSearchBar,
DailyDashBoard, DailyDashBoard,
PlayTimeSearchBar,
UserContent, UserContent,
SegmentContent,
RetentionContent, RetentionContent,
PlayTimeContent, CurrencyConsumeContent,
DailySearchBar, CurrencyAcquireContent,
DailyActiveUserContent, ItemAcquireContent,
DailyMedalContent, ItemConsumeContent
}; };

View File

@@ -5,6 +5,7 @@ import { MenuImageDelete, MenuImageUpload } from '../../apis';
import { IMAGE_MAX_SIZE } from '../../assets/data/adminConstants'; import { IMAGE_MAX_SIZE } from '../../assets/data/adminConstants';
import { useAlert } from '../../context/AlertProvider'; import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types'; import { alertTypes } from '../../assets/data/types';
import { ImagePreview } from '../../styles/Components';
const ImageUploadBtn = ({ disabled, const ImageUploadBtn = ({ disabled,
onImageUpload, onImageUpload,
@@ -31,6 +32,18 @@ const ImageUploadBtn = ({ disabled,
if (!file) return; 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(); const fileExt = file.name.split('.').pop().toLowerCase();
if (fileExt !== 'png' && fileExt !== 'jpg' && fileExt !== 'jpeg') { if (fileExt !== 'png' && fileExt !== 'jpg' && fileExt !== 'jpeg') {
@@ -220,14 +233,6 @@ const PreviewContainer = styled.div`
overflow: hidden; 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` const PreviewInfo = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { NavLink, useNavigate } from 'react-router-dom'; import { NavLink, useNavigate, useLocation } from 'react-router-dom';
import arrowIcon from '../../../assets/img/icon/icon-tab.png'; import { ConfigProvider, Menu, theme } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { authList } from '../../../store/authList'; import { authList } from '../../../store/authList';
@@ -7,7 +7,6 @@ import Modal from '../modal/Modal';
import { BtnWrapper, ButtonClose, ModalText } from '../../../styles/Components'; import { BtnWrapper, ButtonClose, ModalText } from '../../../styles/Components';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Button from '../button/Button'; import Button from '../button/Button';
import { useLocation } from 'react-router-dom';
import { AuthInfo } from '../../../apis'; import { AuthInfo } from '../../../apis';
import { getMenuConfig } from '../../../utils'; import { getMenuConfig } from '../../../utils';
import { adminAuthLevel } from '../../../assets/data/types'; import { adminAuthLevel } from '../../../assets/data/types';
@@ -15,47 +14,55 @@ import { adminAuthLevel } from '../../../assets/data/types';
const Navi = () => { const Navi = () => {
const token = sessionStorage.getItem('token'); const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList); const userInfo = useRecoilValue(authList);
const menu = getMenuConfig(userInfo); const menuConfig = getMenuConfig(userInfo);
const [modalClose, setModalClose] = useState('hidden');
const [logoutModalClose, setLogoutModalClose] = useState('hidden');
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); 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 handleToken = async () => {
const tokenStatus = await AuthInfo(token); const tokenStatus = await AuthInfo(token);
if (tokenStatus.message === '잘못된 타입의 토큰입니다.') {
tokenStatus.message === '잘못된 타입의 토큰입니다.' && setLogoutModalClose('view'); setLogoutModalClose('view');
}
}; };
useEffect(() => { useEffect(() => {
handleToken(); handleToken();
}, [token]); }, [token]);
const handleTopMenu = e => { // 메뉴 아이템 클릭 핸들러
e.preventDefault(); const handleMenuClick = ({ key }) => {
e.target.classList.toggle('active'); handleToken();
}; };
const handleLink = e => { // 서브메뉴 열기/닫기 핸들러
let topActive = document.querySelectorAll('nav .active'); const handleOpenChange = (keys) => {
let currentTopMenu = e.target.closest('ul').previousSibling; setOpenKeys(keys);
for (let i = 0; i < topActive.length; i++) {
if (topActive[i] !== currentTopMenu) {
topActive[i].classList.remove('active');
}
}
handleToken();
}; };
// 등록 완료 모달 // 등록 완료 모달
const handleModalClose = () => { const handleModalClose = () => {
if (modalClose === 'hidden') { setModalClose(modalClose === 'hidden' ? 'view' : 'hidden');
setModalClose('view');
} else {
setModalClose('hidden');
}
}; };
// 로그아웃 안내 모달 // 로그아웃 안내 모달
@@ -65,7 +72,6 @@ const Navi = () => {
} else { } else {
setLogoutModalClose('hidden'); setLogoutModalClose('hidden');
sessionStorage.removeItem('token'); sessionStorage.removeItem('token');
navigate('/'); navigate('/');
} }
}; };
@@ -79,41 +85,56 @@ const Navi = () => {
default: default:
return submenu.authLevel === adminAuthLevel.NONE && userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === submenu.id); 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 ( return (
<> <>
<nav> <StyledNavWrapper>
<ul> <ConfigProvider
{menu.map((item, idx) => { theme={{
return ( algorithm: theme.darkAlgorithm,
<li key={idx}> }}
{item.access && ( >
<TopMenu to={item.link} onClick={handleTopMenu}> <StyledMenu
{item.title} theme="dark"
</TopMenu> mode="inline"
)} openKeys={openKeys}
<SubMenu> selectedKeys={selectedKeys}
{item.submenu && userInfo && onOpenChange={handleOpenChange}
item.submenu.map((submenu, idx) => { onClick={handleMenuClick}
return ( items={getMenuItems()}
<SubMenuItem key={idx} $isclickable={isClickable(submenu) ? 'true' : 'false'}> multiple={true}
<NavLink />
to={isClickable(submenu) ? submenu.link : location.pathname} </ConfigProvider>
onClick={e => { </StyledNavWrapper>
isClickable(submenu) ? handleLink(e) : handleModalClose();
}}>
{submenu.title}
</NavLink>
</SubMenuItem>
);
})}
</SubMenu>
</li>
);
})}
</ul>
</nav>
{/* 접근 불가 모달 */} {/* 접근 불가 모달 */}
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={modalClose}> <Modal min="440px" $padding="40px" $bgcolor="transparent" $view={modalClose}>
<BtnWrapper $justify="flex-end"> <BtnWrapper $justify="flex-end">
@@ -145,61 +166,15 @@ const Navi = () => {
export default Navi; export default Navi;
const TopMenu = styled(NavLink)` const StyledNavWrapper = styled.div`
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` const StyledMenu = styled(Menu)`
display: none;
`; `;
const SubMenuItem = styled.li` const MenuItemLink = styled(NavLink)`
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 { &.active {
color: ${props => (props.$isclickable === 'false' ? '#818181' : '#2c2c2c')};
font-weight: ${props => (props.$isclickable === 'false' ? 400 : 600)}; 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 { 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 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 { useRecoilState } from 'recoil';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
import { AuthLogout, AuthInfo } from '../../../apis'; import { AuthLogout, AuthInfo } from '../../../apis';
import { BtnWrapper, ModalText } from '../../../styles/Components';
import { authList } from '../../../store/authList'; 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 Profile = () => {
const location = useLocation();
const { showModal } = useAlert();
const [infoData, setInfoData] = useRecoilState(authList); const [infoData, setInfoData] = useRecoilState(authList);
const [errorModal, setErrorModal] = useState('hidden');
const navigate = useNavigate(); const navigate = useNavigate();
@@ -40,91 +44,123 @@ const Profile = () => {
// 필수값 입력 모달창 // 필수값 입력 모달창
const handleErrorModal = () => { const handleErrorModal = () => {
if (errorModal === 'hidden') { showModal('USER_LOGOUT_CONFIRM', {
setErrorModal('view'); type: alertTypes.confirm,
} else { onConfirm: () => handleLogout()
setErrorModal('hidden'); });
}
}; };
// 카테고리별 첫 번째 아이템 링크 찾기
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 ( return (
<> <>
<ProfileWrapper> <StyledHeader>
<UserWrapper>{infoData.name && <Username>{infoData.name.length > 20 ? infoData.name.slice(0, 20) + '...' : infoData.name}</Username>}</UserWrapper> <StyledBreadcrumb items={breadcrumbItems} />
<Link> <ProfileContainer>
<LogoutBtn onClick={handleErrorModal}>로그아웃</LogoutBtn> <StyledAvatar
</Link> size={32}
</ProfileWrapper> icon={<UserOutlined />}
{/* 로그아웃 확인 모달 */} />
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={errorModal}> {infoData.name &&
<BtnWrapper $justify="flex-end"> <StyledUsername>
<ButtonClose onClick={handleErrorModal} /> {infoData.name.length > 20 ? infoData.name.slice(0, 20) + '...' : infoData.name}
</BtnWrapper> </StyledUsername>
<ModalText $align="center"> }
로그아웃 하시겠습니까? <Tooltip title="로그아웃">
<br /> <StyledLogoutButton
(로그아웃 저장되지 않은 값은 초기화 됩니다.) type="text"
</ModalText> icon={<LogoutOutlined />}
<BtnWrapper $gap="10px"> onClick={handleErrorModal}
<Button text="취소" theme="line" size="large" width="100%" handleClick={handleErrorModal} /> />
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleLogout} /> </Tooltip>
</BtnWrapper> </ProfileContainer>
</Modal>
</StyledHeader>
</> </>
); );
}; };
export default Profile; export default Profile;
const ProfileWrapper = styled.div` const StyledHeader = styled(Header)`
background: #f6f6f6; background: #f6f6f6;
padding: 20px; padding: 0 20px;
display: flex; display: flex;
flex-wrap: wrap; justify-content: space-between;
gap: 30px;
word-break: break-all;
justify-content: flex-end;
align-items: center; align-items: center;
height: 64px;
`; `;
const LogoutBtn = styled.button` const StyledBreadcrumb = styled(Breadcrumb)`
color: #2c2c2c; font-size: 15px;
line-height: 1; font-weight: 600;
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 UserWrapper = styled.div` const ProfileContainer = styled.div`
padding-left: 35px;
position: relative;
font-size: 18px;
display: flex; display: flex;
align-items: center;
gap: 12px;
`;
&:before { const StyledAvatar = styled(Avatar)`
background: url('${UserIcon}') 50% 50% no-repeat; `;
width: 24px;
height: 24px; const StyledUsername = styled(Text)`
content: ''; font-weight: 600;
display: block; font-size: 18px;
position: absolute; color: rgba(0, 0, 0, 0.85);
left: 0; `;
top: 50%;
transform: translate(0, -50%); const StyledLogoutButton = styled(AntButton)`
color: rgba(0, 0, 0, 0.45);
transition: color 0.3s;
font-size: 18px;
&:hover {
color: #1677ff;
background: transparent;
} }
`;
const Username = styled.div`
font-weight: 700;
padding-right: 3px;
`;
const ButtonClose = styled.button`
width: 16px;
height: 16px;
background: url(${CloseIcon}) 50% 50% no-repeat;
`; `;

View File

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

View File

@@ -2,7 +2,9 @@ import React from 'react';
import { Row, Col, Form, Input, Select, DatePicker, TimePicker, InputNumber, Switch, Button, Checkbox } from 'antd'; import { Row, Col, Form, Input, Select, DatePicker, TimePicker, InputNumber, Switch, Button, Checkbox } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { AnimatedTabs } from '../index';
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
const { TextArea } = Input;
/** /**
* 위치 지정 가능한 그리드 형태 상세 정보 표시 컴포넌트 * 위치 지정 가능한 그리드 형태 상세 정보 표시 컴포넌트
@@ -50,9 +52,15 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
handler, handler,
min, min,
max, max,
step,
format, format,
required, required,
showTime showTime,
tabItems,
activeKey,
onTabChange,
maxLength,
rows: textareaRows
} = item; } = item;
// 현재 값 가져오기 (formData에서 또는 항목에서) // 현재 값 가져오기 (formData에서 또는 항목에서)
@@ -81,10 +89,35 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
value={currentValue} value={currentValue}
min={min} min={min}
max={max} max={max}
step={step || 1}
onChange={(value) => onChange(key, value, handler)} onChange={(value) => onChange(key, value, handler)}
placeholder={placeholder || `${label} 입력`} 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': case 'select':
return ( return (
<Select <Select
@@ -92,10 +125,11 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
value={currentValue} value={currentValue}
onChange={(value) => onChange(key, value, handler)} onChange={(value) => onChange(key, value, handler)}
placeholder={placeholder || `${label} 선택`} placeholder={placeholder || `${label} 선택`}
popupMatchSelectWidth={false}
> >
{options && options.map((option) => ( {options && options.map((option) => (
<Select.Option key={option.value} value={option.value}> <Select.Option key={option.value} value={option.value}>
{option.label} {option.name}
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>
@@ -105,6 +139,8 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
return ( return (
<DatePicker <DatePicker
{...commonProps} {...commonProps}
allowClear={false}
showTime={showTime || false}
value={currentValue ? dayjs(currentValue) : null} value={currentValue ? dayjs(currentValue) : null}
format={format || 'YYYY-MM-DD'} format={format || 'YYYY-MM-DD'}
onChange={(date) => onChange(key, date, handler)} onChange={(date) => onChange(key, date, handler)}
@@ -125,12 +161,25 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
currentValue.end ? dayjs(currentValue.end) : null currentValue.end ? dayjs(currentValue.end) : null
] : null)} ] : null)}
format={format || 'YYYY-MM-DD HH:mm:ss'} format={format || 'YYYY-MM-DD HH:mm:ss'}
onChange={(dates, dateStrings) => { onChange={(dates) => {
if (dates) { if (dates && dates.length === 2) {
// 두 개의 별도 필드에 각각 업데이트 // 두 개의 별도 필드에 각각 업데이트
if (item.keys) { if (item.keys) {
onChange(item.keys.start, dates[0], handler); // 두 개의 onChange를 순차적으로 호출하는 대신
onChange(item.keys.end, dates[1], handler); // 한 번에 두 필드를 모두 업데이트하는 방식으로 변경
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 { } else {
// 기존 방식 지원 (하위 호환성) // 기존 방식 지원 (하위 호환성)
onChange(key, { onChange(key, {
@@ -141,8 +190,17 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
} else { } else {
// 두 필드 모두 비우기 // 두 필드 모두 비우기
if (item.keys) { if (item.keys) {
onChange(item.keys.start, null, handler); const updatedData = {
onChange(item.keys.end, null, handler); ...formData,
[item.keys.start]: null,
[item.keys.end]: null
};
if (handler) {
handler(null, key, updatedData);
} else {
onChange('dateRange_update', updatedData, null);
}
} else { } else {
onChange(key, { start: null, end: null }, handler); onChange(key, { start: null, end: null }, handler);
} }
@@ -191,15 +249,24 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
case 'tab': case 'tab':
return <AnimatedTabs return <AnimatedTabs
items={tabItems} items={tabItems}
activeKey={activeLanguage} activeKey={activeKey}
onChange={handleTabChange} onChange={onTabChange}
/> />
case 'custom': case 'custom':
return item.render ? item.render(formData, onChange) : null; return item.render ? item.render(formData, onChange) : null;
case 'label':
default: default:
return <div>{currentValue}</div>; return <div style={{
padding: '4px 11px',
minHeight: '32px',
lineHeight: '24px',
fontSize: '15px',
color: currentValue ? '#000' : '#bfbfbf'
}}>
{currentValue || placeholder || ''}
</div>;
} }
}; };
@@ -249,13 +316,15 @@ const StatusDisplay = ({ status }) => {
let color = ''; let color = '';
let text = ''; let text = '';
switch (status) { const lowerStatus = typeof status === 'string' ? status.toLowerCase() : status;
switch (lowerStatus) {
case 'wait': case 'wait':
color = '#faad14'; color = '#FAAD14';
text = '대기'; text = '대기';
break; break;
case 'running': case 'running':
color = '#52c41a'; color = '#4287f5';
text = '진행중'; text = '진행중';
break; break;
case 'finish': case 'finish':
@@ -271,7 +340,7 @@ const StatusDisplay = ({ status }) => {
text = '삭제'; text = '삭제';
break; break;
default: default:
color = '#1890ff'; color = '#DEBB46';
text = status; text = status;
} }

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

@@ -22,24 +22,36 @@ const DetailLayout = ({
}) => { }) => {
// 값 변경 핸들러 // 값 변경 핸들러
const handleChange = (key, value, handler) => { const handleChange = (key, value, handler) => {
// 핸들러가 있으면 핸들러 실행 let updatedFormData = { ...formData };
if (handler) {
handler(value, key, formData);
}
// dateRange 전용 업데이트 처리
if (key === 'dateRange_update') {
updatedFormData = value; // value가 이미 완전히 업데이트된 객체
}
// 키가 점 표기법이면 중첩 객체 업데이트 // 키가 점 표기법이면 중첩 객체 업데이트
if (key.includes('.')) { else if (key.includes('.')) {
const [parentKey, childKey] = key.split('.'); const [parentKey, childKey] = key.split('.');
onChange({ updatedFormData = {
...formData, ...formData,
[parentKey]: { [parentKey]: {
...formData[parentKey], ...formData[parentKey],
[childKey]: value [childKey]: value
} }
}); };
} else { } else {
// 일반 키는 직접 업데이트 // 일반 키는 직접 업데이트
onChange({ ...formData, [key]: value }); updatedFormData = {
...formData,
[key]: value
};
}
// 핸들러가 있으면 핸들러 실행 (업데이트된 데이터를 전달)
if (handler) {
handler(value, key, updatedFormData);
} else {
// 핸들러가 없으면 직접 onChange 호출
onChange(updatedFormData);
} }
}; };

View File

@@ -1,5 +1,9 @@
import Layout from './Layout'; import Layout from './Layout';
import LoginLayout from './LoginLayout'; import LoginLayout from './LoginLayout';
import MainLayout from './MainLayout'; 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> </SearchRow>
)} )}
{isSearch && {isSearch &&
<SearchRow> <SearchRow direction={direction}>
<BtnWrapper $gap="8px"> <BtnWrapper $gap="8px">
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" /> <Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
<Button theme="reset" handleClick={onReset} type="button" /> <Button theme="reset" handleClick={onReset} type="button" />

View File

@@ -0,0 +1,390 @@
import { ExcelDownButton } from '../../../styles/ModuleComponents';
import { useCallback, useEffect, useState } from 'react';
const CSVDownloadButton = ({ tableRef, data, fileName = 'download.csv', onLoadingChange }) => {
const [isDownloading, setIsDownloading] = useState(false);
const [lastProgress, setLastProgress] = useState(0);
// 타임아웃 감지 및 처리
useEffect(() => {
let timeoutTimer;
if (isDownloading && lastProgress >= 95) {
// 최종 단계에서 타임아웃 감지 타이머 설정
timeoutTimer = setTimeout(() => {
// 진행 상태가 여전히 변하지 않았다면 타임아웃으로 간주
if (isDownloading && lastProgress >= 95) {
console.log("CSV download timeout detected, completing process");
setIsDownloading(false);
if (onLoadingChange) {
onLoadingChange({ loading: false, progress: 100 });
}
}
}, 10000); // 10초 타임아웃 (CSV는 Excel보다 빠르므로 시간 단축)
}
return () => {
if (timeoutTimer) clearTimeout(timeoutTimer);
};
}, [isDownloading, lastProgress, onLoadingChange]);
const flattenObject = (obj, prefix = '') => {
return Object.keys(obj).reduce((acc, key) => {
const prefixedKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
Object.assign(acc, flattenObject(obj[key], prefixedKey));
} else if (Array.isArray(obj[key])) {
// 배열은 JSON 문자열로 변환
acc[prefixedKey] = JSON.stringify(obj[key]);
} else {
acc[prefixedKey] = obj[key];
}
return acc;
}, {});
};
const updateLoadingState = (newProgress) => {
setLastProgress(newProgress);
if (onLoadingChange && typeof onLoadingChange === 'function') {
onLoadingChange({loading: true, progress: newProgress});
}
};
// CSV 문자열 이스케이프 처리
const escapeCSVField = (field) => {
if (field === null || field === undefined) {
return '';
}
const str = String(field);
// 쉼표, 따옴표, 줄바꿈이 있는 경우 따옴표로 감싸고 내부 따옴표는 두 개로 변환
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
return '"' + str.replace(/"/g, '""') + '"';
}
return str;
};
// 배열을 CSV 행으로 변환
const arrayToCSVRow = (array) => {
return array.map(escapeCSVField).join(',');
};
const downloadTableCSV = async () => {
return new Promise((resolve, reject) => {
try {
if (!tableRef || !tableRef.current) {
reject(new Error('테이블 참조가 없습니다.'));
return;
}
updateLoadingState(10);
// 메인 스레드에서 데이터 추출
const tableElement = tableRef.current;
const headerRows = tableElement.getElementsByTagName('thead')[0].getElementsByTagName('tr');
const bodyRows = tableElement.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
// 일반 행만 포함 (상세 행 제외)
const normalBodyRows = Array.from(bodyRows).filter(row => {
const hasTdWithColspan = Array.from(row.cells).some(cell => cell.hasAttribute('colspan'));
return !hasTdWithColspan;
});
// 헤더 데이터 추출
const headers = Array.from(headerRows[0].cells).map(cell => cell.textContent);
updateLoadingState(30);
// 바디 데이터 추출
const bodyData = normalBodyRows.map(row =>
Array.from(row.cells).map(cell => cell.textContent)
);
updateLoadingState(50);
// CSV 생성을 비동기로 처리
setTimeout(() => {
try {
// CSV 문자열 생성
let csvContent = '';
// 헤더 추가
csvContent += arrayToCSVRow(headers) + '\n';
updateLoadingState(70);
// 데이터 행들을 청크로 처리
const chunkSize = 1000;
let currentIndex = 0;
function processDataChunk() {
const end = Math.min(currentIndex + chunkSize, bodyData.length);
for (let i = currentIndex; i < end; i++) {
csvContent += arrayToCSVRow(bodyData[i]) + '\n';
}
currentIndex = end;
const progress = 70 + Math.floor((currentIndex / bodyData.length) * 20);
updateLoadingState(progress);
if (currentIndex < bodyData.length) {
// 아직 처리할 데이터가 남아있으면 다음 청크 처리 예약
setTimeout(processDataChunk, 0);
} else {
// 모든 데이터 처리 완료 후 다운로드
finishCSVDownload(csvContent);
}
}
function finishCSVDownload(csvContent) {
try {
updateLoadingState(95);
// BOM 추가 (한글 깨짐 방지)
const BOM = '\uFEFF';
const csvWithBOM = BOM + csvContent;
// Blob 생성 및 다운로드
const blob = new Blob([csvWithBOM], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
updateLoadingState(100);
resolve();
} catch (error) {
reject(error);
}
}
// 첫 번째 청크 처리 시작
processDataChunk();
} catch (error) {
reject(error);
}
}, 0);
} catch (error) {
reject(error);
}
});
};
const chunkArray = (array, chunkSize) => {
const chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
};
const downloadDataCSV = async () => {
return new Promise(async (resolve, reject) => {
try {
if (!data || data.length === 0) {
reject(new Error('다운로드할 데이터가 없습니다.'));
return;
}
updateLoadingState(5);
// 데이터 플랫 변환 과정을 더 작은 청크로 나누기
const dataChunkSize = 2000;
const dataChunks = chunkArray(data, dataChunkSize);
let flattenedData = [];
for (let i = 0; i < dataChunks.length; i++) {
await new Promise(resolve => {
setTimeout(() => {
// 청크 내 아이템들을 플랫하게 변환
const chunkData = dataChunks[i].map(item => {
// 기본 필드
const baseData = {
'logTime': item.logTime,
'GUID': item.userGuid === 'None' ? '' : item.userGuid,
'Nickname': item.userNickname === 'None' ? '' : item.userNickname,
'Account ID': item.accountId === 'None' ? '' : item.accountId,
'Action': item.action,
'Domain': item.domain === 'None' ? '' : item.domain,
'Tran ID': item.tranId
};
// Actor 데이터 플랫하게 추가
const actorData = item.header && item.header.Actor ?
flattenObject(item.header.Actor, 'Actor') : {};
// Infos 데이터 플랫하게 추가
let infosData = {};
if (item.body && item.body.Infos && Array.isArray(item.body.Infos)) {
item.body.Infos.forEach((info) => {
infosData = {
...infosData,
...flattenObject(info, `Info`)
};
});
}
return {
...baseData,
...actorData,
...infosData
};
});
flattenedData = [...flattenedData, ...chunkData];
const progress = 5 + Math.floor((i + 1) / dataChunks.length * 10);
updateLoadingState(progress);
resolve();
}, 0);
});
}
// 모든 항목의 모든 키 수집하여 헤더 생성
const allKeys = new Set();
// 헤더 수집도 청크로 나누기
for (let i = 0; i < flattenedData.length; i += dataChunkSize) {
await new Promise(resolve => {
setTimeout(() => {
const end = Math.min(i + dataChunkSize, flattenedData.length);
for (let j = i; j < end; j++) {
Object.keys(flattenedData[j]).forEach(key => allKeys.add(key));
}
const progress = 15 + Math.floor((i + dataChunkSize) / flattenedData.length * 5);
updateLoadingState(progress);
resolve();
}, 0);
});
}
const headers = Array.from(allKeys);
updateLoadingState(25);
// CSV 생성
let csvContent = '';
// 헤더 추가
csvContent += arrayToCSVRow(headers) + '\n';
updateLoadingState(30);
// 청크로 데이터 나누기
const chunkSize = 1000;
const rowChunks = chunkArray(flattenedData, chunkSize);
// 각 청크 처리
let processedCount = 0;
for (const chunk of rowChunks) {
await new Promise(resolve => {
setTimeout(() => {
// 각 청크의 데이터를 CSV 행으로 변환
chunk.forEach(item => {
const row = headers.map(header => {
const value = item[header] !== undefined ? item[header] : '';
return value;
});
csvContent += arrayToCSVRow(row) + '\n';
});
processedCount += chunk.length;
// 진행률 계산 및 콜백 호출
const newProgress = Math.min(90, Math.round((processedCount / flattenedData.length) * 60) + 30);
updateLoadingState(newProgress);
resolve();
}, 0);
});
// 메모리 정리를 위한 가비지 컬렉션 힌트
if (processedCount % (chunkSize * 5) === 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
updateLoadingState(95);
// 파일 다운로드
setTimeout(() => {
try {
// BOM 추가 (한글 깨짐 방지)
const BOM = '\uFEFF';
const csvWithBOM = BOM + csvContent;
// Blob 생성 및 다운로드
const blob = new Blob([csvWithBOM], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', fileName);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
updateLoadingState(100);
resolve();
} catch (error) {
reject(error);
}
}, 100);
} catch (error) {
reject(error);
}
});
};
const handleDownload = useCallback(async () => {
if (isDownloading) return; // 이미 다운로드 중이면 중복 실행 방지
setIsDownloading(true);
setLastProgress(0);
if (onLoadingChange) onLoadingChange({loading: true, progress: 0});
try {
if (tableRef) {
await downloadTableCSV();
} else if (data) {
await downloadDataCSV();
} else {
alert('유효한 데이터 소스가 없습니다.');
}
} catch (error) {
console.error('CSV download failed:', error);
alert('CSV 다운로드 중 오류가 발생했습니다.');
} finally {
// 다운로드 완료 후 짧은 지연 시간을 두어 100% 상태를 잠시 보여줌
setTimeout(() => {
setIsDownloading(false);
if (onLoadingChange) onLoadingChange({loading: false, progress: 100});
}, 500);
}
}, [tableRef, data, fileName, isDownloading, onLoadingChange]);
return (
<ExcelDownButton onClick={handleDownload} disabled={isDownloading}>
{isDownloading ? '다운로드 중...' : '엑셀 다운로드'}
</ExcelDownButton>
);
};
export default CSVDownloadButton;

View File

@@ -1,113 +0,0 @@
import * as XLSX from 'xlsx-js-style';
import { ExcelDownButton } from '../../../styles/ModuleComponents';
const ExcelDownloadButton = ({ tableRef, fileName = 'download.xlsx', sheetName = 'Sheet1' }) => {
const isNumeric = (value) => {
// 숫자 또는 숫자 문자열인지 확인
return !isNaN(value) && !isNaN(parseFloat(value));
};
const downloadExcel = () => {
try {
if (!tableRef.current) return;
const tableElement = tableRef.current;
const headerRows = tableElement.getElementsByTagName('thead')[0].getElementsByTagName('tr');
const bodyRows = tableElement.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
// 헤더 데이터 추출
const headers = Array.from(headerRows[0].cells).map(cell => cell.textContent);
// 바디 데이터 추출 및 숫자 타입 처리
const bodyData = Array.from(bodyRows).map(row =>
Array.from(row.cells).map(cell => {
const value = cell.textContent;
return isNumeric(value) ? parseFloat(value) : value;
})
);
// 워크북 생성
const wb = XLSX.utils.book_new();
// 테두리 스타일 정의
const borderStyle = {
style: "thin",
color: { rgb: "000000" }
};
// 스타일 정의
const centerStyle = {
font: {
name: "맑은 고딕",
sz: 11
},
alignment: {
horizontal: 'right',
vertical: 'right'
},
border: {
top: borderStyle,
bottom: borderStyle,
left: borderStyle,
right: borderStyle
}
};
const headerStyle = {
alignment: {
horizontal: 'center',
vertical: 'center'
},
fill: {
fgColor: { rgb: "d9e1f2" },
patternType: "solid"
}
};
// 데이터에 스타일 적용
const wsData = [
// 헤더 행
headers.map(h => ({
v: h,
s: headerStyle
})),
// 데이터 행들
...bodyData.map(row =>
row.map(cell => ({
v: cell,
s: centerStyle
}))
)
];
// 워크시트 생성
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 열 너비 설정 (최소 8, 최대 50)
ws['!cols'] = headers.map((_, index) => {
const maxLength = Math.max(
headers[index].length * 2,
...bodyData.map(row => String(row[index] || '').length * 1.2)
);
return { wch: Math.max(8, Math.min(50, maxLength)) };
});
// 워크시트를 워크북에 추가
XLSX.utils.book_append_sheet(wb, ws, sheetName);
// 엑셀 파일 다운로드
XLSX.writeFile(wb, fileName);
} catch (error) {
console.error('Excel download failed:', error);
alert('엑셀 다운로드 중 오류가 발생했습니다.');
}
};
return (
<ExcelDownButton onClick={downloadExcel}>
엑셀 다운로드
</ExcelDownButton>
);
};
export default ExcelDownloadButton;

View File

@@ -1,53 +0,0 @@
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
const DotsButton = styled.button`
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #f0f0f0;
border: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
&:hover {
background-color: #e0e0e0;
}
/* 점 스타일링 */
.dot {
width: 3px;
height: 3px;
border-radius: 50%;
background-color: #333;
margin: 2px 0;
}
`;
const VerticalDotsButton = ({ text, type = 'button', errorMessage, handleClick, size, width, height, borderColor, disabled, name }) => {
return (
<DotsButton
onSubmit={e => e.preventDefault()}
type={type}
disabled={disabled}
onClick={handleClick}
size={size}
bordercolor={borderColor}
width={width}
height={height}
name={name}
>
<div className="dot"></div>
<div className="dot"></div>
<div className="dot"></div>
</DotsButton>
);
};
export default VerticalDotsButton;

View File

@@ -1,57 +1,123 @@
import React from 'react'; import React from 'react';
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import styled from 'styled-components'; import styled, { keyframes } from 'styled-components';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
// 통합된 애니메이션 탭 컴포넌트 // 통합된 애니메이션 탭 컴포넌트
const AnimatedTabs = ({ items, activeKey, onChange }) => { const AnimatedTabs = ({ items, activeKey, onChange, tabPosition = 'center' }) => {
// 각 항목의 children을 애니메이션 래퍼로 감싸기
const tabItems = items.map(item => ({
key: item.key,
label: item.label,
children: (
<AnimatedContent key={`content-${item.key}`}>
{item.children}
</AnimatedContent>
)
// children: (
// <AnimatePresence mode="wait">
// <motion.div
// key={activeKey}
// initial={{ opacity: 0, x: 50 }}
// animate={{ opacity: 1, x: 0 }}
// exit={{ opacity: 0, x: -50 }}
// transition={{
// type: "spring",
// stiffness: 300,
// damping: 30
// }}
// >
// {item.children}
// </motion.div>
// </AnimatePresence>
// )
}));
return ( return (
<StyledTabs <StyledTabs
type="card"
activeKey={activeKey} activeKey={activeKey}
onChange={onChange} onChange={onChange}
centered={true} centered={tabPosition === 'center'}
> items={tabItems}
{items.map(item => ( />
<Tabs.TabPane
tab={item.label}
key={item.key}
>
<AnimatePresence mode="wait">
<motion.div
key={activeKey}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{
type: "spring",
stiffness: 300,
damping: 30
}}
>
{item.children}
</motion.div>
</AnimatePresence>
</Tabs.TabPane>
))}
</StyledTabs>
); );
}; };
const slideInRight = keyframes`
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
`;
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;
const AnimatedContent = styled.div`
animation: ${slideInRight} 0.3s ease-out;
/* 대안으로 더 부드러운 페이드 인 효과 */
/* animation: ${fadeIn} 0.4s ease-out; */
`;
// const AnimatedTabs = ({ items, activeKey, onChange }) => {
// return (
// <StyledTabs
// activeKey={activeKey}
// onChange={onChange}
// centered={true}
// >
// {items.map(item => (
// <Tabs.TabPane
// tab={item.label}
// key={item.key}
// >
// <AnimatePresence mode="wait">
// <motion.div
// key={activeKey}
// initial={{ opacity: 0, x: 50 }}
// animate={{ opacity: 1, x: 0 }}
// exit={{ opacity: 0, x: -50 }}
// transition={{
// type: "spring",
// stiffness: 300,
// damping: 30
// }}
// >
// {item.children}
// </motion.div>
// </AnimatePresence>
// </Tabs.TabPane>
// ))}
// </StyledTabs>
// );
// };
const StyledTabs = styled(Tabs)` const StyledTabs = styled(Tabs)`
margin-top: 20px; margin-top: 20px;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; //align-items: center;
.ant-tabs-nav { //.ant-tabs-nav {
margin-bottom: 16px; // margin-bottom: 16px;
width: 80%; // width: 80%;
} //}
.ant-tabs-nav-wrap {
justify-content: center;
}
.ant-tabs-tab { .ant-tabs-tab {
padding: 8px 16px; padding: 8px 16px;

View File

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

View File

@@ -1,231 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
const AIMessageInput = ({ onSendMessage }) => {
const [isOpen, setIsOpen] = useState(false);
const [message, setMessage] = useState('');
const [isSending, setIsSending] = useState(false);
const textareaRef = useRef(null);
const modalRef = useRef(null);
// 텍스트 영역 높이 자동 조절
useEffect(() => {
if (textareaRef.current && isOpen) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
}
}, [message, isOpen]);
// 모달 외부 클릭시 닫기
useEffect(() => {
const handleClickOutside = (event) => {
if (modalRef.current && !modalRef.current.contains(event.target)) {
closeModal();
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
// 모달 열기
const openModal = () => {
setIsOpen(true);
// 모달이 열린 후 텍스트 영역에 포커스
setTimeout(() => {
if (textareaRef.current) {
textareaRef.current.focus();
}
}, 100);
};
// 모달 닫기
const closeModal = () => {
setIsOpen(false);
setMessage('');
};
// 메시지 전송 처리
const handleSendMessage = () => {
if (message.trim() && !isSending) {
setIsSending(true);
// 메시지 전송 처리
if (onSendMessage) {
onSendMessage(message);
}
// 입력 초기화 및 상태 업데이트
setMessage('');
setIsSending(false);
// 모달 닫기
closeModal();
}
};
// 엔터 키 처리 (Shift+Enter 줄바꿈, Enter 전송)
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
return (
<>
{/* 메뉴 버튼 */}
<MenuButton onClick={openModal}>
<div className="dot"></div>
<div className="dot"></div>
<div className="dot"></div>
</MenuButton>
{/* 모달 오버레이 */}
<ModalOverlay isOpen={isOpen}>
<InputContainer ref={modalRef} isOpen={isOpen}>
<MessageInput
ref={textareaRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="메시지를 입력하세요..."
rows={1}
/>
<SendButton
onClick={handleSendMessage}
disabled={!message.trim() || isSending}
>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
</svg>
</SendButton>
</InputContainer>
</ModalOverlay>
</>
);
};
export default AIMessageInput;
const ModalOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: ${props => props.isOpen ? 'flex' : 'none'};
justify-content: center;
align-items: center;
z-index: 1000;
`;
// 메인 컨테이너
const InputContainer = styled.div`
width: 90%;
max-width: 600px;
border: 1px solid #e0e0e0;
border-radius: 12px;
background-color: #ffffff;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
position: relative;
animation: ${props => props.isOpen ? 'slideUp 0.3s ease-out' : 'none'};
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
// 메시지 입력 영역
const MessageInput = styled.textarea`
width: 100%;
min-height: 60px;
max-height: 200px;
padding: 16px 60px 16px 16px;
border: none;
outline: none;
resize: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-size: 16px;
line-height: 1.5;
background: transparent;
&::placeholder {
color: #9e9ea7;
}
`;
// 전송 버튼
const SendButton = styled.button`
position: absolute;
bottom: 12px;
right: 12px;
width: 38px;
height: 38px;
border-radius: 50%;
background-color: #5436DA;
color: white;
border: none;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: #4527D0;
}
&:disabled {
background-color: #DADCE0;
cursor: not-allowed;
}
svg {
width: 18px;
height: 18px;
fill: white;
}
`;
// 메뉴 버튼 (세로 점 세개)
const MenuButton = styled.button`
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #f0f0f0;
border: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: #e0e0e0;
}
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #666;
margin: 2px 0;
}
`;

View File

@@ -97,20 +97,20 @@ const LogDetailModal = ({ detailView,
const allChangedItems = []; const allChangedItems = [];
changedData.forEach((item, itemIndex) => { changedData.forEach((item, itemIndex) => {
if (item.changed && Array.isArray(item.changed)) { if (item.domain.changed && Array.isArray(item.domain.changed)) {
item.changed.forEach((changedItem) => { item.domain.changed.forEach((changedItem) => {
allChangedItems.push({ allChangedItems.push({
...changedItem, ...changedItem,
// 어떤 데이터 항목에서 온 것인지 구분하기 위한 정보 추가 // 어떤 데이터 항목에서 온 것인지 구분하기 위한 정보 추가
sourceIndex: itemIndex, sourceIndex: itemIndex,
sourceInfo: { sourceInfo: {
dbType: item.dbType, dbType: item.dbType,
timestamp: item.timestamp, logTime: item.logTime,
operationType: item.operationType, operationType: item.domain.operationType,
historyType: item.historyType, historyType: item.historyType,
tableName: item.tableName, tableName: item.tableName,
tranId: item.tranId, tranId: item.tranId,
userId: item.userId worker: item.worker
} }
}); });
}); });
@@ -164,8 +164,8 @@ const LogDetailModal = ({ detailView,
<td>{item.fieldName}</td> <td>{item.fieldName}</td>
<td>{formatValue(item.newValue)}</td> <td>{formatValue(item.newValue)}</td>
<td>{formatValue(item.oldValue)}</td> <td>{formatValue(item.oldValue)}</td>
<td>{item.sourceInfo.userId}</td> <td>{item.sourceInfo.worker}</td>
<td>{convertKTC(item.sourceInfo.timestamp, false)}</td> <td>{convertKTC(item.sourceInfo.logTime, false)}</td>
</tr> </tr>
) )
})} })}

View File

@@ -38,6 +38,8 @@ const ModalBg = styled(motion.div)`
min-width: 1080px; min-width: 1080px;
display: ${props => (props.$view === 'hidden' ? 'none' : 'block')}; display: ${props => (props.$view === 'hidden' ? 'none' : 'block')};
z-index: 20; z-index: 20;
overflow-y: auto;
overflow-x: auto;
`; `;
const ModalContainer = styled.div` const ModalContainer = styled.div`
@@ -52,9 +54,18 @@ const ModalWrapper = styled(motion.div)`
min-width: ${props => props.min || 'auto'}; min-width: ${props => props.min || 'auto'};
padding: ${props => props.$padding || '30px'}; padding: ${props => props.$padding || '30px'};
border-radius: 30px; border-radius: 30px;
max-height: 90%; max-height: calc(100vh - 40px);
overflow: auto; overflow: auto;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3); box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
/*모바일*/
@media (max-width: 768px) {
min-width: unset;
max-width: calc(100vw - 20px);
max-height: calc(100vh - 20px);
padding: ${props => props.$padding || '20px'};
border-radius: 20px;
}
`; `;
const Modal = ({ children, $padding, min, $view, $bgcolor }) => { const Modal = ({ children, $padding, min, $view, $bgcolor }) => {

View File

@@ -1,4 +1,4 @@
import React, { useState, Fragment, useEffect } from 'react'; import React, { useState, Fragment, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Button from '../common/button/Button'; import Button from '../common/button/Button';
@@ -18,7 +18,7 @@ import {
FormStatusWarning, FormStatusWarning,
FormButtonContainer, FormButtonContainer,
} from '../../styles/ModuleComponents'; } from '../../styles/ModuleComponents';
import { Modal, SingleDatePicker, SingleTimePicker } from '../common'; import { DetailLayout, Modal, SingleDatePicker, SingleTimePicker } from '../common';
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants'; import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
import { convertKTCDate } from '../../utils'; import { convertKTCDate } from '../../utils';
import { import {
@@ -64,16 +64,6 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
} }
}, [modalType, content]); }, [modalType, content]);
useEffect(() => {
if(modalType === TYPE_REGISTRY && configData?.length > 0){
setResultData(prev => ({
...prev,
round_count: configData[0].default_round_count,
round_time: configData[0].round_time
}));
}
}, [modalType, configData]);
useEffect(() => { useEffect(() => {
if (checkCondition()) { if (checkCondition()) {
setIsNullValue(false); setIsNullValue(false);
@@ -82,84 +72,12 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
} }
}, [resultData]); }, [resultData]);
// 시작 날짜 변경 핸들러 const opGameMode = useMemo(() => {
const handleStartDateChange = (date) => { return gameModeData?.map(item => ({
if (!date) return; value: item.id,
name: `${item.desc}(${item.id})`
const newDate = new Date(date); })) || [];
}, [gameModeData]);
if(resultData.repeat_type !== NONE && resultData.event_end_dt){
const endDate = new Date(resultData.event_end_dt);
const startDay = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
showToast('DATE_START_DIFF_END_WARNING', {type: alertTypes.warning});
return;
}
}
setResultData(prev => ({
...prev,
event_start_dt: newDate
}));
};
// 시작 시간 변경 핸들러
const handleStartTimeChange = (time) => {
if (!time) return;
const newDateTime = resultData.event_start_dt
? new Date(resultData.event_start_dt)
: new Date();
newDateTime.setHours(
time.getHours(),
time.getMinutes(),
0,
0
);
setResultData(prev => ({
...prev,
event_start_dt: newDateTime
}));
};
// 종료 날짜 변경 핸들러
const handleEndDateChange = (date) => {
if (!date || !resultData.event_start_dt) return;
const startDate = new Date(resultData.event_start_dt);
const endDate = new Date(date);
// 일자만 비교하기 위해 년/월/일만 추출
const startDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
if (endDay <= startDay) {
showToast('DATE_START_DIFF_END_WARNING', {type: alertTypes.warning});
return;
}
setResultData(prev => ({
...prev,
event_end_dt: endDate
}));
};
const handleConfigChange = (e) => {
const config = configData.find(data => String(data.id) === String(e.target.value));
if (config) {
setResultData({
...resultData,
config_id: config.id,
round_time: config.round_time
});
} else {
showToast('Config not found for value:', e.target.value, {type: alertTypes.warning});
}
}
const handleReset = () => { const handleReset = () => {
setDetailData({}); setDetailData({});
@@ -190,6 +108,18 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
return; return;
} }
//최소 진행시간
if(resultData.event_operation_time < 10){
showToast('BATTLE_EVENT_MODAL_OPERATION_TIME_MIN_CHECK_WARNING', {type: alertTypes.warning});
return;
}
//최대 진행시간
if(resultData.repeat_type !== 'NONE' && resultData.event_operation_time > 1400){
showToast('BATTLE_EVENT_MODAL_OPERATION_TIME_MAX_CHECK_WARNING', {type: alertTypes.warning});
return;
}
// if(resultData.round_time === 0){ // if(resultData.round_time === 0){
// const config = configData.find(data => data.id === resultData.config_id); // const config = configData.find(data => data.id === resultData.config_id);
// setResultData({ ...resultData, round_time: config.round_time }); // setResultData({ ...resultData, round_time: config.round_time });
@@ -202,9 +132,15 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
break; break;
case "registConfirm": case "registConfirm":
const params = {
...resultData,
event_operation_time: resultData.event_operation_time * 60
};
if(isView('modify')){ if(isView('modify')){
await withLoading( async () => { await withLoading( async () => {
return await BattleEventModify(token, content?.id, resultData); return await BattleEventModify(token, content?.id, params);
}).then(data => { }).then(data => {
if(data.result === "SUCCESS") { if(data.result === "SUCCESS") {
showToast('UPDATE_COMPLETED', {type: alertTypes.success}); showToast('UPDATE_COMPLETED', {type: alertTypes.success});
@@ -221,7 +157,7 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
} }
else{ else{
await withLoading( async () => { await withLoading( async () => {
return await BattleEventSingleRegist(token, resultData); return await BattleEventSingleRegist(token, params);
}).then(data => { }).then(data => {
if(data.result === "SUCCESS") { if(data.result === "SUCCESS") {
showToast('REGIST_COMPLTE', {type: alertTypes.success}); showToast('REGIST_COMPLTE', {type: alertTypes.success});
@@ -244,6 +180,7 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
return ( return (
resultData.event_start_dt !== '' resultData.event_start_dt !== ''
&& resultData.group_id !== '' && resultData.group_id !== ''
&& resultData.game_mode_id > 0
&& resultData.event_name !== '' && resultData.event_name !== ''
&& (resultData.repeat_type === 'NONE' || (resultData.repeat_type !== 'NONE' && resultData.event_end_dt !== '')) && (resultData.repeat_type === 'NONE' || (resultData.repeat_type !== 'NONE' && resultData.event_end_dt !== ''))
); );
@@ -265,117 +202,122 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
case "round": case "round":
case "hot": case "hot":
case "mode": case "mode":
case "operation_time":
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === battleEventStatusType.stop)); return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === battleEventStatusType.stop));
default: default:
return modalType === TYPE_MODIFY && (content?.status !== battleEventStatusType.stop); return modalType === TYPE_MODIFY && (content?.status !== battleEventStatusType.stop);
} }
} }
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 2,
type: 'text',
key: 'group_id',
label: '그룹 ID',
disabled: !isView('group'),
width: '150px',
},
{
row: 0,
col: 2,
colSpan: 2,
type: 'text',
key: 'event_name',
label: '이벤트명',
disabled: !isView('name'),
width: '250px',
},
{
row: 1,
col: 0,
colSpan: 2,
type: 'date',
key: 'event_start_dt',
label: '시작일시',
disabled: !isView('start_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '200px',
showTime: true
},
{
row: 1,
col: 2,
colSpan: 2,
type: 'number',
key: 'event_operation_time',
label: '진행시간(분)',
disabled: !isView('operation_time'),
width: '100px',
min: 10,
},
{
row: 3,
col: 0,
colSpan: 2,
type: 'select',
key: 'repeat_type',
label: '반복',
disabled: !isView('repeat'),
width: '150px',
options: battleRepeatType
},
...(resultData?.repeat_type !== 'NONE' ? [{
row: 3,
col: 2,
colSpan: 2,
type: 'date',
key: 'event_end_dt',
label: '종료일',
disabled: !isView('end_dt'),
format: 'YYYY-MM-DD',
width: '200px'
}] : []),
{
row: 4,
col: 0,
colSpan: 2,
type: 'select',
key: 'game_mode_id',
label: '게임 모드',
disabled: !isView('mode'),
width: '150px',
options: opGameMode
},
{
row: 4,
col: 2,
colSpan: 2,
type: 'select',
key: 'hot_time',
label: '핫타임',
disabled: !isView('hot'),
width: '150px',
options: battleEventHotTime.map(value => ({
value: value,
name: `${value}`
}))
},
]
}
];
return ( return (
<> <>
<Modal min="760px" $view={detailView}> <Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "전투시스템 이벤트 등록" : isView('modify') ? "전투시스템 이벤트 수정" : "전투시스템 이벤트 상세"}</Title> <Title $align="center">{isView('registry') ? "전투시스템 이벤트 등록" : isView('modify') ? "전투시스템 이벤트 수정" : "전투시스템 이벤트 상세"}</Title>
<MessageWrapper> <DetailLayout
<FormRowGroup> itemGroups={itemGroups}
<FormLabel>그룹 ID</FormLabel> formData={resultData}
<FormInput onChange={setResultData}
type="text" disabled={false}
disabled={!isView('group')} columnCount={4}
width='150px'
value={resultData?.group_id}
onChange={e => setResultData({ ...resultData, group_id: e.target.value })}
/> />
<FormLabel>이벤트명</FormLabel>
<FormInput
type="text"
disabled={!isView('name')}
width='300px'
value={resultData?.event_name}
onChange={e => setResultData({ ...resultData, event_name: e.target.value })}
/>
</FormRowGroup>
<FormRowGroup>
<SingleDatePicker
label="시작일자"
disabled={!isView('start_dt')}
dateLabel="시작 일자"
onDateChange={handleStartDateChange}
selectedDate={resultData?.event_start_dt}
/>
<SingleTimePicker
label="시작시간"
disabled={!isView('start_dt')}
selectedTime={resultData?.event_start_dt}
onTimeChange={handleStartTimeChange}
/>
</FormRowGroup>
<FormRowGroup>
<FormLabel>반복</FormLabel>
<SelectInput value={resultData?.repeat_type} onChange={e => setResultData({ ...resultData, repeat_type: e.target.value })} disabled={!isView('repeat')} width="150px">
{battleRepeatType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
{resultData?.repeat_type !== 'NONE' &&
<SingleDatePicker
label="종료일자"
disabled={!isView('end_dt')}
dateLabel="종료 일자"
onDateChange={handleEndDateChange}
selectedDate={resultData?.event_end_dt}
/>
}
</FormRowGroup>
<FormRowGroup>
{/*<FormLabel>라운드 시간</FormLabel>*/}
{/*<SelectInput value={resultData.config_id} onChange={handleConfigChange} disabled={!isView('config')} width="200px">*/}
{/* {configData && configData?.map((data, index) => (*/}
{/* <option key={index} value={data.id}>*/}
{/* {data.desc}({data.id})*/}
{/* </option>*/}
{/* ))}*/}
{/*</SelectInput>*/}
<FormLabel>라운드 </FormLabel>
<SelectInput value={resultData.round_count} onChange={e => setResultData({ ...resultData, round_count: e.target.value })} disabled={!isView('round')} width="100px">
{battleEventRoundCount.map((data, index) => (
<option key={index} value={data}>
{data}
</option>
))}
</SelectInput>
</FormRowGroup>
<FormRowGroup>
{/*<FormLabel>배정 포드</FormLabel>*/}
{/*<SelectInput value={resultData.reward_group_id} onChange={e => setResultData({ ...resultData, reward_group_id: e.target.value })} disabled={!isView('reward')} width="200px">*/}
{/* {rewardData && rewardData?.map((data, index) => (*/}
{/* <option key={index} value={data.group_id}>*/}
{/* {data.desc}({data.group_id})*/}
{/* </option>*/}
{/* ))}*/}
{/*</SelectInput>*/}
<FormLabel>게임 모드</FormLabel>
<SelectInput value={resultData.game_mode_id} onChange={e => setResultData({ ...resultData, game_mode_id: e.target.value })} disabled={!isView('mode')} width="200px">
{gameModeData && gameModeData?.map((data, index) => (
<option key={index} value={data.id}>
{data.desc}({data.id})
</option>
))}
</SelectInput>
<FormLabel>핫타임</FormLabel>
<SelectInput value={resultData.hot_time} onChange={e => setResultData({ ...resultData, hot_time: e.target.value })} disabled={!isView('hot')} width="100px">
{battleEventHotTime.map((data, index) => (
<option key={index} value={data}>
{data}
</option>
))}
</SelectInput>
</FormRowGroup>
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>} {!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
</MessageWrapper>
<BtnWrapper $gap="10px" $marginTop="10px"> <BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar> <FormStatusBar>
<FormStatusLabel> <FormStatusLabel>
@@ -432,9 +374,10 @@ export const initData = {
reward_group_id: 1, reward_group_id: 1,
round_count: 1, round_count: 1,
hot_time: 1, hot_time: 1,
game_mode_id: 1, game_mode_id: '',
event_start_dt: '', event_start_dt: '',
event_end_dt: '' event_end_dt: '',
event_operation_time: 10
} }
export default BattleEventModal; export default BattleEventModal;

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, Fragment } from 'react'; import { useState, useEffect, Fragment } from 'react';
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, Textarea, SearchBarAlert } from '../../styles/Components'; import { Input, Button as AntButton, Select, Alert, Space, Card, Row, Col } from 'antd';
import { Title, BtnWrapper } from '../../styles/Components';
import Button from '../common/button/Button'; import Button from '../common/button/Button';
import Modal from '../common/modal/Modal'; import Modal from '../common/modal/Modal';
import { EventIsItem, EventModify } from '../../apis'; import { EventIsItem, EventModify } from '../../apis';
@@ -10,16 +11,13 @@ import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { authType, benItems, commonStatus, currencyItemCode } from '../../assets/data'; import { authType, benItems, commonStatus, currencyItemCode } from '../../assets/data';
import { import {
AppendRegistBox, AppendRegistTable, AreaBtnClose, DetailRegistInfo, DetailState
BtnDelete, DetailInputItem, DetailInputRow,
DetailModalWrapper, RegistGroup, DetailRegistInfo, DetailState,
Item, ItemList, LangArea
} from '../../styles/ModuleComponents'; } from '../../styles/ModuleComponents';
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../utils'; import { convertKTC, timeDiffMinute, convertKTCDate } from '../../utils';
import DateTimeInput from '../common/input/DateTimeInput';
import { useLoading } from '../../context/LoadingProvider'; import { useLoading } from '../../context/LoadingProvider';
import { useAlert } from '../../context/AlertProvider'; import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types'; import { alertTypes } from '../../assets/data/types';
import { DetailLayout } from '../common';
const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => { const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => {
const userInfo = useRecoilValue(authList); const userInfo = useRecoilValue(authList);
@@ -31,21 +29,14 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
const id = content && content.id; const id = content && content.id;
const updateAuth = userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventUpdate); const updateAuth = userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventUpdate);
const [time, setTime] = useState({ const [activeLanguage, setActiveLanguage] = useState('KO');
start_hour: '00',
start_min: '00',
end_hour: '00',
end_min: '00',
}); //시간 정보
const [item, setItem] = useState(''); const [item, setItem] = useState('');
const [itemCount, setItemCount] = useState(''); const [itemCount, setItemCount] = useState(1);
const [resource, setResource] = useState('19010001'); const [resource, setResource] = useState('19010001');
const [resourceCount, setResourceCount] = useState(''); const [resourceCount, setResourceCount] = useState(1);
const [resultData, setResultData] = useState({}); const [resultData, setResultData] = useState({});
const [isNullValue, setIsNullValue] = useState(false);
// 과거 판단 // 과거 판단
const [isPast, setIsPast] = useState(false); const [isPast, setIsPast] = useState(false);
const [isChanged, setIsChanged] = useState(false); const [isChanged, setIsChanged] = useState(false);
@@ -65,13 +56,8 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
event_type: content.event_type, event_type: content.event_type,
mail_list: content.mail_list, mail_list: content.mail_list,
item_list: content.item_list, item_list: content.item_list,
}); status: content.status,
delete_desc: content.delete_desc
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); start_dt_KTC < (new Date) ? setIsPast(true) : setIsPast(false);
@@ -90,29 +76,97 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
} }
}, [updateAuth, isPast]); }, [updateAuth, isPast]);
useEffect(() => {
if (conditionCheck()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
useEffect(() => { useEffect(() => {
setItemCheckMsg(''); setItemCheckMsg('');
}, [item]); }, [item]);
// 아이템 수량 숫자 체크 const getLanguageTabItems = () => {
const handleItemCount = e => { return resultData.mail_list?.map(mail => ({
if (e.target.value === '0' || e.target.value === '-0') { key: mail.language,
setItemCount('1'); label: mail.language,
e.target.value = '1'; children: (
} else if (e.target.value < 0) { <div style={{ padding: '10px', minHeight: '400px', height: 'auto' }}>
let plusNum = Math.abs(e.target.value); <Row gutter={[16, 24]}>
setItemCount(plusNum); <Col span={24}>
} else { <div>
setItemCount(e.target.value); <label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
제목 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input
value={mail.title || ''}
placeholder="우편 제목을 입력하세요"
maxLength={30}
readOnly={isReadOnly}
onChange={(e) => updateMailData(mail.language, 'title', e.target.value.trimStart())}
showCount
size="large"
/>
</div>
</Col>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
내용 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input.TextArea
value={mail.content || ''}
placeholder="우편 내용을 입력하세요"
readOnly={isReadOnly}
rows={8}
maxLength={2000}
showCount
onChange={(e) => {
if (e.target.value.length > 2000) return;
updateMailData(mail.language, 'content', e.target.value.trimStart());
}}
style={{
resize: 'vertical',
minHeight: '200px',
maxHeight: '400px'
}}
/>
</div>
</Col>
</Row>
</div>
),
closable: resultData.mail_list?.length > 1 && !isReadOnly, // 마지막 하나가 아니고 읽기전용이 아닐 때만 삭제 가능
})) || [];
};
const updateMailData = (language, field, value) => {
const updatedMailList = resultData.mail_list.map(mail =>
mail.language === language
? { ...mail, [field]: value }
: mail
);
setResultData({ ...resultData, mail_list: updatedMailList });
setIsChanged(true);
};
const handleTabClose = (targetKey) => {
if (resultData.mail_list.length <= 1) return;
const filterList = resultData.mail_list.filter(el => el.language !== targetKey);
setResultData({ ...resultData, mail_list: filterList });
// 삭제된 탭이 현재 활성 탭이었다면 첫 번째 탭으로 변경
if (activeLanguage === targetKey) {
setActiveLanguage(filterList[0]?.language || 'KO');
} }
setIsChanged(true);
}; };
// 아이템 추가 // 아이템 추가
@@ -152,19 +206,6 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
setResultData({ ...resultData, item_list: filterList }); 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) => { const handleResourceList = (e) => {
if(resource.length === 0 || resourceCount.length === 0) return; if(resource.length === 0 || resourceCount.length === 0) return;
@@ -186,45 +227,9 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
setResourceCount(''); 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 = () => { const handleReset = () => {
setBtnValidation(false); setBtnValidation(false);
setIsNullValue(false);
setIsChanged(false); setIsChanged(false);
}; };
@@ -281,6 +286,197 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
} }
}; };
// 아이템 목록 렌더링 컴포넌트
const renderItemList = () => {
return (
<div>
{resultData.item_list && resultData.item_list.length > 0 && (
<Space wrap>
{resultData.item_list.map((data, index) => (
<Card
key={index}
title={data.item_name}
size="small"
extra={
!isReadOnly && (
<AntButton
type="text"
danger
size="small"
onClick={() => onItemRemove(index)}
>
X
</AntButton>
)
}
style={{ minWidth: '150px' }}
>
<div>
<div>{data.item}</div>
<div>수량: {data.item_cnt}</div>
</div>
</Card>
))}
</Space>
)}
</div>
);
};
// 아이템 추가 컴포넌트
const renderItemAdd = () => {
return (
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="Item Meta id 입력"
value={item}
onChange={(e) => setItem(e.target.value.trimStart())}
disabled={isReadOnly}
style={{ width: '200px' }}
/>
<Input
type="number"
placeholder="수량"
value={itemCount}
onChange={(e) => setItemCount(e.target.value)}
disabled={isReadOnly}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleItemList}
disabled={itemCount.length === 0 || item.length === 0 || isReadOnly}
>
추가
</AntButton>
</Space.Compact>
);
};
// 자원 추가 컴포넌트
const renderResourceAdd = () => {
return (
<Space.Compact style={{ width: '100%' }}>
<Select
value={resource}
onChange={setResource}
disabled={isReadOnly}
style={{ width: '200px' }}
placeholder="자원 선택"
>
{currencyItemCode.map((data, index) => (
<Select.Option key={index} value={data.value}>
{data.name}
</Select.Option>
))}
</Select>
<Input
type="number"
placeholder="수량"
value={resourceCount}
disabled={isReadOnly}
onChange={(e) => setResourceCount(e.target.value)}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleResourceList}
disabled={resourceCount.length === 0 || resource.length === 0 || isReadOnly}
>
추가
</AntButton>
</Space.Compact>
);
};
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 2,
type: 'dateRange',
keys: {
start: 'start_dt',
end: 'end_dt'
},
label: '이벤트 기간',
disabled: isReadOnly,
format: 'YYYY-MM-DD HH:mm',
showTime: true,
startLabel: '시작 일시',
endLabel: '종료 일시'
},
{
row: 0,
col: 2,
colSpan: 1,
type: 'custom',
key: 'status',
label: '이벤트 상태',
render: () => detailState(resultData.status)
},
...(resultData.status === commonStatus.delete ? [{
row: 0,
col: 3,
colSpan: 1,
type: 'display',
key: 'delete_desc',
label: '삭제 사유',
value: resultData.delete_desc || ''
}] : [{
row: 0,
col: 3,
colSpan: 1,
type: 'custom',
key: 'empty_space',
label: '',
render: () => <div></div>
}]),
{
row: 1,
col: 0,
colSpan: 4,
type: 'tab',
key: 'language_tabs',
tabItems: getLanguageTabItems(),
activeKey: activeLanguage,
onTabChange: setActiveLanguage,
onTabClose: handleTabClose
},
{
row: 2,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_add',
label: '아이템 추가',
render: renderItemAdd
},
{
row: 3,
col: 0,
colSpan: 4,
type: 'custom',
key: 'resource_add',
label: '자원 추가',
render: renderResourceAdd
},
{
row: 4,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_list',
render: renderItemList
}
]
}
];
return ( return (
<> <>
<Modal min="960px" $view={detailView}> <Modal min="960px" $view={detailView}>
@@ -297,201 +493,22 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
)} )}
</DetailRegistInfo> </DetailRegistInfo>
} }
<DetailModalWrapper>
{content &&
<RegistGroup>
<DetailInputRow>
<DateTimeInput
title="이벤트 기간"
dateName="시작 일자"
selectedDate={convertKTCDate(content.start_dt)}
handleSelectedDate={data => handleDateChange(data, 'start')}
onChange={e => handleTimeChange(e, 'start')}
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/> />
<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> {itemCheckMsg && (
} <Alert
{resultData.mail_list && message={itemCheckMsg}
resultData.mail_list.map(data => { type="error"
return ( style={{ marginTop: '8px', width: '300px' }}
<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}>
{currencyItemCode.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"> <BtnWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
<Button <Button
text="확인" text="확인"

View File

@@ -0,0 +1,311 @@
import React, { useState, Fragment, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../common/button/Button';
import {
Title,
BtnWrapper,
SearchBarAlert,
} from '../../styles/Components';
import {
FormStatusBar,
FormStatusLabel,
FormStatusWarning,
FormButtonContainer,
} from '../../styles/ModuleComponents';
import { DetailLayout, Modal} from '../common';
import { TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
import { convertKTCDate } from '../../utils';
import {
opCommonStatus,
} from '../../assets/data/options';
import { alertTypes, commonStatus } from '../../assets/data/types';
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import { EventModify, EventSingleRegist } from '../../apis';
const EventModal = ({ modalType, detailView, handleDetailView, content, setDetailData, eventActionData }) => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const { showToast, showModal } = useAlert();
const {withLoading} = useLoading();
const [isNullValue, setIsNullValue] = useState(false);
const [resultData, setResultData] = useState(initData);
useEffect(() => {
if(modalType === TYPE_MODIFY && content && Object.keys(content).length > 0){
setResultData({
id: content.id,
title: content.title,
global_event_action_id: content.global_event_action_id,
personal_event_action_id: content.personal_event_action_id,
status: content.status,
max_point: content.max_point,
start_dt: convertKTCDate(content.start_dt),
end_dt: convertKTCDate(content.end_dt)
});
}
}, [modalType, content]);
useEffect(() => {
if (checkCondition()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
const opEventActionMode = useMemo(() => {
return eventActionData?.map(item => ({
value: item.id,
name: `${item.description}(${item.id})`
})) || [];
}, [eventActionData]);
const handleReset = () => {
setDetailData({});
setResultData(initData);
handleDetailView();
}
const handleSubmit = async (type, param = null) => {
switch (type) {
case "submit":
if (!checkCondition()) return;
const minAllowedTime = new Date(new Date().getTime() + 10 * 60000);
const startDt = resultData.start_dt;
const endDt = resultData.end_dt;
// if (modalType === TYPE_REGISTRY && startDt < minAllowedTime) {
// showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning});
// return;
// }
// if(resultData.repeat_type !== 'NONE' && !isValidDayRange(startDt, endDt)) {
// showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning});
// return;
// }
//
// //화면에 머물면서 상태는 안바꼈을 경우가 있기에 시작시간 지났을경우 차단
// if (modalType === TYPE_REGISTRY && startDt < new Date()) {
// showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning});
// return;
// }
showModal(isView('modify') ? 'BATTLE_EVENT_UPDATE_CONFIRM' : 'BATTLE_EVENT_REGIST_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('registConfirm')
});
break;
case "registConfirm":
const params = {
...resultData
};
if(isView('modify')){
await withLoading( async () => {
return await EventModify(token, content?.id, params);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('UPDATE_COMPLETED', {type: alertTypes.success});
}else{
showToast('UPDATE_FAIL', {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
handleReset();
});
}
else{
await withLoading( async () => {
return await EventSingleRegist(token, params);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('REGIST_COMPLTE', {type: alertTypes.success});
}else{
showToast('REGIST_FAIL', {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
handleReset();
});
}
break;
}
}
const checkCondition = () => {
return (
resultData.start_dt !== ''
&& resultData.end_dt !== ''
&& resultData.title !== ''
&& resultData.global_event_action_id > 0
&& resultData.personal_event_action_id > 0
);
};
const isView = (label) => {
switch (label) {
case "modify":
return modalType === TYPE_MODIFY && (content?.status === commonStatus.wait);
case "registry":
case "mode":
return modalType === TYPE_REGISTRY
case "start_dt":
case "end_dt":
case "max_point":
case "name":
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === commonStatus.wait));
default:
return modalType === TYPE_MODIFY && (content?.status !== commonStatus.wait);
}
}
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 2,
type: 'text',
key: 'title',
label: '이벤트명',
disabled: !isView('name'),
width: '300px',
},
{
row: 1,
col: 0,
colSpan: 2,
type: 'date',
key: 'start_dt',
label: '시작일시',
disabled: !isView('start_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '200px',
showTime: true
},
{
row: 1,
col: 2,
colSpan: 2,
type: 'date',
key: 'end_dt',
label: '종료일시',
disabled: !isView('end_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '200px',
showTime: true
},
{
row: 4,
col: 0,
colSpan: 2,
type: 'select',
key: 'global_event_action_id',
label: '기여도 이벤트 모드',
disabled: !isView('mode'),
width: '150px',
options: opEventActionMode
},
{
row: 4,
col: 2,
colSpan: 2,
type: 'number',
key: 'max_point',
label: '기여도 목표점수',
disabled: !isView('max_point'),
width: '150px'
},
{
row: 5,
col: 0,
colSpan: 2,
type: 'select',
key: 'personal_event_action_id',
label: '개인제작 이벤트 모드',
disabled: !isView('mode'),
width: '150px',
options: opEventActionMode
},
]
}
];
return (
<>
<Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "통합 이벤트 등록" : isView('modify') ? "통합 이벤트 수정" : "통합 이벤트 상세"}</Title>
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
<BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar>
<FormStatusLabel>
현재상태: {opCommonStatus.find(data => data.value === content?.status)?.name || "등록"}
</FormStatusLabel>
<FormStatusWarning>
{isView('registry') ? '' : t('EVENT_MODAL_STATUS_WARNING')}
</FormStatusWarning>
</FormStatusBar>
<FormButtonContainer $gap="5px">
{isView() ?
<Button
text="확인"
name="확인버튼"
theme="line"
handleClick={() => handleReset()}
/>
:
<>
<Button
text="취소"
theme="line"
handleClick={() => showModal('CANCEL_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleReset()
})}
/>
<Button
type="submit"
text={isView('modify') ? "수정" : "등록"}
name="등록버튼"
theme={
checkCondition()
? 'primary'
: 'disable'
}
handleClick={() => handleSubmit('submit')}
/>
</>
}
</FormButtonContainer>
</BtnWrapper>
</Modal>
</>
);
};
export const initData = {
title: '',
start_dt: '',
end_dt: '',
global_event_action_id: '',
personal_event_action_id: '',
max_point: 0
}
export default EventModal;

View File

@@ -1,4 +1,4 @@
import { useState, Fragment, useEffect } from 'react'; import React, { useState, Fragment, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Button from '../common/button/Button'; import Button from '../common/button/Button';
import Loading from '../common/Loading'; import Loading from '../common/Loading';
@@ -21,7 +21,7 @@ import {
NoticeInputItem2, BoxWrapper, FormStatusBar, FormStatusLabel, FormStatusWarning, FormButtonContainer, NoticeInputItem2, BoxWrapper, FormStatusBar, FormStatusLabel, FormStatusWarning, FormButtonContainer,
} from '../../styles/ModuleComponents'; } from '../../styles/ModuleComponents';
import { modalTypes } from '../../assets/data'; import { modalTypes } from '../../assets/data';
import {DynamicModal, Modal, DateTimeRangePicker} from '../common'; import { DynamicModal, Modal, DateTimeRangePicker, DetailLayout } from '../common';
import { LandAuctionModify, LandAuctionSingleRegist } from '../../apis'; import { LandAuctionModify, LandAuctionSingleRegist } from '../../apis';
import { import {
AUCTION_MIN_MINUTE_TIME, AUCTION_MIN_MINUTE_TIME,
@@ -36,6 +36,7 @@ import { convertKTCDate } from '../../utils';
import { msToMinutes } from '../../utils/date'; import { msToMinutes } from '../../utils/date';
import { useAlert } from '../../context/AlertProvider'; import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types'; import { alertTypes } from '../../assets/data/types';
import { battleEventHotTime, battleRepeatType } from '../../assets/data/options';
const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, setDetailData, landData, buildingData }) => { const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, setDetailData, landData, buildingData }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -62,10 +63,8 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
currency_type: content.currency_type, currency_type: content.currency_type,
start_price: content.start_price, start_price: content.start_price,
resv_start_dt: convertKTCDate(content.resv_start_dt), resv_start_dt: convertKTCDate(content.resv_start_dt),
resv_end_dt: convertKTCDate(content.resv_end_dt),
auction_start_dt: convertKTCDate(content.auction_start_dt), auction_start_dt: convertKTCDate(content.auction_start_dt),
auction_end_dt: convertKTCDate(content.auction_end_dt), auction_end_dt: convertKTCDate(content.auction_end_dt),
message_list: content.message_list,
}); });
const land = landData.find(land => land.id === parseInt(content.land_id)); const land = landData.find(land => land.id === parseInt(content.land_id));
setSelectLand(land); setSelectLand(land);
@@ -86,76 +85,26 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
} }
}, [resetDateTime]); }, [resetDateTime]);
// 입력 수량 처리 const opLand = useMemo(() => {
const handleCount = e => { return landData?.map(item => ({
const regex = /^\d*\.?\d{0,2}$/; value: item.id,
if (!regex.test(e.target.value) && e.target.value !== '-') { name: `${item.name}(${item.id})`
return; })) || [];
} }, [landData]);
let count = 0; const handleLand = (value, key, currentFormData) => {
if (e.target.value === '-0') { let land_id = value;
count = 1;
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
count = plusNum;
} else{
count = e.target.value;
}
setResultData((prevState) => ({
...prevState,
start_price: count,
}));
};
const handleReservationChange = {
start: (date) => {
setResultData(prev => ({ ...prev, resv_start_dt: date }));
},
end: (date) => {
setResultData(prev => ({ ...prev, resv_end_dt: date }));
}
};
const handleAuctionChange = {
start: (date) => {
setResultData(prev => ({ ...prev, auction_start_dt: date }));
},
end: (date) => {
setResultData(prev => ({ ...prev, auction_end_dt: date }));
}
};
// 입력 글자 제한
const handleInputData = e => {
if (e.target.value.length > 250) {
return;
}
const updatedMessages = resultData.message_list.map(msg =>
msg.language === message_lang
? { ...msg, content: e.target.value.trimStart() }
: msg
);
setResultData(prev => ({
...prev,
message_list: updatedMessages
}));
};
// 언어 선택
const handleLanguage = e => {
setMessage_lang(e.target.value);
if(!resultData.message_list.some(({language}) => language === e.target.value))
setResultData({ ...resultData, message_list: [...resultData.message_list, {language: e.target.value, content: ''}] })
}
const handleLand = e => {
const land_id = e.target.value;
const land = landData.find(land => land.id === parseInt(land_id)); const land = landData.find(land => land.id === parseInt(land_id));
const instance = buildingData.find(building => building.id === parseInt(land.buildingId))?.socket; const instance = buildingData.find(building => building.id === parseInt(land.buildingId))?.socket;
setSelectLand(land); setSelectLand(land);
setResultData({ ...resultData, land_id: land_id, land_name: land.name, land_size: land.size, land_socket: instance }); const updatedData = {
...currentFormData,
land_name: land.name,
land_size: land.size,
land_socket: instance
};
setResultData(updatedData);
} }
const handleReset = () => { const handleReset = () => {
@@ -256,13 +205,12 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
&& resultData.resv_start_dt !== '' && resultData.resv_start_dt !== ''
&& resultData.resv_end_dt !== '' && resultData.resv_end_dt !== ''
&& resultData.land_id !== '' && resultData.land_id !== ''
// && resultData.message_list?.every(data => data.content !== '')
); );
}; };
const isView = (label) => { const isView = (label) => {
switch (label) { switch (label) {
case "recv": case "resv":
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY && content?.status === landAuctionStatusType.wait); return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY && content?.status === landAuctionStatusType.wait);
case "auction": case "auction":
case "price": case "price":
@@ -282,131 +230,117 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
} }
} }
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 4,
type: 'select',
key: 'land_id',
label: '랜드선택',
disabled: !isView('registry'),
width: '400px',
options: opLand,
handler: handleLand
},
{
row: 1,
col: 0,
colSpan: 4,
type: 'display',
key: 'land_name',
label: '랜드 이름',
width: '400px',
placeholder: '랜드를 선택하세요'
},
{
row: 2,
col: 0,
colSpan: 2,
type: 'display',
key: 'land_size',
label: '랜드 크기',
placeholder: '랜드를 선택하세요',
width: '200px'
},
{
row: 2,
col: 2,
colSpan: 2,
type: 'display',
key: 'land_socket',
label: '인스턴스 수',
placeholder: '랜드를 선택하세요',
width: '200px',
},
{
row: 3,
col: 0,
colSpan: 2,
type: 'select',
key: 'currency_type',
label: '입찰재화',
disabled: true,
width: '200px',
options: CurrencyType
},
{
row: 3,
col: 2,
colSpan: 2,
type: 'number',
key: 'start_price',
label: '입찰시작가',
disabled: !isView('price'),
width: '200px',
min: 0,
step: 0.01
},
{
row: 4,
col: 0,
colSpan: 4,
type: 'date',
key: 'resv_start_dt',
label: '예약시작일',
disabled: !isView('resv'),
width: '200px',
format: 'YYYY-MM-DD HH:mm',
showTime: true
},
{
row: 5,
col: 0,
colSpan: 4,
type: 'dateRange',
keys: {
start: 'auction_start_dt',
end: 'auction_end_dt'
},
label: '경매기간',
disabled: !isView('auction'),
width: '400px',
format: 'YYYY-MM-DD HH:mm',
showTime: true
}
]
}
];
return ( return (
<> <>
<Modal min="760px" $view={detailView}> <Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "랜드 경매 등록" : isView('modify') ? "랜드 경매 수정" : "랜드 경매 상세"}</Title> <Title $align="center">{isView('registry') ? "랜드 경매 등록" : isView('modify') ? "랜드 경매 수정" : "랜드 경매 상세"}</Title>
<MessageWrapper> <DetailLayout
<FormRowGroup> itemGroups={itemGroups}
<FormLabel>랜드선택</FormLabel> formData={resultData}
<SelectInput value={resultData.land_id} onChange={e => handleLand(e)} disabled={!isView('registry')} width="400px"> onChange={setResultData}
{landData && landData.map((data, index) => ( disabled={false}
<option key={index} value={data.id}> columnCount={4}
{data.name}({data.id})
</option>
))}
</SelectInput>
</FormRowGroup>
<FormRowGroup>
<FormLabel>랜드 이름</FormLabel>
<FormInput
type="text"
disabled={true}
width='400px'
value={resultData?.land_name}
/> />
</FormRowGroup>
<FormRowGroup>
<FormLabel>랜드 크기</FormLabel>
<FormInput
type="text"
disabled={true}
width='200px'
value={resultData?.land_size}
/>
<FormLabel>인스턴스 </FormLabel>
<FormInput
type="text"
disabled={true}
width='200px'
value={resultData?.land_socket}
/>
</FormRowGroup>
<FormRowGroup>
<FormLabel>입찰 재화</FormLabel>
<SelectInput value={resultData.currency_type} width='200px' disabled={true} >
{CurrencyType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<FormLabel>입찰시작가</FormLabel>
<FormInput
type="number"
name="price"
value={resultData.start_price}
step={"0.01"}
min={0}
width='200px'
disabled={!isView('price')}
onChange={e => handleCount(e)}
/>
</FormRowGroup>
<DateTimeRangePicker
label="예약기간"
startDate={resultData.resv_start_dt}
endDate={resultData.resv_end_dt}
onStartDateChange={handleReservationChange.start}
onEndDateChange={handleReservationChange.end}
pastDate={new Date()}
disabled={!isView('recv')}
startLabel="시작 일자"
endLabel="종료 일자"
reset={resetDateTime}
/>
<DateTimeRangePicker
label="경매기간"
startDate={resultData.auction_start_dt}
endDate={resultData.auction_end_dt}
onStartDateChange={handleAuctionChange.start}
onEndDateChange={handleAuctionChange.end}
pastDate={new Date()}
disabled={!isView('auction')}
startLabel="시작 일자"
endLabel="종료 일자"
reset={resetDateTime}
/>
{/*<NoticeInputRow2>*/}
{/* <InputLabel>*/}
{/* 메세지 작성[경매 시작 5분전 공지 - 미구현]*/}
{/* </InputLabel>*/}
{/* <NoticeInputItem2>*/}
{/* <InputLabel>언어</InputLabel>*/}
{/* <SelectInput onChange={e => handleLanguage(e) } value={message_lang}>*/}
{/* {languageType.map((data, index) => (*/}
{/* <option key={index} value={data.value}>*/}
{/* {data.name}*/}
{/* </option>*/}
{/* ))}*/}
{/* </SelectInput>*/}
{/* </NoticeInputItem2>*/}
{/*</NoticeInputRow2>*/}
{/*<BoxWrapper>*/}
{/* {resultData.message_list.map(content => {*/}
{/* return (*/}
{/* <Fragment key={content.language}>*/}
{/* {message_lang === content.language && (*/}
{/* <FormTextAreaWrapper>*/}
{/* <FormTextArea*/}
{/* name="content"*/}
{/* id={content.language}*/}
{/* value={content.content}*/}
{/* onChange={e => handleInputData(e)}*/}
{/* maxLength={250}*/}
{/* disabled={!isView('message')}*/}
{/* />*/}
{/* </FormTextAreaWrapper>*/}
{/* )}*/}
{/* </Fragment>*/}
{/* );*/}
{/* })}*/}
{/*</BoxWrapper>*/}
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>} {!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
</MessageWrapper>
<BtnWrapper $gap="10px" $marginTop="10px"> <BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar> <FormStatusBar>
<FormStatusLabel> <FormStatusLabel>
@@ -463,14 +397,8 @@ export const initData = {
currency_type: 'Calium', currency_type: 'Calium',
start_price: 0, start_price: 0,
resv_start_dt: '', resv_start_dt: '',
resv_end_dt: '',
auction_start_dt: '', auction_start_dt: '',
auction_end_dt: '', auction_end_dt: ''
message_list: [
{ language: 'KO', content: '' },
{ language: 'EN', content: '' },
{ language: 'JA', content: '' },
],
} }
export const initLandData = { export const initLandData = {

View File

@@ -2,6 +2,21 @@ import { styled } from 'styled-components';
import RadioInput from '../common/input/Radio'; import RadioInput from '../common/input/Radio';
import React, { useState, useEffect, Fragment } from 'react'; import React, { useState, useEffect, Fragment } from 'react';
import CheckBox from '../common/input/CheckBox'; import CheckBox from '../common/input/CheckBox';
import {
Input,
Button as AntButton,
Select,
Alert,
Space,
Card,
Row,
Col,
Checkbox,
Radio,
DatePicker,
TimePicker
} from 'antd';
import dayjs from 'dayjs';
import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, DatePickerWrapper, Textarea} from '../../styles/Components'; import { Title, SelectInput, BtnWrapper, TextInput, Label, InputLabel, DatePickerWrapper, Textarea} from '../../styles/Components';
import Button from '../common/button/Button'; import Button from '../common/button/Button';
@@ -32,6 +47,7 @@ import { useLoading } from '../../context/LoadingProvider';
import { alertTypes, currencyCodeTypes } from '../../assets/data/types'; import { alertTypes, currencyCodeTypes } from '../../assets/data/types';
import { userType2 } from '../../assets/data/options'; import { userType2 } from '../../assets/data/options';
import { STORAGE_MAIL_COPY } from '../../assets/data/adminConstants'; import { STORAGE_MAIL_COPY } from '../../assets/data/adminConstants';
import { DetailLayout } from '../common';
const MailDetailModal = ({ detailView, handleDetailView, content }) => { const MailDetailModal = ({ detailView, handleDetailView, content }) => {
const userInfo = useRecoilValue(authList); const userInfo = useRecoilValue(authList);
@@ -46,10 +62,11 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
const [sendHour, setSendHour] = useState('00'); const [sendHour, setSendHour] = useState('00');
const [sendMin, setSendMin] = useState('00'); const [sendMin, setSendMin] = useState('00');
const [activeLanguage, setActiveLanguage] = useState('KO');
const [item, setItem] = useState(''); const [item, setItem] = useState('');
const [itemCount, setItemCount] = useState(''); const [itemCount, setItemCount] = useState(1);
const [resource, setResource] = useState(currencyCodeTypes.gold); const [resource, setResource] = useState(currencyCodeTypes.gold);
const [resourceCount, setResourceCount] = useState(''); const [resourceCount, setResourceCount] = useState(1);
const [resultData, setResultData] = useState({ const [resultData, setResultData] = useState({
is_reserve: false, is_reserve: false,
@@ -94,6 +111,7 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
mail_list: content.mail_list, mail_list: content.mail_list,
item_list: content.item_list, item_list: content.item_list,
guid: content.target, guid: content.target,
send_status: content.send_status,
file_name: content.receive_type === 'MULTIPLE' ? content.target : null file_name: content.receive_type === 'MULTIPLE' ? content.target : null
}); });
@@ -122,6 +140,136 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
} }
},[updateAuth, content, resultData]) },[updateAuth, content, resultData])
// 발송 상태 렌더링
const renderSendStatus = () => {
const status = resultData.send_status;
let color = '';
let text = '';
switch (status) {
case 'WAIT':
color = '#FAAD14';
text = '대기';
break;
case 'FINISH':
color = '#52c41a';
text = '완료';
break;
case 'FAIL':
color = '#ff4d4f';
text = '실패';
break;
default:
color = '#d9d9d9';
text = status || '알 수 없음';
}
return (
<span style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: '4px',
backgroundColor: color,
color: 'white',
fontSize: '14px',
fontWeight: '500'
}}>
{text}
</span>
);
};
// 탭 항목 생성
const getLanguageTabItems = () => {
return resultData.mail_list?.map(mail => ({
key: mail.language,
label: mail.language,
children: (
<div style={{ padding: '10px', minHeight: '400px', height: 'auto' }}>
<Row gutter={[16, 24]}>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
제목 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input
value={mail.title || ''}
placeholder="우편 제목을 입력하세요"
maxLength={30}
readOnly={isView}
onChange={(e) => updateMailData(mail.language, 'title', e.target.value.trimStart())}
showCount
size="large"
/>
</div>
</Col>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
내용 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input.TextArea
value={mail.content || ''}
placeholder="우편 내용을 입력하세요"
readOnly={isView}
rows={6}
maxLength={2000}
showCount
onChange={(e) => {
if (e.target.value.length > 2000) return;
updateMailData(mail.language, 'content', e.target.value.trimStart());
}}
style={{
resize: 'vertical',
minHeight: '200px',
maxHeight: '400px'
}}
/>
</div>
</Col>
</Row>
</div>
),
closable: resultData.mail_list?.length > 1 && !isView,
})) || [];
};
// 메일 데이터 업데이트
const updateMailData = (language, field, value) => {
const updatedMailList = resultData.mail_list.map(mail =>
mail.language === language
? { ...mail, [field]: value }
: mail
);
setResultData({ ...resultData, mail_list: updatedMailList });
setIsChanged(true);
};
// 탭 삭제 핸들러
const handleTabClose = (targetKey) => {
if (resultData.mail_list.length <= 1) return;
const filterList = resultData.mail_list.filter(el => el.language !== targetKey);
setResultData({ ...resultData, mail_list: filterList });
if (activeLanguage === targetKey) {
setActiveLanguage(filterList[0]?.language || 'KO');
}
setIsChanged(true);
};
// 아이템 수량 숫자 체크 // 아이템 수량 숫자 체크
const handleItemCount = e => { const handleItemCount = e => {
if (e.target.value === '0' || e.target.value === '-0') { if (e.target.value === '0' || e.target.value === '-0') {
@@ -338,12 +486,282 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
} }
} }
const handleDateTimeChange = (datetime) => {
setResultData({ ...resultData, send_dt: datetime.toDate() });
setIsChanged(true);
};
// 아이템 목록 렌더링
const renderItemList = () => {
return (
<div>
{resultData.item_list && resultData.item_list.length > 0 ? (
<Space wrap>
{resultData.item_list.map((data, index) => (
<Card
key={index}
title={data.item_name}
size="small"
extra={
!isView && (
<AntButton
type="text"
danger
size="small"
onClick={() => onItemRemove(index)}
>
X
</AntButton>
)
}
style={{ minWidth: '150px' }}
>
<div>
<div>{data.item}</div>
<div>수량: {data.item_cnt}</div>
</div>
</Card>
))}
</Space>
) : (
<Alert message="등록된 아이템이 없습니다." type="info" showIcon />
)}
</div>
);
};
// 아이템 추가 컴포넌트
const renderItemAdd = () => {
return (
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="Item Meta id 입력"
value={item}
onChange={(e) => setItem(e.target.value.trimStart())}
disabled={isView}
style={{ width: '200px' }}
/>
<Input
type="number"
placeholder="수량"
value={itemCount}
onChange={(e) => setItemCount(e.target.value)}
disabled={isView}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleItemList}
disabled={itemCount.length === 0 || item.length === 0 || isView}
>
추가
</AntButton>
</Space.Compact>
);
};
// 자원 추가 컴포넌트
const renderResourceAdd = () => {
return (
<div>
<Space.Compact style={{ width: '100%', marginBottom: '8px' }}>
<Select
value={resource}
onChange={setResource}
disabled={isView}
style={{ width: '200px' }}
placeholder="자원 선택"
>
{currencyItemCode.map((data, index) => (
<Select.Option key={index} value={data.value}>
{data.name}
</Select.Option>
))}
</Select>
<Input
type="number"
placeholder="수량"
value={resourceCount}
disabled={isView}
onChange={(e) => setResourceCount(e.target.value)}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleResourceList}
disabled={resourceCount.length === 0 || resource.length === 0 || isView}
>
추가
</AntButton>
</Space.Compact>
{resource === currencyCodeTypes.calium && (
<div style={{ fontSize: '12px', color: '#666' }}>
잔여 수량: {caliumTotalData}
</div>
)}
</div>
);
};
// 수신대상 렌더링
const renderReceiver = () => {
return (
<div>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Radio.Group value={resultData.receive_type} disabled>
<Space direction="vertical">
<Radio value="SINGLE">
단일: {resultData.receive_type === 'SINGLE' ? resultData.guid : ''}
</Radio>
<Radio value="MULTIPLE">
복수: {resultData.receive_type === 'MULTIPLE' ? excelFile : ''}
</Radio>
</Space>
</Radio.Group>
</div>
</Space>
</div>
);
};
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 1,
type: 'custom',
key: 'is_reserve',
render: () => (
<Checkbox
checked={resultData.is_reserve}
disabled
onChange={(e) => {
setResultData({ ...resultData, is_reserve: e.target.checked });
setIsChanged(true);
}}
>
예약 발송
</Checkbox>
)
},
...(resultData.is_reserve ? [{
row: 0,
col: 1,
colSpan: 2,
type: 'custom',
key: 'send_dt',
label: '발송 시간',
render: () => (
<DatePicker
showTime
allowClear={false}
value={resultData.send_dt ? dayjs(resultData.send_dt) : null}
onChange={handleDateTimeChange}
disabled={!content?.is_reserve || isView}
format="YYYY-MM-DD HH:mm"
style={{ width: '200px' }}
disabledDate={(current) => current && current < dayjs().startOf('day')}
/>
)
}] : []),
{
row: 1,
col: 0,
colSpan: 1,
type: 'select',
key: 'mail_type',
label: '우편 타입',
value: resultData.mail_type,
disabled: isView,
options: [
{ value: 'SELECT', name: '타입 선택' },
...mailType.filter(data => data.value !== 'ALL')
],
handler: (value) => {
setResultData({ ...resultData, mail_type: value });
setIsChanged(true);
}
},
{
row: 1,
col: 2,
colSpan: 1,
type: 'custom',
key: 'send_status',
label: '발송상태',
render: renderSendStatus
},
{
row: 2,
col: 1,
colSpan: 1,
type: 'select',
key: 'user_type',
label: '수신대상 타입',
value: resultData.user_type,
disabled: true,
options: userType2
},
{
row: 2,
col: 2,
colSpan: 2,
type: 'custom',
key: 'receiver',
label: '수신대상',
render: renderReceiver
},
{
row: 3,
col: 0,
colSpan: 4,
type: 'tab',
key: 'language_tabs',
tabItems: getLanguageTabItems(),
activeKey: activeLanguage,
onTabChange: setActiveLanguage,
onTabClose: handleTabClose
},
{
row: 4,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_add',
label: '아이템 추가',
render: renderItemAdd
},
{
row: 5,
col: 0,
colSpan: 4,
type: 'custom',
key: 'resource_add',
label: '자원 추가',
render: renderResourceAdd
},
{
row: 6,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_list',
render: renderItemList
}
]
}
];
return ( return (
<> <>
<Modal min="960px" $view={detailView}> <Modal min="960px" $view={detailView}>
<Title $align="center">우편 상세 정보</Title> <Title $align="center">우편 상세 정보</Title>
{content && <> {content && <RegistInfo>
<RegistInfo>
<span>등록자 : {content.create_by}</span> <span>등록자 : {content.create_by}</span>
<span>등록일 : {convertKTC(content.create_dt, false)}</span> <span>등록일 : {convertKTC(content.create_dt, false)}</span>
{typeof content.update_by !== 'undefined' && ( {typeof content.update_by !== 'undefined' && (
@@ -352,321 +770,14 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
<span>수정일 : {convertKTC(content.update_dt, false)}</span> <span>수정일 : {convertKTC(content.update_dt, false)}</span>
</> </>
)} )}
</RegistInfo> </RegistInfo>}
<ModalWrapper> <DetailLayout
<RegistGroup> itemGroups={itemGroups}
<InputRow> formData={resultData}
<CheckBox onChange={setResultData}
label="예약 발송" disabled={false}
id="reserve" columnCount={4}
checked={resultData && resultData.is_reserve}
setData={e => {
setResultData({ ...resultData, is_reserve: e.target.checked });
setIsChanged(true);
}}
disabled={(content.is_reserve === false) || isView}
/> />
{content.is_reserve === false ? (
<></>
) : (
resultData.is_reserve === true && (
<InputItem>
<InputLabel>발송 시간</InputLabel>
<InputGroup>
<DatePickerWrapper>
<DatePickerComponent
readOnly={(content.is_reserve === false) || isView}
name={initialData.send_dt}
selectedDate={resultData ? resultData.send_dt : initialData.send_dt}
handleSelectedDate={data => handleSelectedDate(data)}
pastDate={new Date()}
/>
</DatePickerWrapper>
<SelectInput
onChange={e => handleSendTime(e)}
id="hour"
disabled={(content.is_reserve === false) || isView}
value={
resultData && String(new Date(resultData.send_dt).getHours()) < 10
? '0' + String(new Date(resultData.send_dt).getHours())
: resultData && String(new Date(resultData.send_dt).getHours())
}>
{HourList.map(hour => (
<option value={hour} key={hour}>
{hour}
</option>
))}
</SelectInput>
<SelectInput
onChange={e => {
handleSendTime(e);
setIsChanged(true);
}}
id="min"
disabled={(content.is_reserve === false) || isView}
value={
resultData && String(new Date(resultData.send_dt).getMinutes()) < 10
? '0' + String(new Date(resultData.send_dt).getMinutes())
: resultData && String(new Date(resultData.send_dt).getMinutes())
}>
{MinuteList.map(min => (
<option value={min} key={min}>
{min}
</option>
))}
</SelectInput>
</InputGroup>
</InputItem>
)
)}
<InputItem>
<InputLabel>우편 타입</InputLabel>
<SelectInput
onChange={e => {
setResultData({ ...resultData, mail_type: e.target.value });
setIsChanged(true);
}}
value={resultData.mail_type}
disabled={isView}>
<option value="SELECT">타입 선택</option>
{mailType.filter(data => data.value !== 'ALL').map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</InputItem>
<InputItem>
<InputLabel>발송상태</InputLabel>
<div>
{initialData.send_status === 'WAIT' && <MailState>대기</MailState>}
{initialData.send_status === 'FINISH' && <MailState result="success">완료</MailState>}
{initialData.send_status === 'FAIL' && <MailState result="fail">실패</MailState>}
</div>
</InputItem>
</InputRow>
<MailReceiver>
<InputItem>
<InputLabel>수신대상</InputLabel>
<InputItem>
<SelectInput onChange={e => setResultData({ ...resultData, user_type: e.target.value })}
value={resultData.user_type}
disabled={true}>
{userType2.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</InputItem>
<div>
<InputGroup>
<RadioInput
label="단일"
id="SINGLE"
name="receiver"
value="SINGLE"
disabled={true}
fontWeight="600"
checked={resultData.receive_type === 'SINGLE'}
/>
<TextInput
disabled={true}
value={resultData.receive_type === 'SINGLE' && resultData.guid !== '' ? resultData.guid : ''}
/>
</InputGroup>
<InputGroup>
<RadioInput
label="복수"
id="MULTIPLE"
name="receiver"
value="MULTIPLE"
fontWeight="600"
disabled={true}
/>
<MailRegistUploadBtn
disabled={true}
setResultData={setResultData}
resultData={resultData}
setExcelFile={setExcelFile}
handleDetailDelete={() => {}}
disabledBtn={true}
excelName={excelFile}
setExcelName={setExcelFile}
downloadData={downloadData}
status={initialData.send_status}
/>
</InputGroup>
</div>
</InputItem>
</MailReceiver>
</RegistGroup>
{resultData.mail_list &&
resultData?.mail_list?.map(data => {
return (
<Fragment key={data.language}>
<MailRegistBox>
<LangArea>
언어 : {data.language}
{btnValidation === false ? (
<BtnClose
disabled={true}
onClick={e => {
e.preventDefault();
onLangDelete(data.language);
}}
/>
) : (
<BtnClose opacity="10%" />
)}
</LangArea>
<MailRegistTable>
<tbody>
<tr>
<th width="120">
<Label>제목</Label>
</th>
<td>
<InputItem>
<TextInput
placeholder="우편 제목 입력"
maxLength="30"
id={data.language}
value={data.title}
readOnly={(content.is_reserve === false) || isView}
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);
}}
/>
</InputItem>
</td>
</tr>
<tr>
<th>
<Label>내용</Label>
</th>
<td>
<Textarea
value={data.content}
readOnly={isView}
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>
</MailRegistTable>
</MailRegistBox>
</Fragment>
);
})}
<MailRegistBox>
<MailRegistTable>
<tbody>
<tr>
<th width="120">
<Label>아이템 첨부</Label>
</th>
<td>
<InputItem>
<TextInput
placeholder="Item Meta id 입력"
value={item}
onChange={e => {
let list = [];
list = e.target.value.trimStart();
setItem(list);
}}
disabled={isView}
/>
<TextInput
placeholder="수량"
value={itemCount}
type="number"
onChange={e => handleItemCount(e)}
width="90px"
disabled={isView}
/>
<Button
text="추가"
theme={itemCount.length === 0 || item.length === 0 ? 'disable' : 'search'}
disabled={isView}
handleClick={handleItemList}
/>
</InputItem>
</td>
</tr>
<tr>
<th width="120">
<Label>자원 첨부</Label>
</th>
<td>
<InputItem>
<SelectInput onChange={e => setResource(e.target.value)} value={resource} disabled={isView}>
{currencyItemCode.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
placeholder="수량"
type="number"
value={resourceCount}
disabled={isView}
onChange={e => handleResourceCount(e)}
width="200px"
/>
<Button
text="추가"
theme={resourceCount.length === 0 || resource.length === 0 ? 'disable' : 'search'}
disabled={isView}
handleClick={handleResourceList}
width="100px"
height="35px"
/>
{resource === currencyCodeTypes.calium &&
<Label>(잔여 수량: {caliumTotalData})</Label>}
</InputItem>
<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>
{!isView && <BtnDelete onClick={() => onItemRemove(index)}></BtnDelete>}
</Item>
);
})}
</ItemList>
)}
</div>
</td>
</tr>
</tbody>
</MailRegistTable>
</MailRegistBox>
</ModalWrapper>
</>}
<BtnWrapper $justify="flex-end" $gap="10px" $paddingTop="20px"> <BtnWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
<Button <Button
text="확인" text="확인"

View File

@@ -1,38 +1,19 @@
import React, { useState, useEffect, Fragment } from 'react'; import React, { useState, useEffect, Fragment } from 'react';
import styled, { css, keyframes } from 'styled-components'; import styled from 'styled-components';
import { import { Title, ButtonGroupWrapper, } from '../../styles/Components';
Title,
SelectInput,
BtnWrapper,
TextInput,
Label,
InputLabel,
Textarea,
SearchBarAlert,
ButtonGroupWrapper,
} from '../../styles/Components';
import Button from '../common/button/Button';
import Modal from '../common/modal/Modal'; import Modal from '../common/modal/Modal';
import { EventIsItem, EventModify, MenuBannerModify } from '../../apis'; import { MenuBannerModify } from '../../apis';
import { authList } from '../../store/authList'; import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next'; import { authType, commonStatus } from '../../assets/data';
import { authType, benItems, commonStatus, currencyItemCode } from '../../assets/data'; import { convertKTCDate } from '../../utils';
import {
DetailInputItem, DetailInputRow,
DetailModalWrapper, RegistGroup, DetailRegistInfo, DetailState, FormRowGroup, FormLabel, FormInput,
} from '../../styles/ModuleComponents';
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../utils';
import DateTimeInput from '../common/input/DateTimeInput';
import { useLoading } from '../../context/LoadingProvider'; import { useLoading } from '../../context/LoadingProvider';
import { useAlert } from '../../context/AlertProvider'; import { useAlert } from '../../context/AlertProvider';
import { alertTypes, battleEventStatusType, languageNames } from '../../assets/data/types'; import { alertTypes, languageNames } from '../../assets/data/types';
import { Tabs, Image as AntImage, Spin } from 'antd'; import { Image as AntImage } from 'antd';
import { TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants'; import { AntButton, DetailLayout } from '../common';
import { AntButton, DateTimeRangePicker, DetailLayout, SingleTimePicker } from '../common';
import AnimatedTabs from '../common/control/AnimatedTabs';
function renderImageContent(imageData) { function renderImageContent(imageData) {
if (!imageData) { if (!imageData) {
@@ -77,7 +58,6 @@ function renderImageContent(imageData) {
const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => { const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => {
const userInfo = useRecoilValue(authList); const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const token = sessionStorage.getItem('token'); const token = sessionStorage.getItem('token');
const {withLoading} = useLoading(); const {withLoading} = useLoading();
const {showModal, showToast} = useAlert(); const {showModal, showToast} = useAlert();
@@ -94,17 +74,12 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
const [resultData, setResultData] = useState(initData); const [resultData, setResultData] = useState(initData);
const [activeLanguage, setActiveLanguage] = useState('KO'); const [activeLanguage, setActiveLanguage] = useState('KO');
// 이미지 프리로드를 위한 상태
const [allImagesLoaded, setAllImagesLoaded] = useState(false);
const [showTabContent, setShowTabContent] = useState(false);
const [loadedImages, setLoadedImages] = useState([]);
const [totalImageCount, setTotalImageCount] = useState(0);
const [tabItems, setTabItems] = useState([]); const [tabItems, setTabItems] = useState([]);
useEffect(() => { useEffect(() => {
if(content){ if(content){
console.log(content); // console.log(content);
const start_dt_KTC = convertKTCDate(content.start_dt); const start_dt_KTC = convertKTCDate(content.start_dt);
const end_dt_KTC = convertKTCDate(content.end_dt); const end_dt_KTC = convertKTCDate(content.end_dt);
@@ -131,13 +106,6 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
useEffect(() => { useEffect(() => {
if (content && content.image_list) { if (content && content.image_list) {
// 초기화
setAllImagesLoaded(false);
setShowTabContent(false);
setLoadedImages([]);
// 이미지 개수 설정
setTotalImageCount(content.image_list ? content.image_list.length : 0);
// 첫 번째 언어를 활성 언어로 설정 // 첫 번째 언어를 활성 언어로 설정
if (content.image_list && content.image_list.length > 0) { if (content.image_list && content.image_list.length > 0) {
@@ -156,105 +124,9 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
})) : []; })) : [];
setTabItems(newTabItems); setTabItems(newTabItems);
// 모든 이미지 프리로딩 시작
setTimeout(() => {
preloadAllImages();
}, 100);
} }
}, [content]); }, [content]);
const preloadAllImages = () => {
if (!content || !content.image_list || content.image_list.length === 0) {
// 이미지가 없는 경우 바로 로딩 완료 처리
// console.log('이미지가 없습니다. 로딩 완료 처리합니다.');
setAllImagesLoaded(true);
setShowTabContent(true);
return;
}
// console.log(`총 ${content.image_list.length}개의 이미지 로딩을 시작합니다.`);
// 이미지 개수가 0이면 로딩 완료 처리
if (content.image_list.length === 0) {
setAllImagesLoaded(true);
setShowTabContent(true);
return;
}
let loadedCount = 0;
// 이미지 로드 완료 이벤트 핸들러
const handleImageLoad = (url) => {
loadedCount++;
// console.log(`이미지 로드 완료 (${loadedCount}/${content.image_list.length}): ${url}`);
// 모든 이미지가 로드되었는지 확인
if (loadedCount >= content.image_list.length) {
// console.log('모든 이미지 로딩 완료!');
setAllImagesLoaded(true);
setShowTabContent(true);
}
};
// 각 이미지에 대해 프리로드 객체 생성
content.image_list.forEach(img => {
if (img.title) {
// console.log(`이미지 로딩 시작: ${img.title}`);
const image = new Image();
image.onload = () => handleImageLoad(img.title);
image.onerror = () => {
console.log(`이미지 로드 실패: ${img.title}`);
handleImageLoad(img.title); // 오류 시에도 카운트
};
image.src = img.title; // src 속성은 onload/onerror 핸들러 설정 후에 설정
} else {
// console.log('이미지 URL이 없습니다.');
handleImageLoad('empty'); // URL이 없는 경우에도 카운트
}
});
// 안전장치: 5초 후에도 로딩이 완료되지 않으면 강제로 완료 처리
setTimeout(() => {
if (!allImagesLoaded) {
// console.log('시간 초과로 로딩 강제 완료');
setAllImagesLoaded(true);
setShowTabContent(true);
}
}, 5000);
};
// 날짜 처리
// const handleDateChange = (data, type) => {
// const date = new Date(data);
// setResultData({
// ...resultData,
// [`${type}_dt`]: combineDateTime(date, time[`${type}_hour`], time[`${type}_min`]),
// });
// };
// 시간 처리
const handleTimeChange = (e, type) => {
const { id, value } = e.target;
const newTime = { ...time, [`${type}_${id}`]: value };
setTime(newTime);
const date = resultData[`${type}_dt`] ? new Date(resultData[`${type}_dt`]) : new Date();
setResultData({
...resultData,
[`${type}_dt`]: combineDateTime(date, newTime[`${type}_hour`], newTime[`${type}_min`]),
});
};
const handleDateChange = {
start: (date) => {
setResultData(prev => ({ ...prev, start_dt: date }));
},
end: (date) => {
setResultData(prev => ({ ...prev, end_dt: date }));
}
};
// 확인 버튼 후 다 초기화 // 확인 버튼 후 다 초기화
const handleReset = () => { const handleReset = () => {
@@ -279,6 +151,7 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
case "submit": case "submit":
if (!checkCondition()) return; if (!checkCondition()) return;
// console.log(resultData);
showModal('MENU_BANNER_UPDATE_SAVE', { showModal('MENU_BANNER_UPDATE_SAVE', {
type: alertTypes.confirm, type: alertTypes.confirm,
onConfirm: () => handleSubmit('updateConfirm') onConfirm: () => handleSubmit('updateConfirm')
@@ -287,51 +160,44 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
case "updateConfirm": case "updateConfirm":
withLoading( async () => { withLoading( async () => {
return await MenuBannerModify(token, id, resultData); return await MenuBannerModify(token, id, resultData);
}).then(result => {
if(result.result === 'ERROR'){
showToast(result.data.message, {
type: alertTypes.error
});
}else if(result.result === 'SUCCESS'){
showToast('UPDATE_COMPLETED', {type: alertTypes.success, duration: 4000});
}
}).catch(error => { }).catch(error => {
showToast('API_FAIL', {type: alertTypes.error}); showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => { }).finally(() => {
showToast('UPDATE_COMPLETED', {type: alertTypes.success});
handleDetailView(); handleDetailView();
}); });
break; break;
} }
} }
const detailState = (status) => {
switch (status) {
case commonStatus.wait:
return <DetailState>대기</DetailState>;
case commonStatus.running:
return <DetailState>진행중</DetailState>;
case commonStatus.finish:
return <DetailState result={commonStatus.finish}>만료</DetailState>;
case commonStatus.fail:
return <DetailState result={commonStatus.fail}>실패</DetailState>;
case commonStatus.delete:
return <DetailState result={commonStatus.delete}>삭제</DetailState>;
default:
return null;
}
};
//true 수정불가, false 수정가능
const isView = (fieldName) => { const isView = (fieldName) => {
if (!updateAuth) return false;
if (fieldName === 'editButton') { if (fieldName === 'editButton') {
// updateAuth가 없거나 FINISH 상태면 수정 버튼 숨김 (false 반환) // updateAuth가 없거나 FINISH 상태면 수정 버튼 숨김
return !updateAuth || content?.status === commonStatus.finish; return !updateAuth || content?.status === commonStatus.finish;
} }
switch (content?.status) { if (!updateAuth) return false;
case commonStatus.running:
// RUNNING 상태일 때는 end_dt와 order_id만 수정 가능 if(content.status === commonStatus.wait){
return fieldName !== 'date' && fieldName !== 'order_id'; return true;
case commonStatus.wait: }else if(content.status === commonStatus.running){
switch(fieldName){
case 'order_id':
case 'end_dt':
return true; return true;
default: default:
return false; return false;
} }
}else{
return false;
}
} }
const itemGroups = [ const itemGroups = [
@@ -371,12 +237,35 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
row: 2, row: 2,
col: 0, col: 0,
colSpan: 2, colSpan: 2,
type: 'dateRange', type: 'date',
key: 'dateRange', key: 'start_dt',
keys: {start: 'start_dt', end: 'end_dt'}, label: '시작일',
label: '기간', disabled: !isView('start_dt'),
disabled: !isView('date'), format: 'YYYY-MM-DD HH:mm',
format: 'YYYY-MM-DD HH:mm' width: '250px',
showTime: true
},
{
row: 2,
col: 2,
colSpan: 2,
type: 'date',
key: 'end_dt',
label: '종료일',
disabled: !isView('end_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '250px',
showTime: true
},
{
row: 3,
col: 0,
colSpan: 4,
type: 'tab',
key: 'languageTabs',
tabItems: tabItems,
activeKey: activeLanguage,
onTabChange: handleTabChange
}, },
] ]
} }
@@ -393,76 +282,6 @@ const MenuBannerDetailModal = ({ detailView, handleDetailView, content, setDetai
disabled={!updateAuth} disabled={!updateAuth}
columnCount={4} columnCount={4}
/> />
{/*<DetailModalWrapper>*/}
{/* {content &&*/}
{/* <RegistGroup>*/}
{/* <FormRowGroup>*/}
{/* <DetailInputItem>*/}
{/* <FormLabel>제목</FormLabel>*/}
{/* <FormInput*/}
{/* type="text"*/}
{/* value={content.title}*/}
{/* disabled={isView('title')}*/}
{/* onChange={e => setResultData({ ...resultData, title: e.target.value })}*/}
{/* width="300px"*/}
{/* />*/}
{/* </DetailInputItem>*/}
{/* <DetailInputItem>*/}
{/* <FormLabel>순서</FormLabel>*/}
{/* <FormInput*/}
{/* placeholder="순서번호"*/}
{/* type="number"*/}
{/* value={content.order_id}*/}
{/* disabled={isView('order_id')}*/}
{/* onChange={e => setResultData({ ...resultData, order_id: e.target.value })}*/}
{/* width="200px"*/}
{/* />*/}
{/* </DetailInputItem>*/}
{/* </FormRowGroup>*/}
{/* <FormRowGroup>*/}
{/* <DateTimeRangePicker*/}
{/* label="예약기간"*/}
{/* startDate={resultData.start_dt}*/}
{/* endDate={resultData.end_dt}*/}
{/* onStartDateChange={handleDateChange.start}*/}
{/* onEndDateChange={handleDateChange.end}*/}
{/* pastDate={new Date()}*/}
{/* disabled={isView('date')}*/}
{/* startLabel="시작 일자"*/}
{/* endLabel="종료 일자"*/}
{/* // reset={resetDateTime}*/}
{/* />*/}
{/* </FormRowGroup>*/}
{/* <FormRowGroup>*/}
{/* <DetailInputItem>*/}
{/* <FormLabel>상태</FormLabel>*/}
{/* <div>{detailState(content.status)}</div>*/}
{/* </DetailInputItem>*/}
{/* </FormRowGroup>*/}
{/* {content.image_list && content.image_list.length > 0 && (*/}
{/* <FormRowGroup style={{display: 'flex', justifyContent: 'center', width: '100%'}}>*/}
{/* <DetailInputItem style={{width: '100%'}}>*/}
{/* {!showTabContent ? (*/}
{/* <LoadingContainer>*/}
{/* <Spin size="large" tip="이미지 로딩 중..." />*/}
{/* </LoadingContainer>*/}
{/* ) : (*/}
{/* <ContentWrapper $isLoaded={showTabContent}>*/}
{/* <AnimatedTabs*/}
{/* items={tabItems}*/}
{/* activeKey={activeLanguage}*/}
{/* onChange={handleTabChange}*/}
{/* />*/}
{/* </ContentWrapper>*/}
{/* )}*/}
{/* </DetailInputItem>*/}
{/* </FormRowGroup>*/}
{/* )}*/}
{/* </RegistGroup>*/}
{/* }*/}
{/*</DetailModalWrapper>*/}
<ButtonGroupWrapper $justify="flex-end" $gap="10px" $paddingTop="20px"> <ButtonGroupWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
<AntButton <AntButton
text="확인" text="확인"
@@ -503,43 +322,6 @@ const initData = {
] ]
} }
const StyledTabs = styled(Tabs)`
margin-top: 20px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
.ant-tabs-nav {
margin-bottom: 16px;
width: 80%;
}
.ant-tabs-nav-wrap {
justify-content: center;
}
.ant-tabs-tab {
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
}
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: #1890ff;
font-weight: 600;
}
.ant-tabs-ink-bar {
background-color: #1890ff;
}
.ant-tabs-content-holder {
width: 100%;
}
`;
const ImageContainer = styled.div` const ImageContainer = styled.div`
padding: 16px; padding: 16px;
display: flex; display: flex;
@@ -590,22 +372,3 @@ const NoImagePlaceholder = styled.div`
border-radius: 8px; border-radius: 8px;
`; `;
// 로딩 인디케이터를 위한 컨테이너
const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 300px;
width: 100%;
`;
// 컨텐츠 래퍼 - 로딩 상태에 따라 가시성 설정
const ContentWrapper = styled.div`
width: 100%;
opacity: ${props => props.$isLoaded ? 1 : 0};
transition: opacity 0.3s ease-in-out;
height: ${props => props.$isLoaded ? 'auto' : '0'};
overflow: hidden;
`;

View File

@@ -0,0 +1,379 @@
import React, { useState, Fragment, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../common/button/Button';
import {
Title,
BtnWrapper,
SearchBarAlert, SelectInput,
} from '../../styles/Components';
import {
FormInput,
FormLabel,
MessageWrapper,
FormRowGroup,
FormStatusBar,
FormStatusLabel,
FormStatusWarning,
FormButtonContainer,
} from '../../styles/ModuleComponents';
import { DetailLayout, Modal, SingleDatePicker, SingleTimePicker } from '../common';
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
import { convertKTCDate } from '../../utils';
import {
battleEventHotTime,
battleEventRoundCount,
battleEventStatus,
battleRepeatType, opCommonStatus,
} from '../../assets/data/options';
import { BattleEventModify, BattleEventSingleRegist } from '../../apis/Battle';
import { alertTypes, battleEventStatusType, commonStatus } from '../../assets/data/types';
import { isValidDayRange } from '../../utils/date';
import { useAlert } from '../../context/AlertProvider';
import { useLoading } from '../../context/LoadingProvider';
import { RankingScheduleModify, RankingScheduleSingleRegist } from '../../apis';
const RankingModal = ({ modalType, detailView, handleDetailView, content, setDetailData, rankingData, eventActionData }) => {
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const { showToast, showModal } = useAlert();
const {withLoading} = useLoading();
const [isNullValue, setIsNullValue] = useState(false);
const [resultData, setResultData] = useState(initData);
useEffect(() => {
if(modalType === TYPE_MODIFY && content && Object.keys(content).length > 0){
setResultData({
guid: content.guid,
id: content.id,
title: content.title,
meta_id: content.meta_id,
event_action_id: content.event_action_id,
refresh_interval: content.refresh_interval,
initialization_interval: content.initialization_interval,
snapshot_interval: content.snapshot_interval,
status: content.status,
start_dt: convertKTCDate(content.start_dt),
end_dt: convertKTCDate(content.end_dt),
base_dt: convertKTCDate(content.base_dt),
});
}
}, [modalType, content]);
useEffect(() => {
if (checkCondition()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
const opRankingMode = useMemo(() => {
return rankingData?.map(item => ({
value: item.id,
name: `${item.desc}(${item.id})`
})) || [];
}, [rankingData]);
const opEventActionMode = useMemo(() => {
return eventActionData?.map(item => ({
value: item.id,
name: `${item.description}(${item.id})`
})) || [];
}, [eventActionData]);
const handleReset = () => {
setDetailData({});
setResultData(initData);
handleDetailView();
}
const handleSubmit = async (type, param = null) => {
switch (type) {
case "submit":
if (!checkCondition()) return;
// const minAllowedTime = new Date(new Date().getTime() + 10 * 60000);
// const startDt = resultData.event_start_dt;
// const endDt = resultData.event_end_dt;
// if (modalType === TYPE_REGISTRY && startDt < minAllowedTime) {
// showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning});
// return;
// }
// if(resultData.repeat_type !== 'NONE' && !isValidDayRange(startDt, endDt)) {
// showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning});
// return;
// }
//
// //화면에 머물면서 상태는 안바꼈을 경우가 있기에 시작시간 지났을경우 차단
// if (modalType === TYPE_REGISTRY && startDt < new Date()) {
// showToast('BATTLE_EVENT_MODAL_START_DT_WARNING', {type: alertTypes.warning});
// return;
// }
showModal(isView('modify') ? 'SCHEDULE_UPDATE_CONFIRM' : 'SCHEDULE_REGIST_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleSubmit('registConfirm')
});
break;
case "registConfirm":
const params = {
...resultData
};
if(isView('modify')){
await withLoading( async () => {
return await RankingScheduleModify(token, content?.id, params);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('UPDATE_COMPLETED', {type: alertTypes.success});
}else{
showToast('UPDATE_FAIL', {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
handleReset();
});
}
else{
await withLoading( async () => {
return await RankingScheduleSingleRegist(token, params);
}).then(data => {
if(data.result === "SUCCESS") {
showToast('REGIST_COMPLTE', {type: alertTypes.success});
}else{
showToast('REGIST_FAIL', {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
handleReset();
});
}
break;
}
}
const checkCondition = () => {
return (
resultData.start_dt !== ''
&& resultData.end_dt !== ''
&& resultData.base_dt !== ''
&& resultData.meta_id > 0
&& resultData.event_action_id > 0
&& resultData.title !== ''
&& resultData.refresh_interval > 0
&& resultData.initialization_interval > 0
&& resultData.snapshot_interval > 0
);
};
const isView = (label) => {
switch (label) {
case "modify":
return modalType === TYPE_MODIFY && (content?.status === commonStatus.wait);
case "registry":
return modalType === TYPE_REGISTRY
case "start_dt":
case "end_dt":
case "base_dt":
case "name":
case "refresh_interval":
case "init_interval":
case "snapshot_interval":
case "mode":
case "eventActionMode":
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === commonStatus.wait));
default:
return modalType === TYPE_MODIFY && (content?.status !== commonStatus.wait);
}
}
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 2,
type: 'text',
key: 'title',
label: '스케줄러명',
disabled: !isView('name'),
width: '300px',
},
{
row: 1,
col: 0,
colSpan: 2,
type: 'date',
key: 'start_dt',
label: '시작일시',
disabled: !isView('start_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '200px',
showTime: true
},
{
row: 1,
col: 2,
colSpan: 2,
type: 'date',
key: 'end_dt',
label: '종료일시',
disabled: !isView('end_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '200px',
showTime: true
},
{
row: 2,
col: 0,
colSpan: 2,
type: 'date',
key: 'base_dt',
label: '기준일시',
disabled: !isView('base_dt'),
format: 'YYYY-MM-DD HH:mm',
width: '200px',
showTime: true
},
{
row: 2,
col: 2,
colSpan: 2,
type: 'number',
key: 'refresh_interval',
label: '새로고침 주기(분)',
disabled: !isView('refresh_interval'),
width: '100px',
min: 0,
},
{
row: 3,
col: 0,
colSpan: 2,
type: 'number',
key: 'initialization_interval',
label: '초기화 주기(분)',
disabled: !isView('init_interval'),
width: '100px',
min: 0,
},
{
row: 3,
col: 2,
colSpan: 2,
type: 'number',
key: 'snapshot_interval',
label: '스냅샷 주기(분)',
disabled: !isView('snapshot_interval'),
width: '100px',
min: 0,
},
{
row: 4,
col: 0,
colSpan: 2,
type: 'select',
key: 'meta_id',
label: '랭킹 모드',
disabled: !isView('mode'),
width: '150px',
options: opRankingMode
},
{
row: 4,
col: 2,
colSpan: 2,
type: 'select',
key: 'event_action_id',
label: '이벤트 액션 그룹',
disabled: !isView('eventActionMode'),
width: '150px',
options: opEventActionMode
},
]
}
];
return (
<>
<Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "랭킹 스케줄러 등록" : isView('modify') ? "랭킹 스케줄러 수정" : "랭킹 스케줄러 상세"}</Title>
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
<BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar>
<FormStatusLabel>
현재상태: {opCommonStatus.find(data => data.value === content?.status)?.name || "등록"}
</FormStatusLabel>
<FormStatusWarning>
{isView('registry') ? '' : t('SCHEDULE_MODAL_STATUS_WARNING')}
</FormStatusWarning>
</FormStatusBar>
<FormButtonContainer $gap="5px">
{isView() ?
<Button
text="확인"
name="확인버튼"
theme="line"
handleClick={() => handleReset()}
/>
:
<>
<Button
text="취소"
theme="line"
handleClick={() => showModal('CANCEL_CONFIRM', {
type: alertTypes.confirm,
onConfirm: () => handleReset()
})}
/>
<Button
type="submit"
text={isView('modify') ? "수정" : "등록"}
name="등록버튼"
theme={
checkCondition()
? 'primary'
: 'disable'
}
handleClick={() => handleSubmit('submit')}
/>
</>
}
</FormButtonContainer>
</BtnWrapper>
</Modal>
</>
);
};
export const initData = {
guid: '',
title: '',
start_dt: '',
end_dt: '',
base_dt: '',
refresh_interval: 60,
initialization_interval: 0,
snapshot_interval: 1440,
meta_id: '',
event_action_id: '',
}
export default RankingModal;

View File

@@ -0,0 +1,540 @@
import { useState, useEffect, Fragment } from 'react';
import { Input, Button as AntButton, Select, Alert, Space, Card, Row, Col } from 'antd';
import { Title, BtnWrapper } from '../../styles/Components';
import Button from '../common/button/Button';
import Modal from '../common/modal/Modal';
import { EventIsItem, RewardEventModify } from '../../apis';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import { authType, benItems, commonStatus, currencyItemCode } from '../../assets/data';
import {
DetailRegistInfo, DetailState
} from '../../styles/ModuleComponents';
import { convertKTC, timeDiffMinute, convertKTCDate } from '../../utils';
import { useLoading } from '../../context/LoadingProvider';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { DetailLayout } from '../common';
const RewardEventDetailModal = ({ 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 [activeLanguage, setActiveLanguage] = useState('KO');
const [item, setItem] = useState('');
const [itemCount, setItemCount] = useState(1);
const [resource, setResource] = useState('19010001');
const [resourceCount, setResourceCount] = useState(1);
const [resultData, setResultData] = useState({});
// 과거 판단
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,
status: content.status,
delete_desc: content.delete_desc
});
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(() => {
setItemCheckMsg('');
}, [item]);
const getLanguageTabItems = () => {
return resultData.mail_list?.map(mail => ({
key: mail.language,
label: mail.language,
children: (
<div style={{ padding: '10px', minHeight: '400px', height: 'auto' }}>
<Row gutter={[16, 24]}>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
제목 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input
value={mail.title || ''}
placeholder="우편 제목을 입력하세요"
maxLength={30}
readOnly={isReadOnly}
onChange={(e) => updateMailData(mail.language, 'title', e.target.value.trimStart())}
showCount
size="large"
/>
</div>
</Col>
<Col span={24}>
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: 'rgba(0, 0, 0, 0.85)',
fontSize: '14px'
}}>
내용 <span style={{ color: '#ff4d4f' }}>*</span>
</label>
<Input.TextArea
value={mail.content || ''}
placeholder="우편 내용을 입력하세요"
readOnly={isReadOnly}
rows={8}
maxLength={2000}
showCount
onChange={(e) => {
if (e.target.value.length > 2000) return;
updateMailData(mail.language, 'content', e.target.value.trimStart());
}}
style={{
resize: 'vertical',
minHeight: '200px',
maxHeight: '400px'
}}
/>
</div>
</Col>
</Row>
</div>
),
closable: resultData.mail_list?.length > 1 && !isReadOnly, // 마지막 하나가 아니고 읽기전용이 아닐 때만 삭제 가능
})) || [];
};
const updateMailData = (language, field, value) => {
const updatedMailList = resultData.mail_list.map(mail =>
mail.language === language
? { ...mail, [field]: value }
: mail
);
setResultData({ ...resultData, mail_list: updatedMailList });
setIsChanged(true);
};
const handleTabClose = (targetKey) => {
if (resultData.mail_list.length <= 1) return;
const filterList = resultData.mail_list.filter(el => el.language !== targetKey);
setResultData({ ...resultData, mail_list: filterList });
// 삭제된 탭이 현재 활성 탭이었다면 첫 번째 탭으로 변경
if (activeLanguage === targetKey) {
setActiveLanguage(filterList[0]?.language || 'KO');
}
setIsChanged(true);
};
// 아이템 추가
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 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 = currencyItemCode.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 handleReset = () => {
setBtnValidation(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 RewardEventModify(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;
}
};
// 아이템 목록 렌더링 컴포넌트
const renderItemList = () => {
return (
<div>
{resultData.item_list && resultData.item_list.length > 0 && (
<Space wrap>
{resultData.item_list.map((data, index) => (
<Card
key={index}
title={data.item_name}
size="small"
extra={
!isReadOnly && (
<AntButton
type="text"
danger
size="small"
onClick={() => onItemRemove(index)}
>
X
</AntButton>
)
}
style={{ minWidth: '150px' }}
>
<div>
<div>{data.item}</div>
<div>수량: {data.item_cnt}</div>
</div>
</Card>
))}
</Space>
)}
</div>
);
};
// 아이템 추가 컴포넌트
const renderItemAdd = () => {
return (
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="Item Meta id 입력"
value={item}
onChange={(e) => setItem(e.target.value.trimStart())}
disabled={isReadOnly}
style={{ width: '200px' }}
/>
<Input
type="number"
placeholder="수량"
value={itemCount}
onChange={(e) => setItemCount(e.target.value)}
disabled={isReadOnly}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleItemList}
disabled={itemCount.length === 0 || item.length === 0 || isReadOnly}
>
추가
</AntButton>
</Space.Compact>
);
};
// 자원 추가 컴포넌트
const renderResourceAdd = () => {
return (
<Space.Compact style={{ width: '100%' }}>
<Select
value={resource}
onChange={setResource}
disabled={isReadOnly}
style={{ width: '200px' }}
placeholder="자원 선택"
>
{currencyItemCode.map((data, index) => (
<Select.Option key={index} value={data.value}>
{data.name}
</Select.Option>
))}
</Select>
<Input
type="number"
placeholder="수량"
value={resourceCount}
disabled={isReadOnly}
onChange={(e) => setResourceCount(e.target.value)}
style={{ width: '120px' }}
min={1}
/>
<AntButton
type="primary"
onClick={handleResourceList}
disabled={resourceCount.length === 0 || resource.length === 0 || isReadOnly}
>
추가
</AntButton>
</Space.Compact>
);
};
const itemGroups = [
{
items: [
{
row: 0,
col: 0,
colSpan: 2,
type: 'dateRange',
keys: {
start: 'start_dt',
end: 'end_dt'
},
label: '이벤트 기간',
disabled: isReadOnly,
format: 'YYYY-MM-DD HH:mm',
showTime: true,
startLabel: '시작 일시',
endLabel: '종료 일시'
},
{
row: 0,
col: 2,
colSpan: 1,
type: 'custom',
key: 'status',
label: '이벤트 상태',
render: () => detailState(resultData.status)
},
...(resultData.status === commonStatus.delete ? [{
row: 0,
col: 3,
colSpan: 1,
type: 'display',
key: 'delete_desc',
label: '삭제 사유',
value: resultData.delete_desc || ''
}] : [{
row: 0,
col: 3,
colSpan: 1,
type: 'custom',
key: 'empty_space',
label: '',
render: () => <div></div>
}]),
{
row: 1,
col: 0,
colSpan: 4,
type: 'tab',
key: 'language_tabs',
tabItems: getLanguageTabItems(),
activeKey: activeLanguage,
onTabChange: setActiveLanguage,
onTabClose: handleTabClose
},
{
row: 2,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_add',
label: '아이템 추가',
render: renderItemAdd
},
{
row: 3,
col: 0,
colSpan: 4,
type: 'custom',
key: 'resource_add',
label: '자원 추가',
render: renderResourceAdd
},
{
row: 4,
col: 0,
colSpan: 4,
type: 'custom',
key: 'item_list',
render: renderItemList
}
]
}
];
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>
}
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
{itemCheckMsg && (
<Alert
message={itemCheckMsg}
type="error"
style={{ marginTop: '8px', width: '300px' }}
/>
)}
<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 RewardEventDetailModal;

View File

@@ -0,0 +1,150 @@
import { useCallback, useEffect, useState } from 'react';
import { InputLabel, TextInput } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { AssetsIndexView, CurrencyAcquireIndexView, ItemIndexView } from '../../apis';
export const useAssetsIndexSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate());
return date;
})(),
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await AssetsIndexView(
token,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result);
return result;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, updateSearchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
start_dt: now,
end_dt: new Date(),
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const AssetsIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default AssetsIndexSearchBar;

View File

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

View File

@@ -1,11 +1,15 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components'; import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar'; import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { logAction, logDomain, userSearchType2 } from '../../assets/data/options'; import { logAction, logDomain, userSearchType2 } from '../../assets/data/options';
import { BusinessLogList } from '../../apis/Log'; import { BusinessLogList } from '../../apis/Log';
import {SearchFilter} from '../ServiceManage'; import {SearchFilter} from '../ServiceManage';
import { useAlert } from '../../context/AlertProvider'; import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types'; import { alertTypes } from '../../assets/data/types';
import dayjs from 'dayjs';
import { DatePicker } from 'antd';
import DateRangePicker from '../common/Date/DateRangePicker';
const { RangePicker } = DatePicker;
export const useBusinessLogSearch = (token, initialPageSize) => { export const useBusinessLogSearch = (token, initialPageSize) => {
const {showToast} = useAlert(); const {showToast} = useAlert();
@@ -16,16 +20,8 @@ export const useBusinessLogSearch = (token, initialPageSize) => {
log_action: 'None', log_action: 'None',
log_domain: 'BASE', log_domain: 'BASE',
tran_id: '', tran_id: '',
start_dt: (() => { start_dt: dayjs().subtract(1, 'day').startOf('day'),
const date = new Date(); end_dt: dayjs().subtract(1, 'day').endOf('day'),
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
filters: [], filters: [],
order_by: 'ASC', order_by: 'ASC',
page_size: initialPageSize, page_size: initialPageSize,
@@ -92,16 +88,14 @@ export const useBusinessLogSearch = (token, initialPageSize) => {
}, [searchParams, fetchData]); }, [searchParams, fetchData]);
const handleReset = useCallback(async () => { const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = { const resetParams = {
search_type: 'GUID', search_type: 'GUID',
search_data: '', search_data: '',
log_action: 'None', log_action: 'None',
log_domain: 'BASE', log_domain: 'BASE',
tran_id: '', tran_id: '',
start_dt: now, start_dt: dayjs().subtract(1, 'day').startOf('day'),
end_dt: now, end_dt: dayjs().subtract(1, 'day').endOf('day'),
filters: [], filters: [],
order_by: 'ASC', order_by: 'ASC',
page_size: initialPageSize, page_size: initialPageSize,
@@ -197,11 +191,11 @@ const BusinessLogSearchBar = ({ searchParams, onSearch, onReset }) => {
</>, </>,
<> <>
<InputLabel>일자</InputLabel> <InputLabel>일자</InputLabel>
<SearchPeriod <DateRangePicker
startDate={searchParams.start_dt} value={[searchParams.start_dt, searchParams.end_dt]}
handleStartDate={date => onSearch({ start_dt: date }, false)} onChange={(dates) => {
endDate={searchParams.end_dt} onSearch({ start_dt: dates[0], end_dt: dates[1] }, false);
handleEndDate={date => onSearch({ end_dt: date }, false)} }}
/> />
</>, </>,
]; ];

View File

@@ -3,9 +3,10 @@ import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { getOptionsArray } from '../../utils'; import { getOptionsArray } from '../../utils';
import { PageSkeleton } from '../Skeleton/SearchSkeleton'; import { PageSkeleton } from '../Skeleton/SearchSkeleton';
import { DateRangePicker } from '../common';
const renderSearchField = (field, searchParams, onSearch) => { const renderSearchField = (field, searchParams, onSearch) => {
const { type, id, label, placeholder, width, optionsRef } = field; const { type, id, label, placeholder, width, optionsRef, format, showTime } = field;
switch (type) { switch (type) {
case 'text': case 'text':
@@ -47,14 +48,45 @@ const renderSearchField = (field, searchParams, onSearch) => {
); );
case 'period': case 'period':
const startDateValue = searchParams[field.startDateId];
const endDateValue = searchParams[field.endDateId];
// 날짜 값이 있을 때만 배열로 변환
const rangeValue = (startDateValue && endDateValue) ?
[startDateValue, endDateValue] :
null;
return ( return (
<> <>
{label && label !== 'undefined' && <InputLabel $require={field.required}>{label}</InputLabel>} {label && label !== 'undefined' && <InputLabel $require={field.required}>{label}</InputLabel>}
<SearchPeriod <DateRangePicker
startDate={searchParams[field.startDateId]} value={rangeValue}
handleStartDate={date => onSearch({ [field.startDateId]: date }, false)} onChange={(dates) => {
endDate={searchParams[field.endDateId]} if (dates && dates.length === 2) {
handleEndDate={date => onSearch({ [field.endDateId]: date }, false)} let startDate = dates[0];
let endDate = dates[1];
if(!showTime) {
// 시작 날짜는 00:00:00으로 설정
startDate = startDate.startOf('day');
// 종료 날짜는 23:59:59로 설정
endDate = endDate.endOf('day');
}
onSearch({
[field.startDateId]: startDate.format('YYYY-MM-DD HH:mm:ss'),
[field.endDateId]: endDate.format('YYYY-MM-DD HH:mm:ss')
}, false);
} else {
onSearch({
[field.startDateId]: '',
[field.endDateId]: ''
}, false);
}
}}
showTime={showTime ||false}
format={format || "YYYY-MM-DD"}
placeholder={['시작일', '종료일']}
style={{ width: width || '280px' }}
/> />
</> </>
); );

View File

@@ -0,0 +1,165 @@
import { InputLabel, SelectInput } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { CurrencyType } from '../../assets/data';
import { CurrencyAcquireIndexView } from '../../apis';
export const useCurrencyAcquireIndexSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate());
return date;
})(),
currency_type: 'Gold',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await CurrencyAcquireIndexView(
token,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.currency_type,
"Acquire",
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result);
return result;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
start_dt: now,
end_dt: new Date(),
currency_type: 'Gold',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const CurrencyAcquireIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>,
<>
<InputLabel>재화종류</InputLabel>
<SelectInput value={searchParams.currency_type} onChange={e => onSearch({ currency_type: e.target.value }, false)} >
{CurrencyType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default CurrencyAcquireIndexSearchBar;

View File

@@ -0,0 +1,165 @@
import { InputLabel, SelectInput } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { CurrencyType } from '../../assets/data';
import { CurrencyAcquireIndexView } from '../../apis';
export const useCurrencyConsumeIndexSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate());
return date;
})(),
currency_type: 'Gold',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const initialLoad = async () => {
await fetchData(searchParams);
};
initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await CurrencyAcquireIndexView(
token,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.currency_type,
"Consume",
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result);
return result;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
start_dt: now,
end_dt: new Date(),
currency_type: 'Gold',
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const CurrencyConsumeIndexSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>,
<>
<InputLabel>재화종류</InputLabel>
<SelectInput value={searchParams.currency_type} onChange={e => onSearch({ currency_type: e.target.value }, false)} >
{CurrencyType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
];
return <SearchBarLayout firstColumnData={searchList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default CurrencyConsumeIndexSearchBar;

View File

@@ -0,0 +1,241 @@
import { TextInput, InputLabel, SelectInput, InputGroup } from '../../styles/Components';
import { SearchBarLayout, SearchPeriod } from '../common/SearchBar';
import { useCallback, useEffect, useState } from 'react';
import {
amountDeltaType,
countDeltaType,
CurrencyType,
itemTypeLarge,
logAction,
userSearchType2,
} from '../../assets/data/options';
import {
getCurrencyItemList,
} from '../../apis/Log';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
export const useCurrencyItemLogSearch = (token, initialPageSize) => {
const {showToast} = useAlert();
const [searchParams, setSearchParams] = useState({
search_type: 'GUID',
search_data: '',
tran_id: '',
log_action: 'None',
currency_type: 'None',
amount_delta_type: 'None',
start_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
end_dt: (() => {
const date = new Date();
date.setDate(date.getDate() - 1);
return date;
})(),
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
});
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
// const initialLoad = async () => {
// await fetchData(searchParams);
// };
//
// initialLoad();
}, [token]);
const fetchData = useCallback(async (params) => {
if (!token) return;
try {
setLoading(true);
const result = await getCurrencyItemList(
token,
params.search_type,
params.search_data,
params.tran_id,
params.log_action,
params.currency_type,
params.amount_delta_type,
params.start_dt.toISOString(),
params.end_dt.toISOString(),
params.order_by,
params.page_size,
params.page_no
);
if(result.result === "ERROR_LOG_MEMORY_LIMIT"){
showToast('LOG_MEMORY_LIMIT_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR_MONGODB_QUERY"){
showToast('LOG_MONGGDB_QUERY_WARNING', {type: alertTypes.error});
}else if(result.result === "ERROR"){
showToast(result.result, {type: alertTypes.error});
}
setData(result.data);
return result.data;
} catch (error) {
showToast('error', {type: alertTypes.error});
throw error;
} finally {
setLoading(false);
}
}, [token]);
const updateSearchParams = useCallback((newParams) => {
setSearchParams(prev => ({
...prev,
...newParams
}));
}, []);
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
const updatedParams = {
...searchParams,
...newParams,
page_no: newParams.page_no || 1 // Reset to first page on new search
};
updateSearchParams(updatedParams);
if (executeSearch) {
return await fetchData(updatedParams);
}
return null;
}, [searchParams, fetchData]);
const handleReset = useCallback(async () => {
const now = new Date();
now.setDate(now.getDate() - 1);
const resetParams = {
search_type: 'GUID',
search_data: '',
tran_id: '',
log_action: 'None',
currency_type: 'None',
amount_delta_type: 'None',
start_dt: now,
end_dt: now,
order_by: 'ASC',
page_size: initialPageSize,
page_no: 1
};
setSearchParams(resetParams);
return await fetchData(resetParams);
}, [initialPageSize, fetchData]);
const handlePageChange = useCallback(async (newPage) => {
return await handleSearch({ page_no: newPage }, true);
}, [handleSearch]);
const handlePageSizeChange = useCallback(async (newSize) => {
return await handleSearch({ page_size: newSize, page_no: 1 }, true);
}, [handleSearch]);
const handleOrderByChange = useCallback(async (newOrder) => {
return await handleSearch({ order_by: newOrder }, true);
}, [handleSearch]);
return {
searchParams,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams
};
};
const CurrencyItemLogSearchBar = ({ searchParams, onSearch, onReset }) => {
const handleSubmit = event => {
event.preventDefault();
onSearch(searchParams, true);
};
const searchList = [
<>
<InputGroup>
<SelectInput value={searchParams.search_type} onChange={e => onSearch({search_type: e.target.value }, false)}>
{userSearchType2.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput
type="text"
placeholder={searchParams.search_type === 'GUID' ? 'GUID ID 입력' : searchParams.search_type === 'NICKNAME' ? '아바타명 입력' :'Account ID 입력'}
value={searchParams.search_data}
width="260px"
onChange={e => onSearch({ search_data: e.target.value }, false)}
/>
</InputGroup>
</>,
<>
<InputLabel>트랜잭션 ID</InputLabel>
<TextInput
type="text"
placeholder='트랜잭션 ID 입력'
value={searchParams.tran_id}
width="300px"
onChange={e => onSearch({ tran_id: e.target.value }, false)}
/>
</>,
];
const optionList = [
<>
<InputLabel>일자</InputLabel>
<SearchPeriod
startDate={searchParams.start_dt}
handleStartDate={date => onSearch({ start_dt: date }, false)}
endDate={searchParams.end_dt}
handleEndDate={date => onSearch({ end_dt: date }, false)}
/>
</>,
<>
<InputLabel>액션</InputLabel>
<SelectInput value={searchParams.log_action} onChange={e => onSearch({ log_action: e.target.value }, false)} >
{logAction.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
<>
<InputLabel>재화종류</InputLabel>
<SelectInput value={searchParams.currency_type} onChange={e => onSearch({ currency_type: e.target.value }, false)} >
<option value="None">전체</option>
{CurrencyType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
<>
<InputLabel>증감유형</InputLabel>
<SelectInput value={searchParams.amount_delta_type} onChange={e => onSearch({ amount_delta_type: e.target.value }, false)} >
<option value="None">전체</option>
{amountDeltaType.map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
</>,
];
return <SearchBarLayout firstColumnData={searchList} secondColumnData={optionList} direction={'column'} onReset={onReset} handleSubmit={handleSubmit} />;
};
export default CurrencyItemLogSearchBar;

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