diff --git a/index.html b/index.html index 11ab842..9d3ab49 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/babpiens_logo.svg b/public/images/babpiens_logo.svg new file mode 100644 index 0000000..49b0679 --- /dev/null +++ b/public/images/babpiens_logo.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/foods/FoodCard.tsx b/src/components/foods/FoodCard.tsx index 43b9290..6bb85df 100644 --- a/src/components/foods/FoodCard.tsx +++ b/src/components/foods/FoodCard.tsx @@ -11,6 +11,7 @@ interface FoodCardProps extends PropsWithChildren { } const FoodCard = ({ id, info, name, tag, img, ...props }: FoodCardProps) => { + console.log(id, info, name, tag, img, props); return ( diff --git a/src/components/layout/HeaderLanding.tsx b/src/components/layout/HeaderLanding.tsx index 7a958af..9ad49be 100644 --- a/src/components/layout/HeaderLanding.tsx +++ b/src/components/layout/HeaderLanding.tsx @@ -5,7 +5,7 @@ const HeaderLanding = () => {

- Logo + Logo

diff --git a/src/components/thunder/ThunderCard.tsx b/src/components/thunder/ThunderCard.tsx index 45f6c0c..5e171c3 100644 --- a/src/components/thunder/ThunderCard.tsx +++ b/src/components/thunder/ThunderCard.tsx @@ -5,7 +5,7 @@ import { formatAppointmentTime } from '../../utils/formatAppointmentTime'; interface ThunderCardProps { id: string; - meeting_image_url: string; + meeting_image_url: string | null; description: string; paymentMethod: string; appointmentTime: string; diff --git a/src/components/thunder/ThunderClockModal.tsx b/src/components/thunder/ThunderClockModal.tsx index 365ff01..5f24a10 100644 --- a/src/components/thunder/ThunderClockModal.tsx +++ b/src/components/thunder/ThunderClockModal.tsx @@ -3,22 +3,29 @@ import { motion, AnimatePresence } from 'framer-motion'; import { Swiper, SwiperSlide } from 'swiper/react'; import 'swiper/css'; +// ThunderClockModal 컴포넌트 정의 const ThunderClockModal: React.FC<{ isOpen: boolean; onClose: () => void; onTimeSelect: (time: string) => void }> = ({ isOpen, onClose, onTimeSelect, }) => { - const [selectedHour, setSelectedHour] = useState(0); // 선택된 시간을 저장하는 상태, 초기값 0시 - const [selectedMinute, setSelectedMinute] = useState(0); // 선택된 분을 저장 초기값 0분 - const [isEditingHour, setIsEditingHour] = useState(false); // 시간을 편집 중인지 여부 저장 초기값 false - const [isEditingMinute, setIsEditingMinute] = useState(false); // 분을 편집 중인지 여부 저장 초기값 false + // 선택된 시간을 저장하는 상태, 초기값 0시 + const [selectedHour, setSelectedHour] = useState(0); + // 선택된 분을 저장하는 상태, 초기값 0분 + const [selectedMinute, setSelectedMinute] = useState(0); + // 시간을 편집 중인지 여부를 저장하는 상태, 초기값 false + const [isEditingHour, setIsEditingHour] = useState(false); + // 분을 편집 중인지 여부를 저장하는 상태, 초기값 false + const [isEditingMinute, setIsEditingMinute] = useState(false); + // 시간과 분을 선택하고 모달을 닫는 함수 const handleConfirm = () => { const time = `${selectedHour < 10 ? `0${selectedHour}` : selectedHour}:${selectedMinute < 10 ? `0${selectedMinute}` : selectedMinute}`; onTimeSelect(time); onClose(); }; + // 시간 입력값이 변경될 때 호출되는 함수 const handleHourChange = (event: React.ChangeEvent) => { const value = parseInt(event.target.value); if (value >= 0 && value < 24) { @@ -26,6 +33,7 @@ const ThunderClockModal: React.FC<{ isOpen: boolean; onClose: () => void; onTime } }; + // 분 입력값이 변경될 때 호출되는 함수 const handleMinuteChange = (event: React.ChangeEvent) => { const value = parseInt(event.target.value); if (value >= 0 && value < 60) { @@ -33,10 +41,12 @@ const ThunderClockModal: React.FC<{ isOpen: boolean; onClose: () => void; onTime } }; + // 시간 입력 필드를 클릭할 때 호출되는 함수 const handleHourClick = (event: React.MouseEvent) => { event.currentTarget.select(); }; + // 분 입력 필드를 클릭할 때 호출되는 함수 const handleMinuteClick = (event: React.MouseEvent) => { event.currentTarget.select(); }; @@ -45,16 +55,20 @@ const ThunderClockModal: React.FC<{ isOpen: boolean; onClose: () => void; onTime {isOpen && ( + {' '} + {/* // modal의 스타일 설정 */} + {' '} + {/* // modal의 스타일 설정 */}
{isEditingHour ? ( @@ -69,11 +83,13 @@ const ThunderClockModal: React.FC<{ isOpen: boolean; onClose: () => void; onTime /> ) : ( setSelectedHour(swiper.activeIndex)} + direction="vertical" // 슬라이더의 방향 + slidesPerView={3} // 한 번에 보여질 슬라이드 수 (바깥쪽에 보이게 사용자 경험 개선) + centeredSlides={true} // 슬라이드가 중앙에 위치 + onSlideChange={(swiper) => setSelectedHour(swiper.activeIndex)} // 슬라이드가 변경될 때 호출 onClick={() => setIsEditingHour(true)}> + {' '} + {/* // 슬라이드를 클릭할 때 호출 - 시간이 24시간제이므로 24까지 표시되도록 00~23*/} {[...Array(24)].map((_, i) => ( void; onTime
:
+ {/* 슬라이더를 원하지 않는 사용자가 있을 수 있으므로, 직접 시, 분을 클릭하여 사용자가 입력할 수 있도록 input을 제공 */} {isEditingMinute ? ( void; onTime style={{ width: '150%', height: '100%' }} /> ) : ( + // 사용자가 임의로 슬라이더가 가능하게 한다. 슬라이드가 중앙에 위치 1~5값내에서 중앙에 위치하면 그 값을 받을 수 있게 처리 + // 바깥으로는 값으로 인식 안하게 처리 setSelectedMinute(swiper.activeIndex * 5)} - onClick={() => setIsEditingMinute(true)} + direction="vertical" // 슬라이더의 방향 설정 + slidesPerView={3} // 한 번에 보여질 슬라이드 수 설정(바깥쪽에 보이게 사용자 경험 개선) + centeredSlides={true} // 슬라이드가 중앙에 위치하도록 설정 + onSlideChange={(swiper) => setSelectedMinute(swiper.activeIndex * 5)} // 슬라이드가 변경될 때 호출되는 함수 + onClick={() => setIsEditingMinute(true)} // 슬라이드를 클릭할 때 호출되는 함수 + // 슬라이더의 속도 설정 - speed 값 500 speed={500}> + {/* 배열 12 로 구성 - 00~55*/} {[...Array(12)].map((_, i) => ( + {/* 활성화 된 시, 분은 주황색으로 처리, 활성화 안된 시, 분은 회색 그라데이션으로 처리 */} {i * 5 < 10 ? `0${i * 5}` : i * 5} diff --git a/src/components/thunder/ThunderImageModal.tsx b/src/components/thunder/ThunderImageModal.tsx index 8e87a25..75ec7e9 100644 --- a/src/components/thunder/ThunderImageModal.tsx +++ b/src/components/thunder/ThunderImageModal.tsx @@ -21,6 +21,26 @@ const ThunderImageModal: React.FC = ({ isOpen, onClose, setTotalPages(images.length); }, [images]); + useEffect(() => { + // 키보드 이벤트 핸들러 함수 정의 + const handleKeyDown = (event: KeyboardEvent) => { + // Esc 키를 누르면 모달 닫기 + if (event.key === 'Escape') { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + } else { + document.removeEventListener('keydown', handleKeyDown); + } + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen, onClose]); + return ReactDOM.createPortal( {isOpen && ( @@ -39,11 +59,11 @@ const ThunderImageModal: React.FC = ({ isOpen, onClose, transition={{ duration: 0.1 }}> {/* modal 닫기 버튼 */} - + { >([]); // 게시판 목록 상태 추가 const [isLoading, setIsLoading] = useState(true); // 로딩 상태 추가 const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); // 로그인 모달 상태 추가 + const [categories, setCategories] = useState<{ category: string }[]>([]); // 카테고리 목록 상태 추가 const navigate = useNavigate(); // useNavigate 훅 사용 useEffect(() => { setSelectedBoard('전체'); // 컴포넌트가 처음 렌더링될 때 '전체'로 설정 fetchBoardList(); // 게시판 목록 가져오기 + fetchCategories(); // 카테고리 목록 가져오기 }, []); const fetchBoardList = async () => { @@ -41,6 +43,16 @@ const Board = () => { } }; + const fetchCategories = async () => { + try { + const response = await baseInstance.get('/api/categories/reviewfilter/'); + setCategories(response.data); + console.log(response.data); + } catch (error) { + console.error('카테고리 목록을 가져오는 중 오류가 발생했습니다:', error); + } + }; + const checkLogin = () => { const token = getCookie('refresh'); if (!token) { @@ -67,27 +79,19 @@ const Board = () => {

맛있는 이야기의 시작

+
- - - + {categories.map((category, index) => ( + + ))}
@@ -104,7 +108,7 @@ const Board = () => {
{filteredBoardList.map((item) => { if (!item) return null; - console.log('Board Item:', item); + // console.log('Board Item:', item); return ( { const [commentToDelete, setCommentToDelete] = useState(null); const [commentToEdit, setCommentToEdit] = useState(null); const [editCommentText, setEditCommentText] = useState(''); + const [profileImageUrl, setProfileImageUrl] = useState(''); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchBoardItem = async () => { const boardId = window.location.pathname.split('/').pop(); try { - const response = await authInstance.get(`/api/reviews/detail/${boardId}`); + const response = await baseInstance.get(`/api/reviews/detail/${boardId}`); const boardItem = response.data.review; setSelectedBoardItem(boardItem); setComments( @@ -67,8 +69,14 @@ const BoardId = () => { }), ), ); + + // 게시물 작성자의 프로필 이미지를 가져오기 위해 API 요청을 보냄 + const profileResponse = await baseInstance.get(`/api/profile/${boardItem.nickname}`); + // 프로필 이미지 URL을 상태로 설정 + setProfileImageUrl(profileResponse.data.profile_image_url); setIsLoading(false); } catch (error) { + // 게시물 정보를 불러오는 중 오류가 발생했을 때 콘솔에 오류 메시지를 출력 console.error('게시물 정보를 불러오는 중 오류가 발생했습니다:', error); setIsLoading(false); } @@ -189,18 +197,22 @@ const BoardId = () => { return (
+ {/* 카테고리 표시 섹션 */}
+ {/* 카테고리에 따라 다른 텍스트 표시 */} {selectedBoardItem.category === 1 ? '맛집 추천' : '소셜 다이닝 후기'}
+ {/* 게시물 제목 */}
{selectedBoardItem.title}
+ {/* 작성자 정보 */}
프로필 사진 { @@ -211,9 +223,11 @@ const BoardId = () => {
+ {/* 작성자 닉네임 */} {selectedBoardItem.nickname} + {/* 작성 시간 */}
{formattedCreatedAt}
@@ -221,6 +235,7 @@ const BoardId = () => {

{selectedBoardItem.content}

+ {/* 이미지 로딩 중일 때 ContentLoader 표시 */} {selectedBoardItem.review_image_url && !isImageLoaded && ( @@ -228,6 +243,7 @@ const BoardId = () => { )} + {/* 이미지 로딩 완료 후 이미지 표시 */} {selectedBoardItem.review_image_url && ( { } else { timeDisplay = commentTime.toLocaleDateString(); } - return (
{ @@ -20,18 +27,30 @@ const BoardPost = () => { const [isCenterModalOpen, setIsCenterModalOpen] = useState(false); // 중앙 모달의 열림/닫힘 상태 const [selectedCategory, setSelectedCategory] = useState(null); // 선택된 카테고리 방법을 관리 - const [selectedImages, setSelectedImages] = useState([]); // 선택된 이미지 - const [imagePreviews, setImagePreviews] = useState([]); // 이미지 미리보기 + const [selectedImage, setSelectedImage] = useState(null); // 선택된 이미지 + const [imagePreview, setImagePreview] = useState(null); // 이미지 미리보기 const [isUploading, setIsUploading] = useState(false); // 업로드 상태 const [uploadProgress, setUploadProgress] = useState(0); // 업로드 진행률 const [currentUploadingIndex, setCurrentUploadingIndex] = useState(0); // 현재 업로드 중인 이미지 인덱스 - const [representativeImage, setRepresentativeImage] = useState(null); // 대표 이미지 + // const [representativeImage, setRepresentativeImage] = useState(null); // 대표 이미지 const [modalMessage, setModalMessage] = useState({ title1: '', title2: '' }); // 모달 메시지 + const [categories, setCategories] = useState([]); // 카테고리 목록 useEffect(() => { if (!getCookie('refresh')) { navigate('/'); } + + const fetchCategories = async () => { + try { + const response = await authInstance.get('/api/categories/reviewfilter/'); + setCategories(response.data); + } catch (error) { + console.error('카테고리를 가져오는 중 오류가 발생했습니다:', error); + } + }; + + fetchCategories(); }, [navigate]); // 중앙 모달의 열림/닫힘 상태 modal @@ -40,34 +59,23 @@ const BoardPost = () => { }; const handleImageChange = async (event: React.ChangeEvent) => { - if (event.target.files) { - const files = Array.from(event.target.files); - if (selectedImages.length + files.length > 10) { - setModalMessage({ title1: '이미지는 최대 10장까지', title2: '등록할 수 있습니다.' }); - toggleCenterModal(); - return; - } - + if (event.target.files && event.target.files.length > 0) { + const file = event.target.files[0]; setIsUploading(true); setUploadProgress(0); setCurrentUploadingIndex(0); - const compressedFiles = await Promise.all( - files.map(async (file, index) => { - setCurrentUploadingIndex(index + 1); - console.log(`현재 ${index + 1}번째의 이미지를 처리하고 있습니다. (총 ${files.length}개 중)`); - const compressedFile = await compressImageToWebp(file); - setUploadProgress(((index + 1) / files.length) * 100); - return compressedFile; - }), - ); - - setSelectedImages((prevImages) => [...prevImages, ...compressedFiles]); - - const newPreviews = compressedFiles.map((file) => URL.createObjectURL(file)); - setImagePreviews((prevPreviews) => [...prevPreviews, ...newPreviews]); - setIsUploading(false); - console.log('이미지 업로드가 완료되었습니다.'); + try { + const compressedFile = await compressImageToWebp(file); + setSelectedImage(compressedFile); + const preview = URL.createObjectURL(compressedFile); + setImagePreview(preview); + setIsUploading(false); + console.log('이미지 업로드가 완료되었습니다.'); + } catch (error) { + console.error('이미지 업로드 중 오류가 발생했습니다:', error); + setIsUploading(false); + } } }; @@ -84,34 +92,10 @@ const BoardPost = () => { } }; - const handleImageRemove = (index: number, event: React.MouseEvent) => { + const handleImageRemove = (event: React.MouseEvent) => { event.preventDefault(); - setSelectedImages((prevImages) => prevImages.filter((_, i) => i !== index)); - setImagePreviews((prevPreviews) => prevPreviews.filter((_, i) => i !== index)); - if (representativeImage === index) { - setRepresentativeImage(null); - } - }; - - // 이미지 업로드 함수 - const uploadImages = async (images: File[]): Promise => { - const uploadedImageUrls: string[] = []; - for (const image of images) { - const formData = new FormData(); - formData.append('file', image); - try { - const response = await authInstance.post('https://api.babpiens.com/api/common/image/', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - uploadedImageUrls.push(response.data.url); - } catch (error) { - console.error('이미지 업로드 중 오류가 발생했습니다:', error); - throw error; - } - } - return uploadedImageUrls; + setSelectedImage(null); + setImagePreview(null); }; // 폼 데이터 제출 시 미입력 필드에 따른 Modal 알림 @@ -133,47 +117,84 @@ const BoardPost = () => { } try { - const uploadedImageUrls = await uploadImages(selectedImages); + let reviewImageUrl = ''; + if (selectedImage) { + // 선택된 이미지를 새로운 파일로 생성 + const newFile = new File([selectedImage], selectedImage.name); + const formData = new FormData(); + // 폼 데이터에 input_source - s3 에 저정될 폴더이름과 images 파일이 들어갈 공간 추가 + formData.append('input_source', 'board'); + formData.append('images', newFile); + + // 이미지 업로드 API 호출 + const response = await authInstance.post('/api/common/image/', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + + // 업로드된 이미지 URL 저장 + reviewImageUrl = response.data.images_urls[0]; + } + + // 리뷰 생성 API 호출 const response = await authInstance.post('/api/reviews/detail/create/', { - title: data.title, - category_name: selectedCategory, - content: data.content, - input_image: uploadedImageUrls, + title: data.title, // 제목 + category_name: selectedCategory, // 카테고리 이름 + content: data.content, // 내용 + review_image_url: reviewImageUrl, // 리뷰 이미지 URL }); - console.log('폼 제출 성공:', response.data); - setModalMessage({ title1: '폼 제출에 성공했습니다.', title2: '게시물이 등록되었습니다.' }); + + console.log(response.data); + setModalMessage({ title1: '맛있는 발견 글 쓰기가 완료되었습니다.', title2: '' }); + const reviewUuid = response.data.review_uuid; toggleCenterModal(); + navigate(`/board/${reviewUuid}`); } catch (error) { - console.error('서버 요청 중 오류가 발생했습니다:', error); - setModalMessage({ title1: '서버 요청 중 오류가 발생했습니다.', title2: '다시 시도해주세요.' }); + console.error('글 쓰기 중 오류가 발생했습니다:', error); + setModalMessage({ title1: '글 쓰기 중 오류가 발생했습니다.', title2: '다시 시도해주세요.' }); toggleCenterModal(); } }; + { + /* 폼 제출에 문제 알림 modal */ + } + + { + if (modalMessage.title1 === '맛있는 발견 글 쓰기가 완료되었습니다.') { + navigate('/board'); + } else { + toggleCenterModal(); + } + }} + className="mt-4 h-[50px] w-full rounded-xl bg-orange-500 px-4 py-2 font-bold text-white"> + 확인 + + ; + return (
카테고리를 선택해주세요.
-
- - + {categories.map((category: Category) => ( + + ))}
제목
@@ -198,9 +219,7 @@ const BoardPost = () => { }} /> -
- 이미지 등록 - (첫번째 사진이 대표사진이 됩니다.) -
+
이미지 등록
- - {imagePreviews.length > 0 && ( + + {imagePreview && (
- {imagePreviews.map((preview, index) => ( - - - - {index === 0 && ( -
- 대표사진 -
- )} -
- ))} + + + +
+ 대표사진 +
+
)}
@@ -285,26 +289,6 @@ const BoardPost = () => { disabled={isUploading}> {isUploading ? '현재 이미지 처리중입니다' : '등록하기'} - {/* 이미지가 최대 10장이상을 넘어 업로드될 경우에 나오는 modal */} - - { - if (modalMessage.title1 === '폼 제출에 성공했습니다.') { - navigate('/board'); - } else { - toggleCenterModal(); - } - }} - className="mt-4 h-[50px] w-full rounded-xl bg-orange-500 px-4 py-2 font-bold text-white"> - 확인 - -
); diff --git a/src/pages/thunder/ThunderId.tsx b/src/pages/thunder/ThunderId.tsx index 9be7063..f02a55c 100644 --- a/src/pages/thunder/ThunderId.tsx +++ b/src/pages/thunder/ThunderId.tsx @@ -1,14 +1,14 @@ import { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { motion } from 'framer-motion'; import ModalCenter from '../../components/common/ModalCenter'; import ThunderImageModal from '../../components/thunder/ThunderImageModal'; import ContentLoader from 'react-content-loader'; import { format, differenceInMinutes, differenceInHours } from 'date-fns'; import { ko } from 'date-fns/locale'; -import { authInstance } from '../../api/util/instance'; import { NotFound } from '../notfound'; import Loading from '../../components/common/Loading'; +import { authInstance, baseInstance } from '../../api/util/instance'; // ThunderId - 소셜다이닝 글 목록 조회 interface Meeting { @@ -32,6 +32,8 @@ interface MeetingMember { } const ThunderId = () => { + const param = useParams(); + console.log(param); const [isModalCenterOpen, setIsModalCenterOpen] = useState(false); // ModalCenter의 Open/Close 상태를 관리 const [isThunderImageModalOpen, setIsThunderImageModalOpen] = useState(false); // ImageModal Open/Close 상태 관리 const [isLiked, setIsLiked] = useState(false); // 좋아요 상태를 관리하는 상태 @@ -42,12 +44,13 @@ const ThunderId = () => { const [selectedImages, setSelectedImages] = useState([]); // 선택된 이미지 URL들을 관리 const [meetingMembers, setMeetingMembers] = useState([]); // 모임 멤버 정보를 관리 const [isLoading, setIsLoading] = useState(true); + const [profileImageUrl, setProfileImageUrl] = useState(null); // 작성자 프로필 이미지 URL을 관리 useEffect(() => { const fetchMeeting = async () => { try { const meetingId = window.location.pathname.split('/').pop(); - const response = await authInstance.get(`/api/meetings/${meetingId}`); + const response = await baseInstance.get(`/api/meetings/${meetingId}`); console.log('Meeting data:', response.data.meeting); console.log('Meeting members:', response.data.meeting_member); setSelectedMeeting(response.data.meeting); @@ -62,6 +65,14 @@ const ThunderId = () => { fetchMeeting(); }, []); + useEffect(() => { + if (selectedMeeting) { + baseInstance.get(`/api/profile/${selectedMeeting.nickname}/`).then((res) => { + setProfileImageUrl(res.data.profile_image_url); + }); + } + }, [selectedMeeting]); + if (isLoading) { return ; } @@ -83,6 +94,7 @@ const ThunderId = () => { try { const meetingId = window.location.pathname.split('/').pop(); await authInstance.post(`/api/meetings/${meetingId}/join`, { is_host: false }); + await authInstance.post('/api/meetings/member/', { meeting_uuid: selectedMeeting.uuid }); setIsParticipating(true); closeModalCenter(); } catch (error) { @@ -150,7 +162,15 @@ const ThunderId = () => { {/* 작성자 정보 */}
- 프로필 사진 + 프로필 사진 { + (e.target as HTMLImageElement).onerror = null; + (e.target as HTMLImageElement).src = '../images/anonymous_avatars.svg'; + }} + />
diff --git a/src/pages/thunder/ThunderPost.tsx b/src/pages/thunder/ThunderPost.tsx index 1306d70..05b8469 100644 --- a/src/pages/thunder/ThunderPost.tsx +++ b/src/pages/thunder/ThunderPost.tsx @@ -25,20 +25,65 @@ const ThunderPost = () => { const [isCenterModalOpen, setIsCenterModalOpen] = useState(false); // 중앙 모달의 열림/닫힘 상태 const [isThunderClockModalOpen, setIsThunderClockModalOpen] = useState(false); // clock modal의 열림/닫힘 상태 const [isThunderCalendarModalOpen, setIsThunderCalendarModalOpen] = useState(false); // 캘린더 모달의 열림/닫힘 상태 - const [selectedLocation, setSelectedLocation] = useState('지역 선택하기'); // 선택된 위치 + const [selectedLocation, setSelectedLocation] = useState('여기를 눌러 지역 선택하기'); // 선택된 위치 const [selectedPayment, setSelectedPayment] = useState(''); // 선택된 결제 방법 const [selectedAgeGroup, setSelectedAgeGroup] = useState(''); // 선택된 연령대 const [selectedGenderGroup, setSelectedGenderGroup] = useState(''); // 선택된 성별 그룹 const [selectedDate, setSelectedDate] = useState(null); // 선택된 날짜 const [selectedTime, setSelectedTime] = useState(null); // 선택된 시간 - const [selectedImages, setSelectedImages] = useState([]); // 선택된 이미지 - const [imagePreviews, setImagePreviews] = useState([]); // 이미지 미리보기 + const [selectedImage, setSelectedImage] = useState(null); // 선택된 이미지 + const [imagePreview, setImagePreview] = useState(null); // 이미지 미리보기 const [isUploading, setIsUploading] = useState(false); // 업로드 상태 const [uploadProgress, setUploadProgress] = useState(0); // 업로드 진행률 - const [currentUploadingIndex, setCurrentUploadingIndex] = useState(0); // 현재 업로드 중인 이미지 인덱스 const [maxPeople, setMaxPeople] = useState(1); // 최대 인원 - const [representativeImage, setRepresentativeImage] = useState(null); // 대표 이미지 const [modalMessage, setModalMessage] = useState({ title1: '', title2: '' }); // 모달 메시지 + const [paymentMethods, setPaymentMethods] = useState([]); // 결제 방법 목록 + const [ageGroups, setAgeGroups] = useState([]); // 연령대 목록 + const [genderGroups, setGenderGroups] = useState([]); // 성별 그룹 목록 + const [locations, setLocations] = useState([]); // 지역 목록 + + useEffect(() => { + const fetchPaymentMethods = async () => { + try { + const response = await authInstance.get('/api/categories/meetingpaymentfilter/'); + setPaymentMethods(response.data); + } catch (error) { + console.error('결제 방법을 가져오는 중 오류가 발생했습니다:', error); + } + }; + + const fetchAgeGroups = async () => { + try { + const response = await authInstance.get('/api/categories/meetingagefilter/'); + setAgeGroups(response.data); + } catch (error) { + console.error('연령대를 가져오는 중 오류가 발생했습니다:', error); + } + }; + + const fetchGenderGroups = async () => { + try { + const response = await authInstance.get('/api/categories/meetinggenderfilter/'); + setGenderGroups(response.data); + } catch (error) { + console.error('성별 그룹을 가져오는 중 오류가 발생했습니다:', error); + } + }; + + const fetchLocations = async () => { + try { + const response = await authInstance.get('/api/categories/locationfilter/'); + setLocations(response.data); + } catch (error) { + console.error('지역 목록을 가져오는 중 오류가 발생했습니다:', error); + } + }; + + fetchPaymentMethods(); + fetchAgeGroups(); + fetchGenderGroups(); + fetchLocations(); + }, []); const navigate = useNavigate(); useEffect(() => { @@ -79,29 +124,16 @@ const ThunderPost = () => { }; const handleImageChange = async (event: React.ChangeEvent) => { - if (event.target.files) { - const files = Array.from(event.target.files); - if (selectedImages.length + files.length > 10) { - toggleCenterModal(); - return; - } - + if (event.target.files && event.target.files.length > 0) { + const file = event.target.files[0]; setIsUploading(true); setUploadProgress(0); - const compressedFiles = await Promise.all( - files.map(async (file, index) => { - setCurrentUploadingIndex(index + 1); - const compressedFile = await compressImageToWebp(file); - setUploadProgress(((index + 1) / files.length) * 100); - return compressedFile; - }), - ); - - setSelectedImages((prevImages) => [...prevImages, ...compressedFiles]); + const compressedFile = await compressImageToWebp(file); + setSelectedImage(compressedFile); - const newPreviews = compressedFiles.map((file) => URL.createObjectURL(file)); - setImagePreviews((prevPreviews) => [...prevPreviews, ...newPreviews]); + const newPreview = URL.createObjectURL(compressedFile); + setImagePreview(newPreview); setIsUploading(false); } }; @@ -119,18 +151,12 @@ const ThunderPost = () => { } }; - const handleImageRemove = (index: number, event: React.MouseEvent) => { + const handleImageRemove = (event: React.MouseEvent) => { event.preventDefault(); - setSelectedImages((prevImages) => prevImages.filter((_, i) => i !== index)); - setImagePreviews((prevPreviews) => prevPreviews.filter((_, i) => i !== index)); - if (representativeImage === index) { - setRepresentativeImage(null); - } + setSelectedImage(null); + setImagePreview(null); }; - // 모임 장소 목록 정의 - const locations = ['강동, 하남', '강남, 송파', '강북, 노원', '강서, 양천', '관악, 동작', '광진, 성동']; - const increasePeople = () => { setMaxPeople((prev) => (prev < 100 ? prev + 1 : 100)); }; @@ -145,15 +171,27 @@ const ThunderPost = () => { if (!selectedPayment) { missingFieldsList.push('지불 방식'); } + if (!selectedAgeGroup) { + missingFieldsList.push('연령대'); + } + if (!selectedGenderGroup) { + missingFieldsList.push('성별'); + } if (!selectedDate) { missingFieldsList.push('날짜'); } if (!selectedTime) { missingFieldsList.push('시간'); } + if (!selectedLocation) { + missingFieldsList.push('지역'); + } if (!data.title) { missingFieldsList.push('제목'); } + if (!data.content) { + missingFieldsList.push('내용'); + } if (missingFieldsList.length > 0) { setModalMessage({ title1: '필수항목을 선택해주세요.', title2: missingFieldsList.join(', ') }); @@ -162,20 +200,42 @@ const ThunderPost = () => { } try { - const response = await authInstance.post('/api/meeting/detail/create/', { - ...data, + let meetingImageUrl = ''; + // 선택된 이미지를 새로운 파일로 생성 + if (selectedImage) { + const newFile = new File([selectedImage], selectedImage.name); + const formData = new FormData(); + // 폼 데이터에 input_source - s3 에 저정될 폴더이름과 images 파일이 들어갈 공간 추가 + formData.append('input_source', 'meeting'); + formData.append('images', newFile); + + // 이미지 업로드 API 호출 + const response = await authInstance.post('/api/common/image/', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + + // 업로드된 이미지 URL 저장 + meetingImageUrl = response.data.images_urls[0]; + } + + // 리뷰 생성 API 호출 + const response = await authInstance.post('/api/meetings/create/', { title: data.title, description: data.content, - location: selectedLocation, - payment_method: selectedPayment, - age_group: selectedAgeGroup, - gender_group: selectedGenderGroup, + location_name: selectedLocation, + payment_method_name: selectedPayment, + age_group_name: selectedAgeGroup, + gender_group_name: selectedGenderGroup, meeting_time: selectedDate ? `${selectedDate.toISOString().split('T')[0]}T${selectedTime}:00.000Z` : '', maximum: maxPeople, - meeting_image_url: selectedImages, + meeting_image_url: meetingImageUrl || null, }); + console.log(response.data); - toggleModal(); + setModalMessage({ title1: '소셜 다이닝 글 쓰기 작성이 완료되었습니다.', title2: '' }); + const meetingUuid = response.data.meeting_uuid; + toggleCenterModal(); + navigate(`/thunder/${meetingUuid}`); } catch (error) { console.error('폼 제출 중 오류가 발생했습니다:', error); setModalMessage({ title1: '폼 제출 중 오류가 발생했습니다.', title2: '다시 시도해주세요.' }); @@ -183,76 +243,69 @@ const ThunderPost = () => { } }; + + { + toggleCenterModal(); + if (modalMessage.title1 === '소셜 다이닝 글 쓰기 작성이 완료되었습니다.') { + navigate('/thunder'); + } + }} + className="mt-4 h-[50px] w-full rounded-xl bg-orange-500 px-4 py-2 font-bold text-white"> + 확인 + + ; return ( <>
지불 방식을 선택해주세요.
+ {/* backend - /api/categories/meetingpaymentfilter/ 에서 API - GET*/}
- - + {paymentMethods.map((method, index) => ( + + ))}
+ + {/* backend - /api/categories/meetingagefilter/ 에서 API - GET*/}
연령대를 선택해주세요
- - - - + {ageGroups.map((ageGroup, index) => ( + + ))}
+ {/* backend - /api/categories/meetinggenderfilter/ 에서 API - GET */}
성별을 선택해주세요
- - - - - + {genderGroups.map((genderGroup, index) => ( + + ))}
약속시간을 설정해주세요
@@ -332,46 +385,31 @@ const ThunderPost = () => { whileTap={{ scale: 0.95 }} className="flex flex-col items-center justify-center rounded-lg border border-gray-300 px-4 py-2"> 이미지 등록 - {`${selectedImages.length}/10`} + {selectedImage ? '1/1' : '0/1'} - - {imagePreviews.length > 0 && ( + + {imagePreview && (
- {imagePreviews.map((preview, index) => ( -
- {`미리보기 - - {index === 0 && ( -
- 대표사진 -
- )} +
+ 미리보기 + +
+ 대표사진
- ))} +
)}
{isUploading && (
사진 업로드 하는 중입니다.
- 현재 {currentUploadingIndex}장의 사진이 업로드 중입니다. + 현재 {1}장의 사진이 업로드 중입니다.
{ {/* 지역을 선택해주세요 modal open */} + {/* backend - api/categories/locationfilter/ 에서 API - GET */}
지역
- {locations.map((location) => ( + {locations.map((location, index) => (
- {/* 폼 제출 테스트에 따른 modal open*/} - - - 확인 - - - - {/* - <> -

지불 방식: {selectedPayment}

-

연령대: {selectedAgeGroup}

-

성별: {selectedGenderGroup}

-

- 약속시간: {selectedDate ? selectedDate.toLocaleDateString() : ''} {selectedTime} -

-

지역: {selectedLocation}

-

제목: {watch('title')}

-

내용: {watch('description')}

-

이미지 등록-첫번째 사진이 대표사진이 됩니다.

-
- {imagePreviews.map((preview, index) => ( - {`preview-${index}`} - ))} -
-

최대인원: {maxPeople}

- - - 확인 - -
*/} + {/* 폼 제출에 문제 알림 modal open*/} ); };