From ef137ec0f9077b65d04c22ece918ee38fb092174 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 01:44:43 +0900 Subject: [PATCH 01/13] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[activityId]/prefetchActivity.ts | 20 +++++++++------- .../pages/activities/ActivityImageViewer.tsx | 9 +++++++- .../pages/activities/EditDropDown.tsx | 2 +- .../pages/activities/ImageGalleryModal.tsx | 23 +++++++++++++++---- .../bookingCard/BookingContainer.tsx | 3 ++- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/app/activities/[activityId]/prefetchActivity.ts b/src/app/activities/[activityId]/prefetchActivity.ts index c470c22..e1c9552 100644 --- a/src/app/activities/[activityId]/prefetchActivity.ts +++ b/src/app/activities/[activityId]/prefetchActivity.ts @@ -4,7 +4,7 @@ import { getBlurDataURL } from '@/lib/utils/blur'; /** * SSR prefetch용 통합 함수 - * Activity 기본 정보를 서버에서 미리 로드 + 상단 3장 LQIP(blur) 생성 + * Activity 기본 정보를 서버에서 미리 로드 + 모든 이미지 LQIP(blur) 생성 */ // NEW: 반환 타입 정의 @@ -42,7 +42,7 @@ export async function prefetchActivityData(activityId: string): Promise (sub.imageUrl ? getBlurDataURL(sub.imageUrl) : undefined)), ]); - blur = { banner: b, sub: [s0, s1] }; + blur = { + banner: bannerBlur, + sub: subBlurs, + }; + + console.log(`🎨 [SSR] 블러 이미지 생성 완료: 배너 1개 + 서브 ${subBlurs.length}개`); } console.log('✅ [SSR] Activity prefetch 성공', { activityId }); diff --git a/src/components/pages/activities/ActivityImageViewer.tsx b/src/components/pages/activities/ActivityImageViewer.tsx index 8632308..8067404 100644 --- a/src/components/pages/activities/ActivityImageViewer.tsx +++ b/src/components/pages/activities/ActivityImageViewer.tsx @@ -9,6 +9,7 @@ import { useOverlay } from '@/hooks/useOverlay'; import { motion } from 'motion/react'; import { useImageWithFallback } from '@/hooks/useImageWithFallback'; import clsx from 'clsx'; +import { wsrvLoader } from '@/components/common/wsrvLoader'; /** * 이미지를 표시하는 컴포넌트 @@ -58,10 +59,11 @@ export default function ActivityImageViewer({ subImages={subImages} title={title} initialIndex={index} + blurImage={blurImage} /> )); }, - [bannerImageUrl, subImages, title, overlay], + [bannerImageUrl, subImages, title, overlay, blurImage], ); return ( @@ -83,6 +85,7 @@ export default function ActivityImageViewer({ transition={{ duration: 0.3, ease: 'easeOut' }} > {title} {`${title} {`${title} { overlay.open(({ isOpen, close }) => ( { @@ -204,9 +209,13 @@ export default function ImageGalleryModal({ )} {`${title} handleImageLoad(index)} onError={() => { @@ -238,6 +247,9 @@ export default function ImageGalleryModal({ )} {`${title} {`${title} { console.log('🖼️ Thumbnail failed to load:', image.imageUrl); setImageErrors((prev) => ({ ...prev, [index]: true })); diff --git a/src/components/pages/activities/bookingCard/BookingContainer.tsx b/src/components/pages/activities/bookingCard/BookingContainer.tsx index b5c9970..e2565b5 100644 --- a/src/components/pages/activities/bookingCard/BookingContainer.tsx +++ b/src/components/pages/activities/bookingCard/BookingContainer.tsx @@ -70,7 +70,8 @@ export default function BookingContainer({ const month = format(selectedDate, 'MM'); return getAvailableSchedule(activityId, { year, month }); }, - staleTime: 5 * 60 * 1000, // 5분 캐시 + staleTime: 0, + gcTime: 0, enabled: !!selectedDate, // 날짜가 선택된 경우에만 실행 }); const totalPrice = price * memberCount; From 5c63c48629d18f41d0b1f2be3a579f2f3a3adc2e Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 02:29:07 +0900 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=EC=9D=98=20css=20=EA=B2=BD=EA=B3=A0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activities/[activityId]/ActivityClientPage.tsx | 1 - src/app/activities/[activityId]/page.tsx | 4 +--- .../activities/[activityId]/prefetchActivity.ts | 5 +---- .../pages/activities/ImageGalleryModal.tsx | 14 ++++---------- .../bookingCard/BookingConfirm.Modal.tsx | 5 +---- .../activities/bookingCard/BookingContainer.tsx | 7 ------- .../activities/bookingCard/BookingDateInput.tsx | 1 - .../pages/activities/payments/Payments.Modal.tsx | 3 +-- src/hooks/useInfiniteReviews.ts | 4 ---- src/hooks/useSchedulesByDate.ts | 3 --- 10 files changed, 8 insertions(+), 39 deletions(-) diff --git a/src/app/activities/[activityId]/ActivityClientPage.tsx b/src/app/activities/[activityId]/ActivityClientPage.tsx index 91ae50b..09948f7 100644 --- a/src/app/activities/[activityId]/ActivityClientPage.tsx +++ b/src/app/activities/[activityId]/ActivityClientPage.tsx @@ -71,7 +71,6 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient useEffect(() => { if (activity) { addViewed(activity); - console.log('👀 최근 본 목록에 추가됨', activity.title); } }, [activity, addViewed]); diff --git a/src/app/activities/[activityId]/page.tsx b/src/app/activities/[activityId]/page.tsx index 5a74a42..d6b55be 100644 --- a/src/app/activities/[activityId]/page.tsx +++ b/src/app/activities/[activityId]/page.tsx @@ -29,7 +29,6 @@ interface ActivityStaticParams { const ActivityPage = async ({ params }: ActivityPageProps) => { const startTime = performance.now(); - console.log('🎬 [SSR] ActivityPage 시작'); // params 추출 const { activityId } = await params; @@ -53,7 +52,6 @@ export default ActivityPage; // SSG를 위한 정적 경로 생성 export async function generateStaticParams(): Promise { const startTime = performance.now(); - console.log('🏗️ [SSG] generateStaticParams 시작 - 인기 체험 20개 선정'); try { const activities = await getActivitiesList({ @@ -68,7 +66,7 @@ export async function generateStaticParams(): Promise { })); const duration = performance.now() - startTime; - console.log(`⏱️ [SSG] generateStaticParams 완료: ${duration.toFixed(2)}ms`, { + console.log(`⏱️ [SSG] 인기 체험 20개 선정 완료: ${duration.toFixed(2)}ms`, { count: staticParams.length, activityIds: staticParams.map((p) => p.activityId), }); diff --git a/src/app/activities/[activityId]/prefetchActivity.ts b/src/app/activities/[activityId]/prefetchActivity.ts index e1c9552..1134797 100644 --- a/src/app/activities/[activityId]/prefetchActivity.ts +++ b/src/app/activities/[activityId]/prefetchActivity.ts @@ -4,7 +4,7 @@ import { getBlurDataURL } from '@/lib/utils/blur'; /** * SSR prefetch용 통합 함수 - * Activity 기본 정보를 서버에서 미리 로드 + 모든 이미지 LQIP(blur) 생성 + * Activity 기본 정보를 서버에서 미리 로드 + 모든 이미지 blur 생성 */ // NEW: 반환 타입 정의 @@ -14,9 +14,6 @@ export interface PrefetchActivityResult { } export async function prefetchActivityData(activityId: string): Promise { - // CHANGED - console.log('📡 [SSR] Activity 데이터 prefetch 시작', { activityId }); - const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/src/components/pages/activities/ImageGalleryModal.tsx b/src/components/pages/activities/ImageGalleryModal.tsx index fcaa076..8b04885 100644 --- a/src/components/pages/activities/ImageGalleryModal.tsx +++ b/src/components/pages/activities/ImageGalleryModal.tsx @@ -216,7 +216,7 @@ export default function ImageGalleryModal({ alt={`${title} - ${index + 1} 이미지`} width={600} height={400} - className='object-cover' + className='object-cover w-auto' onLoad={() => handleImageLoad(index)} onError={() => { console.log('🖼️ Mobile image failed to load:', image.imageUrl); @@ -235,7 +235,7 @@ export default function ImageGalleryModal({ { handleImageLoad(currentIndex); }} onError={() => { - console.log( - '🖼️ Modal image failed to load:', - allImages[currentIndex]?.imageUrl, - ); setImageErrors((prev) => ({ ...prev, [currentIndex]: true })); }} priority={currentIndex === 0} @@ -341,7 +336,6 @@ export default function ImageGalleryModal({ fill className='object-cover' onError={() => { - console.log('🖼️ Thumbnail failed to load:', image.imageUrl); setImageErrors((prev) => ({ ...prev, [index]: true })); }} /> diff --git a/src/components/pages/activities/bookingCard/BookingConfirm.Modal.tsx b/src/components/pages/activities/bookingCard/BookingConfirm.Modal.tsx index 8af3203..cfb9342 100644 --- a/src/components/pages/activities/bookingCard/BookingConfirm.Modal.tsx +++ b/src/components/pages/activities/bookingCard/BookingConfirm.Modal.tsx @@ -46,8 +46,7 @@ const BookingConfirmModal = ({ reservationData: ReservationRequest; }) => createReservation(activityId, reservationData), - onSuccess: async (data) => { - console.log('🎫 [BookingConfirmModal] 예약 성공:', data); + onSuccess: async () => { onClose(); successToast.run('예약이 완료되었습니다!'); router.push('/mypage/reservation-list'); @@ -63,7 +62,6 @@ const BookingConfirmModal = ({ // 401 Unauthorized 에러인 경우 로그인 페이지로 리다이렉트 if (axiosError?.response?.status === 401) { - console.log('🚨 예약 실패: 로그인 필요'); overlay.open(({ isOpen, close }) => ( { - console.log('🔘 예약 확정 버튼 클릭됨'); makeReservation({ activityId, reservationData: { diff --git a/src/components/pages/activities/bookingCard/BookingContainer.tsx b/src/components/pages/activities/bookingCard/BookingContainer.tsx index e2565b5..ec32994 100644 --- a/src/components/pages/activities/bookingCard/BookingContainer.tsx +++ b/src/components/pages/activities/bookingCard/BookingContainer.tsx @@ -87,7 +87,6 @@ export default function BookingContainer({ ...prev, [dateStr]: newSchedule.times, })); - console.log(`✅ [BookingCard] ${dateStr} 스케줄 업데이트됨`); } } }, [scheduleByDate, isSuccess, selectedDate]); @@ -112,12 +111,6 @@ export default function BookingContainer({ const handleBooking = () => { if (!selectedScheduleTime) return; - console.log('🎫 [BookingCard] 예약 요청:', { - activityId, - selectedScheduleTime, - memberCount, - totalPrice: price * memberCount, - }); overlay.open(({ isOpen, close }) => ( { - console.log('📅 selectedDate 변경:', selectedDate); if (selectedDate) { setYearValue(format(selectedDate, 'yyyy')); setMonthValue(format(selectedDate, 'MM')); diff --git a/src/components/pages/activities/payments/Payments.Modal.tsx b/src/components/pages/activities/payments/Payments.Modal.tsx index 5ac1fdc..09f146c 100644 --- a/src/components/pages/activities/payments/Payments.Modal.tsx +++ b/src/components/pages/activities/payments/Payments.Modal.tsx @@ -57,9 +57,8 @@ export default function PaymentsModal({ isOpen, onClose, title, totalPrice }: Pa customerName: user?.nickname || '홍길동', customerMobilePhone: '01012341234', }) - .then((result) => { + .then(() => { onClose(); - console.log('결제 성공, result:', result); overlay.open(({ isOpen, close }) => ( { - if (process.env.NODE_ENV === 'development') { - console.log('📡 API 요청:', { activityId, pageParam, pageSize }); - } - const result = await getActivityReviews(Number(activityId), { page: Number(pageParam), size: pageSize, diff --git a/src/hooks/useSchedulesByDate.ts b/src/hooks/useSchedulesByDate.ts index 1659f5d..4e62d7c 100644 --- a/src/hooks/useSchedulesByDate.ts +++ b/src/hooks/useSchedulesByDate.ts @@ -22,9 +22,6 @@ export function useSchedulesByDate(baseSchedules: Schedule[]): SchedulesByDate { endTime: schedule.endTime, }); }); - - console.log('📅 [useSchedulesByDate] 날짜별 스케줄 변환:', schedulesByDate); - return schedulesByDate; }, [baseSchedules]); } From ca55b6ea709a162ec43715f57a1ef86d85854884 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 06:25:25 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=EC=B2=B4=ED=97=98=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=9A=A9=20notFoundPage=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/activities/[activityId]/error.tsx | 52 ------------------- src/app/activities/[activityId]/not-found.tsx | 26 ++++++++++ src/app/activities/[activityId]/page.tsx | 6 --- .../[activityId]/prefetchActivity.ts | 8 ++- 4 files changed, 32 insertions(+), 60 deletions(-) delete mode 100644 src/app/activities/[activityId]/error.tsx create mode 100644 src/app/activities/[activityId]/not-found.tsx diff --git a/src/app/activities/[activityId]/error.tsx b/src/app/activities/[activityId]/error.tsx deleted file mode 100644 index e69ec42..0000000 --- a/src/app/activities/[activityId]/error.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; - -interface ErrorProps { - error: Error & { digest?: string }; - reset: () => void; -} - -export default function Error({ error, reset }: ErrorProps) { - useEffect(() => { - console.log('❌ [ERROR] ActivityPage 에러 발생', { - message: error.message, - digest: error.digest, - stack: error.stack, - }); - }, [error]); - - return ( -
-
-
🚧
-

