Skip to content

Commit 9879658

Browse files
committed
Merge branch 'develop' into feat/100
2 parents 3565cd8 + b79f27a commit 9879658

File tree

19 files changed

+313
-166
lines changed

19 files changed

+313
-166
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@
4646
"tw-animate-css": "^1.3.6",
4747
"typescript": "^5"
4848
}
49-
}
49+
}

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/(with-header)/activities/[id]/components/ReviewSection.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { privateInstance } from '@/apis/privateInstance';
99
import ReviewTitle from './ReviewTitle';
1010
import useUserStore from '@/stores/authStore';
1111

12+
import ReviewCardSkeleton from './Skeletons/ReviewCardSkeleton';
13+
1214
interface ReviewSectionProps {
1315
activityId: string;
1416
reviewCount: number;
@@ -67,11 +69,32 @@ function ReviewSection({
6769
}, [reviewData?.reviews]);
6870

6971
if (isLoading) {
70-
return <p className='mt-4 text-gray-500'>리뷰를 불러오는 중입니다...</p>;
72+
return (
73+
<div className='mt-10 flex flex-col space-y-8'>
74+
<div className='relative min-h-350 flex-col'>
75+
<ReviewTitle reviewCount={reviewCount} rating={rating} />
76+
{[...Array(3)].map((_, index) => (
77+
<ReviewCardSkeleton key={index} />
78+
))}
79+
</div>
80+
</div>
81+
);
7182
}
7283

7384
if (!reviewData || reviewData.reviews.length === 0) {
74-
return <p className='mt-4 text-gray-500'>작성된 리뷰가 없습니다.</p>;
85+
return (
86+
<div className='mt-10 flex flex-col space-y-8'>
87+
<div className='relative min-h-350'>
88+
<ReviewTitle reviewCount={reviewCount} rating={rating} />
89+
90+
<div className='pointer-events-none absolute inset-0 z-10 flex items-center justify-center'>
91+
<div className='flex items-center justify-center font-bold'>
92+
<p>작성된 리뷰가 없습니다</p>
93+
</div>
94+
</div>
95+
</div>
96+
</div>
97+
);
7598
}
7699

77100
if (isError) {

src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ interface ReviewTitleProps {
77
reviewCount: number;
88
rating: number;
99
}
10-
export default function ReviewTitle({ reviewCount, rating }: ReviewTitleProps) {
10+
export default function ReviewTitle({
11+
reviewCount = 0,
12+
rating = 0,
13+
}: ReviewTitleProps) {
1114
const [summary, setSummary] = useState('');
1215

1316
useEffect(() => {

src/app/(with-header)/activities/[id]/components/Skeletons/ActivityDetailSkeleton.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import SkeletonBookingInterface from './BookingInterfaceSkeleton';
44
import useUserStore from '@/stores/authStore';
5+
import ReviewCardSkeleton from './ReviewCardSkeleton';
56

67
export default function ActivityDetailSkeleton({ userId }: { userId: number }) {
78
const currentUserId = useUserStore((state) => state.user?.id);
@@ -60,29 +61,26 @@ export default function ActivityDetailSkeleton({ userId }: { userId: number }) {
6061
className={`${isOwner ? 'md:col-span-4' : 'md:col-span-2'} space-y-8`}
6162
>
6263
{/* 장소 */}
63-
<div>
64+
<div className='mb-40'>
6465
<div className='mb-10 h-34 w-90 rounded bg-gray-300' />
6566
<div className='h-[480px] w-full rounded-lg bg-gray-400 shadow-md' />
6667
<div className='mt-8 flex items-center space-x-3'>
6768
<div className='h-6 w-6 rounded-full bg-gray-300' />
68-
<div className='h-5 w-1/2 rounded bg-gray-300' />
69+
<div className='h-20 w-1/2 rounded bg-gray-300' />
6970
</div>
7071
</div>
7172

7273
{/* 리뷰 */}
7374
<div>
74-
<div className='mb-2 h-6 w-24 rounded bg-gray-300' />
75-
<div className='mb-4 h-8 w-20 rounded bg-gray-200' />
76-
{[...Array(3)].map((_, i) => (
77-
<div key={i} className='mb-4 flex gap-4'>
78-
<div className='h-10 w-10 rounded-full bg-gray-300' />
79-
<div className='flex-1 space-y-2'>
80-
<div className='h-4 w-24 rounded bg-gray-200' />
81-
<div className='h-4 w-full rounded bg-gray-200' />
82-
<div className='h-4 w-3/4 rounded bg-gray-200' />
83-
</div>
75+
<div className='mt-10 flex flex-col space-y-8'>
76+
<div className='mb-10 h-34 w-50 rounded bg-gray-300' />
77+
<div className='mb-5 h-50 w-120 rounded bg-gray-300' />
78+
<div className='relative min-h-450 flex-col gap-30'>
79+
{[...Array(3)].map((_, index) => (
80+
<ReviewCardSkeleton key={index} />
81+
))}
8482
</div>
85-
))}
83+
</div>
8684
</div>
8785
</div>
8886
</div>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export default function ReviewCardSkeleton() {
2+
return (
3+
<div className='mb-20 bg-white p-6'>
4+
<div className='space-y-6'>
5+
<div className='flex items-start gap-3'>
6+
<div className='h-40 w-40 flex-shrink-0 animate-pulse rounded-full bg-gray-300' />
7+
8+
<div className='min-w-0 flex-1'>
9+
<div className='mb-10 flex items-center gap-2'>
10+
<div className='h-10 w-30 animate-pulse rounded bg-gray-300' />
11+
<div className='h-10 w-1 animate-pulse rounded bg-gray-300' />
12+
<div className='h-10 w-80 animate-pulse rounded bg-gray-300' />
13+
</div>
14+
<div className='mb-5 h-15 w-400 animate-pulse rounded bg-gray-300' />
15+
<div className='h-15 w-400 animate-pulse rounded bg-gray-300' />
16+
</div>
17+
</div>
18+
</div>
19+
</div>
20+
);
21+
}

src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export const useDeleteActivity = () => {
1515
return useMutation({
1616
mutationFn: deleteActivity,
1717
onSuccess: (_data) => {
18-
queryClient.invalidateQueries({ queryKey: ['activity'] });
18+
queryClient.invalidateQueries({ queryKey: ['activity'] }); // 내 체험 관리
19+
queryClient.invalidateQueries({ queryKey: ['experiences'], exact: false }); // 모든 체험 리스트
20+
queryClient.invalidateQueries({ queryKey: ['popularExperiences'] }); // 인기 체험
1921
router.push(`/`);
2022
},
2123
onError: (error: AxiosError) => {

src/app/(with-header)/components/ExperienceList.tsx

Lines changed: 35 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
'use client';
22

3-
import Dropdown from '@components/Dropdown';
4-
import Pagination from '@components/Pagination';
5-
import { useEffect, useState } from 'react';
3+
import { useState } from 'react';
64
import Link from 'next/link';
5+
import { useQuery } from '@tanstack/react-query';
76

7+
import Dropdown from '@components/Dropdown';
8+
import Pagination from '@components/Pagination';
89
import CategoryFilter from '@/app/(with-header)/components/CategoryFilter';
910
import ExperienceCard from '@/app/(with-header)/components/ExperienceCard';
10-
import { getExperiences } from '@/app/api/experiences/getExperiences';
11+
import { getExperiences, ExperienceListResult } from '@/app/api/experiences/getExperiences';
1112
import { ACTIVITY_CATEGORIES, ActivityCategory } from '@/constants/categories';
1213
import {
1314
SORT_OPTIONS,
1415
SORT_VALUE_MAP,
1516
SORT_LABEL_MAP,
1617
} from '@/constants/SortPrices';
17-
import { Experience } from '@/types/experienceListTypes';
1818

1919
interface ExperienceListProps {
2020
keyword?: string;
@@ -25,57 +25,40 @@ export default function ExperienceList({ keyword, isSearchMode }: ExperienceList
2525
const [currentPage, setCurrentPage] = useState(1);
2626
const [selectedCategory, setSelectedCategory] = useState<ActivityCategory>(ACTIVITY_CATEGORIES[0]);
2727
const [sortOption, setSortOption] = useState<string | undefined>('');
28-
const [experiences, setExperiences] = useState<Experience[]>([]);
29-
const [totalCount, setTotalCount] = useState(0);
3028

31-
useEffect(() => {
32-
const resync = async () => {
33-
const res = await getExperiences({
29+
// TanStack Query 사용 (타입 명시 필수)
30+
const { data, isLoading, error } = useQuery<ExperienceListResult>({
31+
queryKey: ['experiences', currentPage, selectedCategory, sortOption, keyword],
32+
queryFn: () =>
33+
getExperiences({
3434
page: currentPage,
3535
sort: sortOption,
3636
category: selectedCategory,
3737
keyword,
38-
});
39-
40-
setExperiences(res.experiences);
41-
setTotalCount(res.totalCount);
42-
};
43-
44-
resync();
45-
}, [currentPage, sortOption, selectedCategory, keyword]);
46-
47-
useEffect(() => {
48-
if (keyword) {
49-
setSelectedCategory(ACTIVITY_CATEGORIES[0]);
50-
setSortOption('');
51-
setCurrentPage(1);
52-
}
53-
}, [keyword]);
38+
}),
39+
placeholderData: (prev) => prev,
40+
});
5441

42+
const experiences = data?.experiences || [];
43+
const totalCount = data?.totalCount || 0;
5544
const totalPage = Math.ceil(totalCount / 8);
5645

5746
return (
5847
<section className='max-w-1200 m-auto px-24 md:px-0 pb-83'>
59-
60-
{/* 🔍 검색 모드일 때 문구 표시 */}
6148
{isSearchMode && keyword && (
6249
<>
6350
<p className="text-left text-lg font-semibold ml-4 md:ml-0 mt-32">
64-
<span className="text-primary font-bold">"{keyword}"</span>
65-
(으)로 검색한 결과입니다.
51+
<span className="text-primary font-bold">"{keyword}"</span> (으)로 검색한 결과입니다.
6652
</p>
67-
6853
<p className="text-left text-sm font-normal ml-4 md:ml-0 mt-8 mb-16">
6954
<span className="font-semibold">{totalCount}</span>개의 결과
7055
</p>
71-
7256
{experiences.length === 0 && (
7357
<p className="text-center text-gray-500 mt-32">검색 결과가 없습니다.</p>
7458
)}
7559
</>
7660
)}
7761

78-
{/* 🧭 필터/정렬 UI (검색 모드 아닐 때만) */}
7962
{!isSearchMode && (
8063
<div className='flex justify-between items-center mb-40'>
8164
<CategoryFilter
@@ -100,31 +83,31 @@ export default function ExperienceList({ keyword, isSearchMode }: ExperienceList
10083
)}
10184

10285
<div className='m-0'>
103-
{/* 🚂 모든 체험 제목 (검색 아닐 때만) */}
10486
{!isSearchMode && (
10587
<h2 className='text-xl md:text-3xl font-bold'>🛼 모든 체험</h2>
10688
)}
10789

108-
{/* 체험 카드 목록 */}
109-
<div className='grid grid-cols-2 grid-rows-2 md:grid-cols-3 md:grid-rows-3 lg:grid-cols-4 lg:grid-rows-2 gap-8 md:gap-16 lg:gap-24 mt-24'>
110-
{experiences.map((exp) => (
111-
<Link
112-
key={exp.id}
113-
href={`/activities/${exp.id}`} // 아이디 기반 라우팅
114-
>
115-
<ExperienceCard
116-
imageUrl={exp.bannerImageUrl}
117-
price={exp.price}
118-
rating={exp.rating}
119-
reviews={exp.reviewCount}
120-
title={exp.title}
121-
/>
122-
</Link>
123-
))}
124-
</div>
90+
{isLoading ? (
91+
<p className="text-center">체험을 불러오는 중입니다...</p>
92+
) : error ? (
93+
<p className="text-center text-red-500">체험 데이터를 불러오는 데 실패했습니다 😢</p>
94+
) : (
95+
<div className='grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 md:gap-16 lg:gap-24 mt-24'>
96+
{experiences.map((exp) => (
97+
<Link key={exp.id} href={`/activities/${exp.id}`}>
98+
<ExperienceCard
99+
imageUrl={exp.bannerImageUrl}
100+
price={exp.price}
101+
rating={exp.rating}
102+
reviews={exp.reviewCount}
103+
title={exp.title}
104+
/>
105+
</Link>
106+
))}
107+
</div>
108+
)}
125109
</div>
126110

127-
{/* 페이지네이션: 결과 있을 때만 표시 */}
128111
{experiences.length > 0 && (
129112
<Pagination
130113
currentPage={currentPage}

src/app/(with-header)/components/PopularExperiences.tsx

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,24 @@
22

33
import IconArrowLeft from '@assets/svg/left-arrow';
44
import IconArrowRight from '@assets/svg/right-arrow';
5-
import { useEffect, useRef, useState } from 'react';
5+
import { useRef } from 'react';
66
import Link from 'next/link';
7+
import { useQuery } from '@tanstack/react-query';
78

89
import PopularCard from '@/app/(with-header)/components/PopularCard';
9-
import { Experience } from '@/types/experienceListTypes';
10-
11-
import { getPopularExperiences } from '../../api/experiences/getPopularExperiences';
10+
import { getPopularExperiences } from '@/app/api/experiences/getPopularExperiences';
1211

1312
export default function PopularExperiences() {
1413
const sliderRef = useRef<HTMLDivElement>(null);
1514

16-
const [popularExperiences, setPopularExperiences] = useState<Experience[]>([]);
15+
// TanStack Query 도입
16+
const { data, isLoading, error } = useQuery({
17+
queryKey: ['popularExperiences'],
18+
queryFn: getPopularExperiences,
19+
});
1720

18-
// 좌우 버튼 클릭 시 한 장씩 슬라이드 이동
1921
const scrollByCard = (direction: 'left' | 'right') => {
2022
if (!sliderRef.current) return;
21-
2223
const card = sliderRef.current.querySelector('.card');
2324
if (!(card instanceof HTMLElement)) return;
2425

@@ -32,23 +33,11 @@ export default function PopularExperiences() {
3233
});
3334
};
3435

35-
// 인기 체험 목록 불러오기
36-
useEffect(() => {
37-
const fetchPopular = async () => {
38-
try {
39-
const res = await getPopularExperiences();
40-
setPopularExperiences(res.activities);
41-
} catch (error) {
42-
console.error('인기 체험을 불러오는 데 실패했습니다:', error);
43-
}
44-
};
45-
46-
fetchPopular();
47-
}, []);
36+
if (isLoading) return <p className="text-center">인기 체험을 불러오는 중입니다...</p>;
37+
if (error || !data) return <p className="text-center text-red-500">데이터를 불러오는 데 실패했어요 😢</p>;
4838

4939
return (
5040
<section className='pt-24 md:pt-34 pl-24 lg:pl-0 pb-40 lg:pb-33 lg:max-w-1200 lg:w-full mx-auto'>
51-
{/* 섹션 제목 + 좌우 화살표 버튼 */}
5241
<div className='flex justify-between items-center pb-16 md:pb-32 mb-6'>
5342
<h2 className='text-xl md:text-3xl font-bold'>🔥 인기 체험</h2>
5443
<div className='flex gap-2'>
@@ -57,18 +46,13 @@ export default function PopularExperiences() {
5746
</div>
5847
</div>
5948

60-
{/* 가로 슬라이드 카드 리스트 */}
6149
<div
6250
ref={sliderRef}
6351
className='flex gap-16 md:gap-32 lg:gap-24 overflow-x-auto scroll-smooth no-scrollbar'
6452
>
65-
{popularExperiences.map((exp) => (
53+
{data.activities.map((exp) => (
6654
<div key={exp.id} className='flex-shrink-0 card'>
67-
<Link
68-
key={exp.id}
69-
href={`/activities/${exp.id}`} // ✅ 상세페이지로 이동
70-
className='flex-shrink-0 card' // ✅ 여기에 card 클래스 유지
71-
>
55+
<Link href={`/activities/${exp.id}`} className='flex-shrink-0 card'>
7256
<PopularCard
7357
imageUrl={exp.bannerImageUrl}
7458
price={exp.price}

0 commit comments

Comments
 (0)