Skip to content

Commit c9b5721

Browse files
committed
Merge branch 'develop' into feat/93
2 parents 3519711 + 969e0f1 commit c9b5721

File tree

17 files changed

+539
-115
lines changed

17 files changed

+539
-115
lines changed

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

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import Title from './Title';
55
import ImageGrid from './ImageGrid';
66
import BookingInterface from '@/components/FloatingBox/BookingInterface';
77
import LocationMap from '@/components/LocationMap';
8-
import ReviewTitle from './ReviewTitle';
98
import { useQuery } from '@tanstack/react-query';
109
import { privateInstance } from '@/apis/privateInstance';
11-
import { useState, useEffect } from 'react';
10+
import { useState, useCallback } from 'react';
1211
import useUserStore from '@/stores/authStore';
1312
import { padMonth } from '../utils/MonthFormatChange';
13+
import ReviewSection from './ReviewSection';
14+
15+
import ActivityDetailSkeleton from './ActivityDetailSkeleton';
1416

1517
export default function ActivityDetailForm() {
1618
const [year, setYear] = useState(2025);
1719
const [month, setMonth] = useState(7);
18-
const [isOwner, setIsOwner] = useState(false);
1920

2021
const { id } = useParams();
2122

@@ -28,20 +29,11 @@ export default function ActivityDetailForm() {
2829
enabled: !!id,
2930
});
3031

31-
const userId = activityData?.userId;
32-
3332
const currentUserId = useUserStore((state) =>
3433
state.user ? state.user.id : null,
3534
);
36-
37-
useEffect(() => {
38-
if (currentUserId && currentUserId === userId) {
39-
setIsOwner(true);
40-
console.log('니가 작성한 체험임');
41-
} else {
42-
setIsOwner(false);
43-
}
44-
}, [currentUserId, userId]);
35+
const userId = activityData?.userId;
36+
const isOwner = currentUserId && userId && currentUserId === userId;
4537

