메뉴배너 detailGrid 적용
This commit is contained in:
@@ -3,13 +3,17 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@hookform/resolvers": "^3.2.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"antd": "^5.26.1",
|
||||
"axios": "^1.4.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"framer-motion": "^12.19.1",
|
||||
"i18next": "^23.15.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
|
||||
611
src/components/modal/MenuBannerDetailModal.js
Normal file
611
src/components/modal/MenuBannerDetailModal.js
Normal file
@@ -0,0 +1,611 @@
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
|
||||
import {
|
||||
Title,
|
||||
SelectInput,
|
||||
BtnWrapper,
|
||||
TextInput,
|
||||
Label,
|
||||
InputLabel,
|
||||
Textarea,
|
||||
SearchBarAlert,
|
||||
ButtonGroupWrapper,
|
||||
} from '../../styles/Components';
|
||||
import Button from '../common/button/Button';
|
||||
import Modal from '../common/modal/Modal';
|
||||
import { EventIsItem, EventModify, MenuBannerModify } 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 {
|
||||
DetailInputItem, DetailInputRow,
|
||||
DetailModalWrapper, RegistGroup, DetailRegistInfo, DetailState, FormRowGroup, FormLabel, FormInput,
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { convertKTC, combineDateTime, timeDiffMinute, convertKTCDate } from '../../utils';
|
||||
import DateTimeInput from '../common/input/DateTimeInput';
|
||||
import { useLoading } from '../../context/LoadingProvider';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes, battleEventStatusType, languageNames } from '../../assets/data/types';
|
||||
import { Tabs, Image as AntImage, Spin } from 'antd';
|
||||
import { TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
|
||||
import { AntButton, DateTimeRangePicker, DetailLayout, SingleTimePicker } from '../common';
|
||||
import AnimatedTabs from '../common/control/AnimatedTabs';
|
||||
|
||||
function renderImageContent(imageData) {
|
||||
if (!imageData) {
|
||||
return <NoImagePlaceholder>이미지가 없습니다</NoImagePlaceholder>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImageWrapper>
|
||||
<AntImage
|
||||
src={imageData.title}
|
||||
alt={`${imageData.language} 배너 이미지`}
|
||||
style={{ width: '100%', maxHeight: '300px', objectFit: 'contain' }}
|
||||
placeholder={
|
||||
<AntImage
|
||||
preview={false}
|
||||
src={imageData.title}
|
||||
width={300}
|
||||
/>
|
||||
}
|
||||
// preview={{
|
||||
// mask: '미리보기',
|
||||
// maskClassName: 'custom-mask',
|
||||
// }}
|
||||
fallback="data:image/png;base64,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"
|
||||
/>
|
||||
</ImageWrapper>
|
||||
{imageData.content &&
|
||||
<ImageUrlInfo>
|
||||
<UrlLink
|
||||
href={imageData.content}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{imageData.content}
|
||||
</UrlLink>
|
||||
</ImageUrlInfo>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MenuBannerDetailModal = ({ 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.menuBannerUpdate);
|
||||
|
||||
const [time, setTime] = useState({
|
||||
start_hour: '00',
|
||||
start_min: '00',
|
||||
end_hour: '00',
|
||||
end_min: '00',
|
||||
}); //시간 정보
|
||||
|
||||
const [resultData, setResultData] = useState(initData);
|
||||
const [activeLanguage, setActiveLanguage] = useState('KO');
|
||||
// 이미지 프리로드를 위한 상태
|
||||
const [allImagesLoaded, setAllImagesLoaded] = useState(false);
|
||||
const [showTabContent, setShowTabContent] = useState(false);
|
||||
const [loadedImages, setLoadedImages] = useState([]);
|
||||
const [totalImageCount, setTotalImageCount] = useState(0);
|
||||
|
||||
const [tabItems, setTabItems] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if(content){
|
||||
console.log(content);
|
||||
const start_dt_KTC = convertKTCDate(content.start_dt);
|
||||
const end_dt_KTC = convertKTCDate(content.end_dt);
|
||||
|
||||
setResultData({
|
||||
id: content.id,
|
||||
title: content.title,
|
||||
start_dt: start_dt_KTC,
|
||||
end_dt: end_dt_KTC,
|
||||
status: content.status,
|
||||
order_id: content.order_id,
|
||||
is_link: content.is_link,
|
||||
image_list: content.image_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')
|
||||
});
|
||||
}
|
||||
|
||||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
if (content && content.image_list) {
|
||||
// 초기화
|
||||
setAllImagesLoaded(false);
|
||||
setShowTabContent(false);
|
||||
setLoadedImages([]);
|
||||
|
||||
// 이미지 개수 설정
|
||||
setTotalImageCount(content.image_list ? content.image_list.length : 0);
|
||||
|
||||
// 첫 번째 언어를 활성 언어로 설정
|
||||
if (content.image_list && content.image_list.length > 0) {
|
||||
setActiveLanguage(content.image_list[0].language);
|
||||
}
|
||||
|
||||
// 동적으로 탭 아이템 생성
|
||||
const newTabItems = content.image_list ? content.image_list.map(imageData => ({
|
||||
key: imageData.language,
|
||||
label: languageNames[imageData.language] || imageData.language,
|
||||
children: (
|
||||
<ImageContainer>
|
||||
{renderImageContent(imageData)}
|
||||
</ImageContainer>
|
||||
)
|
||||
})) : [];
|
||||
|
||||
setTabItems(newTabItems);
|
||||
|
||||
// 모든 이미지 프리로딩 시작
|
||||
setTimeout(() => {
|
||||
preloadAllImages();
|
||||
}, 100);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const preloadAllImages = () => {
|
||||
if (!content || !content.image_list || content.image_list.length === 0) {
|
||||
// 이미지가 없는 경우 바로 로딩 완료 처리
|
||||
// console.log('이미지가 없습니다. 로딩 완료 처리합니다.');
|
||||
setAllImagesLoaded(true);
|
||||
setShowTabContent(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`총 ${content.image_list.length}개의 이미지 로딩을 시작합니다.`);
|
||||
|
||||
// 이미지 개수가 0이면 로딩 완료 처리
|
||||
if (content.image_list.length === 0) {
|
||||
setAllImagesLoaded(true);
|
||||
setShowTabContent(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let loadedCount = 0;
|
||||
|
||||
// 이미지 로드 완료 이벤트 핸들러
|
||||
const handleImageLoad = (url) => {
|
||||
loadedCount++;
|
||||
// console.log(`이미지 로드 완료 (${loadedCount}/${content.image_list.length}): ${url}`);
|
||||
|
||||
// 모든 이미지가 로드되었는지 확인
|
||||
if (loadedCount >= content.image_list.length) {
|
||||
// console.log('모든 이미지 로딩 완료!');
|
||||
setAllImagesLoaded(true);
|
||||
setShowTabContent(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 각 이미지에 대해 프리로드 객체 생성
|
||||
content.image_list.forEach(img => {
|
||||
if (img.title) {
|
||||
// console.log(`이미지 로딩 시작: ${img.title}`);
|
||||
const image = new Image();
|
||||
image.onload = () => handleImageLoad(img.title);
|
||||
image.onerror = () => {
|
||||
console.log(`이미지 로드 실패: ${img.title}`);
|
||||
handleImageLoad(img.title); // 오류 시에도 카운트
|
||||
};
|
||||
image.src = img.title; // src 속성은 onload/onerror 핸들러 설정 후에 설정
|
||||
} else {
|
||||
// console.log('이미지 URL이 없습니다.');
|
||||
handleImageLoad('empty'); // URL이 없는 경우에도 카운트
|
||||
}
|
||||
});
|
||||
|
||||
// 안전장치: 5초 후에도 로딩이 완료되지 않으면 강제로 완료 처리
|
||||
setTimeout(() => {
|
||||
if (!allImagesLoaded) {
|
||||
// console.log('시간 초과로 로딩 강제 완료');
|
||||
setAllImagesLoaded(true);
|
||||
setShowTabContent(true);
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// 날짜 처리
|
||||
// const handleDateChange = (data, type) => {
|
||||
// const date = new Date(data);
|
||||
// setResultData({
|
||||
// ...resultData,
|
||||
// [`${type}_dt`]: combineDateTime(date, time[`${type}_hour`], time[`${type}_min`]),
|
||||
// });
|
||||
// };
|
||||
|
||||
// 시간 처리
|
||||
const handleTimeChange = (e, type) => {
|
||||
const { id, value } = e.target;
|
||||
const newTime = { ...time, [`${type}_${id}`]: value };
|
||||
setTime(newTime);
|
||||
|
||||
const date = resultData[`${type}_dt`] ? new Date(resultData[`${type}_dt`]) : new Date();
|
||||
|
||||
setResultData({
|
||||
...resultData,
|
||||
[`${type}_dt`]: combineDateTime(date, newTime[`${type}_hour`], newTime[`${type}_min`]),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDateChange = {
|
||||
start: (date) => {
|
||||
setResultData(prev => ({ ...prev, start_dt: date }));
|
||||
},
|
||||
end: (date) => {
|
||||
setResultData(prev => ({ ...prev, end_dt: date }));
|
||||
}
|
||||
};
|
||||
|
||||
// 확인 버튼 후 다 초기화
|
||||
const handleReset = () => {
|
||||
};
|
||||
|
||||
const checkCondition = () => {
|
||||
return (
|
||||
(resultData.start_dt.length !== 0) &&
|
||||
(resultData.end_dt.length !== 0) &&
|
||||
resultData.title !== '' &&
|
||||
resultData.order_id !== ''
|
||||
);
|
||||
};
|
||||
|
||||
// 탭 변경 핸들러
|
||||
const handleTabChange = (key) => {
|
||||
setActiveLanguage(key);
|
||||
};
|
||||
|
||||
const handleSubmit = async (type, param = null) => {
|
||||
switch (type) {
|
||||
case "submit":
|
||||
if (!checkCondition()) return;
|
||||
|
||||
showModal('MENU_BANNER_UPDATE_SAVE', {
|
||||
type: alertTypes.confirm,
|
||||
onConfirm: () => handleSubmit('updateConfirm')
|
||||
});
|
||||
break;
|
||||
case "updateConfirm":
|
||||
withLoading( async () => {
|
||||
return await MenuBannerModify(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;
|
||||
}
|
||||
};
|
||||
|
||||
//true 수정불가, false 수정가능
|
||||
const isView = (fieldName) => {
|
||||
if (!updateAuth) return false;
|
||||
|
||||
if (fieldName === 'editButton') {
|
||||
// updateAuth가 없거나 FINISH 상태면 수정 버튼 숨김 (false 반환)
|
||||
return !updateAuth || content?.status === commonStatus.finish;
|
||||
}
|
||||
|
||||
switch (content?.status) {
|
||||
case commonStatus.running:
|
||||
// RUNNING 상태일 때는 end_dt와 order_id만 수정 가능
|
||||
return fieldName !== 'date' && fieldName !== 'order_id';
|
||||
case commonStatus.wait:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const itemGroups = [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
row: 0,
|
||||
col: 0,
|
||||
colSpan: 2,
|
||||
type: 'text',
|
||||
key: 'title',
|
||||
label: '제목',
|
||||
disabled: !isView('title'),
|
||||
width: '250px',
|
||||
},
|
||||
{
|
||||
row: 0,
|
||||
col: 2,
|
||||
colSpan: 2,
|
||||
type: 'number',
|
||||
key: 'order_id',
|
||||
label: '순서',
|
||||
disabled: !isView('order_id'),
|
||||
width: '100px',
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
row: 1,
|
||||
col: 0,
|
||||
colSpan: 2,
|
||||
type: 'status',
|
||||
key: 'status',
|
||||
label: '상태',
|
||||
value: resultData.status,
|
||||
},
|
||||
{
|
||||
row: 2,
|
||||
col: 0,
|
||||
colSpan: 2,
|
||||
type: 'dateRange',
|
||||
key: 'dateRange',
|
||||
keys: {start: 'start_dt', end: 'end_dt'},
|
||||
label: '기간',
|
||||
disabled: !isView('date'),
|
||||
format: 'YYYY-MM-DD HH:mm'
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal min="960px" $view={detailView}>
|
||||
<Title $align="center">배너 상세 정보</Title>
|
||||
<DetailLayout
|
||||
itemGroups={itemGroups}
|
||||
formData={resultData}
|
||||
onChange={setResultData}
|
||||
disabled={!updateAuth}
|
||||
columnCount={4}
|
||||
/>
|
||||
{/*<DetailModalWrapper>*/}
|
||||
{/* {content &&*/}
|
||||
{/* <RegistGroup>*/}
|
||||
{/* <FormRowGroup>*/}
|
||||
{/* <DetailInputItem>*/}
|
||||
{/* <FormLabel>제목</FormLabel>*/}
|
||||
{/* <FormInput*/}
|
||||
{/* type="text"*/}
|
||||
{/* value={content.title}*/}
|
||||
{/* disabled={isView('title')}*/}
|
||||
{/* onChange={e => setResultData({ ...resultData, title: e.target.value })}*/}
|
||||
{/* width="300px"*/}
|
||||
{/* />*/}
|
||||
{/* </DetailInputItem>*/}
|
||||
{/* <DetailInputItem>*/}
|
||||
{/* <FormLabel>순서</FormLabel>*/}
|
||||
{/* <FormInput*/}
|
||||
{/* placeholder="순서번호"*/}
|
||||
{/* type="number"*/}
|
||||
{/* value={content.order_id}*/}
|
||||
{/* disabled={isView('order_id')}*/}
|
||||
{/* onChange={e => setResultData({ ...resultData, order_id: e.target.value })}*/}
|
||||
{/* width="200px"*/}
|
||||
{/* />*/}
|
||||
{/* </DetailInputItem>*/}
|
||||
{/* </FormRowGroup>*/}
|
||||
{/* <FormRowGroup>*/}
|
||||
{/* <DateTimeRangePicker*/}
|
||||
{/* label="예약기간"*/}
|
||||
{/* startDate={resultData.start_dt}*/}
|
||||
{/* endDate={resultData.end_dt}*/}
|
||||
{/* onStartDateChange={handleDateChange.start}*/}
|
||||
{/* onEndDateChange={handleDateChange.end}*/}
|
||||
{/* pastDate={new Date()}*/}
|
||||
{/* disabled={isView('date')}*/}
|
||||
{/* startLabel="시작 일자"*/}
|
||||
{/* endLabel="종료 일자"*/}
|
||||
{/* // reset={resetDateTime}*/}
|
||||
{/* />*/}
|
||||
{/* </FormRowGroup>*/}
|
||||
{/* <FormRowGroup>*/}
|
||||
{/* <DetailInputItem>*/}
|
||||
{/* <FormLabel>상태</FormLabel>*/}
|
||||
{/* <div>{detailState(content.status)}</div>*/}
|
||||
{/* </DetailInputItem>*/}
|
||||
{/* </FormRowGroup>*/}
|
||||
{/* {content.image_list && content.image_list.length > 0 && (*/}
|
||||
{/* <FormRowGroup style={{display: 'flex', justifyContent: 'center', width: '100%'}}>*/}
|
||||
{/* <DetailInputItem style={{width: '100%'}}>*/}
|
||||
{/* {!showTabContent ? (*/}
|
||||
{/* <LoadingContainer>*/}
|
||||
{/* <Spin size="large" tip="이미지 로딩 중..." />*/}
|
||||
{/* </LoadingContainer>*/}
|
||||
{/* ) : (*/}
|
||||
{/* <ContentWrapper $isLoaded={showTabContent}>*/}
|
||||
{/* <AnimatedTabs*/}
|
||||
{/* items={tabItems}*/}
|
||||
{/* activeKey={activeLanguage}*/}
|
||||
{/* onChange={handleTabChange}*/}
|
||||
{/* />*/}
|
||||
{/* </ContentWrapper>*/}
|
||||
{/* )}*/}
|
||||
|
||||
{/* </DetailInputItem>*/}
|
||||
{/* </FormRowGroup>*/}
|
||||
{/* )}*/}
|
||||
|
||||
{/* </RegistGroup>*/}
|
||||
{/* }*/}
|
||||
{/*</DetailModalWrapper>*/}
|
||||
<ButtonGroupWrapper $justify="flex-end" $gap="10px" $paddingTop="20px">
|
||||
<AntButton
|
||||
text="확인"
|
||||
theme="line"
|
||||
name="확인버튼"
|
||||
onClick={() => {
|
||||
handleDetailView();
|
||||
handleReset();
|
||||
setDetailData('');
|
||||
}}
|
||||
/>
|
||||
{!isView('editButton') && (
|
||||
<AntButton
|
||||
type="submit"
|
||||
text="수정"
|
||||
id="수정버튼"
|
||||
theme={checkCondition() ? 'primary' : 'disable'}
|
||||
onClick={() => handleSubmit('submit')}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroupWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuBannerDetailModal;
|
||||
|
||||
const initData = {
|
||||
title: '',
|
||||
is_link: false,
|
||||
start_dt: '',
|
||||
end_dt: '',
|
||||
image_list: [
|
||||
{ language: 'KO', content: '', title: '' },
|
||||
{ language: 'EN', content: '', title: '' },
|
||||
{ language: 'JA', content: '', title: '' },
|
||||
]
|
||||
}
|
||||
|
||||
const StyledTabs = styled(Tabs)`
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 16px;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ImageContainer = styled.div`
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #f0f0f0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const ImageUrlInfo = styled.div`
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const UrlLink = styled.a`
|
||||
color: #1890ff;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`;
|
||||
|
||||
const NoImagePlaceholder = styled.div`
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0f2f5;
|
||||
color: #8c8c8c;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
// 로딩 인디케이터를 위한 컨테이너
|
||||
const LoadingContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
// 컨텐츠 래퍼 - 로딩 상태에 따라 가시성 설정
|
||||
const ContentWrapper = styled.div`
|
||||
width: 100%;
|
||||
opacity: ${props => props.$isLoaded ? 1 : 0};
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
height: ${props => props.$isLoaded ? 'auto' : '0'};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
import React, { useState, Fragment, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../common/button/Button';
|
||||
import Loading from '../common/Loading';
|
||||
|
||||
import {
|
||||
Title,
|
||||
BtnWrapper,
|
||||
SearchBarAlert, SelectInput,
|
||||
} from '../../styles/Components';
|
||||
|
||||
import {
|
||||
FormInput,
|
||||
FormLabel,
|
||||
MessageWrapper,
|
||||
FormRowGroup,
|
||||
FormStatusBar,
|
||||
FormStatusLabel,
|
||||
FormStatusWarning,
|
||||
FormButtonContainer,
|
||||
} from '../../styles/ModuleComponents';
|
||||
import { modalTypes } from '../../assets/data';
|
||||
import { DynamicModal, Modal, SingleDatePicker, SingleTimePicker } from '../common';
|
||||
import { NONE, TYPE_MODIFY, TYPE_REGISTRY } from '../../assets/data/adminConstants';
|
||||
import { useModal } from '../../hooks/hook';
|
||||
import { convertKTCDate } from '../../utils';
|
||||
import {
|
||||
battleEventHotTime,
|
||||
battleEventRoundCount,
|
||||
battleEventStatus,
|
||||
battleRepeatType,
|
||||
} from '../../assets/data/options';
|
||||
import { BattleEventModify, BattleEventSingleRegist } from '../../apis/Battle';
|
||||
import { battleEventStatusType } from '../../assets/data/types';
|
||||
import { isValidDayRange } from '../../utils/date';
|
||||
|
||||
const MenuBannerModal = ({ modalType, detailView, handleDetailView, content, setDetailData, configData, rewardData }) => {
|
||||
const { t } = useTranslation();
|
||||
const token = sessionStorage.getItem('token');
|
||||
|
||||
const [loading, setLoading] = useState(false); // 로딩 창
|
||||
const {
|
||||
modalState,
|
||||
handleModalView,
|
||||
handleModalClose
|
||||
} = useModal({
|
||||
cancel: 'hidden',
|
||||
registConfirm: 'hidden',
|
||||
registComplete: 'hidden'
|
||||
});
|
||||
|
||||
const [isNullValue, setIsNullValue] = useState(false); // 데이터 값 체크
|
||||
const [alertMsg, setAlertMsg] = useState('');
|
||||
const [resultData, setResultData] = useState(initData); //데이터 정보
|
||||
|
||||
useEffect(() => {
|
||||
if(modalType === TYPE_MODIFY && content && Object.keys(content).length > 0){
|
||||
setResultData({
|
||||
group_id: content.group_id,
|
||||
event_id: content.event_id,
|
||||
event_name: content.event_name,
|
||||
repeat_type: content.repeat_type,
|
||||
config_id: content.config_id,
|
||||
reward_group_id: content.reward_group_id,
|
||||
round_count: content.round_count,
|
||||
hot_time: content.hot_time,
|
||||
round_time: content.round_time,
|
||||
status: content.status,
|
||||
event_start_dt: convertKTCDate(content.event_start_dt),
|
||||
event_end_dt: content.event_end_dt,
|
||||
event_operation_time: content.event_operation_time,
|
||||
});
|
||||
}
|
||||
}, [modalType, content]);
|
||||
|
||||
useEffect(() => {
|
||||
if(modalType === TYPE_REGISTRY && configData?.length > 0){
|
||||
setResultData(prev => ({
|
||||
...prev,
|
||||
round_count: configData[0].default_round_count
|
||||
}));
|
||||
}
|
||||
}, [modalType, configData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (checkCondition()) {
|
||||
setIsNullValue(false);
|
||||
} else {
|
||||
setIsNullValue(true);
|
||||
}
|
||||
}, [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) {
|
||||
setAlertMsg(t('DATE_START_DIFF_END_WARNING'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setResultData(prev => ({
|
||||
...prev,
|
||||
event_start_dt: newDate
|
||||
}));
|
||||
};
|
||||
|
||||
// 시작 시간 변경 핸들러
|
||||
const handleStartTimeChange = (time) => {
|
||||
if (!time) return;
|
||||
|
||||
const newDateTime = resultData.event_start_dt
|
||||
? new Date(resultData.event_start_dt)
|
||||
: new Date();
|
||||
|
||||
newDateTime.setHours(
|
||||
time.getHours(),
|
||||
time.getMinutes(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
setResultData(prev => ({
|
||||
...prev,
|
||||
event_start_dt: newDateTime
|
||||
}));
|
||||
};
|
||||
|
||||
// 종료 날짜 변경 핸들러
|
||||
const handleEndDateChange = (date) => {
|
||||
if (!date || !resultData.event_start_dt) return;
|
||||
|
||||
const startDate = new Date(resultData.event_start_dt);
|
||||
const endDate = new Date(date);
|
||||
|
||||
// 일자만 비교하기 위해 년/월/일만 추출
|
||||
const startDay = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
||||
const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
||||
|
||||
if (endDay <= startDay) {
|
||||
setAlertMsg(t('DATE_START_DIFF_END_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 {
|
||||
console.warn('Config not found for value:', e.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setDetailData({});
|
||||
setResultData(initData);
|
||||
handleDetailView();
|
||||
}
|
||||
|
||||
const handleSubmit = async (type, param = null) => {
|
||||
switch (type) {
|
||||
case "submit":
|
||||
if (!checkCondition()) return;
|
||||
|
||||
const minAllowedTime = new Date(new Date().getTime() + 10 * 60000);
|
||||
const startDt = resultData.event_start_dt;
|
||||
const endDt = resultData.event_end_dt;
|
||||
if (modalType === TYPE_REGISTRY && startDt < minAllowedTime) {
|
||||
setAlertMsg(t('BATTLE_EVENT_MODAL_START_DT_WARNING'));
|
||||
return;
|
||||
}
|
||||
if(resultData.repeat_type !== 'NONE' && !isValidDayRange(startDt, endDt)) {
|
||||
setAlertMsg(t('DATE_START_DIFF_END_WARNING'))
|
||||
return;
|
||||
}
|
||||
|
||||
//화면에 머물면서 상태는 안바꼈을 경우가 있기에 시작시간 지났을경우 차단
|
||||
if (modalType === TYPE_REGISTRY && startDt < new Date()) {
|
||||
setAlertMsg(t('BATTLE_EVENT_MODAL_START_DT_WARNING'));
|
||||
return;
|
||||
}
|
||||
|
||||
if(resultData.round_time === 0){
|
||||
const config = configData.find(data => data.id === resultData.config_id);
|
||||
setResultData({ ...resultData, round_time: config.round_time });
|
||||
}
|
||||
|
||||
handleModalView('registConfirm');
|
||||
break;
|
||||
case "cancel":
|
||||
handleModalView('cancel');
|
||||
break;
|
||||
case "cancelConfirm":
|
||||
handleModalClose('cancel');
|
||||
handleReset();
|
||||
break;
|
||||
case "registConfirm":
|
||||
setLoading(true);
|
||||
|
||||
if(isView('modify')){
|
||||
await BattleEventModify(token, content?.id, resultData).then(data => {
|
||||
setLoading(false);
|
||||
handleModalClose('registConfirm');
|
||||
if(data.result === "SUCCESS") {
|
||||
handleModalView('registComplete');
|
||||
}else if(data.result === "ERROR_BATTLE_EVENT_TIME_OVER"){
|
||||
setAlertMsg(t('BATTLE_EVENT_MODAL_TIME_CHECK_WARNING'));
|
||||
}else{
|
||||
setAlertMsg(t('UPDATE_FAIL'));
|
||||
}
|
||||
}).catch(reason => {
|
||||
setAlertMsg(t('API_FAIL'));
|
||||
});
|
||||
}
|
||||
else{
|
||||
await BattleEventSingleRegist(token, resultData).then(data => {
|
||||
setLoading(false);
|
||||
handleModalClose('registConfirm');
|
||||
if(data.result === "SUCCESS") {
|
||||
handleModalView('registComplete');
|
||||
}else if(data.result === "ERROR_BATTLE_EVENT_TIME_OVER"){
|
||||
setAlertMsg(t('BATTLE_EVENT_MODAL_TIME_CHECK_WARNING'));
|
||||
}else{
|
||||
setAlertMsg(t('REGIST_FAIL'));
|
||||
}
|
||||
}).catch(reason => {
|
||||
setAlertMsg(t('API_FAIL'));
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "registComplete":
|
||||
handleModalClose('registComplete');
|
||||
handleReset();
|
||||
window.location.reload();
|
||||
break;
|
||||
case "warning":
|
||||
setAlertMsg('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const checkCondition = () => {
|
||||
return (
|
||||
resultData.event_start_dt !== ''
|
||||
&& resultData.group_id !== ''
|
||||
&& resultData.event_name !== ''
|
||||
&& (resultData.repeat_type === 'NONE' || (resultData.repeat_type !== 'NONE' && resultData.event_end_dt !== ''))
|
||||
);
|
||||
};
|
||||
|
||||
const isView = (label) => {
|
||||
switch (label) {
|
||||
case "modify":
|
||||
return modalType === TYPE_MODIFY && (content?.status === battleEventStatusType.stop);
|
||||
case "start_dt":
|
||||
case "repeat":
|
||||
case "registry":
|
||||
return modalType === TYPE_REGISTRY
|
||||
case "end_dt":
|
||||
case "group":
|
||||
case "name":
|
||||
case "config":
|
||||
case "reward":
|
||||
case "round":
|
||||
case "hot":
|
||||
return modalType === TYPE_REGISTRY || (modalType === TYPE_MODIFY &&(content?.status === battleEventStatusType.stop));
|
||||
default:
|
||||
return modalType === TYPE_MODIFY && (content?.status !== battleEventStatusType.stop);
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
<SingleTimePicker
|
||||
label="시작시간"
|
||||
disabled={!isView('start_dt')}
|
||||
selectedTime={resultData?.event_start_dt}
|
||||
onTimeChange={handleStartTimeChange}
|
||||
/>
|
||||
</FormRowGroup>
|
||||
<FormRowGroup>
|
||||
<FormLabel>반복</FormLabel>
|
||||
<SelectInput value={resultData?.repeat_type} onChange={e => setResultData({ ...resultData, repeat_type: e.target.value })} disabled={!isView('repeat')} width="150px">
|
||||
{battleRepeatType.map((data, index) => (
|
||||
<option key={index} value={data.value}>
|
||||
{data.name}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
{resultData?.repeat_type !== 'NONE' &&
|
||||
<SingleDatePicker
|
||||
label="종료일자"
|
||||
disabled={!isView('end_dt')}
|
||||
dateLabel="종료 일자"
|
||||
onDateChange={handleEndDateChange}
|
||||
selectedDate={resultData?.event_end_dt}
|
||||
/>
|
||||
}
|
||||
</FormRowGroup>
|
||||
<FormRowGroup>
|
||||
<FormLabel>라운드 시간</FormLabel>
|
||||
<SelectInput value={resultData.config_id} onChange={handleConfigChange} disabled={!isView('config')} width="200px">
|
||||
{configData && configData?.map((data, index) => (
|
||||
<option key={index} value={data.id}>
|
||||
{data.desc}({data.id})
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
<FormLabel>라운드 수</FormLabel>
|
||||
<SelectInput value={resultData.round_count} onChange={e => setResultData({ ...resultData, round_count: e.target.value })} disabled={!isView('round')} width="100px">
|
||||
{battleEventRoundCount.map((data, index) => (
|
||||
<option key={index} value={data}>
|
||||
{data}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</FormRowGroup>
|
||||
<FormRowGroup>
|
||||
<FormLabel>배정 포드</FormLabel>
|
||||
<SelectInput value={resultData.reward_group_id} onChange={e => setResultData({ ...resultData, reward_group_id: e.target.value })} disabled={!isView('reward')} width="200px">
|
||||
{rewardData && rewardData?.map((data, index) => (
|
||||
<option key={index} value={data.group_id}>
|
||||
{data.desc}({data.group_id})
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
<FormLabel>핫타임</FormLabel>
|
||||
<SelectInput value={resultData.hot_time} onChange={e => setResultData({ ...resultData, hot_time: e.target.value })} disabled={!isView('hot')} width="100px">
|
||||
{battleEventHotTime.map((data, index) => (
|
||||
<option key={index} value={data}>
|
||||
{data}배
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</FormRowGroup>
|
||||
|
||||
{!isView() && isNullValue && <SearchBarAlert $marginTop="25px" $align="right">{t('REQUIRED_VALUE_CHECK')}</SearchBarAlert>}
|
||||
</MessageWrapper>
|
||||
|
||||
<BtnWrapper $gap="10px" $marginTop="10px">
|
||||
<FormStatusBar>
|
||||
<FormStatusLabel>
|
||||
현재상태: {battleEventStatus.find(data => data.value === content?.status)?.name || "등록"}
|
||||
</FormStatusLabel>
|
||||
<FormStatusWarning>
|
||||
{isView('registry') ? '' : t('BATTLE_EVENT_MODAL_STATUS_WARNING')}
|
||||
</FormStatusWarning>
|
||||
</FormStatusBar>
|
||||
<FormButtonContainer $gap="5px">
|
||||
{isView() ?
|
||||
<Button
|
||||
text="확인"
|
||||
name="확인버튼"
|
||||
theme="line"
|
||||
handleClick={() => handleReset()}
|
||||
/>
|
||||
:
|
||||
<>
|
||||
<Button text="취소" theme="line" handleClick={() => handleSubmit('cancel')} />
|
||||
<Button
|
||||
type="submit"
|
||||
text={isView('modify') ? "수정" : "등록"}
|
||||
name="등록버튼"
|
||||
theme={
|
||||
checkCondition()
|
||||
? 'primary'
|
||||
: 'disable'
|
||||
}
|
||||
handleClick={() => handleSubmit('submit')}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</FormButtonContainer>
|
||||
</BtnWrapper>
|
||||
</Modal>
|
||||
|
||||
{/* 확인 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.confirmOkCancel}
|
||||
view={modalState.registConfirmModal}
|
||||
modalText={isView('modify') ? t('BATTLE_EVENT_UPDATE_CONFIRM') : t('BATTLE_EVENT_REGIST_CONFIRM')}
|
||||
handleSubmit={() => handleSubmit('registConfirm')}
|
||||
handleCancel={() => handleModalClose('registConfirm')}
|
||||
/>
|
||||
{/* 완료 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.completed}
|
||||
view={modalState.registCompleteModal}
|
||||
modalText={isView('modify') ? t('UPDATE_COMPLETED') : t('REGIST_COMPLTE')}
|
||||
handleSubmit={() => handleSubmit('registComplete')}
|
||||
/>
|
||||
{/* 취소 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.confirmOkCancel}
|
||||
view={modalState.cancelModal}
|
||||
modalText={t('CANCEL_CONFIRM')}
|
||||
handleCancel={() => handleModalClose('cancel')}
|
||||
handleSubmit={() => handleSubmit('cancelConfirm')}
|
||||
/>
|
||||
{/* 경고 모달 */}
|
||||
<DynamicModal
|
||||
modalType={modalTypes.completed}
|
||||
view={alertMsg ? 'view' : 'hidden'}
|
||||
modalText={alertMsg}
|
||||
handleSubmit={() => handleSubmit('warning')}
|
||||
/>
|
||||
{loading && <Loading/>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const initData = {
|
||||
group_id: '',
|
||||
event_name: '',
|
||||
repeat_type: 'NONE',
|
||||
config_id: 1,
|
||||
round_time: 0,
|
||||
reward_group_id: 1,
|
||||
round_count: 1,
|
||||
hot_time: 1,
|
||||
event_start_dt: '',
|
||||
event_end_dt: ''
|
||||
}
|
||||
|
||||
export default MenuBannerModal;
|
||||
|
||||
@@ -139,6 +139,7 @@ const resources = {
|
||||
MENU_BANNER_REGIST_CONFIRM: "배너를 등록하시겠습니까?",
|
||||
MENU_BANNER_SELECT_DELETE: "선택된 배너를 삭제하시겠습니까?",
|
||||
MENU_BANNER_REGIST_CANCEL: "배너 등록을 취소하시겠습니까?\n\r취소 시 설정된 값은 반영되지 않습니다.",
|
||||
MENU_BANNER_UPDATE_SAVE: "배너 정보 수정사항을 \r\n저장하시겠습니까?",
|
||||
//아이템
|
||||
ITEM_DELETE_CONFIRM: '해당 아이템을 삭제하시겠습니까?\r\n* 한번 삭제한 아이템은 다시 복구할 수 없습니다.',
|
||||
ITEM_RESTORE_CONFIRM: '해당 아이템을 복구하시겠습니까?',
|
||||
|
||||
@@ -14,13 +14,13 @@ import { INITIAL_PAGE_LIMIT } from '../../assets/data/adminConstants';
|
||||
import { useModal, useTable, withAuth } from '../../hooks/hook';
|
||||
import { MenuBannerDelete, MenuBannerDetailView } from '../../apis';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import MenuBannerModal from '../../components/modal/MenuBannerModal';
|
||||
import tableInfo from '../../assets/data/pages/menuBannerTable.json'
|
||||
import { CommonSearchBar, useCommonSearch } from '../../components/ServiceManage';
|
||||
import { useAlert } from '../../context/AlertProvider';
|
||||
import { alertTypes } from '../../assets/data/types';
|
||||
import { useLoading } from '../../context/LoadingProvider';
|
||||
import useEnhancedCommonSearch from '../../hooks/useEnhancedCommonSearch';
|
||||
import MenuBannerDetailModal from '../../components/modal/MenuBannerDetailModal';
|
||||
|
||||
const MenuBanner = () => {
|
||||
const tableRef = useRef(null);
|
||||
@@ -38,7 +38,6 @@ const MenuBanner = () => {
|
||||
} = useModal({
|
||||
detail: 'hidden',
|
||||
});
|
||||
const [modalType, setModalType] = useState('regist');
|
||||
|
||||
const {
|
||||
config,
|
||||
@@ -67,9 +66,8 @@ const MenuBanner = () => {
|
||||
const handleAction = async (action, item = null) => {
|
||||
switch (action) {
|
||||
case "detail":
|
||||
await MenuBannerDetailView(token, item).then(data => {
|
||||
setDetailData(data.event_detail);
|
||||
setModalType('modify');
|
||||
await MenuBannerDetailView(token, item.id).then(data => {
|
||||
setDetailData(data.detail);
|
||||
handleModalView('detail');
|
||||
});
|
||||
break;
|
||||
@@ -183,8 +181,7 @@ const MenuBanner = () => {
|
||||
/>
|
||||
|
||||
{/* 상세 */}
|
||||
<MenuBannerModal
|
||||
modalType={modalType}
|
||||
<MenuBannerDetailModal
|
||||
detailView={modalState.detailModal}
|
||||
handleDetailView={() => handleModalClose('detail')}
|
||||
content={detailData}
|
||||
|
||||
@@ -174,6 +174,21 @@ export const BtnWrapper = styled.div`
|
||||
padding-top: ${props => props.$paddingTop};
|
||||
`;
|
||||
|
||||
/**
|
||||
* 버튼 그룹을 위한 스타일드 컴포넌트
|
||||
*/
|
||||
export const ButtonGroupWrapper = styled.div`
|
||||
width: ${props => props.width};
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-flow: ${props => props.$flow};
|
||||
justify-content: ${props => props.$justify};
|
||||
gap: ${props => props.$gap};
|
||||
margin-top: ${props => props.$marginTop};
|
||||
margin-bottom: ${props => props.$marginBottom};
|
||||
padding-top: ${props => props.$paddingTop};
|
||||
`;
|
||||
|
||||
export const Title = styled.h2`
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
@@ -744,4 +759,12 @@ export const TotalRow = styled.tr`
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ImagePreview = styled.img`
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px 4px 0 0;
|
||||
background-color: #f6f6f6;
|
||||
`;
|
||||
@@ -139,19 +139,31 @@ export const responseFileDownload = (response, options = {}) => {
|
||||
const contentType = response.headers['content-type'] || response.headers['Content-Type'];
|
||||
const contentDisposition = response.headers['content-disposition'] || response.headers['Content-Disposition'];
|
||||
|
||||
// Excel이나 ZIP 파일 형식 검증
|
||||
if (!contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') &&
|
||||
!contentType.includes('application/zip')) {
|
||||
console.log(response);
|
||||
// Excel, CSV, ZIP 파일 형식 검증 (CSV 추가)
|
||||
const isValidType = contentType && (
|
||||
contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') ||
|
||||
contentType.includes('text/csv') ||
|
||||
contentType.includes('application/zip')
|
||||
);
|
||||
|
||||
if (!isValidType) {
|
||||
console.log('Invalid content type:', contentType);
|
||||
console.log('Full response:', response);
|
||||
throw new Error(`잘못된 파일 형식입니다. Content-Type: ${contentType}`);
|
||||
}
|
||||
|
||||
let fileName = defaultFileName;
|
||||
let fileExtension = '.xlsx';
|
||||
let mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
let fileExtension = '.csv';
|
||||
let mimeType = 'text/csv; charset=UTF-8';
|
||||
|
||||
// ZIP 파일 처리
|
||||
if (contentType && contentType.includes('application/zip')) {
|
||||
// 파일 타입별 처리
|
||||
if (contentType.includes('text/csv')) {
|
||||
// CSV 파일 처리
|
||||
fileExtension = '.csv';
|
||||
mimeType = 'text/csv; charset=UTF-8';
|
||||
fileName = `${defaultFileName}`;
|
||||
} else if (contentType.includes('application/zip')) {
|
||||
// ZIP 파일 처리
|
||||
fileExtension = '.zip';
|
||||
mimeType = 'application/zip';
|
||||
fileName = `${defaultFileName}_multiple_files`;
|
||||
@@ -161,7 +173,13 @@ export const responseFileDownload = (response, options = {}) => {
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (fileNameMatch && fileNameMatch[1]) {
|
||||
fileName = decodeURIComponent(fileNameMatch[1].replace(/['"]/g, ''));
|
||||
let extractedFileName = fileNameMatch[1].replace(/['"]/g, '');
|
||||
try {
|
||||
fileName = decodeURIComponent(extractedFileName);
|
||||
} catch (e) {
|
||||
// decodeURIComponent 실패 시 원본 사용
|
||||
fileName = extractedFileName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fileName = fileName + fileExtension;
|
||||
@@ -169,13 +187,15 @@ export const responseFileDownload = (response, options = {}) => {
|
||||
|
||||
const blob = new Blob([response.data], { type: mimeType });
|
||||
|
||||
// 파일 유효성 검사
|
||||
// 파일 유효성 검사 (CSV는 더 작을 수 있으므로 조건 완화)
|
||||
if (blob.size === 0) {
|
||||
throw new Error('다운로드된 파일이 비어있습니다.');
|
||||
}
|
||||
|
||||
if (blob.size < 1024) {
|
||||
throw new Error('파일 크기가 너무 작습니다. 올바른 Excel 파일이 아닐 수 있습니다.');
|
||||
// CSV 파일은 크기 검사 완화
|
||||
const minSize = contentType.includes('text/csv') ? 100 : 1024;
|
||||
if (blob.size < minSize) {
|
||||
throw new Error(`파일 크기가 너무 작습니다. 올바른 파일이 아닐 수 있습니다. (${blob.size} bytes)`);
|
||||
}
|
||||
|
||||
// 파일 다운로드 실행
|
||||
|
||||
Reference in New Issue
Block a user