체험 정보를 불러올 수 없습니다

-

- 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요. -

-
- - -
- {process.env.NODE_ENV === 'development' && ( -
- 개발자 정보 -
-              {error.message}
-            
-
- )} -
-
- ); -} diff --git a/src/app/activities/[activityId]/not-found.tsx b/src/app/activities/[activityId]/not-found.tsx new file mode 100644 index 0000000..e389018 --- /dev/null +++ b/src/app/activities/[activityId]/not-found.tsx @@ -0,0 +1,26 @@ +'use client'; + +import Lottie from 'lottie-react'; +import loadingLottie from '@/assets/lottie/404 Error - Doodle animation.json'; +import Link from 'next/link'; + +export default function ActivityNotFound() { + return ( +
+
+ +
+ +
체험을 찾을 수 없습니다.
+

+ 체험이 존재하지 않거나 삭제되었을 수 있습니다. +

+ + 홈으로 + +
+ ); +} diff --git a/src/app/activities/[activityId]/page.tsx b/src/app/activities/[activityId]/page.tsx index d6b55be..0b864fa 100644 --- a/src/app/activities/[activityId]/page.tsx +++ b/src/app/activities/[activityId]/page.tsx @@ -28,17 +28,11 @@ interface ActivityStaticParams { } const ActivityPage = async ({ params }: ActivityPageProps) => { - const startTime = performance.now(); - // params 추출 const { activityId } = await params; - // Activity 데이터 prefetch const { dehydratedState, blur } = await prefetchActivityData(activityId); - const duration = performance.now() - startTime; - console.log(`⏱️ [SSR] ActivityPage 완료: ${duration.toFixed(2)}ms`, { activityId }); - return ( }> diff --git a/src/app/activities/[activityId]/prefetchActivity.ts b/src/app/activities/[activityId]/prefetchActivity.ts index 1134797..b64c0fe 100644 --- a/src/app/activities/[activityId]/prefetchActivity.ts +++ b/src/app/activities/[activityId]/prefetchActivity.ts @@ -1,6 +1,7 @@ import { QueryClient, dehydrate, type DehydratedState } from '@tanstack/react-query'; import { getActivityDetail } from '@/app/api/activities'; import { getBlurDataURL } from '@/lib/utils/blur'; +import { notFound } from 'next/navigation'; /** * SSR prefetch용 통합 함수 @@ -36,7 +37,6 @@ export async function prefetchActivityData(activityId: string): Promise getActivityDetail(numericId), - // 필요시 staleTime/gcTime 부여 가능 }); // 캐시된 activity를 꺼내서 모든 이미지의 blur 생성 @@ -63,14 +63,18 @@ export async function prefetchActivityData(activityId: string): Promise Date: Mon, 15 Sep 2025 06:59:45 +0900 Subject: [PATCH 04/13] =?UTF-8?q?fix:=20=EC=A0=95=EA=B5=90=ED=95=9C=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/activities/[activityId]/page.tsx | 2 +- .../pages/activities/ActivitySkeleton.tsx | 109 ++++++++++-------- 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/app/activities/[activityId]/page.tsx b/src/app/activities/[activityId]/page.tsx index 0b864fa..592b6aa 100644 --- a/src/app/activities/[activityId]/page.tsx +++ b/src/app/activities/[activityId]/page.tsx @@ -32,7 +32,7 @@ const ActivityPage = async ({ params }: ActivityPageProps) => { const { activityId } = await params; // Activity 데이터 prefetch const { dehydratedState, blur } = await prefetchActivityData(activityId); - + return ; return ( }> diff --git a/src/components/pages/activities/ActivitySkeleton.tsx b/src/components/pages/activities/ActivitySkeleton.tsx index 858448c..666a71f 100644 --- a/src/components/pages/activities/ActivitySkeleton.tsx +++ b/src/components/pages/activities/ActivitySkeleton.tsx @@ -1,61 +1,70 @@ +import { Skeleton } from '@/components/ui/skeleton'; + /** * 선언형 스켈레톤 로딩 컴포넌트 * Suspense fallback으로 사용 */ export default function ActivitySkeleton() { return ( -
-
-
- {/* 이미지 영역 스켈레톤 */} -
-
-
- {Array.from({ length: 4 }, (_, i) => ( -
- ))} -
-
- - {/* 정보 영역 스켈레톤 */} -
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ {/* 이미지 영역 스켈레톤 */} +
+ + + +
+ {/* 제목 및 기본 정보 스켈레톤 */} +
+ + + +
+
+ {/* 설명 스켈레톤 */} +
+ + +
+ {/* 지도 스켈레톤 */} +
+ + + +
+
+ {/* 리뷰 스켈레톤 */} +
+ +
+ + + + +
+ {/* 리뷰 리스트 스켈레톤 */} +
+ {Array.from({ length: 3 }, (_, i) => ( + + ))} +
+
-
- - {/* 하단 섹션 스켈레톤 */} -
- {/* 지도 섹션 스켈레톤 */} -
-
-
-
- - {/* 리뷰 섹션 스켈레톤 */} -
-
-
- {Array.from({ length: 3 }, (_, i) => ( -
-
-
-
-
-
-
-
- ))} + {/* 사이드바 스켈레톤 */} +
+
+ + + +
+ + +
+
From a6ee9be4da8bf002ace2ada15a750709c6b5a982 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 07:02:03 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix:=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20=EC=9E=91=EC=97=85=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?return=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/activities/[activityId]/ActivityClientPage.tsx | 2 +- src/app/activities/[activityId]/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/activities/[activityId]/ActivityClientPage.tsx b/src/app/activities/[activityId]/ActivityClientPage.tsx index 09948f7..a285526 100644 --- a/src/app/activities/[activityId]/ActivityClientPage.tsx +++ b/src/app/activities/[activityId]/ActivityClientPage.tsx @@ -25,7 +25,7 @@ interface ActivityClientProps { export default function ActivityClient({ activityId, blurImage }: ActivityClientProps) { const [isOwner, setIsOwner] = useState(false); - const user = useUserStore((state) => state.user); + const { user } = useUserStore(); // 1. 정적 데이터 (이미지, 주소, 제목, 설명) - 긴 캐시 const { data: staticInfo } = useSuspenseQuery({ diff --git a/src/app/activities/[activityId]/page.tsx b/src/app/activities/[activityId]/page.tsx index 592b6aa..0b864fa 100644 --- a/src/app/activities/[activityId]/page.tsx +++ b/src/app/activities/[activityId]/page.tsx @@ -32,7 +32,7 @@ const ActivityPage = async ({ params }: ActivityPageProps) => { const { activityId } = await params; // Activity 데이터 prefetch const { dehydratedState, blur } = await prefetchActivityData(activityId); - return ; + return ( }> From 08107114de294ba78402406eeac14a180470d473 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 13:46:25 +0900 Subject: [PATCH 06/13] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=EB=A5=BC=20=EC=9C=84=ED=95=B4=20web?= =?UTF-8?q?p=20=ED=8F=AC=EB=A7=B7=20=EB=B0=8F=20=EC=A0=81=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=ED=95=84=ED=84=B0=EB=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/wsrvLoader.tsx | 5 ++++- .../pages/activities/ActivityImageViewer.tsx | 12 ++++++------ .../pages/activities/ImageGalleryModal.tsx | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/common/wsrvLoader.tsx b/src/components/common/wsrvLoader.tsx index 85ff229..1f40439 100644 --- a/src/components/common/wsrvLoader.tsx +++ b/src/components/common/wsrvLoader.tsx @@ -7,5 +7,8 @@ export const wsrvLoader = ({ width: number; quality?: number; }) => { - return `https://wsrv.nl/?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 75}`; + // WebP 포맷과 적응형 크기 지원으로 이미지 최적화 + const actualQuality = quality || 75; + + return `https://wsrv.nl/?url=${encodeURIComponent(src)}&w=${width}&q=${actualQuality}&output=webp&af`; }; diff --git a/src/components/pages/activities/ActivityImageViewer.tsx b/src/components/pages/activities/ActivityImageViewer.tsx index 8067404..c40a63a 100644 --- a/src/components/pages/activities/ActivityImageViewer.tsx +++ b/src/components/pages/activities/ActivityImageViewer.tsx @@ -85,11 +85,11 @@ export default function ActivityImageViewer({ transition={{ duration: 0.3, ease: 'easeOut' }} > wsrvLoader({ ...props, quality: 80 })} src={bannerImage.src} alt={title} fill - sizes='(max-width: 768px) 50vw, 25vw' + sizes='(max-width: 1024px) 100vw, 50vw' className='object-cover cursor-pointer' onClick={() => handleImageClick(0)} onError={bannerImage.onError} @@ -128,12 +128,12 @@ export default function ActivityImageViewer({ transition={{ duration: 0.3, ease: 'easeOut' }} > wsrvLoader({ ...props, quality: 75 })} loading='lazy' src={subImage1.src} alt={`${title} 서브 이미지 1`} fill - sizes='(max-width: 768px) 50vw, 25vw' + sizes='(max-width: 1024px) 50vw, 25vw' className='object-cover cursor-pointer' onClick={() => handleImageClick(1)} onError={subImage1.onError} @@ -160,12 +160,12 @@ export default function ActivityImageViewer({ transition={{ duration: 0.3, ease: 'easeOut' }} > wsrvLoader({ ...props, quality: 75 })} loading='lazy' src={subImage2.src} alt={`${title} 서브 이미지 2`} fill - sizes='(max-width: 768px) 50vw, 25vw' + sizes='(max-width: 1024px) 50vw, 25vw' className='object-cover cursor-pointer' onClick={() => handleImageClick(2)} onError={subImage2.onError} diff --git a/src/components/pages/activities/ImageGalleryModal.tsx b/src/components/pages/activities/ImageGalleryModal.tsx index 8b04885..d2fc329 100644 --- a/src/components/pages/activities/ImageGalleryModal.tsx +++ b/src/components/pages/activities/ImageGalleryModal.tsx @@ -209,7 +209,7 @@ export default function ImageGalleryModal({
)} wsrvLoader({ ...props, quality: 75 })} placeholder='blur' blurDataURL={allBlurImageURLs[index]} src={getCurrentImageSrc(index)} @@ -247,7 +247,7 @@ export default function ImageGalleryModal({
)} wsrvLoader({ ...props, quality: 85 })} placeholder='blur' blurDataURL={allBlurImageURLs[currentIndex]} src={getCurrentImageSrc(currentIndex)} @@ -327,7 +327,7 @@ export default function ImageGalleryModal({ whileTap={{ scale: 0.95 }} > wsrvLoader({ ...props, quality: 60 })} loading='lazy' placeholder='blur' blurDataURL={allBlurImageURLs[index]} From 9c19844c1ed6267401eca27acced113a9df175bc Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 16:00:41 +0900 Subject: [PATCH 07/13] =?UTF-8?q?fix:=20modal,=20maps,=20review=EC=97=90?= =?UTF-8?q?=20intersection=20observer=20=EA=B8=B0=EB=B0=98=20lazy,=20dynam?= =?UTF-8?q?ic=20=20load=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[activityId]/ActivityClientPage.tsx | 39 ++++++++++++++---- src/components/common/naverMaps/NaverMap.tsx | 4 +- .../pages/activities/ActivityImageViewer.tsx | 10 +++-- .../bookingCard/BookingContainer.tsx | 8 +++- src/hooks/useIntersectionObserver.ts | 40 +++++++++++++++++++ 5 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 src/hooks/useIntersectionObserver.ts diff --git a/src/app/activities/[activityId]/ActivityClientPage.tsx b/src/app/activities/[activityId]/ActivityClientPage.tsx index a285526..a39dd07 100644 --- a/src/app/activities/[activityId]/ActivityClientPage.tsx +++ b/src/app/activities/[activityId]/ActivityClientPage.tsx @@ -12,6 +12,7 @@ import Marker from '@/components/common/naverMaps/Marker'; import ImageMarker from '@/components/common/naverMaps/ImageMarker'; import { activityQueryKeys } from './queryKeys'; import { useUserStore } from '@/store/userStore'; +import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'; /** * ActivityClient 컴포넌트 @@ -27,6 +28,16 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient const [isOwner, setIsOwner] = useState(false); const { user } = useUserStore(); + // Intersection Observer for performance optimization + const [mapRef, isMapVisible] = useIntersectionObserver({ + rootMargin: '100px', + triggerOnce: true, + }); + const [reviewRef, isReviewVisible] = useIntersectionObserver({ + rootMargin: '200px', + triggerOnce: true, + }); + // 1. 정적 데이터 (이미지, 주소, 제목, 설명) - 긴 캐시 const { data: staticInfo } = useSuspenseQuery({ queryKey: [...activityQueryKeys.detail(activityId), 'static'], @@ -106,23 +117,35 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient
{/* 주소 섹션 */} -
+

오시는 길

{activity.address}

- - - - - + {isMapVisible ? ( + + + + + + ) : ( +
+ 지도 로딩 준비 중... +
+ )}

{/* 후기 섹션 */} -
+

체험 후기

{activity.reviewCount}개

- + {isReviewVisible ? ( + + ) : ( +
+ 후기 로딩 준비 중... +
+ )}
diff --git a/src/components/common/naverMaps/NaverMap.tsx b/src/components/common/naverMaps/NaverMap.tsx index 9830513..8b6d228 100644 --- a/src/components/common/naverMaps/NaverMap.tsx +++ b/src/components/common/naverMaps/NaverMap.tsx @@ -1,12 +1,12 @@ 'use client'; import { motion, AnimatePresence } from 'framer-motion'; -import { Suspense, useId } from 'react'; +import { Suspense, useId, lazy } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import NaverMapSkeleton from './NaverMapSkeleton'; import NaverMapError from './NaverMapError'; -import NaverMapCore from './NaverMapCore'; +const NaverMapCore = lazy(() => import('./NaverMapCore')); /** * NaverMap 컴포넌트 * - 네이버 지도 API를 사용한 지도 렌더링 컴포넌트 diff --git a/src/components/pages/activities/ActivityImageViewer.tsx b/src/components/pages/activities/ActivityImageViewer.tsx index c40a63a..362a0b4 100644 --- a/src/components/pages/activities/ActivityImageViewer.tsx +++ b/src/components/pages/activities/ActivityImageViewer.tsx @@ -4,7 +4,6 @@ import { SubImage } from '@/types/activities.type'; import Image from 'next/image'; import { useCallback } from 'react'; import { Expand, ImageIcon } from 'lucide-react'; -import ImageGalleryModal from '@/components/pages/activities/ImageGalleryModal'; import { useOverlay } from '@/hooks/useOverlay'; import { motion } from 'motion/react'; import { useImageWithFallback } from '@/hooks/useImageWithFallback'; @@ -47,9 +46,14 @@ export default function ActivityImageViewer({ // 남은 이미지 개수 = 전체 - 표시된 3개 const remainingCount = Math.max(0, allImages.length - 3); - // 이미지 클릭 핸들러 + // 이미지 클릭 핸들러 (Dynamic Import) const handleImageClick = useCallback( - (index: number) => { + async (index: number) => { + // Dynamic import로 모달 로딩 + const { default: ImageGalleryModal } = await import( + '@/components/pages/activities/ImageGalleryModal' + ); + // 모달 열기 overlay.open(({ isOpen, close }) => ( { + const handleBooking = async () => { if (!selectedScheduleTime) return; + // Dynamic import로 예약 확인 모달 로딩 + const { default: BookingConfirmModal } = await import( + '@/components/pages/activities/bookingCard/BookingConfirm.Modal' + ); + overlay.open(({ isOpen, close }) => ( , boolean] { + const { threshold = 0.1, rootMargin = '0px', triggerOnce = true } = options; + const [isIntersecting, setIsIntersecting] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const element = ref.current; + if (!element) return; + + const observer = new IntersectionObserver( + ([entry]) => { + setIsIntersecting(entry.isIntersecting); + + // triggerOnce가 true이고 요소가 보이면 관찰 중단 + if (triggerOnce && entry.isIntersecting) { + observer.unobserve(element); + } + }, + { threshold, rootMargin }, + ); + + observer.observe(element); + + return () => { + observer.unobserve(element); + }; + }, [threshold, rootMargin, triggerOnce]); + + return [ref, isIntersecting]; +} From 9d904797c44b90e07064ec7ce3281527cfa45c99 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 16:26:40 +0900 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20review=20component=20=EA=B0=80=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=98=EA=B2=8C=20lazy=EB=A1=9C?= =?UTF-8?q?=20=EB=B6=88=EB=9F=AC=EC=99=80=EC=A0=B8=EC=84=9C=20=ED=8D=BC?= =?UTF-8?q?=ED=8F=AC=EB=A8=BC=EC=8A=A4=EB=A5=BC=20=EB=82=AE=EC=B6=94?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[activityId]/ActivityClientPage.tsx | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/app/activities/[activityId]/ActivityClientPage.tsx b/src/app/activities/[activityId]/ActivityClientPage.tsx index a39dd07..ab65abd 100644 --- a/src/app/activities/[activityId]/ActivityClientPage.tsx +++ b/src/app/activities/[activityId]/ActivityClientPage.tsx @@ -13,6 +13,8 @@ import ImageMarker from '@/components/common/naverMaps/ImageMarker'; import { activityQueryKeys } from './queryKeys'; import { useUserStore } from '@/store/userStore'; import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'; +import { motion } from 'framer-motion'; +import { MapPinned } from 'lucide-react'; /** * ActivityClient 컴포넌트 @@ -33,10 +35,6 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient rootMargin: '100px', triggerOnce: true, }); - const [reviewRef, isReviewVisible] = useIntersectionObserver({ - rootMargin: '200px', - triggerOnce: true, - }); // 1. 정적 데이터 (이미지, 주소, 제목, 설명) - 긴 캐시 const { data: staticInfo } = useSuspenseQuery({ @@ -127,25 +125,32 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient ) : ( -
- 지도 로딩 준비 중... +
+ {/* MapPinned 아이콘 하나가 중앙에서 콩콩 뛰는 애니메이션 */} + + +
)}
{/* 후기 섹션 */} -
+