4638
const { data: schedulesData } = useQuery({
4739
queryKey: ['available-schedule', id, year, month],
@@ -71,10 +63,15 @@ export default function ActivityDetailForm() {
7163
enabled: !!id && !!year && !!month,
7264
});
7365

74-
66+
const handleMonthChange = useCallback((year: number, month: number) => {
67+
setTimeout(() => {
68+
setYear(year);
69+
setMonth(month);
70+
});
71+
}, []);
7572

7673
if (isLoading || !activityData) {
77-
return <div>로딩 중...</div>;
74+
return <ActivityDetailSkeleton userId={activityData?.userId} />;
7875
}
7976

8077
const subImageUrls = activityData.subImages.map(
@@ -83,7 +80,14 @@ export default function ActivityDetailForm() {
8380

8481
return (
8582
<div className='mx-auto max-w-1200 p-4 sm:px-20 lg:p-8'>
86-
<Title {...activityData} isOwner={isOwner} />
83+
<Title
84+
title={activityData.title}
85+
category={activityData.category}
86+
rating={activityData.rating}
87+
reviewCount={activityData.reviewCount}
88+
address={activityData.address}
89+
isOwner={isOwner}
90+
/>
8791
<ImageGrid
8892
mainImage={activityData.bannerImageUrl}
8993
subImages={subImageUrls}
@@ -96,19 +100,16 @@ export default function ActivityDetailForm() {
96100
>
97101
<div className={`${isOwner ? 'md:col-span-2' : 'md:col-span-2'}`}>
98102
<h2 className='mb-4 pb-2 text-2xl font-bold'>체험 설명</h2>
99-
<p className='whitespace-pre-line'>{activityData.description}</p>
103+
<p className='leading-relaxed whitespace-pre-line'>
104+
{activityData.description}
105+
</p>
100106
</div>
101107

102108
{!isOwner && (
103109
<div className='md:row-span-2'>
104110
<BookingInterface
105111
schedules={schedulesData ?? []}
106-
onMonthChange={(year, month) => {
107-
setTimeout(() => {
108-
setYear(year);
109-
setMonth(month);
110-
}, 0);
111-
}}
112+
onMonthChange={handleMonthChange}
112113
isOwner={isOwner}
113114
price={activityData.price}
114115
/>
@@ -118,7 +119,12 @@ export default function ActivityDetailForm() {
118119
<div className={`${isOwner ? 'md:col-span-4' : 'md:col-span-2'}`}>
119120
<h2 className='mb-4 pb-2 text-2xl font-bold'>체험 장소</h2>
120121
<LocationMap address={activityData.address} />
121-
<ReviewTitle />
122+
123+
<ReviewSection
124+
activityId={id as string}
125+
reviewCount={activityData.reviewCount}
126+
rating={activityData.rating}
127+
/>
122128
</div>
123129
</div>
124130
</div>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use client';
2+
3+
import SkeletonBookingInterface from './Skeletons/BookingInterfaceSkeleton';
4+
import useUserStore from '@/stores/authStore';
5+
6+
export default function ActivityDetailSkeleton({ userId }: { userId: number }) {
7+
const currentUserId = useUserStore((state) => state.user?.id);
8+
const isOwner = currentUserId && userId && currentUserId === userId;
9+
10+
console.log(isOwner);
11+
return (
12+
<div className='mx-auto max-w-1200 animate-pulse p-4 sm:px-20 lg:p-8'>
13+
{/* 타이틀부분 */}
14+
15+
<div className='mb-6 flex items-start justify-between'>
16+
<div className='flex w-full flex-col gap-10'>
17+
<div className='h-16 w-24 rounded bg-gray-300' />
18+
<div className='h-42 w-3/4 rounded bg-gray-300' />
19+
<div className='flex gap-10'>
20+
<div className='h-20 w-50 rounded bg-gray-300' />
21+
<div className='h-20 w-170 rounded bg-gray-300' />
22+
</div>
23+
</div>
24+
{isOwner && <div className='h-8 w-8 rounded-full bg-gray-300' />}
25+
</div>
26+
27+
{/* 이미지그리드 */}
28+
<div className='relative block aspect-square h-[300px] w-full overflow-hidden rounded-lg bg-gray-300 md:hidden' />
29+
<div className='hidden h-[500px] grid-cols-4 grid-rows-4 gap-6 md:grid'>
30+
<div className='col-span-2 row-span-4 rounded-lg bg-gray-300' />
31+
{[...Array(4)].map((_, i) => (
32+
<div
33+
key={i}
34+
className='col-span-1 row-span-2 rounded-lg bg-gray-300'
35+
/>
36+
))}
37+
</div>
38+
39+
{/* 설명/예약인터페이스/장소 */}
40+
<div
41+
className={`mt-86 grid gap-10 ${
42+
isOwner ? 'md:grid-cols-2' : 'md:grid-cols-3'
43+
} grid-cols-1`}
44+
>
45+
{/* 설명 */}
46+
<div className='md:col-span-2'>
47+
<div className='mb-10 h-34 w-90 rounded bg-gray-300' />
48+
<div className='mb-4 h-180 w-full rounded bg-gray-300' />
49+
</div>
50+
51+
{/* 예약인터페이스 */}
52+
{!isOwner && (
53+
<div className='md:row-span-2'>
54+
<SkeletonBookingInterface />
55+
</div>
56+
)}
57+
58+
{/* 체험 장소/리뷰 */}
59+
<div
60+
className={`${isOwner ? 'md:col-span-4' : 'md:col-span-2'} space-y-8`}
61+
>
62+
{/* 장소 */}
63+
<div>
64+
<div className='mb-10 h-34 w-90 rounded bg-gray-300' />
65+
<div className='h-[480px] w-full rounded-lg bg-gray-400 shadow-md' />
66+
<div className='mt-8 flex items-center space-x-3'>
67+
<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>
70+
</div>
71+
72+
{/* 리뷰 */}
73+
<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>
84+
</div>
85+
))}
86+
</div>
87+
</div>
88+
</div>
89+
</div>
90+
);
91+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default function ReviewCard({
66
date,
77
reviewText,
88
avatarSrc,
9+
isBlured = false,
910
}: ReviewCardProps) {
1011
return (
1112
<div className='flex max-w-md justify-start gap-6 p-6 text-black md:max-w-2xl'>
@@ -16,7 +17,11 @@ export default function ReviewCard({
1617
<p className='text-black'>|</p>
1718
<p className='text-gray-600'>{date}</p>
1819
</div>
19-
<p className='text-sm leading-relaxed text-black md:text-lg'>
20+
<p
21+
className={`text-sm leading-relaxed md:text-lg ${
22+
isBlured ? 'text-gray-300 select-none' : 'text-black'
23+
}`}
24+
>
2025
{reviewText}
2126
</p>
2227
</div>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use client';
2+
3+
import { useState, useCallback } from 'react';
4+
import ReviewCard from './ReviewCard';
5+
import Pagination from '@/components/Pagination';
6+
import { useQuery } from '@tanstack/react-query';
7+
import { privateInstance } from '@/apis/privateInstance';
8+
import ReviewTitle from './ReviewTitle';
9+
import useUserStore from '@/stores/authStore';
10+
11+
interface ReviewSectionProps {
12+
activityId: string;
13+
reviewCount: number;
14+
rating: number;
15+
}
16+
17+
interface ReviewProps {
18+
id: string;
19+
user: {
20+
nickname: string;
21+
profileImageUrl: string;
22+
};
23+
createdAt: string;
24+
content: string;
25+
}
26+
27+
export default function ReviewSection({
28+
activityId,
29+
reviewCount,
30+
rating,
31+
}: ReviewSectionProps) {
32+
const [page, setPage] = useState(1);
33+
const size = 3;
34+
35+
const { user } = useUserStore();
36+
37+
const {
38+
data: reviewData,
39+
isLoading,
40+
isError,
41+
} = useQuery({
42+
queryKey: ['reviews', activityId, page],
43+
queryFn: async () => {
44+
const res = await privateInstance.get(
45+
`/activities/${activityId}/review?page=${page}&size=${size}`,
46+
);
47+
return res.data;
48+
},
49+
enabled: !!activityId,
50+
});
51+
52+
const ReviewComponent = useCallback(() => {
53+
return reviewData?.reviews.map((review: ReviewProps) => (
54+
<ReviewCard
55+
key={review.id}
56+
userName={review.user.nickname}
57+
avatarSrc={review.user.profileImageUrl}
58+
date={new Date(review.createdAt).toLocaleDateString('ko-KR', {
59+
year: 'numeric',
60+
month: '2-digit',
61+
day: '2-digit',
62+
})}
63+
reviewText={review.content}
64+
/>
65+
));
66+
}, [reviewData?.reviews]);
67+
68+
if (isLoading) {
69+
return <p className='mt-4 text-gray-500'>리뷰를 불러오는 중입니다...</p>;
70+
}
71+
72+
if (!reviewData || reviewData.reviews.length === 0) {
73+
return <p className='mt-4 text-gray-500'>작성된 리뷰가 없습니다.</p>;
74+
}
75+
76+
if (isError) {
77+
throw new Error('에러발생');
78+
}
79+
80+
return (
81+
<div className='mt-10 flex flex-col space-y-8'>
82+
<ReviewTitle reviewCount={reviewCount} rating={rating} />
83+
84+
<div className='relative min-h-350'>
85+
<div className={user ? '' : 'blur-sm'}>{ReviewComponent()}</div>
86+
87+
{!user && (
88+
<div className='pointer-events-none absolute inset-0 z-10 flex items-center justify-center'>
89+
<div className='rounded-md bg-white/70 px-4 py-2 shadow-md'>
90+
<p className='text-sm font-semibold text-gray-700'>
91+
로그인 후 리뷰 전체 내용을 확인할 수 있어요
92+
</p>
93+
</div>
94+
</div>
95+
)}
96+
</div>
97+
98+
<Pagination
99+
currentPage={page}
100+
totalPage={Math.ceil(reviewData.totalCount / size)}
101+
onPageChange={(newPage) => setPage(newPage)}
102+
/>
103+
</div>
104+
);
105+
}

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
1+
'use client';
2+
13
import Star from '@assets/svg/star';
4+
import { useState, useEffect } from 'react';
5+
6+
interface ReviewTitleProps {
7+
reviewCount: number;
8+
rating: number;
9+
}
10+
export default function ReviewTitle({ reviewCount, rating }: ReviewTitleProps) {
11+
const [summary, setSummary] = useState('');
12+
13+
useEffect(() => {
14+
const handleSummary = () => {
15+
if (rating >= 4.5) {
16+
setSummary('매우 만족');
17+
} else if (rating >= 3) {
18+
setSummary('만족');
19+
} else if (rating >= 2.5) {
20+
setSummary('보통');
21+
} else {
22+
setSummary('별로에요');
23+
}
24+
};
25+
26+
handleSummary();
27+
}, [reviewCount]);
228

3-
export default function ReviewTitle() {
429
return (
530
<div className='mt-10 mb-10 flex flex-col'>
631
<h2 className='text-2xl font-bold'>후기</h2>
732

833
<div className='mt-15 flex items-center gap-15'>
9-
<h2 className='text-4xl font-bold'>4.2</h2>
34+
<h2 className='text-4xl font-bold'>{rating}</h2>
1035
<div className='flex flex-col gap-10'>
11-
<p>매우 만족</p>
36+
<p>{summary}</p>
1237
<div className='flex items-center'>
1338
<Star />
14-
<p>1300개 후기</p>
39+
<p>{reviewCount}개의 후기</p>
1540
</div>
1641
</div>
1742
</div>

0 commit comments

Comments
 (0)