event > rewardEvent 변경

월드이벤트(event) 추가
This commit is contained in:
2025-09-15 16:23:48 +09:00
parent e25bcdc86e
commit f78a4912a6
11 changed files with 2095 additions and 280 deletions

View File

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

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

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

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

View File

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

View File

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

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

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

@@ -1,52 +1,43 @@
import React, { useState, Fragment } from 'react';
import React, { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import {
EventDelete,
EventDetailView, LogHistory,
} from '../../apis';
import { authList } from '../../store/authList';
import { authType, commonStatus, modalTypes, eventStatus } from '../../assets/data';
import { Title, FormWrapper, TableStyle, MailTitle, TableWrapper, TextInput, InputItem } from '../../styles/Components';
import CheckBox from '../../components/common/input/CheckBox';
import Button from '../../components/common/button/Button';
import EventDetailModal from '../../components/modal/EventDetailModal';
import Pagination from '../../components/common/Pagination/Pagination';
import 'react-datepicker/dist/react-datepicker.css';
import DynamicModal from '../../components/common/modal/DynamicModal';
import AuthModal from '../../components/common/modal/AuthModal';
import ViewTableInfo from '../../components/common/Table/ViewTableInfo';
import { convertKTC, timeDiffMinute } from '../../utils';
import tableInfo from '../../assets/data/pages/eventTable.json'
import {
ModalInputItem,
ModalSubText,
RegistInputItem,
StatusLabel, StatusWapper,
} from '../../styles/ModuleComponents';
import { useLoading } from '../../context/LoadingProvider';
import { useAlert } from '../../context/AlertProvider';
import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage';
import { useModal, useTable } from '../../hooks/hook';
authType, commonStatus as CommonStatus,
} from '../../assets/data';
import { Title, FormWrapper} from '../../styles/Components';
import {
Pagination,
CaliTable, TableHeader,
} from '../../components/common';
import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import { useDataFetch, useModal, useTable, withAuth } from '../../hooks/hook';
import {
EventActionView, EventDelete,
EventDetailView,
LogHistory,
} from '../../apis';
import { CommonSearchBar } from '../../components/ServiceManage';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
import { useLoading } from '../../context/LoadingProvider';
import useEnhancedCommonSearch from '../../hooks/useEnhancedCommonSearch';
import { historyTables } from '../../assets/data/data';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import {AnimatedPageWrapper} from '../../components/common/Layout';
import { AnimatedPageWrapper } from '../../components/common/Layout';
import EventModal from '../../components/modal/EventModal';
const Event = () => {
const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const {withLoading} = useLoading();
const {showToast} = useAlert();
const tableRef = useRef(null);
const navigate = useNavigate();
const [detailData, setDetailData] = useState('');
const { showToast, showModal } = useAlert();
const {withLoading} = useLoading();
const token = sessionStorage.getItem('token');
const [detailData, setDetailData] = useState({});
const [historyData, setHistoryData] = useState({});
const [modalType, setModalType] = useState('regist');
const {
modalState,
@@ -54,10 +45,8 @@ const Event = () => {
handleModalClose
} = useModal({
detail: 'hidden',
delete: 'hidden',
history: 'hidden'
history: 'hidden',
});
const [deleteDesc, setDeleteDesc] = useState('');
const {
config,
@@ -65,35 +54,34 @@ const Event = () => {
data: dataList,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams,
configLoaded
} = useCommonSearchOld("eventSearch");
loading,
configLoaded,
handlePageChange,
handlePageSizeChange
} = useEnhancedCommonSearch("eventSearch");
const {
data: eventActionData
} = useDataFetch(() => EventActionView(token), [token]);
const {
selectedRows,
handleSelectRow,
isRowSelected
} = useTable(dataList?.list || [], {mode: 'single'});
// 상세보기 호출
const handleDetailModal = async (e, id) => {
await EventDetailView(token, id).then(data => {
setDetailData(data);
});
e.preventDefault();
handleModalView('detail');
};
const handleModalSubmit = async (type, param = null) => {
switch (type) {
const handleAction = async (action, item = null) => {
switch (action) {
case "regist":
setModalType('regist');
handleModalView('detail');
break;
case "history":
const params = {};
params.db_type = "MYSQL"
params.sql_id = param.id;
params.sql_id = item.id;
params.table_name = historyTables.event
await LogHistory(token, params).then(data => {
@@ -101,191 +89,124 @@ const Event = () => {
handleModalView('history');
});
break;
case "delete":
const delete_check = selectedRows.every(row => {
const timeDiff = timeDiffMinute(convertKTC(row.start_dt), (new Date));
return row.add_flag || (timeDiff < 30);
case "detail":
await EventDetailView(token, item.id).then(data => {
setDetailData(data.detail);
setModalType('modify');
handleModalView('detail');
});
if(delete_check){
showToast('EVENT_TIME_LIMIT_UPDATE', {type: alertTypes.warning});
return;
}
break;
case "delete":
handleModalView('delete');
showModal('EVENT_SELECT_DELETE', {
type: alertTypes.confirm,
onConfirm: () => handleAction('deleteConfirm')
});
break;
case "deleteConfirm":
let list = [];
const low = selectedRows[0];
if(deleteDesc.length === 0){
showToast('INPUT_REASON_EMPTY_WARNING', {type: alertTypes.warning});
return;
}
let isChecked = false;
selectedRows.map(data => {
const row = dataList.list.find(row => row.id === Number(data.id));
if(row.status !== commonStatus.wait) isChecked = true;
list.push({
id: data.id,
delete_desc: deleteDesc
});
});
handleModalClose('delete');
setDeleteDesc('');
if(isChecked) {
showToast('EVENT_WARNING_DELETE', {type: alertTypes.warning});
if(low.status !== CommonStatus.wait) {
showToast('DELETE_STATUS_ONLY_WAIT', {type: alertTypes.warning});
return;
}
await withLoading(async () => {
return await EventDelete(token, list);
return await EventDelete(token, low.id);
}).then(data => {
showToast('DEL_COMPLETE', {type: alertTypes.success});
}).catch(error => {
if(data.result === "SUCCESS") {
showToast('DEL_COMPLETE', {type: alertTypes.success});
}else{
showToast('DELETE_FAIL', {type: alertTypes.error});
}
}).catch(reason => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
handleSearch(updateSearchParams);
})
});
break;
default:
break;
}
}
};
return (
<>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventRead) ? (
<AuthModal />
) : (
<AnimatedPageWrapper>
<Title>출석 보상 이벤트 관리</Title>
<FormWrapper>
<CommonSearchBar
config={config}
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventDelete) && (
<Button theme={selectedRows.length === 0 ? 'disable' : 'line'} text="선택 삭제" handleClick={() => handleModalSubmit('delete')} />
)}
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) && (
<Button
theme="primary"
text="이벤트 등록"
type="button"
handleClick={e => {
e.preventDefault();
navigate('/servicemanage/event/eventregist');
}}
/>
)}
</ViewTableInfo>
<TableWrapper>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th width="40">
</th>
<th width="80">번호</th>
<th width="100">이벤트 상태</th>
<th width="210">시작 일시</th>
<th width="210">종료 일시</th>
<th>우편 제목</th>
<th width="110">확인 / 수정</th>
<th width="200">히스토리</th>
</tr>
</thead>
<tbody>
{dataList?.list?.map(event => (
<Fragment key={event.row_num}>
<tr>
<td>
<CheckBox name={'select'} id={event.id}
setData={(e) => handleSelectRow(e, event)}
checked={isRowSelected(event.id)} />
</td>
<td>{event.row_num}</td>
<StatusWapper>
<StatusLabel $status={event.status}>
{eventStatus.map(data => data.value === event.status && data.name)}
</StatusLabel>
</StatusWapper>
<td>{convertKTC(event.start_dt)}</td>
<td>{convertKTC(event.end_dt)}</td>
<MailTitle>{event.title}</MailTitle>
<td>
<Button theme="line" text="상세보기"
handleClick={e => handleDetailModal(e, event.id)} />
</td>
<td><Button theme="line" text="히스토리"
handleClick={e => handleModalSubmit('history', event)} />
</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<AnimatedPageWrapper>
<Title>통합 이벤트 관리</Title>
<Pagination postsPerPage={searchParams.pageSize} totalPosts={dataList?.total_all} setCurrentPage={handlePageChange} currentPage={searchParams.currentPage} pageLimit={INITIAL_PAGE_LIMIT} />
{/* 조회조건 */}
<FormWrapper>
<CommonSearchBar
config={config}
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
{/*상세*/}
<EventDetailModal
detailView={modalState.detailModal}
handleDetailView={() =>{
handleModalClose('detail');
handleSearch(updateSearchParams);
}}
content={detailData}
setDetailData={setDetailData}
/>
{/* 조회헤더 */}
<TableHeader
config={tableInfo.header}
total={dataList?.total}
total_all={dataList?.total_all}
handleOrderBy={handleOrderByChange}
handlePageSize={handlePageSizeChange}
selectedRows={selectedRows}
onAction={handleAction}
navigate={navigate}
/>
<LogDetailModal
viewMode="changed"
detailView={modalState.historyModal}
handleDetailView={() => handleModalClose('history')}
changedData={historyData}
title="히스토리"
/>
{/* 조회테이블 */}
<CaliTable
columns={tableInfo.columns}
data={dataList?.list}
selectedRows={selectedRows}
onSelectRow={handleSelectRow}
onAction={handleAction}
refProp={tableRef}
loading={loading}
isRowSelected={isRowSelected}
/>
<DynamicModal
modalType={modalTypes.childOkCancel}
view={modalState.deleteModal}
handleCancel={() => handleModalClose('delete')}
handleSubmit={() => handleModalSubmit('deleteConfirm')}
>
<ModalInputItem>
{t('EVENT_SELECT_DELETE')}
<RegistInputItem>
<TextInput
placeholder="사유 입력"
maxLength="30"
value={deleteDesc}
onChange={e => {
if (e.target.value.length > 30) return;
setDeleteDesc(e.target.value.trimStart())
}}
/>
</RegistInputItem>
<ModalSubText $color={deleteDesc.length > 29 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({deleteDesc.length}/30)</ModalSubText>
</ModalInputItem>
</DynamicModal>
</AnimatedPageWrapper>
)}
</>
);
{/* 페이징 */}
<Pagination
postsPerPage={searchParams?.pageSize}
totalPosts={dataList?.total_all}
setCurrentPage={handlePageChange}
currentPage={searchParams?.currentPage}
pageLimit={INITIAL_PAGE_LIMIT}
/>
{/* 상세 */}
<EventModal
modalType={modalType}
detailView={modalState.detailModal}
handleDetailView={() =>{
handleModalClose('detail');
handleSearch(updateSearchParams);
}}
content={detailData}
setDetailData={setDetailData}
eventActionData={eventActionData}
/>
<LogDetailModal
viewMode="changed"
detailView={modalState.historyModal}
handleDetailView={() => handleModalClose('history')}
changedData={historyData}
title="히스토리"
/>
</AnimatedPageWrapper>
)
};
export default Event;
export default withAuth(authType.worldEventRead)(Event);

