diff --git a/next.config.ts b/next.config.ts index ced41739..7ad5ff2f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -17,6 +17,10 @@ const nextConfig: NextConfig = { }, ], }, + + compiler: { + removeConsole: process.env.NODE_ENV === 'production', + }, }; export default nextConfig; diff --git a/public/assets/img/default-bg.png b/public/assets/img/default-bg.png new file mode 100644 index 00000000..58a4a779 Binary files /dev/null and b/public/assets/img/default-bg.png differ diff --git a/src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx b/src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx index d078857a..3b5cd6c0 100644 --- a/src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx +++ b/src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx @@ -138,7 +138,7 @@ export default function ActivityDetailForm() { diff --git a/src/app/(with-header)/activities/[id]/components/ImageGrid.tsx b/src/app/(with-header)/activities/[id]/components/ImageGrid.tsx index e5cd38e3..a993a3d0 100644 --- a/src/app/(with-header)/activities/[id]/components/ImageGrid.tsx +++ b/src/app/(with-header)/activities/[id]/components/ImageGrid.tsx @@ -4,21 +4,33 @@ import Image from 'next/image'; import React, { useState } from 'react'; import { ImageGridProps } from '@/types/activityDetailType'; import { AnimatePresence, motion } from 'framer-motion'; +import Modal from '@/components/Modal'; +import { DEFAULT_BG } from '@/constants/AvatarConstants'; function ImageGrid({ mainImage, subImages }: ImageGridProps) { - const images = [mainImage, ...subImages]; - + const [image, setImage] = useState([mainImage, ...subImages]); const [currentIndex, setCurrentIndex] = useState(0); const [direction, setDirection] = useState(0); + const [isOpen, setIsOpen] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); + + const handleImageClick = (image: string) => { + setSelectedImage(image); + setIsOpen(true); + }; + + const handleImageError = (index: number) => { + setImage((prev) => prev.map((src, i) => (i === index ? DEFAULT_BG : src))); + }; const prevSlide = () => { setDirection(-1); - setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1)); + setCurrentIndex((prev) => (prev === 0 ? image.length - 1 : prev - 1)); }; const nextSlide = () => { setDirection(1); - setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1)); + setCurrentIndex((prev) => (prev === image.length - 1 ? 0 : prev + 1)); }; const variants = { @@ -26,10 +38,7 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) { x: direction > 0 ? 300 : -300, opacity: 0, }), - center: { - x: 0, - opacity: 1, - }, + center: { x: 0, opacity: 1 }, exit: (direction: number) => ({ x: direction > 0 ? -300 : 300, opacity: 0, @@ -55,11 +64,13 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) { className='absolute inset-0' > {` handleImageError(currentIndex)} /> @@ -81,7 +92,7 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) {
- {images.map((_, i) => ( + {image.map((_, i) => (
-
+
handleImageClick(mainImage)} + className='relative col-span-2 row-span-4 hover:animate-pulse' + > 메인이미지 handleImageError(0)} />
- {subImages.slice(0, 4).map((image, index) => ( + {image.slice(1, 5).map((image, index) => (
handleImageClick(image)} className='relative col-span-1 row-span-2 h-full hover:animate-pulse' > {`서브이미지 handleImageError(index + 1)} />
))}
+ + + + + + +
+ {selectedImage && ( + 확대 이미지 + )} +
+
+ + +
+
); } diff --git a/src/app/(with-header)/activities/[id]/components/ReviewSection.tsx b/src/app/(with-header)/activities/[id]/components/ReviewSection.tsx index 51a889cb..c2d45cc1 100644 --- a/src/app/(with-header)/activities/[id]/components/ReviewSection.tsx +++ b/src/app/(with-header)/activities/[id]/components/ReviewSection.tsx @@ -9,24 +9,9 @@ import { privateInstance } from '@/apis/privateInstance'; import ReviewTitle from './ReviewTitle'; import useUserStore from '@/stores/authStore'; import cn from '@/lib/cn'; - import ReviewCardSkeleton from './Skeletons/ReviewCardSkeleton'; - -interface ReviewSectionProps { - activityId: string; - reviewCount: number; - rating: number; -} - -interface ReviewProps { - id: string; - user: { - nickname: string; - profileImageUrl: string; - }; - createdAt: string; - content: string; -} +import { ReviewSectionProps } from '@/types/activityDetailType'; +import { ReviewProps } from '@/types/activityDetailType'; function ReviewSection({ activityId, diff --git a/src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx b/src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx index 3f378639..7dadf062 100644 --- a/src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx +++ b/src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx @@ -2,11 +2,8 @@ import Star from '@assets/svg/star'; import { useState, useEffect } from 'react'; +import { ReviewTitleProps } from '@/types/activityDetailType'; -interface ReviewTitleProps { - reviewCount: number; - rating: number; -} export default function ReviewTitle({ reviewCount = 0, rating = 0, diff --git a/src/app/(with-header)/activities/[id]/components/Title.tsx b/src/app/(with-header)/activities/[id]/components/Title.tsx index c8bf18da..4f618076 100644 --- a/src/app/(with-header)/activities/[id]/components/Title.tsx +++ b/src/app/(with-header)/activities/[id]/components/Title.tsx @@ -11,15 +11,7 @@ import { useParams, useRouter } from 'next/navigation'; import { useQueryClient } from '@tanstack/react-query'; import { useDeleteActivity } from '../hooks/useDeleteActivity'; import Popup from '@/components/Popup'; - -interface TitleProps { - title: string; - category: string; - rating: number; - reviewCount: number; - address: string; - isOwner: boolean; -} +import { TitleProps } from '@/types/activityDetailType'; function Title({ title, @@ -43,10 +35,9 @@ function Title({ const handleDeleteConfirm = () => { if (!id) return; - mutate(id as string); + mutate(Number(id)); setIsPopupOpen(false); }; - return ( <> diff --git a/src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts b/src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts index 7d6dfff4..14470f20 100644 --- a/src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts +++ b/src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts @@ -4,7 +4,7 @@ import { AxiosError } from 'axios'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; -const deleteActivity = async (id: string) => { +const deleteActivity = async (id: number) => { const response = await privateInstance.delete(`/deleteActivity/${id}`); return response.data; }; @@ -23,6 +23,7 @@ export const useDeleteActivity = () => { }); queryClient.invalidateQueries({ queryKey: ['popularExperiences'] }); router.push(`/`); + toast.success('체험이 삭제되었습니다!'); }, onError: (error: AxiosError) => { const responseData = error.response?.data as diff --git a/src/app/(with-header)/activities/[id]/mock/mock.ts b/src/app/(with-header)/activities/[id]/mock/mock.ts deleted file mode 100644 index fe2f95bb..00000000 --- a/src/app/(with-header)/activities/[id]/mock/mock.ts +++ /dev/null @@ -1,58 +0,0 @@ -export const mockActivity = { - id: 5088, - userId: 2145, - title: '함께 배우면 즐거운 스트릿댄스', - description: '둠칫 둠칫 두둠칫', - category: '투어', - price: 10000, - address: '서울특별시 강남구 테헤란로 427', - bannerImageUrl: '/test/image1.png', - rating: 0, - reviewCount: 0, - createdAt: '2025-07-16T16:49:19.971Z', - updatedAt: '2025-07-16T16:49:19.971Z', - subImages: [ - { - id: 10643, - imageUrl: '/test/image2.png', - }, - { - id: 10644, - imageUrl: '/test/image3.png', - }, - { - id: 10645, - imageUrl: '/test/image4.png', - }, - { - id: 10646, - imageUrl: '/test/image5.png', - }, - ], - schedules: [ - { - id: 21515, - date: '2025-12-01', - startTime: '12:00', - endTime: '13:00', - }, - { - id: 21516, - date: '2025-12-05', - startTime: '12:00', - endTime: '13:00', - }, - { - id: 21517, - date: '2025-12-05', - startTime: '13:00', - endTime: '14:00', - }, - { - id: 21518, - date: '2025-12-05', - startTime: '14:00', - endTime: '15:00', - }, - ], -}; diff --git a/src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx b/src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx index 75595352..90328d5f 100644 --- a/src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx +++ b/src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx @@ -6,11 +6,7 @@ import { ScheduleSelectForm } from '../../components/ScheduleSelectForm'; import { ImageSection } from '../../components/ImageSection'; import { useEditActivityForm } from '../hooks/useEditActivityForm'; import EditActivityFormSkeleton from '../../loading'; - -interface SubImageType { - id?: number; - url: string | File; -} +import { SubImageType } from '@/types/addEditExperienceType'; export default function EditActivityForm() { const { diff --git a/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts b/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts index 6290a048..9a79d610 100644 --- a/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts +++ b/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts @@ -9,15 +9,10 @@ import { ActivityDetailEdit, Schedule } from '@/types/activityDetailType'; import { AxiosError } from 'axios'; import { toast } from 'sonner'; import { notFound } from 'next/navigation'; -import { validateSchedules } from '../../utils/dateValidatoin'; - -interface SubImageType { - id?: number; - url: string | File; -} +import { SubImageType } from '@/types/addEditExperienceType'; export const useEditActivityForm = () => { - const { id } = useParams() as { id: string }; + const { id } = useParams(); const router = useRouter(); const queryClient = useQueryClient(); @@ -209,9 +204,16 @@ export const useEditActivityForm = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const validationMessage = validateSchedules(dates); - if (validationMessage) { - toast.error(validationMessage); + if ( + !title || + !category || + !description || + !address || + !price || + !mainImage || + dates.length === 0 + ) { + toast.error('소개이미지를 제외한 모든값은 필수값입니다!'); return; } try { diff --git a/src/app/(with-header)/myactivity/components/AddressInput.tsx b/src/app/(with-header)/myactivity/components/AddressInput.tsx index 86a52d93..49321eba 100644 --- a/src/app/(with-header)/myactivity/components/AddressInput.tsx +++ b/src/app/(with-header)/myactivity/components/AddressInput.tsx @@ -5,20 +5,8 @@ import DaumPostcode from 'react-daum-postcode'; import { useState } from 'react'; import Input from '@/components/Input'; import Button from '@/components/Button'; - -interface AddressInputProps { - onAddressChange: (address: string) => void; - address: string; -} - -interface PostcodeData { - address: string; - addressType: 'R' | 'J'; - bname: string; - buildingName: string; - zonecode: string; - userSelectedType: string; -} +import { AddressInputProps } from '@/types/addEditExperienceType'; +import { PostcodeData } from '@/types/addEditExperienceType'; export default function AddressInput({ onAddressChange, diff --git a/src/app/(with-header)/myactivity/components/CategoryInput.tsx b/src/app/(with-header)/myactivity/components/CategoryInput.tsx index 2fecdacf..e9c059f0 100644 --- a/src/app/(with-header)/myactivity/components/CategoryInput.tsx +++ b/src/app/(with-header)/myactivity/components/CategoryInput.tsx @@ -1,9 +1,5 @@ import ChevronIcon from '@assets/svg/chevron'; // 아이콘 경로는 맞게 조정 - -interface CategoryProps { - category?: string; - onCategoryChange: (value: string) => void; -} +import { CategoryProps } from '@/types/addEditExperienceType'; export default function CategoryInput({ category, @@ -16,7 +12,7 @@ export default function CategoryInput({ */}
{/* 커스텀 화살표 아이콘 */} -
+
diff --git a/src/app/(with-header)/myactivity/components/FormSection.tsx b/src/app/(with-header)/myactivity/components/FormSection.tsx index cb33e7cc..f3534de7 100644 --- a/src/app/(with-header)/myactivity/components/FormSection.tsx +++ b/src/app/(with-header)/myactivity/components/FormSection.tsx @@ -1,10 +1,5 @@ import type React from 'react'; - -interface FormSectionProps { - title: string; - children: React.ReactNode; - description?: string; -} +import { FormSectionProps } from '@/types/addEditExperienceType'; export function FormSection({ title, diff --git a/src/app/(with-header)/myactivity/components/ImagePreview.tsx b/src/app/(with-header)/myactivity/components/ImagePreview.tsx index 1948f6e5..31a0de6b 100644 --- a/src/app/(with-header)/myactivity/components/ImagePreview.tsx +++ b/src/app/(with-header)/myactivity/components/ImagePreview.tsx @@ -1,14 +1,8 @@ 'use client'; import IconClose from '@assets/svg/close'; -import { useState,useEffect } from 'react'; - -interface ImagePreviewProps { - image: File | string; - onRemove: () => void; - alt: string; - className?: string; -} +import { useState, useEffect } from 'react'; +import { ImagePreviewProps } from '@/types/addEditExperienceType'; export function ImagePreview({ image, @@ -43,10 +37,14 @@ export function ImagePreview({
); diff --git a/src/app/(with-header)/myactivity/components/ImageSection.tsx b/src/app/(with-header)/myactivity/components/ImageSection.tsx index eddd9b32..24390caa 100644 --- a/src/app/(with-header)/myactivity/components/ImageSection.tsx +++ b/src/app/(with-header)/myactivity/components/ImageSection.tsx @@ -2,22 +2,14 @@ import { MainImageSelect } from './MainImageSelect'; import { SubImageSelect } from './SubImageSelect'; - -interface ImagesSectionProps { - mainImage: string | File | null; - subImage: (string | File)[]; - onMainImageSelect: (file: File) => void; - onMainImageRemove: () => void; - onSubImageAdd: (files: File[]) => void; - onSubImageRemove: (index: number) => void; -} +import { ImagesSectionProps } from '@/types/addEditExperienceType'; export function ImageSection({ mainImage, subImage, onMainImageSelect, onMainImageRemove, - onSubImageAdd, + onSubImageAdd, onSubImageRemove, }: ImagesSectionProps) { return ( diff --git a/src/app/(with-header)/myactivity/components/ImageUpload.tsx b/src/app/(with-header)/myactivity/components/ImageUpload.tsx index 12a9bb93..68465be4 100644 --- a/src/app/(with-header)/myactivity/components/ImageUpload.tsx +++ b/src/app/(with-header)/myactivity/components/ImageUpload.tsx @@ -1,13 +1,7 @@ 'use client'; import type React from 'react'; - -interface ImageUploadProps { - onImageSelect: (file: File) => void; - multiple?: boolean; - className?: string; - children?: React.ReactNode; -} +import { ImageUploadProps } from '@/types/addEditExperienceType'; export function ImageUpload({ onImageSelect, diff --git a/src/app/(with-header)/myactivity/components/InfoSection.tsx b/src/app/(with-header)/myactivity/components/InfoSection.tsx index c507ac6a..7f1631b5 100644 --- a/src/app/(with-header)/myactivity/components/InfoSection.tsx +++ b/src/app/(with-header)/myactivity/components/InfoSection.tsx @@ -4,19 +4,7 @@ import Input from '@/components/Input'; import AddressInput from './AddressInput'; import CategoryInput from './CategoryInput'; import Textarea from '@/components/Textarea'; - -interface InfoSectionProps { - title?: string; - category?: string; - price?: string; - description?: string; - address?: string; - onTitleChange: (value: string) => void; - onCategoryChange: (value: string) => void; - onPriceChange: (value: string) => void; - onDescriptionChange: (value: string) => void; - onAddressChange: (value: string) => void; -} +import { InfoSectionProps } from '@/types/addEditExperienceType'; export function InfoSection({ title = '', diff --git a/src/app/(with-header)/myactivity/components/MainImageSelect.tsx b/src/app/(with-header)/myactivity/components/MainImageSelect.tsx index 354cd84d..22c461d0 100644 --- a/src/app/(with-header)/myactivity/components/MainImageSelect.tsx +++ b/src/app/(with-header)/myactivity/components/MainImageSelect.tsx @@ -2,12 +2,7 @@ import { ImageUpload } from './ImageUpload'; import { ImagePreview } from './ImagePreview'; - -interface MainImageSelectProps { - mainImage: File | string | null; - onImageSelect: (file: File) => void; - onImageRemove: () => void; -} +import { MainImageSelectProps } from '@/types/addEditExperienceType'; export function MainImageSelect({ mainImage, diff --git a/src/app/(with-header)/myactivity/components/ScheduleSelect.tsx b/src/app/(with-header)/myactivity/components/ScheduleSelect.tsx index c64fd3f5..50ee8531 100644 --- a/src/app/(with-header)/myactivity/components/ScheduleSelect.tsx +++ b/src/app/(with-header)/myactivity/components/ScheduleSelect.tsx @@ -2,19 +2,7 @@ import Input from '@/components/Input'; import IconClose from '@assets/svg/close'; - -interface ScheduleSelectProps { - index: number; - isRemovable: boolean; - onAddDate: () => void; - onRemove: (index: number) => void; - onDateChange: (index: number, value: string) => void; - onStartTimeChange: (index: number, value: string) => void; - onEndTimeChange: (index: number, value: string) => void; - date: string; - startTime: string; - endTime: string; -} +import { ScheduleSelectProps } from '@/types/addEditExperienceType'; export function ScheduleSelect({ index, diff --git a/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx b/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx index 4e63ec11..0a0361a5 100644 --- a/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx +++ b/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx @@ -2,29 +2,9 @@ import { toast } from 'sonner'; import { ScheduleSelect } from './ScheduleSelect'; -import { Schedule } from '@/types/activityDetailType'; - -interface ScheduleSelectFormProps { - dates: Schedule[]; - onAddDate: () => void; - onRemoveDate: (index: number) => void; - onDateChange: ( - index: number, - field: keyof Omit, - value: string, - ) => void; -} - -function isPastDate(dateStr: string) { - const selected = new Date(dateStr); - const today = new Date(); - today.setHours(0, 0, 0, 0); - return selected < today; -} - -function isInvalidTimeRange(start: string, end: string) { - return start >= end; -} +import { ScheduleSelectFormProps } from '@/types/addEditExperienceType'; +import { isPastDate } from '../utils/dateValidatoin'; +import { isInvalidTimeRange } from '../utils/dateValidatoin'; export function ScheduleSelectForm({ dates, diff --git a/src/app/(with-header)/myactivity/components/SubImageSelect.tsx b/src/app/(with-header)/myactivity/components/SubImageSelect.tsx index ff8ae544..1e0a30cc 100644 --- a/src/app/(with-header)/myactivity/components/SubImageSelect.tsx +++ b/src/app/(with-header)/myactivity/components/SubImageSelect.tsx @@ -1,11 +1,6 @@ import { ImagePreview } from './ImagePreview'; import { ImageUpload } from './ImageUpload'; - -interface SubImageSelectProps { - subImage: (string | File)[]; - onImagesAdd: (files: File[]) => void; - onImageRemove: (index: number) => void; -} +import { SubImageSelectProps } from '@/types/addEditExperienceType'; export function SubImageSelect({ subImage, diff --git a/src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts b/src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts index e3ca6d53..e21806d6 100644 --- a/src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts +++ b/src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts @@ -8,12 +8,7 @@ import axios from 'axios'; import { uploadImage } from '../utils/uploadImage'; import { privateInstance } from '@/apis/privateInstance'; import { useQueryClient } from '@tanstack/react-query'; - -export interface DateSlot { - date: string; - startTime: string; - endTime: string; -} +import { DateSlot } from '@/types/addEditExperienceType'; export const useCreateActivityForm = () => { const [dates, setDates] = useState([ @@ -132,9 +127,10 @@ export const useCreateActivityForm = () => { !description || !address || !price || + !mainImage || dates.length === 0 ) { - toast.error('모든 필드를 입력해주세요.'); + toast.error('소개이미지를 제외한 모든값은 필수값입니다!'); return; } mutation.mutate(); diff --git a/src/app/(with-header)/myactivity/utils/dateValidatoin.ts b/src/app/(with-header)/myactivity/utils/dateValidatoin.ts index 6aa91ce6..c5a2283e 100644 --- a/src/app/(with-header)/myactivity/utils/dateValidatoin.ts +++ b/src/app/(with-header)/myactivity/utils/dateValidatoin.ts @@ -29,3 +29,14 @@ export function validateSchedules(schedules: Schedule[]) { return null; } + +export function isPastDate(dateStr: string) { + const selected = new Date(dateStr); + const today = new Date(); + today.setHours(0, 0, 0, 0); + return selected < today; +} + +export function isInvalidTimeRange(start: string, end: string) { + return start >= end; +} diff --git a/src/components/DatePicker/CalendarBody.tsx b/src/components/DatePicker/CalendarBody.tsx index 195dcfd2..27c8f3bc 100644 --- a/src/components/DatePicker/CalendarBody.tsx +++ b/src/components/DatePicker/CalendarBody.tsx @@ -2,6 +2,7 @@ import type dayjs from 'dayjs'; import { CalendarBodyProps } from '@/types/datePickerTypes'; +import cn from '@/lib/cn'; export default function CalendarBody({ viewDate, @@ -51,36 +52,43 @@ export default function CalendarBody({ ); diff --git a/src/components/FloatingBox/BookingButton.tsx b/src/components/FloatingBox/BookingButton.tsx index 3ae771ea..93e66286 100644 --- a/src/components/FloatingBox/BookingButton.tsx +++ b/src/components/FloatingBox/BookingButton.tsx @@ -1,13 +1,6 @@ import React from 'react'; import { cn } from '@/lib/utils'; - -interface BookingButtonProps { - onClick: () => void; - children: React.ReactNode; - disabled?: boolean; - onBooking?: boolean; - className?: string; -} +import { BookingButtonProps } from '@/types/bookingInterfaceType'; export default function BookingButton({ onClick, diff --git a/src/components/FloatingBox/BookingInterface.tsx b/src/components/FloatingBox/BookingInterface.tsx index 5158b1a1..53046992 100644 --- a/src/components/FloatingBox/BookingInterface.tsx +++ b/src/components/FloatingBox/BookingInterface.tsx @@ -9,12 +9,7 @@ import TotalPriceDisplay from './TotalPriceDisplay'; import BookingModal from '@/ui/BookingModal'; import DatePicker from '../DatePicker/DatePicker'; import { SchedulesProps } from '@/types/activityDetailType'; -import { privateInstance } from '@/apis/privateInstance'; -import { useParams } from 'next/navigation'; -import { AxiosError } from 'axios'; -import useUserStore from '@/stores/authStore'; -import { toast } from 'sonner'; -import { useState } from 'react'; +import { useBooking } from '@/hooks/useBooking'; export default function BookingInterface({ schedules, @@ -27,62 +22,16 @@ export default function BookingInterface({ isOwner: boolean; price: number; }) { - const [onBooking, setOnBooking] = useState(false); - const { user } = useUserStore(); const setIsOpen = useBookingStore((state) => state.setIsOpen); + const { + onBooking, + handleBooking, + isBookable, + buttonText, selectedDate, selectedTime, - participants, - selectedTimeId, - setToInitial, - } = useBookingStore(); - - const { id } = useParams(); - - const handleBooking = async () => { - setOnBooking(true); - try { - await privateInstance.post(`/activities/${id}/reservation`, { - selectedTimeId, - participants, - }); - - toast.success('예약되었습니다!'); - setToInitial(); - } catch (err) { - const error = err as AxiosError; - const responseData = error.response?.data as - | { error?: string; message?: string } - | undefined; - - console.error('전체 에러:', error); - - toast.error( - responseData?.error || - responseData?.message || - error.message || - '예약에 실패했습니다.', - ); - } finally { - setOnBooking(false); - } - }; - - const isLoggedIn = !!user; - const isBookable = - !!selectedDate && - !!selectedTime && - !!selectedTimeId && - !!participants && - !isOwner && - isLoggedIn; - - const buttonText = !isLoggedIn - ? '로그인이 필요한 기능입니다' - : isOwner - ? '본인이 등록한 체험입니다' - : '예약하기'; + } = useBooking(isOwner); return (
diff --git a/src/components/FloatingBox/PriceDisplay.tsx b/src/components/FloatingBox/PriceDisplay.tsx index 4fd270d9..60620f1a 100644 --- a/src/components/FloatingBox/PriceDisplay.tsx +++ b/src/components/FloatingBox/PriceDisplay.tsx @@ -1,9 +1,18 @@ export default function PriceDisplay({ price }: { price: number }) { return ( -
-
- ₩{price.toLocaleString('ko-KR')}{' '} - / 인 +
+
+

+ ₩{price.toLocaleString('ko-KR')} + /인 +

+ +
+
+ + : 예약 가능 일자 + +
); diff --git a/src/components/FloatingBox/TabletPopup.tsx b/src/components/FloatingBox/TabletPopup.tsx deleted file mode 100644 index e3f01a3f..00000000 --- a/src/components/FloatingBox/TabletPopup.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// 'use client'; - -// import useBookingStore from '@/stores/Booking/useBookingStore'; -// import IconClose from '@assets/svg/close'; -// import DatePicker from '../DatePicker/DatePicker'; -// import TimeSelector from './TimeSelector'; -// import { SchedulesProps } from '@/types/activityDetailType'; - -// export default function TabletPopup({schedules}:SchedulesProps) { -// const isOpen = useBookingStore((state) => state.isOpen); -// const setIsOpen = useBookingStore((state) => state.setIsOpen); - -// if (!isOpen) return null; - -// return ( -//
-//
-//

날짜

-// -//
- -// - -// -//
-// ); -// } diff --git a/src/components/Modal/Example/TestModal.tsx b/src/components/Modal/Example/TestModal.tsx deleted file mode 100644 index a630f878..00000000 --- a/src/components/Modal/Example/TestModal.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client'; - -import Modal from '@/components/Modal'; - -export default function TestModal({ - isOpen, - setIsOpen, -}: { - isOpen: boolean; - setIsOpen: React.Dispatch>; -}) { - //예시 동작 - function TestAction() { - window.alert('리뷰 작성'); - setIsOpen(false); - } - - return ( - // 외부에서 정의된 모달 제어 state를 받아서 사용 - - - - 제목 - - - 제어모달예시 - - - - - - - ); -} diff --git a/src/components/Modal/Example/TestModalButton.tsx b/src/components/Modal/Example/TestModalButton.tsx deleted file mode 100644 index b9af0759..00000000 --- a/src/components/Modal/Example/TestModalButton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import TestModal from './TestModal'; - -export default function TestModalButton() { - const [isOpen, setIsOpen] = useState(false); //외부에서 모달을 제어하기위한 state 정의 - - return ( -
- - - {/* props로 state를 내려줌 */} - -
- ); -} diff --git a/src/components/Modal/Example/UncontrolledModal.tsx b/src/components/Modal/Example/UncontrolledModal.tsx deleted file mode 100644 index ffc909c8..00000000 --- a/src/components/Modal/Example/UncontrolledModal.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Modal from '..'; - -export default function UncontrolledModal() { - return ( - - 비제어 모달 열기 - - - 제목 - - - 비제어모달 - - - - - ); -} diff --git a/src/components/Modal/Example/readme b/src/components/Modal/Example/readme deleted file mode 100644 index 4331d421..00000000 --- a/src/components/Modal/Example/readme +++ /dev/null @@ -1,38 +0,0 @@ -모달 제어/비제어 사용 예시를 보여드리기위해 만들어둔 폴더입니다. - -제어모달예시 - TestModalButton.tsx , TestModal.tsx - -비제어모달예시 - UncontrolledModal.tsx - - ---- - -# 제어(Controlled) 모달 - -- 외부에서 모달의 열림/닫힘 상태를 직접 관리하는 방식입니다. -- 보통 `useState`로 상태를 만들고, `isOpen`, `onOpenChange`(또는 setState)를 모달에 전달합니다. -- 모달을 열거나 닫는 로직을 컴포넌트 외부에서 제어할 수 있어, 복잡한 조건이나 여러 곳에서 모달 상태를 변경해야 할 때 유용합니다. - -**예시 파일:** -- `TestModalButton.tsx` -- `TestModal.tsx` - - ---- - -# 비제어(Uncontrolled) 모달 - -- **모달 컴포넌트 내부에서 열림/닫힘 상태를 자체적으로 관리**하는 방식입니다. -- 별도의 상태 관리가 필요 없고, 간단하게 모달을 띄우고 닫을 때 사용하기 좋습니다. -- 외부에서 모달의 상태를 직접 제어할 수 없으므로, 단순한 용도에 적합합니다. - -**예시 파일:** -- `UncontrolledModal.tsx` - ---- - -## 요약 - -- **제어 모달**: 외부에서 상태를 관리하며, 복잡한 상황에 적합(어떤 상황발생시 트리거 되어 열리고 특정 액션(폼 작성후 제출등)이 트리거되어 닫혀야하는모달) -- **비제어 모달**: 내부에서 상태를 관리하며, 간단한 상황에 적합(엮인 동작이 없는 단순 정보출력용 모달) - diff --git a/src/constants/AvatarConstants.ts b/src/constants/AvatarConstants.ts index 0e5eeb80..b47adefe 100644 --- a/src/constants/AvatarConstants.ts +++ b/src/constants/AvatarConstants.ts @@ -5,3 +5,6 @@ export const AVATAR_SIZE = { }; export const DEFAULT_IMG = '/assets/svg/profile-default.svg'; + + +export const DEFAULT_BG = '/assets/img/default-bg.png'; diff --git a/src/hooks/useBooking.ts b/src/hooks/useBooking.ts new file mode 100644 index 00000000..d2abbad6 --- /dev/null +++ b/src/hooks/useBooking.ts @@ -0,0 +1,74 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { privateInstance } from '@/apis/privateInstance'; +import { AxiosError } from 'axios'; +import { toast } from 'sonner'; +import useBookingStore from '@/stores/Booking/useBookingStore'; +import useUserStore from '@/stores/authStore'; +import { useMutation } from '@tanstack/react-query'; + +export const useBooking = (isOwner: boolean) => { + const { id } = useParams(); + const { user } = useUserStore(); + + const { + selectedDate, + selectedTime, + participants, + selectedTimeId, + setToInitial, + } = useBookingStore(); + + const bookingMutation = useMutation({ + mutationFn: async () => { + return await privateInstance.post(`/activities/${id}/reservation`, { + selectedTimeId, + participants, + }); + }, + onSuccess: () => { + toast.success('예약되었습니다!'); + setToInitial(); + }, + onError: (err) => { + const error = err as AxiosError; + const responseData = error.response?.data as + | { error?: string; message?: string } + | undefined; + + console.error('전체 에러:', error); + + toast.error( + responseData?.error || + responseData?.message || + error.message || + '예약에 실패했습니다.', + ); + }, + }); + + const isLoggedIn = !!user; + const isBookable = + !!selectedDate && + !!selectedTime && + !!selectedTimeId && + !!participants && + !isOwner && + isLoggedIn; + + const buttonText = !isLoggedIn + ? '로그인이 필요한 기능입니다' + : isOwner + ? '본인이 등록한 체험입니다' + : '예약하기'; + + return { + onBooking: bookingMutation.isPending, + handleBooking: () => bookingMutation.mutate(), + isBookable, + buttonText, + selectedDate, + selectedTime, + }; +}; diff --git a/src/types/activityDetailType.ts b/src/types/activityDetailType.ts index 0f254019..8fbe42f0 100644 --- a/src/types/activityDetailType.ts +++ b/src/types/activityDetailType.ts @@ -86,3 +86,33 @@ export interface ActivityDetailEdit { }[]; schedules: Schedule[]; } + +export interface TitleProps { + title: string; + category: string; + rating: number; + reviewCount: number; + address: string; + isOwner: boolean; +} + +export interface ReviewTitleProps { + reviewCount: number; + rating: number; +} + +export interface ReviewSectionProps { + activityId: number; + reviewCount: number; + rating: number; +} + +export interface ReviewProps { + id: string; + user: { + nickname: string; + profileImageUrl: string; + }; + createdAt: string; + content: string; +} diff --git a/src/types/addEditExperienceType.ts b/src/types/addEditExperienceType.ts new file mode 100644 index 00000000..ae53d765 --- /dev/null +++ b/src/types/addEditExperienceType.ts @@ -0,0 +1,111 @@ +import { Schedule } from '@/types/activityDetailType'; + +export interface SubImageType { + id?: number; + url: string | File; +} + +export interface AddressInputProps { + onAddressChange: (address: string) => void; + address: string; +} + +export interface PostcodeData { + address: string; + addressType: 'R' | 'J'; + bname: string; + buildingName: string; + zonecode: string; + userSelectedType: string; +} + +export interface CategoryProps { + category?: string; + onCategoryChange: (value: string) => void; +} + +export interface FormSectionProps { + title: string; + children: React.ReactNode; + description?: string; +} + +export interface ImagePreviewProps { + image: File | string; + onRemove: () => void; + alt: string; + className?: string; +} + +export interface ImagesSectionProps { + mainImage: string | File | null; + subImage: (string | File)[]; + onMainImageSelect: (file: File) => void; + onMainImageRemove: () => void; + onSubImageAdd: (files: File[]) => void; + onSubImageRemove: (index: number) => void; +} + +export interface ImageUploadProps { + onImageSelect: (file: File) => void; + multiple?: boolean; + className?: string; + children?: React.ReactNode; +} + +export interface InfoSectionProps { + title?: string; + category?: string; + price?: string; + description?: string; + address?: string; + onTitleChange: (value: string) => void; + onCategoryChange: (value: string) => void; + onPriceChange: (value: string) => void; + onDescriptionChange: (value: string) => void; + onAddressChange: (value: string) => void; +} + +export interface MainImageSelectProps { + mainImage: File | string | null; + onImageSelect: (file: File) => void; + onImageRemove: () => void; +} + + +export interface SubImageSelectProps { + subImage: (string | File)[]; + onImagesAdd: (files: File[]) => void; + onImageRemove: (index: number) => void; +} + +export interface ScheduleSelectProps { + index: number; + isRemovable: boolean; + onAddDate: () => void; + onRemove: (index: number) => void; + onDateChange: (index: number, value: string) => void; + onStartTimeChange: (index: number, value: string) => void; + onEndTimeChange: (index: number, value: string) => void; + date: string; + startTime: string; + endTime: string; +} + +export interface ScheduleSelectFormProps { + dates: Schedule[]; + onAddDate: () => void; + onRemoveDate: (index: number) => void; + onDateChange: ( + index: number, + field: keyof Omit, + value: string, + ) => void; +} + + +export interface DateSlot { + date: string; + startTime: string; + endTime: string; +} diff --git a/src/types/bookingInterfaceType.ts b/src/types/bookingInterfaceType.ts new file mode 100644 index 00000000..6720b0eb --- /dev/null +++ b/src/types/bookingInterfaceType.ts @@ -0,0 +1,7 @@ +export interface BookingButtonProps { + onClick: () => void; + children: React.ReactNode; + disabled?: boolean; + onBooking?: boolean; + className?: string; +}