체험 후기

{activity.reviewCount}개

- {isReviewVisible ? ( - - ) : ( -
- 후기 로딩 준비 중... -
- )} +
From 62a729556d6976c7b671aacc529abf3da800dc08 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 16:42:25 +0900 Subject: [PATCH 09/13] =?UTF-8?q?fix:=20=EB=8F=99=EC=A0=81=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0(=EA=B0=80=EA=B2=A9,=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=A5=B4,=20=ED=8F=89=EC=A0=90)=EC=9D=98=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[activityId]/ActivityClientPage.tsx | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/app/activities/[activityId]/ActivityClientPage.tsx b/src/app/activities/[activityId]/ActivityClientPage.tsx index ab65abd..9e0bbd1 100644 --- a/src/app/activities/[activityId]/ActivityClientPage.tsx +++ b/src/app/activities/[activityId]/ActivityClientPage.tsx @@ -1,7 +1,8 @@ 'use client'; import { useState, useEffect, useMemo } from 'react'; -import { useSuspenseQuery } from '@tanstack/react-query'; +import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'; import { getActivityDetail } from '@/app/api/activities'; +import type { ActivityDetail } from '@/types/activities.type'; import { useRecentViewedStore } from '@/store/recentlyWatched'; import ActivityImageViewer from '@/components/pages/activities/ActivityImageViewer'; import ActivityInfo from '@/components/pages/activities/ActivityInfo'; @@ -29,6 +30,7 @@ interface ActivityClientProps { export default function ActivityClient({ activityId, blurImage }: ActivityClientProps) { const [isOwner, setIsOwner] = useState(false); const { user } = useUserStore(); + const queryClient = useQueryClient(); // Intersection Observer for performance optimization const [mapRef, isMapVisible] = useIntersectionObserver({ @@ -56,11 +58,31 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient gcTime: 60 * 60 * 1000, // 1시간 메모리 보관 }); - // 2. 동적 데이터 (가격, 스케줄, 평점) - 짧은 캐시 + // 2. 동적 데이터 (가격, 스케줄, 평점) - 짧은 캐시, 캐시 재사용 최적화 const { data: dynamicInfo } = useSuspenseQuery({ queryKey: [...activityQueryKeys.detail(activityId), 'dynamic'], - queryFn: () => getActivityDetail(Number(activityId)), - select: (data) => ({ + queryFn: (): Promise => { + // 캐시된 데이터가 있으면 재사용, 없으면 새로 호출 + const cachedData = queryClient.getQueryData([ + ...activityQueryKeys.detail(activityId), + 'static', + ]); + const cachedState = queryClient.getQueryState([ + ...activityQueryKeys.detail(activityId), + 'static', + ]); + + // static 캐시가 fresh하면(2분 이내) 재사용 + if ( + cachedData && + cachedState?.dataUpdatedAt && + Date.now() - cachedState.dataUpdatedAt < 2 * 60 * 1000 + ) { + return Promise.resolve(cachedData); // 캐시 재사용 + } + return getActivityDetail(Number(activityId)); // 새 호출 + }, + select: (data: ActivityDetail) => ({ price: data.price, schedules: data.schedules, rating: data.rating, From 552cf76d8feafc48fc29f0ce506000c678d2afb4 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 17:53:33 +0900 Subject: [PATCH 10/13] =?UTF-8?q?fix:=20error=20=EB=B0=8F=20not=20found=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=9A=94=EC=86=8C=EB=A5=BC=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.js | 1 + package-lock.json | 186 ++++++++++++++++++ package.json | 2 + .../[activityId]/ActivityClientPage.tsx | 6 +- src/app/activities/[activityId]/not-found.tsx | 24 ++- src/app/error.tsx | 24 ++- .../pages/activities/ReviewCard.tsx | 5 +- 7 files changed, 236 insertions(+), 12 deletions(-) diff --git a/next.config.js b/next.config.js index a3e230d..4771757 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ const nextConfig = { images: { unoptimized: true, diff --git a/package-lock.json b/package-lock.json index c8f5c70..48ea462 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@next/bundle-analyzer": "^15.5.3", "@storybook/addon-essentials": "7.6.20", "@storybook/addon-interactions": "7.6.20", "@storybook/addon-links": "7.6.20", @@ -4261,6 +4262,16 @@ "node": ">=6" } }, + "node_modules/@next/bundle-analyzer": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.5.3.tgz", + "integrity": "sha512-l2NxnWHP2gWHbomAlz/wFnN2jNCx/dpr7P/XWeOLhULiyKkXSac8O8SjxRO/8FNhr2l4JNtWVKk82Uya4cZYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "webpack-bundle-analyzer": "4.10.1" + } + }, "node_modules/@next/env": { "version": "14.2.32", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.32.tgz", @@ -4599,6 +4610,13 @@ "node": ">=8.9.0" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -13133,6 +13151,13 @@ "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", "license": "MIT" }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -13772,6 +13797,13 @@ "node": ">= 0.4" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -16501,6 +16533,22 @@ "dev": true, "license": "MIT" }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -16702,6 +16750,13 @@ ], "license": "MIT" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -19457,6 +19512,16 @@ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", "license": "MIT" }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -20192,6 +20257,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -23304,6 +23379,21 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -24606,6 +24696,16 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -25639,6 +25739,92 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-dev-middleware": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", diff --git a/package.json b/package.json index f0f6f17..8a92116 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev", "build": "next build", + "analyze": "npx @next/bundle-analyzer .next/static/chunks/", "start": "next start", "lint": "next lint", "prepare": "husky", @@ -74,6 +75,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@next/bundle-analyzer": "^15.5.3", "@storybook/addon-essentials": "7.6.20", "@storybook/addon-interactions": "7.6.20", "@storybook/addon-links": "7.6.20", diff --git a/src/app/activities/[activityId]/ActivityClientPage.tsx b/src/app/activities/[activityId]/ActivityClientPage.tsx index 9e0bbd1..249f8b3 100644 --- a/src/app/activities/[activityId]/ActivityClientPage.tsx +++ b/src/app/activities/[activityId]/ActivityClientPage.tsx @@ -32,7 +32,6 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient const { user } = useUserStore(); const queryClient = useQueryClient(); - // Intersection Observer for performance optimization const [mapRef, isMapVisible] = useIntersectionObserver({ rootMargin: '100px', triggerOnce: true, @@ -58,7 +57,7 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient gcTime: 60 * 60 * 1000, // 1시간 메모리 보관 }); - // 2. 동적 데이터 (가격, 스케줄, 평점) - 짧은 캐시, 캐시 재사용 최적화 + // 2. 동적 데이터 (가격, 스케줄, 평점) - 짧은 캐시, 재사용 최적화 const { data: dynamicInfo } = useSuspenseQuery({ queryKey: [...activityQueryKeys.detail(activityId), 'dynamic'], queryFn: (): Promise => { @@ -72,7 +71,7 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient 'static', ]); - // static 캐시가 fresh하면(2분 이내) 재사용 + // static 캐시가 fresh하면 재사용 if ( cachedData && cachedState?.dataUpdatedAt && @@ -148,7 +147,6 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient ) : (
- {/* MapPinned 아이콘 하나가 중앙에서 콩콩 뛰는 애니메이션 */} import('lottie-react')); export default function ActivityNotFound() { + const [animationData, setAnimationData] = useState(null); + + useEffect(() => { + // 404 애니메이션 동적 로드 (305KB) + import('@/assets/lottie/404 Error - Doodle animation.json') + .then((module) => setAnimationData(module.default)) + .catch(() => setAnimationData(null)); + }, []); + return (
- + {animationData ? ( +
} + > + + + ) : ( +
+ )}
체험을 찾을 수 없습니다.
diff --git a/src/app/error.tsx b/src/app/error.tsx index 61d3caf..22ef81f 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,8 +1,9 @@ 'use client'; -import Lottie from 'lottie-react'; -import loadingLottie from '@/assets/lottie/T-rex.json'; import Link from 'next/link'; +import { lazy, Suspense, useState, useEffect } from 'react'; + +const Lottie = lazy(() => import('lottie-react')); export default function GlobalError({ error, @@ -11,10 +12,27 @@ export default function GlobalError({ error: Error & { digest?: string }; reset: () => void; }) { + const [animationData, setAnimationData] = useState(null); + + useEffect(() => { + // 동적으로 Lottie JSON 로드 + import('@/assets/lottie/T-rex.json') + .then((module) => setAnimationData(module.default)) + .catch(() => setAnimationData(null)); + }, []); + return (
- + {animationData ? ( +
} + > + + + ) : ( +
+ )}
뭔가 잘못되었습니다.
{error.message} diff --git a/src/components/pages/activities/ReviewCard.tsx b/src/components/pages/activities/ReviewCard.tsx index 650cc25..63123c3 100644 --- a/src/components/pages/activities/ReviewCard.tsx +++ b/src/components/pages/activities/ReviewCard.tsx @@ -2,12 +2,13 @@ import { Stars } from '@/components/common/Stars'; import { Review } from '@/types/reviews.type'; +import { memo } from 'react'; interface ReviewCardProps { review: Review; } -export function ReviewCard({ review }: ReviewCardProps) { +export const ReviewCard = memo(function ReviewCard({ review }: ReviewCardProps) { return (
@@ -26,4 +27,4 @@ export function ReviewCard({ review }: ReviewCardProps) {

{review.content}

); -} +}); From 4867c72f5452f03bcf1572ea776182b7191b90d7 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 18:01:12 +0900 Subject: [PATCH 11/13] =?UTF-8?q?fix:=20aria-label=EC=9D=84=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EC=95=98=EB=8D=98=20?= =?UTF-8?q?=EA=B3=B3=EB=93=A4=EC=9D=84=20=EC=88=98=EC=A0=95=ED=96=88?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pages/activities/EditDropDown.tsx | 1 + src/components/pages/activities/bookingCard/BookingMember.tsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/components/pages/activities/EditDropDown.tsx b/src/components/pages/activities/EditDropDown.tsx index ae49c87..62bf51b 100644 --- a/src/components/pages/activities/EditDropDown.tsx +++ b/src/components/pages/activities/EditDropDown.tsx @@ -120,6 +120,7 @@ export function EditDropDown({ activityId, isOwner, open, setOpen }: EditDropDow
{/* 트리거 버튼 */}
); -} +}); + +export default ActivityInfo; diff --git a/src/components/pages/activities/bookingCard/BookingContainer.tsx b/src/components/pages/activities/bookingCard/BookingContainer.tsx index 53ec6d4..eae130b 100644 --- a/src/components/pages/activities/bookingCard/BookingContainer.tsx +++ b/src/components/pages/activities/bookingCard/BookingContainer.tsx @@ -4,7 +4,10 @@ import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { format } from 'date-fns'; import BookingCardDesktop from './BookingCardDesktop'; -import BookingCardMobile from './BookingCardMobile'; +import { lazy, Suspense } from 'react'; + +// 모바일 예약 카드 지연 로딩 +const BookingCardMobile = lazy(() => import('./BookingCardMobile')); import BookingError from '@/components/pages/activities/bookingCard/BookingError'; import { ErrorBoundary } from 'react-error-boundary'; import { getAvailableSchedule } from '@/app/api/activities'; @@ -189,7 +192,15 @@ export default function BookingContainer({ - + +
+
+ } + > + + From 2b223814457f119aa8a54feb9624a5eceb734356 Mon Sep 17 00:00:00 2001 From: Hwigeon Date: Mon, 15 Sep 2025 20:24:14 +0900 Subject: [PATCH 13/13] =?UTF-8?q?fix:=20=EC=A7=80=EB=8F=84=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[activityId]/ActivityClientPage.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/app/activities/[activityId]/ActivityClientPage.tsx b/src/app/activities/[activityId]/ActivityClientPage.tsx index 9d8743a..d0bda85 100644 --- a/src/app/activities/[activityId]/ActivityClientPage.tsx +++ b/src/app/activities/[activityId]/ActivityClientPage.tsx @@ -14,8 +14,6 @@ import ImageMarker from '@/components/common/naverMaps/ImageMarker'; import { activityQueryKeys } from './queryKeys'; import { useUserStore } from '@/store/userStore'; import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'; -import { motion } from 'framer-motion'; -import { MapPinned } from 'lucide-react'; /** * ActivityClient 컴포넌트 @@ -146,21 +144,7 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient ) : ( -
- - - -
+
)}