11'use client' ;
22import { useState , useEffect , useMemo } from 'react' ;
3- import { useSuspenseQuery } from '@tanstack/react-query' ;
3+ import { useSuspenseQuery , useQueryClient } from '@tanstack/react-query' ;
44import { getActivityDetail } from '@/app/api/activities' ;
5+ import type { ActivityDetail } from '@/types/activities.type' ;
56import { useRecentViewedStore } from '@/store/recentlyWatched' ;
67import ActivityImageViewer from '@/components/pages/activities/ActivityImageViewer' ;
78import ActivityInfo from '@/components/pages/activities/ActivityInfo' ;
@@ -12,6 +13,7 @@ import Marker from '@/components/common/naverMaps/Marker';
1213import ImageMarker from '@/components/common/naverMaps/ImageMarker' ;
1314import { activityQueryKeys } from './queryKeys' ;
1415import { useUserStore } from '@/store/userStore' ;
16+ import { useIntersectionObserver } from '@/hooks/useIntersectionObserver' ;
1517
1618/**
1719 * ActivityClient 컴포넌트
@@ -25,7 +27,13 @@ interface ActivityClientProps {
2527
2628export default function ActivityClient ( { activityId, blurImage } : ActivityClientProps ) {
2729 const [ isOwner , setIsOwner ] = useState < boolean > ( false ) ;
28- const user = useUserStore ( ( state ) => state . user ) ;
30+ const { user } = useUserStore ( ) ;
31+ const queryClient = useQueryClient ( ) ;
32+
33+ const [ mapRef , isMapVisible ] = useIntersectionObserver ( {
34+ rootMargin : '100px' ,
35+ triggerOnce : true ,
36+ } ) ;
2937
3038 // 1. 정적 데이터 (이미지, 주소, 제목, 설명) - 긴 캐시
3139 const { data : staticInfo } = useSuspenseQuery ( {
@@ -47,11 +55,31 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient
4755 gcTime : 60 * 60 * 1000 , // 1시간 메모리 보관
4856 } ) ;
4957
50- // 2. 동적 데이터 (가격, 스케줄, 평점) - 짧은 캐시
58+ // 2. 동적 데이터 (가격, 스케줄, 평점) - 짧은 캐시, 재사용 최적화
5159 const { data : dynamicInfo } = useSuspenseQuery ( {
5260 queryKey : [ ...activityQueryKeys . detail ( activityId ) , 'dynamic' ] ,
53- queryFn : ( ) => getActivityDetail ( Number ( activityId ) ) ,
54- select : ( data ) => ( {
61+ queryFn : ( ) : Promise < ActivityDetail > => {
62+ // 캐시된 데이터가 있으면 재사용, 없으면 새로 호출
63+ const cachedData = queryClient . getQueryData < ActivityDetail > ( [
64+ ...activityQueryKeys . detail ( activityId ) ,
65+ 'static' ,
66+ ] ) ;
67+ const cachedState = queryClient . getQueryState ( [
68+ ...activityQueryKeys . detail ( activityId ) ,
69+ 'static' ,
70+ ] ) ;
71+
72+ // static 캐시가 fresh하면 재사용
73+ if (
74+ cachedData &&
75+ cachedState ?. dataUpdatedAt &&
76+ Date . now ( ) - cachedState . dataUpdatedAt < 2 * 60 * 1000
77+ ) {
78+ return Promise . resolve ( cachedData ) ; // 캐시 재사용
79+ }
80+ return getActivityDetail ( Number ( activityId ) ) ; // 새 호출
81+ } ,
82+ select : ( data : ActivityDetail ) => ( {
5583 price : data . price ,
5684 schedules : data . schedules ,
5785 rating : data . rating ,
@@ -69,11 +97,10 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient
6997 const addViewed = useRecentViewedStore ( ( s ) => s . addViewed ) ;
7098
7199 useEffect ( ( ) => {
72- if ( activity ) {
100+ if ( activity ?. id ) {
73101 addViewed ( activity ) ;
74- console . log ( '👀 최근 본 목록에 추가됨' , activity . title ) ;
75102 }
76- } , [ activity , addViewed ] ) ;
103+ } , [ activity , addViewed ] ) ; // eslint 경고 해결을 위해 원복하되, 조건문 최적화
77104
78105 useEffect ( ( ) => {
79106 if ( user ?. id === activity . userId ) {
@@ -107,14 +134,18 @@ export default function ActivityClient({ activityId, blurImage }: ActivityClient
107134 </ section >
108135 < hr className = 'border-gray-100' />
109136 { /* 주소 섹션 */ }
110- < section className = 'flex flex-col gap-2' >
137+ < section ref = { mapRef } className = 'flex flex-col gap-2' >
111138 < h2 className = 'text-lg font-semibold' > 오시는 길</ h2 >
112139 < p className = 'text-sm text-gray-600' > { activity . address } </ p >
113- < NaverMap address = { activity . address } height = '256px' zoom = { 12 } >
114- < Marker address = { activity . address } id = 'image-marker' >
115- < ImageMarker src = { activity . bannerImageUrl } alt = '주소 마커' size = { 40 } />
116- </ Marker >
117- </ NaverMap >
140+ { isMapVisible ? (
141+ < NaverMap address = { activity . address } height = '256px' zoom = { 12 } >
142+ < Marker address = { activity . address } id = 'image-marker' >
143+ < ImageMarker src = { activity . bannerImageUrl } alt = '주소 마커' size = { 40 } />
144+ </ Marker >
145+ </ NaverMap >
146+ ) : (
147+ < div className = 'h-64 bg-gray-100 rounded-lg animate-pulse flex items-center justify-center' > </ div >
148+ ) }
118149 </ section >
119150 < hr className = 'border-gray-100' />
120151 { /* 후기 섹션 */ }
0 commit comments