View File

@@ -0,0 +1,291 @@
import React, { useState, Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import {
RewardEventDelete,
RewardEventDetailView, LogHistory,
} from '../../apis';
import { authList } from '../../store/authList';
import { authType, commonStatus, modalTypes, eventStatus } from '../../assets/data';
import { Title, FormWrapper, TableStyle, MailTitle, TableWrapper, TextInput, InputItem } from '../../styles/Components';
import CheckBox from '../../components/common/input/CheckBox';
import Button from '../../components/common/button/Button';
import RewardEventDetailModal from '../../components/modal/RewardEventDetailModal';
import Pagination from '../../components/common/Pagination/Pagination';
import 'react-datepicker/dist/react-datepicker.css';
import DynamicModal from '../../components/common/modal/DynamicModal';
import AuthModal from '../../components/common/modal/AuthModal';
import ViewTableInfo from '../../components/common/Table/ViewTableInfo';
import { convertKTC, timeDiffMinute } from '../../utils';
import {
ModalInputItem,
ModalSubText,
RegistInputItem,
StatusLabel, StatusWapper,
} from '../../styles/ModuleComponents';
import { useLoading } from '../../context/LoadingProvider';
import { useAlert } from '../../context/AlertProvider';
import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage';
import { useModal, useTable } from '../../hooks/hook';
import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
import { alertTypes } from '../../assets/data/types';
import useCommonSearchOld from '../../hooks/useCommonSearchOld';
import { historyTables } from '../../assets/data/data';
import LogDetailModal from '../../components/common/modal/LogDetailModal';
import {AnimatedPageWrapper} from '../../components/common/Layout';
const RewardEvent = () => {
const token = sessionStorage.getItem('token');
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const {withLoading} = useLoading();
const {showToast} = useAlert();
const navigate = useNavigate();
const [detailData, setDetailData] = useState('');
const [historyData, setHistoryData] = useState({});
const {
modalState,
handleModalView,
handleModalClose
} = useModal({
detail: 'hidden',
delete: 'hidden',
history: 'hidden'
});
const [deleteDesc, setDeleteDesc] = useState('');
const {
config,
searchParams,
data: dataList,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange,
handleOrderByChange,
updateSearchParams,
configLoaded
} = useCommonSearchOld("rewardEventSearch");
const {
selectedRows,
handleSelectRow,
isRowSelected
} = useTable(dataList?.list || [], {mode: 'single'});
// 상세보기 호출
const handleDetailModal = async (e, id) => {
await RewardEventDetailView(token, id).then(data => {
setDetailData(data);
});
e.preventDefault();
handleModalView('detail');
};
const handleModalSubmit = async (type, param = null) => {
switch (type) {
case "history":
const params = {};
params.db_type = "MYSQL"
params.sql_id = param.id;
params.table_name = historyTables.rewardEvent
await LogHistory(token, params).then(data => {
setHistoryData(data);
handleModalView('history');
});
break;
case "delete":
const delete_check = selectedRows.every(row => {
const timeDiff = timeDiffMinute(convertKTC(row.start_dt), (new Date));
return row.add_flag || (timeDiff < 30);
});
if(delete_check){
showToast('EVENT_TIME_LIMIT_UPDATE', {type: alertTypes.warning});
return;
}
handleModalView('delete');
break;
case "deleteConfirm":
let list = [];
if(deleteDesc.length === 0){
showToast('INPUT_REASON_EMPTY_WARNING', {type: alertTypes.warning});
return;
}
let isChecked = false;
selectedRows.map(data => {
const row = dataList.list.find(row => row.id === Number(data.id));
if(row.status !== commonStatus.wait) isChecked = true;
list.push({
id: data.id,
delete_desc: deleteDesc
});
});
handleModalClose('delete');
setDeleteDesc('');
if(isChecked) {
showToast('EVENT_WARNING_DELETE', {type: alertTypes.warning});
return;
}
await withLoading(async () => {
return await RewardEventDelete(token, list);
}).then(data => {
showToast('DEL_COMPLETE', {type: alertTypes.success});
}).catch(error => {
}).finally(() => {
handleSearch(updateSearchParams);
})
break;
}
}
return (
<>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventRead) ? (
<AuthModal />
) : (
<AnimatedPageWrapper>
<Title>출석 보상 이벤트 관리</Title>
<FormWrapper>
<CommonSearchBar
config={config}
searchParams={searchParams}
onSearch={(newParams, executeSearch = true) => {
if (executeSearch) {
handleSearch(newParams);
} else {
updateSearchParams(newParams);
}
}}
onReset={handleReset}
/>
</FormWrapper>
<ViewTableInfo total={dataList?.total} total_all={dataList?.total_all} handleOrderBy={handleOrderByChange} handlePageSize={handlePageSizeChange}>
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventDelete) && (
<Button theme={selectedRows.length === 0 ? 'disable' : 'line'} text="선택 삭제" handleClick={() => handleModalSubmit('delete')} />
)}
{userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) && (
<Button
theme="primary"
text="이벤트 등록"
type="button"
handleClick={e => {
e.preventDefault();
navigate('/servicemanage/rewardevent/eventregist');
}}
/>
)}
</ViewTableInfo>
<TableWrapper>
<TableStyle>
<caption></caption>
<thead>
<tr>
<th width="40">
</th>
<th width="80">번호</th>
<th width="100">이벤트 상태</th>
<th width="210">시작 일시</th>
<th width="210">종료 일시</th>
<th>우편 제목</th>
<th width="110">확인 / 수정</th>
<th width="200">히스토리</th>
</tr>
</thead>
<tbody>
{dataList?.list?.map(event => (
<Fragment key={event.row_num}>
<tr>
<td>
<CheckBox name={'select'} id={event.id}
setData={(e) => handleSelectRow(e, event)}
checked={isRowSelected(event.id)} />
</td>
<td>{event.row_num}</td>
<StatusWapper>
<StatusLabel $status={event.status}>
{eventStatus.map(data => data.value === event.status && data.name)}
</StatusLabel>
</StatusWapper>
<td>{convertKTC(event.start_dt)}</td>
<td>{convertKTC(event.end_dt)}</td>
<MailTitle>{event.title}</MailTitle>
<td>
<Button theme="line" text="상세보기"
handleClick={e => handleDetailModal(e, event.id)} />
</td>
<td><Button theme="line" text="히스토리"
handleClick={e => handleModalSubmit('history', event)} />
</td>
</tr>
</Fragment>
))}
</tbody>
</TableStyle>
</TableWrapper>
<Pagination postsPerPage={searchParams.pageSize} totalPosts={dataList?.total_all} setCurrentPage={handlePageChange} currentPage={searchParams.currentPage} pageLimit={INITIAL_PAGE_LIMIT} />
{/*상세*/}
<RewardEventDetailModal
detailView={modalState.detailModal}
handleDetailView={() =>{
handleModalClose('detail');
handleSearch(updateSearchParams);
}}
content={detailData}
setDetailData={setDetailData}
/>
<LogDetailModal
viewMode="changed"
detailView={modalState.historyModal}
handleDetailView={() => handleModalClose('history')}
changedData={historyData}
title="히스토리"
/>
<DynamicModal
modalType={modalTypes.childOkCancel}
view={modalState.deleteModal}
handleCancel={() => handleModalClose('delete')}
handleSubmit={() => handleModalSubmit('deleteConfirm')}
>
<ModalInputItem>
{t('EVENT_SELECT_DELETE')}
<RegistInputItem>
<TextInput
placeholder="사유 입력"
maxLength="30"
value={deleteDesc}
onChange={e => {
if (e.target.value.length > 30) return;
setDeleteDesc(e.target.value.trimStart())
}}
/>
</RegistInputItem>
<ModalSubText $color={deleteDesc.length > 29 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({deleteDesc.length}/30)</ModalSubText>
</ModalInputItem>
</DynamicModal>
</AnimatedPageWrapper>
)}
</>
);
};
export default RewardEvent;

