모달 스크롤 추가

detailGrid 수정
전투이벤트, 랜드경매, 이벤트, 메일 상세 수정
랜드경매 예약종료일 제거
This commit is contained in:
2025-08-09 09:50:14 +09:00
parent f4b629df52
commit 5143b45610
10 changed files with 1071 additions and 1219 deletions

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

@@ -4,6 +4,7 @@ import styled from 'styled-components';
import dayjs from 'dayjs';
import { AnimatedTabs } from '../index';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
/**
* 위치 지정 가능한 그리드 형태 상세 정보 표시 컴포넌트
@@ -51,12 +52,15 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
handler,
min,
max,
step,
format,
required,
showTime,
tabItems,
activeKey,
onTabChange
onTabChange,
maxLength,
rows: textareaRows
} = item;
// 현재 값 가져오기 (formData에서 또는 항목에서)
@@ -85,10 +89,35 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
value={currentValue}
min={min}
max={max}
step={step || 1}
onChange={(value) => onChange(key, value, handler)}
placeholder={placeholder || `${label} 입력`}
/>;
case 'display':
return <Input
{...commonProps}
value={currentValue || ''}
readOnly
style={{
...commonProps.style,
backgroundColor: '#f5f5f5',
cursor: 'default'
}}
placeholder={placeholder || ''}
/>;
case 'textarea':
return <TextArea
{...commonProps}
value={currentValue || ''}
onChange={(e) => onChange(key, e.target.value, handler)}
placeholder={placeholder}
maxLength={maxLength}
rows={textareaRows || 4}
showCount={!!maxLength}
/>;
case 'select':
return (
<Select
@@ -99,7 +128,7 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
>
{options && options.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
{option.name}
</Select.Option>
))}
</Select>
@@ -131,12 +160,25 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
currentValue.end ? dayjs(currentValue.end) : null
] : null)}
format={format || 'YYYY-MM-DD HH:mm:ss'}
onChange={(dates, dateStrings) => {
if (dates) {
onChange={(dates) => {
if (dates && dates.length === 2) {
// 두 개의 별도 필드에 각각 업데이트
if (item.keys) {
onChange(item.keys.start, dates[0], handler);
onChange(item.keys.end, dates[1], handler);
// 두 개의 onChange를 순차적으로 호출하는 대신
// 한 번에 두 필드를 모두 업데이트하는 방식으로 변경
const updatedData = {
...formData,
[item.keys.start]: dates[0],
[item.keys.end]: dates[1]
};
// handler가 있으면 handler 실행, 없으면 직접 onChange 호출
if (handler) {
handler(dates, key, updatedData);
} else {
// onChange를 통해 전체 업데이트된 데이터를 전달
onChange('dateRange_update', updatedData, null);
}
} else {
// 기존 방식 지원 (하위 호환성)
onChange(key, {
@@ -147,8 +189,17 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
} else {
// 두 필드 모두 비우기
if (item.keys) {
onChange(item.keys.start, null, handler);
onChange(item.keys.end, null, handler);
const updatedData = {
...formData,
[item.keys.start]: null,
[item.keys.end]: null
};
if (handler) {
handler(null, key, updatedData);
} else {
onChange('dateRange_update', updatedData, null);
}
} else {
onChange(key, { start: null, end: null }, handler);
}
@@ -204,8 +255,17 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
case 'custom':
return item.render ? item.render(formData, onChange) : null;
case 'label':
default:
return <div>{currentValue}</div>;
return <div style={{
padding: '4px 11px',
minHeight: '32px',
lineHeight: '24px',
fontSize: '15px',
color: currentValue ? '#000' : '#bfbfbf'
}}>
{currentValue || placeholder || ''}
</div>;
}
};

View File

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

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

@@ -38,6 +38,8 @@ const ModalBg = styled(motion.div)`
min-width: 1080px;
display: ${props => (props.$view === 'hidden' ? 'none' : 'block')};
z-index: 20;
overflow-y: auto;
overflow-x: auto;
`;
const ModalContainer = styled.div`
@@ -52,9 +54,18 @@ const ModalWrapper = styled(motion.div)`
min-width: ${props => props.min || 'auto'};
padding: ${props => props.$padding || '30px'};
border-radius: 30px;
max-height: 90%;
max-height: calc(100vh - 40px);
overflow: auto;
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 }) => {

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 Button from '../common/button/Button';
@@ -18,7 +18,7 @@ import {
FormStatusWarning,
FormButtonContainer,
} 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 { convertKTCDate } from '../../utils';
import {
@@ -64,16 +64,6 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}
}, [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(() => {
if (checkCondition()) {
setIsNullValue(false);
@@ -82,104 +72,12 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}
}, [resultData]);
// 시작 날짜 변경 핸들러
const handleStartDateChange = (date) => {
if (!date) return;
const newDate = new Date(date);
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 handleEndTimeChange = (time) => {
if (!time) return;
const newDateTime = resultData.event_end_time
? new Date(resultData.event_end_time)
: new Date();
newDateTime.setHours(
time.getHours(),
time.getMinutes(),
0,
0
);
setResultData(prev => ({
...prev,
event_end_time: 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 opGameMode = useMemo(() => {
return gameModeData?.map(item => ({
value: item.id,
name: `${item.desc}(${item.id})`
})) || [];
}, [gameModeData]);
const handleReset = () => {
setDetailData({});
@@ -282,6 +180,7 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
return (
resultData.event_start_dt !== ''
&& resultData.group_id !== ''
&& resultData.game_mode_id > 0
&& resultData.event_name !== ''
&& (resultData.repeat_type === 'NONE' || (resultData.repeat_type !== 'NONE' && resultData.event_end_dt !== ''))
);
@@ -310,122 +209,115 @@ const BattleEventModal = ({ modalType, detailView, handleDetailView, content, se
}
}
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 (
<>
<Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "전투시스템 이벤트 등록" : isView('modify') ? "전투시스템 이벤트 수정" : "전투시스템 이벤트 상세"}</Title>
<MessageWrapper>
<FormRowGroup>
<FormLabel>그룹 ID</FormLabel>
<FormInput
type="text"
disabled={!isView('group')}
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}
/>
</FormRowGroup>
<FormRowGroup>
<SingleTimePicker
label="시작시간"
disabled={!isView('start_dt')}
selectedTime={resultData?.event_start_dt}
onTimeChange={handleStartTimeChange}
/>
<FormLabel>진행시간()</FormLabel>
<FormInput
type="number"
disabled={!isView('operation_time')}
width='100px'
min={10}
value={resultData?.event_operation_time}
onChange={e => setResultData({ ...resultData, event_operation_time: e.target.value })}
/>
</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>
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
</MessageWrapper>
<BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar>
<FormStatusLabel>
@@ -482,7 +374,7 @@ export const initData = {
reward_group_id: 1,
round_count: 1,
hot_time: 1,
game_mode_id: 1,
game_mode_id: '',
event_start_dt: '',
event_end_dt: '',
event_operation_time: 10

View File

@@ -1,6 +1,7 @@
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 Modal from '../common/modal/Modal';
import { EventIsItem, EventModify } from '../../apis';
@@ -10,16 +11,13 @@ import { useRecoilValue } from 'recoil';
import { useTranslation } from 'react-i18next';
import { authType, benItems, commonStatus, currencyItemCode } from '../../assets/data';
import {
AppendRegistBox, AppendRegistTable, AreaBtnClose,
BtnDelete, DetailInputItem, DetailInputRow,
DetailModalWrapper, RegistGroup, DetailRegistInfo, DetailState,
Item, ItemList, LangArea
DetailRegistInfo, DetailState
} from '../../styles/ModuleComponents';
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../utils';
import DateTimeInput from '../common/input/DateTimeInput';
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 EventDetailModal = ({ detailView, handleDetailView, content, setDetailData }) => {
const userInfo = useRecoilValue(authList);
@@ -31,21 +29,14 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
const id = content && content.id;
const updateAuth = userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === authType.eventUpdate);
const [time, setTime] = useState({
start_hour: '00',
start_min: '00',
end_hour: '00',
end_min: '00',
}); //시간 정보
const [activeLanguage, setActiveLanguage] = useState('KO');
const [item, setItem] = useState('');
const [itemCount, setItemCount] = useState('');
const [itemCount, setItemCount] = useState(1);
const [resource, setResource] = useState('19010001');
const [resourceCount, setResourceCount] = useState('');
const [resourceCount, setResourceCount] = useState(1);
const [resultData, setResultData] = useState({});
const [isNullValue, setIsNullValue] = useState(false);
// 과거 판단
const [isPast, setIsPast] = useState(false);
const [isChanged, setIsChanged] = useState(false);
@@ -65,13 +56,8 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
event_type: content.event_type,
mail_list: content.mail_list,
item_list: content.item_list,
});
setTime({ ...time,
start_hour: String(start_dt_KTC.getHours()).padStart(2, '0'),
start_min: String(start_dt_KTC.getMinutes()).padStart(2, '0'),
end_hour: String(end_dt_KTC.getHours()).padStart(2, '0'),
end_min: String(end_dt_KTC.getMinutes()).padStart(2, '0')
status: content.status,
delete_desc: content.delete_desc
});
start_dt_KTC < (new Date) ? setIsPast(true) : setIsPast(false);
@@ -90,29 +76,97 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
}
}, [updateAuth, isPast]);
useEffect(() => {
if (conditionCheck()) {
setIsNullValue(false);
} else {
setIsNullValue(true);
}
}, [resultData]);
useEffect(() => {
setItemCheckMsg('');
}, [item]);
// 아이템 수량 숫자 체크
const handleItemCount = e => {
if (e.target.value === '0' || e.target.value === '-0') {
setItemCount('1');
e.target.value = '1';
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
setItemCount(plusNum);
} else {
setItemCount(e.target.value);
const 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);
};
// 아이템 추가
@@ -152,19 +206,6 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
setResultData({ ...resultData, item_list: filterList });
};
// 자원 수량 숫자 체크
const handleResourceCount = e => {
if (e.target.value === '0' || e.target.value === '-0') {
setResourceCount('1');
e.target.value = '1';
} else if (e.target.value < 0) {
let plusNum = Math.abs(e.target.value);
setResourceCount(plusNum);
} else {
setResourceCount(e.target.value);
}
};
// 자원 추가
const handleResourceList = (e) => {
if(resource.length === 0 || resourceCount.length === 0) return;
@@ -186,45 +227,9 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
setResourceCount('');
};
// 입력창 삭제
const onLangDelete = language => {
let filterList = resultData.mail_list && resultData.mail_list.filter(el => el.language !== language);
if (filterList.length === 1) setBtnValidation(true);
setIsChanged(true);
setResultData({ ...resultData, mail_list: filterList });
};
// 날짜 처리
const handleDateChange = (data, type) => {
const date = new Date(data);
setResultData({
...resultData,
[`${type}_dt`]: combineDateTime(date, time[`${type}_hour`], time[`${type}_min`]),
});
setIsChanged(true);
};
// 시간 처리
const handleTimeChange = (e, type) => {
const { id, value } = e.target;
const newTime = { ...time, [`${type}_${id}`]: value };
setTime(newTime);
const date = resultData[`${type}_dt`] ? new Date(resultData[`${type}_dt`]) : new Date();
setResultData({
...resultData,
[`${type}_dt`]: combineDateTime(date, newTime[`${type}_hour`], newTime[`${type}_min`]),
});
setIsChanged(true);
};
// 확인 버튼 후 다 초기화
const handleReset = () => {
setBtnValidation(false);
setIsNullValue(false);
setIsChanged(false);
};
@@ -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 (
<>
<Modal min="960px" $view={detailView}>
@@ -297,201 +493,22 @@ const EventDetailModal = ({ detailView, handleDetailView, content, setDetailData
)}
</DetailRegistInfo>
}
<DetailModalWrapper>
{content &&
<RegistGroup>
<DetailInputRow>
<DateTimeInput
title="이벤트 기간"
dateName="시작 일자"
selectedDate={convertKTCDate(content.start_dt)}
handleSelectedDate={data => handleDateChange(data, 'start')}
onChange={e => handleTimeChange(e, 'start')}
/>
<DateTimeInput
dateName="종료 일자"
selectedDate={convertKTCDate(content.end_dt)}
handleSelectedDate={data => handleDateChange(data, 'end')}
onChange={e => handleTimeChange(e, 'end')}
/>
</DetailInputRow>
<DetailInputRow>
<DetailInputItem>
<InputLabel>이벤트 상태</InputLabel>
<div>{detailState(content.status)}</div>
</DetailInputItem>
{content.status === commonStatus.delete &&
<DetailInputItem>
<InputLabel>삭제 사유</InputLabel>
<div>{content.delete_desc}</div>
</DetailInputItem>
}
</DetailInputRow>
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
</RegistGroup>
}
{resultData.mail_list &&
resultData.mail_list.map(data => {
return (
<Fragment key={data.language}>
<AppendRegistBox>
<LangArea>
언어 : {data.language}
{btnValidation === false && !isReadOnly ? (
<AreaBtnClose
onClick={e => {
e.preventDefault();
onLangDelete(data.language);
}}
/>
) : (
<AreaBtnClose opacity="10%" />
)}
</LangArea>
<AppendRegistTable>
<tbody>
<tr>
<th width="120">
<Label>제목</Label>
</th>
<td>
<DetailInputItem>
<TextInput
placeholder="우편 제목 입력"
maxLength="30"
id={data.language}
value={data.title}
readOnly={isReadOnly}
onChange={e => {
let list = [...resultData.mail_list];
let findIndex = resultData.mail_list && resultData.mail_list.findIndex(item => item.language === e.target.id);
list[findIndex].title = e.target.value.trimStart();
setResultData({ ...resultData, mail_list: list });
setIsChanged(true);
}}
/>
</DetailInputItem>
</td>
</tr>
<tr>
<th>
<Label>내용</Label>
</th>
<td>
<Textarea
value={data.content}
readOnly={isReadOnly}
id={data.language}
onChange={e => {
if (e.target.value.length > 2000) {
return;
}
let list = [...resultData.mail_list];
let findIndex = resultData.mail_list && resultData.mail_list.findIndex(item => item.language === e.target.id);
list[findIndex].content = e.target.value.trimStart();
setResultData({ ...resultData, mail_list: list });
setIsChanged(true);
}}
/>
</td>
</tr>
</tbody>
</AppendRegistTable>
</AppendRegistBox>
</Fragment>
);
})}
<AppendRegistBox>
<AppendRegistTable>
<tbody>
<tr>
<th width="120">
<Label>아이템 첨부</Label>
</th>
<td>
<DetailInputItem>
<TextInput
placeholder="Item Meta id 입력"
value={item}
onChange={e => {
let list = [];
list = e.target.value.trimStart();
setItem(list);
}}
disabled={isReadOnly}
/>
<TextInput
placeholder="수량"
value={itemCount}
type="number"
onChange={e => handleItemCount(e)}
width="90px"
disabled={isReadOnly}
/>
<Button
text="추가"
theme={itemCount.length === 0 || item.length === 0 ? 'disable' : 'search'}
handleClick={handleItemList}
/>
{itemCheckMsg && <SearchBarAlert>{itemCheckMsg}</SearchBarAlert>}
</DetailInputItem>
</td>
</tr>
<tr>
<th width="120">
<Label>자원 첨부</Label>
</th>
<td>
<DetailInputItem>
<SelectInput onChange={e => setResource(e.target.value)} value={resource} disabled={isReadOnly}>
{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>
{itemCheckMsg && (
<Alert
message={itemCheckMsg}
type="error"
style={{ marginTop: '8px', width: '300px' }}
/>
)}
<BtnWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
<Button
text="확인"

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 Button from '../common/button/Button';
import Loading from '../common/Loading';
@@ -21,7 +21,7 @@ import {
NoticeInputItem2, BoxWrapper, FormStatusBar, FormStatusLabel, FormStatusWarning, FormButtonContainer,
} from '../../styles/ModuleComponents';
import { modalTypes } from '../../assets/data';
import {DynamicModal, Modal, DateTimeRangePicker} from '../common';
import { DynamicModal, Modal, DateTimeRangePicker, DetailLayout } from '../common';
import { LandAuctionModify, LandAuctionSingleRegist } from '../../apis';
import {
AUCTION_MIN_MINUTE_TIME,
@@ -36,6 +36,7 @@ import { convertKTCDate } from '../../utils';
import { msToMinutes } from '../../utils/date';
import { useAlert } from '../../context/AlertProvider';
import { alertTypes } from '../../assets/data/types';
import { battleEventHotTime, battleRepeatType } from '../../assets/data/options';
const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, setDetailData, landData, buildingData }) => {
const { t } = useTranslation();
@@ -62,7 +63,6 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
currency_type: content.currency_type,
start_price: content.start_price,
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_end_dt: convertKTCDate(content.auction_end_dt),
});
@@ -85,52 +85,26 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
}
}, [resetDateTime]);
// 입력 수량 처리
const handleCount = e => {
const regex = /^\d*\.?\d{0,2}$/;
if (!regex.test(e.target.value) && e.target.value !== '-') {
return;
}
const opLand = useMemo(() => {
return landData?.map(item => ({
value: item.id,
name: `${item.name}(${item.id})`
})) || [];
}, [landData]);
let count = 0;
if (e.target.value === '-0') {
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 handleLand = (value, key, currentFormData) => {
let land_id = value;
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 handleLand = e => {
const land_id = e.target.value;
const land = landData.find(land => land.id === parseInt(land_id));
const instance = buildingData.find(building => building.id === parseInt(land.buildingId))?.socket;
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 = () => {
@@ -236,7 +210,7 @@ const LandAuctionModal = ({ modalType, detailView, handleDetailView, content, se
const isView = (label) => {
switch (label) {
case "recv":
case "resv":
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY && content?.status === landAuctionStatusType.wait);
case "auction":
case "price":
@@ -256,95 +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 (
<>
<Modal min="760px" $view={detailView}>
<Title $align="center">{isView('registry') ? "랜드 경매 등록" : isView('modify') ? "랜드 경매 수정" : "랜드 경매 상세"}</Title>
<MessageWrapper>
<FormRowGroup>
<FormLabel>랜드선택</FormLabel>
<SelectInput value={resultData.land_id} onChange={e => handleLand(e)} disabled={!isView('registry')} width="400px">
{landData && landData.map((data, index) => (
<option key={index} value={data.id}>
{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}
/>
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
</MessageWrapper>
<BtnWrapper $gap="10px" $marginTop="10px">
<FormStatusBar>
<FormStatusLabel>
@@ -401,7 +397,6 @@ export const initData = {
currency_type: 'Calium',
start_price: 0,
resv_start_dt: '',
resv_end_dt: '',
auction_start_dt: '',
auction_end_dt: ''
}

View File

@@ -2,6 +2,21 @@ import { styled } from 'styled-components';
import RadioInput from '../common/input/Radio';
import React, { useState, useEffect, Fragment } from 'react';
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 Button from '../common/button/Button';
@@ -32,6 +47,7 @@ import { useLoading } from '../../context/LoadingProvider';
import { alertTypes, currencyCodeTypes } from '../../assets/data/types';
import { userType2 } from '../../assets/data/options';
import { STORAGE_MAIL_COPY } from '../../assets/data/adminConstants';
import { DetailLayout } from '../common';
const MailDetailModal = ({ detailView, handleDetailView, content }) => {
const userInfo = useRecoilValue(authList);
@@ -46,10 +62,11 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
const [sendHour, setSendHour] = useState('00');
const [sendMin, setSendMin] = useState('00');
const [activeLanguage, setActiveLanguage] = useState('KO');
const [item, setItem] = useState('');
const [itemCount, setItemCount] = useState('');
const [itemCount, setItemCount] = useState(1);
const [resource, setResource] = useState(currencyCodeTypes.gold);
const [resourceCount, setResourceCount] = useState('');
const [resourceCount, setResourceCount] = useState(1);
const [resultData, setResultData] = useState({
is_reserve: false,
@@ -94,6 +111,7 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
mail_list: content.mail_list,
item_list: content.item_list,
guid: content.target,
send_status: content.send_status,
file_name: content.receive_type === 'MULTIPLE' ? content.target : null
});
@@ -122,6 +140,136 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
}
},[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 => {
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 (
<>
<Modal min="960px" $view={detailView}>
<Title $align="center">우편 상세 정보</Title>
{content && <>
<RegistInfo>
{content && <RegistInfo>
<span>등록자 : {content.create_by}</span>
<span>등록일 : {convertKTC(content.create_dt, false)}</span>
{typeof content.update_by !== 'undefined' && (
@@ -352,321 +770,14 @@ const MailDetailModal = ({ detailView, handleDetailView, content }) => {
<span>수정일 : {convertKTC(content.update_dt, false)}</span>
</>
)}
</RegistInfo>
<ModalWrapper>
<RegistGroup>
<InputRow>
<CheckBox
label="예약 발송"
id="reserve"
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>
</>}
</RegistInfo>}
<DetailLayout
itemGroups={itemGroups}
formData={resultData}
onChange={setResultData}
disabled={false}
columnCount={4}
/>
<BtnWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
<Button
text="확인"

View File

@@ -205,7 +205,6 @@ const LandAuction = () => {
{/*<th width="100">경매 재화</th>*/}
<th width="90">경매 시작가</th>
<th width="200">예약 시작일</th>
<th width="200">예약 종료일</th>
<th width="200">경매 시작일</th>
<th width="200">경매 종료일</th>
<th width="200">낙찰 정산일</th>
@@ -236,7 +235,6 @@ const LandAuction = () => {
{/*<td>{auction.currency_type}</td>*/}
<td>{auction.start_price}</td>
<td>{convertKTC(auction.resv_start_dt)}</td>
<td>{convertKTC(auction.resv_end_dt)}</td>
<td>{convertKTC(auction.auction_start_dt)}</td>
<td>{convertKTC(auction.auction_end_dt)}</td>
<td>{convertKTC(auction.close_end_dt)}</td>