diff --git a/src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx b/src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx index c84ba8a2..d078857a 100644 --- a/src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx +++ b/src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx @@ -96,7 +96,7 @@ export default function ActivityDetailForm() { ); return ( -
+
{ + setDirection(-1); setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1)); }; const nextSlide = () => { + setDirection(1); setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1)); }; + const variants = { + enter: (direction: number) => ({ + x: direction > 0 ? 300 : -300, + opacity: 0, + }), + center: { + x: 0, + opacity: 1, + }, + exit: (direction: number) => ({ + x: direction > 0 ? -300 : 300, + opacity: 0, + }), + }; + return ( <> {/* 모바일 */} <div className='relative block aspect-square h-[300px] w-full overflow-hidden rounded-lg md:hidden'> - <Image - src={images[currentIndex]} - alt={`슬라이드 이미지 ${currentIndex + 1}`} - fill - className='object-cover hover:animate-pulse' - /> + <AnimatePresence custom={direction} initial={false}> + <motion.div + key={currentIndex} + custom={direction} + variants={variants} + initial='enter' + animate='center' + exit='exit' + transition={{ + x: { type: 'spring', stiffness: 300, damping: 30 }, + opacity: { duration: 0.2 }, + }} + className='absolute inset-0' + > + <Image + src={images[currentIndex]} + alt={` ${currentIndex + 1}`} + fill + className='rounded-lg object-cover' + priority + /> + </motion.div> + </AnimatePresence> + <button onClick={prevSlide} aria-label='이전 이미지' @@ -34,6 +71,7 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) { > ‹ </button> + <button onClick={nextSlide} aria-label='다음 이미지' @@ -41,6 +79,7 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) { > › </button> + <div className='absolute bottom-2 left-1/2 flex -translate-x-1/2 gap-1'> {images.map((_, i) => ( <div @@ -52,7 +91,8 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) { ))} </div> </div> - {/* PC 태블릿 */} + + {/* PC/태블릿 */} <div className='hidden h-[500px] grid-cols-4 grid-rows-4 gap-6 md:grid'> <div className='relative col-span-2 row-span-4 hover:animate-pulse'> <Image @@ -62,7 +102,6 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) { className='rounded-lg object-cover' /> </div> - {subImages.slice(0, 4).map((image, index) => ( <div key={index} diff --git a/src/app/(with-header)/activities/[id]/components/ReviewSection.tsx b/src/app/(with-header)/activities/[id]/components/ReviewSection.tsx index 103a7cc4..25422803 100644 --- a/src/app/(with-header)/activities/[id]/components/ReviewSection.tsx +++ b/src/app/(with-header)/activities/[id]/components/ReviewSection.tsx @@ -87,9 +87,11 @@ function ReviewSection({ <div className='relative min-h-350'> <ReviewTitle reviewCount={reviewCount} rating={rating} /> - <div className='pointer-events-none absolute inset-0 z-10 flex items-center justify-center'> - <div className='flex items-center justify-center font-bold'> - <p>작성된 리뷰가 없습니다</p> + <div className='pointer-events-none absolute inset-0 z-10 flex h-full items-center justify-center select-none'> + <div className='flex max-w-md items-center justify-center rounded-md bg-white px-20 py-20 shadow-2xl ring-1 ring-gray-200 select-none'> + <p className='text-md text-center font-bold text-gray-800'> + 작성된 후기가 없습니다 + </p> </div> </div> </div> @@ -98,20 +100,20 @@ function ReviewSection({ } if (isError) { - throw new Error('에러발생'); + throw new Error('리뷰섹션에서 에러가 발생했습니다.'); } return ( <div className='mt-10 flex flex-col space-y-8'> <ReviewTitle reviewCount={reviewCount} rating={rating} /> - <div className='relative min-h-350'> + <div className='pointer-events-none relative min-h-350 select-none'> <div className={user ? '' : 'blur-sm'}>{ReviewComponent()}</div> {!user && ( - <div className='pointer-events-none absolute inset-0 z-10 flex items-center justify-center'> - <div className='rounded-md bg-white/70 px-4 py-2 shadow-md'> - <p className='text-sm font-semibold text-gray-700'> + <div className='pointer-events-none absolute inset-0 z-10 flex h-full items-center justify-center select-none'> + <div className='flex max-w-md items-center justify-center rounded-md bg-white px-20 py-20 shadow-2xl ring-1 ring-gray-200 select-none'> + <p className='text-md text-center font-bold text-gray-800'> 로그인 후 리뷰 전체 내용을 확인할 수 있어요 </p> </div> diff --git a/src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx b/src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx index 5f359f74..3f378639 100644 --- a/src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx +++ b/src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx @@ -15,6 +15,11 @@ export default function ReviewTitle({ useEffect(() => { const handleSummary = () => { + if (reviewCount === 0) { + setSummary('작성된 후기가 없습니다'); + return; + } + if (rating >= 4.5) { setSummary('매우 만족'); } else if (rating >= 3) { diff --git a/src/app/(with-header)/activities/[id]/components/Skeleton.tsx b/src/app/(with-header)/activities/[id]/components/Skeleton.tsx deleted file mode 100644 index 89958efb..00000000 --- a/src/app/(with-header)/activities/[id]/components/Skeleton.tsx +++ /dev/null @@ -1,4 +0,0 @@ - -export function Skeleton({ className = '' }: { className?: string }) { - return <div className={`animate-pulse bg-gray-200 ${className}`} />; -} diff --git a/src/app/(with-header)/activities/[id]/components/Title.tsx b/src/app/(with-header)/activities/[id]/components/Title.tsx index e55989a7..c8bf18da 100644 --- a/src/app/(with-header)/activities/[id]/components/Title.tsx +++ b/src/app/(with-header)/activities/[id]/components/Title.tsx @@ -46,6 +46,7 @@ function Title({ mutate(id as string); setIsPopupOpen(false); }; + return ( <> @@ -55,14 +56,14 @@ function Title({ <h1 className='mb-2 text-2xl font-bold text-black md:text-3xl'> {title} </h1> - <div className='flex items-center gap-10 text-sm text-gray-600'> + <div className='flex flex-nowrap items-center gap-30 text-sm text-gray-600'> <div className='flex items-center gap-1'> <Star /> - <span className='font-medium'> + <p className='font-medium'> {rating.toFixed(1)} ({reviewCount}명) - </span> + </p> </div> - <span>{address}</span> + <p>{address}</p> </div> </div> diff --git a/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts b/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts index b9f0bd59..945687dd 100644 --- a/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts +++ b/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts @@ -8,6 +8,7 @@ import { uploadImage } from '../../utils/uploadImage'; import { ActivityDetailEdit, Schedule } from '@/types/activityDetailType'; import { AxiosError } from 'axios'; import { toast } from 'sonner'; +import { notFound } from 'next/navigation'; interface SubImageType { id?: number; @@ -32,7 +33,10 @@ export const useEditActivityForm = () => { const [originalSchedules, setOriginalSchedules] = useState<Schedule[]>([]); const [dates, setDates] = useState<Schedule[]>([]); - const { data, isLoading, isError } = useQuery<ActivityDetailEdit, Error>({ + const { data, isLoading, status, isError, error } = useQuery< + ActivityDetailEdit, + Error + >({ queryKey: ['edit-activity', id], queryFn: async () => { const res = await privateInstance.get(`/activities/${id}`); @@ -40,6 +44,15 @@ export const useEditActivityForm = () => { }, enabled: !!id, }); + if (status === 'error') { + const axiosError = error as AxiosError; + const httpStatus = axiosError.response?.status; + + if (httpStatus === 404) { + console.log('404 에러임'); + notFound(); + } + } useEffect(() => { if (data) { diff --git a/src/components/FloatingBox/BookingButton.tsx b/src/components/FloatingBox/BookingButton.tsx index ce31da89..af11bf37 100644 --- a/src/components/FloatingBox/BookingButton.tsx +++ b/src/components/FloatingBox/BookingButton.tsx @@ -1,10 +1,12 @@ import React from 'react'; +import { cn } from '@/lib/utils'; interface BookingButtonProps { onClick: () => void; children: React.ReactNode; disabled?: boolean; onBooking?: boolean; + className?: string; } export default function BookingButton({ @@ -12,21 +14,24 @@ export default function BookingButton({ children, disabled = false, onBooking = false, + className = '', }: BookingButtonProps) { return ( <button onClick={onClick} disabled={disabled || onBooking} - className={`relative mt-4 mb-6 w-full max-w-sm rounded-lg py-10 font-medium transition-colors ${ + className={cn( + 'relative mt-4 mb-6 w-full max-w-sm rounded-lg py-10 font-medium transition-colors', disabled || onBooking ? 'cursor-not-allowed bg-gray-300 text-gray-500' - : 'bg-green-800 text-white hover:bg-green-900' - }`} + : 'bg-green-800 text-white hover:bg-green-900', + className, + )} > {onBooking ? ( <div className='flex items-center justify-center gap-2'> <span className='h-10 w-10 animate-spin rounded-full border-2 border-white border-t-transparent' /> - <p>예약 중...</p> + <p>...</p> </div> ) : ( children diff --git a/src/components/FloatingBox/BookingInterface.tsx b/src/components/FloatingBox/BookingInterface.tsx index 09c254a0..5158b1a1 100644 --- a/src/components/FloatingBox/BookingInterface.tsx +++ b/src/components/FloatingBox/BookingInterface.tsx @@ -114,10 +114,10 @@ export default function BookingInterface({ <h3 className='mb-4 text-lg font-semibold text-gray-900'>날짜</h3> <button onClick={() => setIsOpen(true)} - className='w-full rounded-lg border border-gray-300 p-3 py-8 text-left text-black hover:bg-gray-50' + className='flex w-full items-center justify-center rounded-lg border border-gray-300 p-3 py-8 text-left text-black hover:bg-gray-50' > {selectedDate && selectedTime ? ( - <h2> + <h2 className='animate-pulse'> {selectedDate instanceof Date ? selectedDate.toLocaleDateString() : selectedDate} @@ -145,18 +145,13 @@ export default function BookingInterface({ </div> {/* 모바일 */} - <div className='fixed right-0 bottom-0 left-0 z-50 block border border-gray-200 bg-white p-6 md:hidden'> + <div className='fixed right-0 bottom-0 left-0 z-50 block h-150 border border-gray-200 bg-white px-20 md:hidden'> <div className='mb-6 flex items-start justify-between'> <div className='flex-1'> - <div className='mb-1 text-xl font-bold text-gray-900'> - ₩{price} - <span className='text-sm font-normal text-gray-600'> - / 총 {participants}인 - </span> - </div> + <PriceDisplay price={price} /> <div onClick={() => setIsOpen(true)} - className='mb-4 text-sm text-gray-600' + className='mb-4 animate-pulse cursor-pointer text-sm text-gray-600' > {selectedDate && selectedTime ? ( <h2> diff --git a/src/components/FloatingBox/PriceDisplay.tsx b/src/components/FloatingBox/PriceDisplay.tsx index c3fd83b3..4fd270d9 100644 --- a/src/components/FloatingBox/PriceDisplay.tsx +++ b/src/components/FloatingBox/PriceDisplay.tsx @@ -2,7 +2,8 @@ export default function PriceDisplay({ price }: { price: number }) { return ( <div className='mt-15 mb-6'> <div className='mb-1 text-2xl font-bold text-black'> - ₩{price} <span className='text-xl font-bold text-gray-800'>/ 인</span> + ₩{price.toLocaleString('ko-KR')}{' '} + <span className='text-xl font-bold text-gray-800'>/ 인</span> </div> </div> ); diff --git a/src/components/LocationMap.tsx b/src/components/LocationMap.tsx index 6ff38df5..d38cb02f 100644 --- a/src/components/LocationMap.tsx +++ b/src/components/LocationMap.tsx @@ -77,7 +77,7 @@ const LocationMap = ({ address }: LocationMapProps) => { return ( <> - <div className='flex h-[480px] w-full flex-col overflow-hidden rounded-lg shadow-md lg:max-w-[1200px]'> + <div className='flex h-[400px] w-full flex-col overflow-hidden rounded-lg shadow-md md:h-[480px] lg:max-w-[1200px]'> {/* 지도 */} <Map center={coords} diff --git a/src/components/Modal/Content.tsx b/src/components/Modal/Content.tsx index 622b8e11..f93d5979 100644 --- a/src/components/Modal/Content.tsx +++ b/src/components/Modal/Content.tsx @@ -31,7 +31,7 @@ export default function ModalContent({ const { isOpen } = useModalContext(); const [isMounted, setIsMounted] = useState(false); - const zIndexClass = zIndex ? `z-[${zIndex}]` : 'z-50'; + const zIndexClass = zIndex ? `z-[${zIndex}]` : 'z-999'; useEffect(() => { setIsMounted(true); diff --git a/src/ui/MobileBookingModal.tsx b/src/ui/MobileBookingModal.tsx index 85a61dc2..47c7852a 100644 --- a/src/ui/MobileBookingModal.tsx +++ b/src/ui/MobileBookingModal.tsx @@ -9,6 +9,7 @@ import BookingButton from '@/components/FloatingBox/BookingButton'; import ParticipantsSelector from '@/components/FloatingBox/ParticipantSelector'; import TotalPriceDisplay from '@/components/FloatingBox/TotalPriceDisplay'; import { SchedulesProps } from '@/types/activityDetailType'; +import { toast } from 'sonner'; export default function MobileModal({ schedules, @@ -27,6 +28,11 @@ export default function MobileModal({ ); const next = () => { + if (step === 'date-time' && !selectedTime) { + toast.error('시간을 선택해주세요.'); + return; + } + setStep((prev) => (prev === 'date-time' ? 'participants' : 'confirm')); }; @@ -34,6 +40,11 @@ export default function MobileModal({ setStep((prev) => (prev === 'confirm' ? 'participants' : 'date-time')); }; + const handleConfirm = () => { + setIsOpen(false); + setStep('date-time'); + }; + // const handleBooking = () => { // alert('예약이 완료되었습니다!'); // setIsOpen(false); @@ -42,7 +53,7 @@ export default function MobileModal({ return ( <Modal isOpen={isOpen} onOpenChange={setIsOpen}> - <Modal.Content zIndex={300}> + <Modal.Content zIndex={9999}> <Modal.Header> <Modal.Title>예약하기</Modal.Title> <Modal.Close /> @@ -67,22 +78,23 @@ export default function MobileModal({ </div> <div className={step === 'confirm' ? 'block' : 'hidden'}> <div className='flex min-h-400 flex-col items-center justify-center gap-20'> - <h3 className='text-xl font-bold'>예약 내역 확인</h3> <div> <p className='font-bold'> - 날짜 및 시간 {selectedDate?.toLocaleDateString()}{' '} - {selectedTime}{' '} + 날짜 및 시간 {selectedDate?.toLocaleDateString()}/ + {selectedTime} </p> </div> <div> <p className='font-bold'>인원 {participants}</p> </div> - <TotalPriceDisplay price={price} /> + <div className='flex items-end justify-end'> + <TotalPriceDisplay price={price} /> + </div> </div> </div> </Modal.Item> <Modal.Footer> - <div className='flex justify-between gap-10'> + <div className='flex justify-between gap-20'> {step !== 'date-time' && ( <button onClick={prev} @@ -99,7 +111,10 @@ export default function MobileModal({ 다음 </button> ) : ( - <BookingButton onClick={() => setIsOpen(false)}> + <BookingButton + className='flex-1 px-10 py-10' + onClick={() => handleConfirm()} + > 확인 </BookingButton> )}