View File

@@ -0,0 +1,467 @@
import React, { useState, Fragment, useEffect } from 'react';
import Button from '../../components/common/button/Button';
import {
Title,
BtnWrapper,
TextInput,
SelectInput,
Label,
InputLabel,
Textarea,
SearchBarAlert,
} from '../../styles/Components';
import { useNavigate } from 'react-router-dom';
import { EventIsItem, RewardEventSingleRegist } from '../../apis';
import { authList } from '../../store/authList';
import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import {
AppendRegistBox, AppendRegistTable, AreaBtnClose,
BtnDelete,
Item,
ItemList, LangArea, ModalItem, ModalItemList, RegistGroup,
RegistInputItem,
RegistInputRow, RegistNotice, RegistTable,
} from '../../styles/ModuleComponents';
import AuthModal from '../../components/common/modal/AuthModal';
import { authType, benItems, currencyItemCode } from '../../assets/data';
import DateTimeInput from '../../components/common/input/DateTimeInput';
import { timeDiffMinute } from '../../utils';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes, currencyCodeTypes } from '../../assets/data/types';
import { useLoading } from '../../context/LoadingProvider';
import { AnimatedPageWrapper } from '../../components/common/Layout';
const RewardEventRegist = () => {
const navigate = useNavigate();
const userInfo = useRecoilValue(authList);
const { t } = useTranslation();
const token = sessionStorage.getItem('token');
const { showToast, showModal } = useAlert();
const { withLoading} = useLoading();
const [item, setItem] = useState(''); // 아이템 값
const [itemCount, setItemCount] = useState(''); // 아이템 개수
const [resource, setResource] = useState(currencyCodeTypes.gold); // 자원 값
const [resourceCount, setResourceCount] = useState(''); // 자원 개수
const [isNullValue, setIsNullValue] = useState(false);
const [btnValidation, setBtnValidation] = useState(false);
const [itemCheckMsg, setItemCheckMsg] = useState('');
const [time, setTime] = useState({
start_hour: '00',
start_min: '00',
end_hour: '00',
end_min: '00',
}); //시간 정보
const [resultData, setResultData] = useState({
is_reserve: false,
start_dt: '',
end_dt: '',
event_type: 'ATTD',
mail_list: [
{
title: '',
content: '',
language: 'KO',
},
{
title: '',
content: '',
language: 'EN',
},
{
title: '',
content: '',
language: 'JA',
}
],
item_list: [],
guid: '',
}); //데이터 정보
useEffect(() => {
if (checkCondition()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
useEffect(() => {
setItemCheckMsg('');
}, [item]);
const combineDateTime = (date, hour, min) => {
if (!date) return null;
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hour, min);
};
// 날짜 처리
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`] ? resultData[`${type}_dt`] : new Date();
setResultData({
...resultData,
[`${type}_dt`]: combineDateTime(date, newTime[`${type}_hour`], newTime[`${type}_min`]),
});
};
// 아이템 수량 숫자 체크
const handleItemCount = e => {
if (e.target.value === '0' || e.target.value === '-0') {
setItemCount('1');
e.target.value = '1';
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
setItemCount(plusNum);
} else {
setItemCount(e.target.value);
}
};
// 아이템 추가
const handleItemList = async () => {
if(benItems.includes(item)){
showToast('MAIL_ITEM_ADD_BEN', {type: alertTypes.warning});
return;
}
if(item.length === 0 || itemCount.length === 0) return;
const token = sessionStorage.getItem('token');
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) {
showToast('MAIL_ITEM_ADD_DUPL', {type: alertTypes.warning});
return;
}
const newItem = { item: item, item_cnt: itemCount, item_name: result.data.data.item_info.item_name };
resultData.item_list.push(newItem);
setItem('');
setItemCount('');
};
// 추가된 아이템 삭제
const onItemRemove = id => {
let filterList = resultData.item_list && resultData.item_list.filter(item => item !== resultData.item_list[id]);
setResultData({ ...resultData, item_list: filterList });
};
// 입력창 삭제
const onLangDelete = language => {
let filterList = resultData.mail_list && resultData.mail_list.filter(el => el.language !== language);
if (filterList.length === 1) {
setBtnValidation(true);
} else {
setBtnValidation(false);
}
setResultData({ ...resultData, mail_list: filterList });
};
// 자원 수량 숫자 체크
const handleResourceCount = e => {
if (e.target.value === '0' || e.target.value === '-0') {
setResourceCount('1');
e.target.value = '1';
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
setResourceCount(plusNum);
} else {
setResourceCount(e.target.value);
}
};
// 자원 추가
const handleResourceList = (e) => {
if(resource.length === 0 || resourceCount.length === 0) return;
const itemIndex = resultData.item_list.findIndex(
(item) => item.item === resource
);
if (itemIndex !== -1) {
const item_cnt = resultData.item_list[itemIndex].item_cnt;
resultData.item_list[itemIndex].item_cnt = Number(item_cnt) + Number(resourceCount);
} else {
const name = currencyItemCode.find(well => well.value === resource).name;
const newItem = { item: resource, item_cnt: resourceCount, item_name: name };
resultData.item_list.push(newItem);
}
setResourceCount('');
};
const handleSubmit = async (type, param = null) => {
switch (type) {
case "submit":
if (!checkCondition()) return;
const timeDiff = timeDiffMinute(resultData.start_dt, (new Date))
if(timeDiff < 60) {
showToast('EVENT_TIME_LIMIT_ADD', {type: alertTypes.warning});
return;
}
showModal('', {
type: alertTypes.confirmChildren,
onConfirm: () => handleSubmit('registConfirm'),
children: <ModalItem>
{t('EVENT_REGIST_CONFIRM')}
{resultData.item_list && (
<ModalItemList>
{resultData.item_list.map((data, index) => {
return (
<Item key={index}>
<span>
{data.item_name} {data.item_cnt.toLocaleString()}
</span>
</Item>
);
})}
</ModalItemList>
)}
</ModalItem>
});
break;
case "registConfirm":
await withLoading(async () => {
return await RewardEventSingleRegist(token, resultData);
}).then((result) => {
showToast('REGIST_COMPLTE', {type: alertTypes.success});
}).catch(() => {
showToast('API_FAIL', {type: alertTypes.error});
}).finally(() => {
callbackPage();
});
break;
}
}
const callbackPage = () => {
navigate('/servicemanage/rewardevent');
}
const checkCondition = () => {
return (
resultData.mail_list.every(data => data.content !== '' && data.title !== '') &&
(resultData.start_dt.length !== 0) &&
(resultData.end_dt.length !== 0)
);
};
return (
<AnimatedPageWrapper>
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.eventUpdate) ? (
<AuthModal/>
) : (
<>
<Title>이벤트 등록</Title>
<RegistGroup>
<RegistInputRow>
<RegistInputItem>
<InputLabel>이벤트 타입</InputLabel>
<SelectInput onChange={e => setResultData({ ...resultData, event_type: e.target.value })} value={resultData.event_type}>
<option value="ATTD">출석 이벤트</option>
</SelectInput>
</RegistInputItem>
<DateTimeInput
title="시작 시간"
dateName="시작 일자"
selectedDate={resultData.start_dt}
handleSelectedDate={data => handleDateChange(data, 'start')}
onChange={e => handleTimeChange(e, 'start')}
/>
<DateTimeInput
title="종료 시간"
dateName="종료 일자"
selectedDate={resultData.end_dt}
handleSelectedDate={data => handleDateChange(data, 'end')}
onChange={e => handleTimeChange(e, 'end')}
/>
</RegistInputRow>
</RegistGroup>
{resultData.mail_list.map((data, idx) => {
return (
<Fragment key={idx}>
<AppendRegistBox>
<LangArea>
언어 : {data.language}
{btnValidation === false ? (
<AreaBtnClose
onClick={e => {
e.preventDefault();
onLangDelete(data.language);
}}
/>
) : (
<AreaBtnClose opacity="10%" />
)}
</LangArea>
<RegistTable>
<tbody>
<tr>
<th width="120">
<Label>제목</Label>
</th>
<td>
<RegistInputItem>
<TextInput
placeholder="우편 제목 입력"
maxLength="30"
id={data.language}
value={data.title}
onChange={e => {
if (e.target.value.length > 30) {
return;
}
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 });
}}
/>
</RegistInputItem>
<RegistNotice $color={data.title.length > 29 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({data.title.length}/30)</RegistNotice>
</td>
</tr>
<tr>
<th>
<Label>내용</Label>
</th>
<td>
<Textarea
maxLength="2000"
value={data.content}
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 });
}}
/>
<RegistNotice $color={data.content.length > 1999 ? 'red' : '#666'}>* 최대 등록 가능 글자수 ({data.content.length}/2000)</RegistNotice>
</td>
</tr>
</tbody>
</RegistTable>
</AppendRegistBox>
</Fragment>
);
})}
<AppendRegistBox>
<AppendRegistTable>
<tbody>
<tr>
<th width="120">
<Label>아이템 첨부</Label>
</th>
<td>
<RegistInputItem>
<TextInput placeholder="Item Meta id 입력" value={item} onChange={e => setItem(e.target.value.trimStart())} />
<TextInput placeholder="수량" type="number" value={itemCount} onChange={e => handleItemCount(e)} width="100px" />
<Button text="추가" theme={itemCount.length === 0 || item.length === 0 ? 'disable' : 'search'} handleClick={handleItemList} width="100px" height="35px" />
{itemCheckMsg && <SearchBarAlert>{itemCheckMsg}</SearchBarAlert>}
</RegistInputItem>
</td>
</tr>
<tr>
<th width="120">
<Label>자원 첨부</Label>
</th>
<td>
<RegistInputItem>
<SelectInput onChange={e => setResource(e.target.value)} value={resource}>
{currencyItemCode.filter(data => data.value !== currencyCodeTypes.calium).map((data, index) => (
<option key={index} value={data.value}>
{data.name}
</option>
))}
</SelectInput>
<TextInput placeholder="수량" type="number" value={resourceCount} onChange={e => handleResourceCount(e)} width="200px" />
<Button text="추가" theme={resourceCount.length === 0 || resource.length === 0 ? 'disable' : 'search'} handleClick={handleResourceList} width="100px" height="35px" />
</RegistInputItem>
<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>
<BtnDelete onClick={() => onItemRemove(index)}></BtnDelete>
</Item>
);
})}
</ItemList>
)}
</div>
</td>
</tr>
</tbody>
</AppendRegistTable>
</AppendRegistBox>
{isNullValue && (
<SearchBarAlert $align="right" $padding="0 0 15px">
{t('NULL_MSG')}
</SearchBarAlert>
)}
<BtnWrapper $justify="flex-end" $gap="10px">
<Button
text="취소"
theme="line"
handleClick={() => showModal('EVENT_REGIST_CANCEL', {
type: alertTypes.confirm,
onConfirm: () => callbackPage()
})}
/>
<Button
type="submit"
text="등록"
theme={checkCondition() ? 'primary' : 'disable'}
handleClick={() => handleSubmit('submit')}
/>
</BtnWrapper>
</>
)}
</AnimatedPageWrapper>
);
};
export default RewardEventRegist;