-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/74 체험 상세페이지 리뷰 페이지네이션 제외 api 연결 #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
96d8702
0277194
da0e894
d9cdc53
333b5ce
80871ac
1bc22f6
63c7ed9
cc036ef
c07729c
533b7cb
812c057
4a6b524
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,124 @@ | ||||||||||
| 'use client'; | ||||||||||
|
|
||||||||||
| import { useParams } from 'next/navigation'; | ||||||||||
| import Title from './Title'; | ||||||||||
| import ImageGrid from './ImageGrid'; | ||||||||||
| import BookingInterface from '@/components/FloatingBox/BookingInterface'; | ||||||||||
| import LocationMap from '@/components/LocationMap'; | ||||||||||
| import ReviewTitle from './ReviewTitle'; | ||||||||||
| import { useQuery } from '@tanstack/react-query'; | ||||||||||
| import { privateInstance } from '@/apis/privateInstance'; | ||||||||||
| import { useState, useEffect } from 'react'; | ||||||||||
| import useUserStore from '@/stores/authStore'; | ||||||||||
| import { padMonth } from '../utils/MonthFormatChange'; | ||||||||||
|
|
||||||||||
| export default function ActivityDetailForm() { | ||||||||||
| const [year, setYear] = useState(2025); | ||||||||||
| const [month, setMonth] = useState(7); | ||||||||||
|
Comment on lines
+16
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하드코딩된 년도와 월을 현재 날짜로 초기화하세요. 2025년 7월로 하드코딩되어 있어, 다른 시기에 사용할 때 문제가 될 수 있습니다. - const [year, setYear] = useState(2025);
- const [month, setMonth] = useState(7);
+ const [year, setYear] = useState(new Date().getFullYear());
+ const [month, setMonth] = useState(new Date().getMonth() + 1);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| const [isOwner, setIsOwner] = useState(false); | ||||||||||
|
|
||||||||||
| const { id } = useParams(); | ||||||||||
|
|
||||||||||
| const { data: activityData, isLoading } = useQuery({ | ||||||||||
| queryKey: ['activity', id], | ||||||||||
| queryFn: async () => { | ||||||||||
| return privateInstance.get(`/activities/${id}`); | ||||||||||
| }, | ||||||||||
| select: (response) => response.data, | ||||||||||
| enabled: !!id, | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| const userId = activityData?.userId; | ||||||||||
|
|
||||||||||
| const currentUserId = useUserStore((state) => | ||||||||||
| state.user ? state.user.id : null, | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| useEffect(() => { | ||||||||||
| if (currentUserId && currentUserId === userId) { | ||||||||||
| setIsOwner(true); | ||||||||||
| console.log('니가 작성한 체험임'); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 디버그용 console.log를 제거하세요. 프로덕션 코드에서 디버그 로그는 제거해야 합니다. - console.log('니가 작성한 체험임');📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| } else { | ||||||||||
| setIsOwner(false); | ||||||||||
| } | ||||||||||
| }, [currentUserId, userId]); | ||||||||||
|
|
||||||||||
| const { data: schedulesData } = useQuery({ | ||||||||||
| queryKey: ['available-schedule', id, year, month], | ||||||||||
| queryFn: async () => { | ||||||||||
| const prevMonth = month === 1 ? 12 : month - 1; | ||||||||||
| const prevYear = month === 1 ? year - 1 : year; | ||||||||||
| const nextMonth = month === 12 ? 1 : month + 1; | ||||||||||
| const nextYear = month === 12 ? year + 1 : year; | ||||||||||
|
|
||||||||||
| const results = await Promise.allSettled([ | ||||||||||
| privateInstance.get( | ||||||||||
| `/activities/${id}/available-schedule?year=${prevYear}&month=${padMonth(prevMonth)}`, | ||||||||||
| ), | ||||||||||
| privateInstance.get( | ||||||||||
| `/activities/${id}/available-schedule?year=${year}&month=${padMonth(month)}`, | ||||||||||
| ), | ||||||||||
| privateInstance.get( | ||||||||||
| `/activities/${id}/available-schedule?year=${nextYear}&month=${padMonth(nextMonth)}`, | ||||||||||
| ), | ||||||||||
| ]); | ||||||||||
| // 성공한 것만 합치기 | ||||||||||
| const data = results | ||||||||||
| .filter((r) => r.status === 'fulfilled') | ||||||||||
| .flatMap((r) => (r.status === 'fulfilled' ? r.value.data : [])); | ||||||||||
| return data; | ||||||||||
| }, | ||||||||||
| enabled: !!id && !!year && !!month, | ||||||||||
| }); | ||||||||||
|
Comment on lines
+46
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 복잡한 스케줄 조회 로직을 별도 함수로 분리하는 것을 고려해보세요. 3개월치 스케줄을 조회하는 로직이 복잡합니다. 가독성과 재사용성을 위해 유틸 함수로 분리하는 것을 권장합니다. 별도 유틸 함수 예시: // utils/scheduleUtils.ts
export const fetchMultipleMonthSchedules = async (
id: string,
year: number,
month: number
) => {
const prevMonth = month === 1 ? 12 : month - 1;
const prevYear = month === 1 ? year - 1 : year;
const nextMonth = month === 12 ? 1 : month + 1;
const nextYear = month === 12 ? year + 1 : year;
const results = await Promise.allSettled([
privateInstance.get(`/activities/${id}/available-schedule?year=${prevYear}&month=${padMonth(prevMonth)}`),
privateInstance.get(`/activities/${id}/available-schedule?year=${year}&month=${padMonth(month)}`),
privateInstance.get(`/activities/${id}/available-schedule?year=${nextYear}&month=${padMonth(nextMonth)}`),
]);
return results
.filter((r) => r.status === 'fulfilled')
.flatMap((r) => (r.status === 'fulfilled' ? r.value.data : []));
};🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| if (isLoading || !activityData) { | ||||||||||
| return <div>로딩 중...</div>; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const subImageUrls = activityData.subImages.map( | ||||||||||
| (image: { imageUrl: string }) => image.imageUrl, | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <div className='mx-auto max-w-1200 p-4 sm:px-20 lg:p-8'> | ||||||||||
| <Title {...activityData} isOwner={isOwner} /> | ||||||||||
| <ImageGrid | ||||||||||
| mainImage={activityData.bannerImageUrl} | ||||||||||
| subImages={subImageUrls} | ||||||||||
| /> | ||||||||||
|
|
||||||||||
| <div | ||||||||||
| className={`mt-86 grid gap-15 ${ | ||||||||||
| isOwner ? 'md:grid-cols-2' : 'md:grid-cols-3' | ||||||||||
| } grid-cols-1`} | ||||||||||
| > | ||||||||||
| <div className={`${isOwner ? 'md:col-span-2' : 'md:col-span-2'}`}> | ||||||||||
| <h2 className='mb-4 pb-2 text-2xl font-bold'>체험 설명</h2> | ||||||||||
| <p className='whitespace-pre-line'>{activityData.description}</p> | ||||||||||
| </div> | ||||||||||
|
|
||||||||||
| {!isOwner && ( | ||||||||||
| <div className='md:row-span-2'> | ||||||||||
| <BookingInterface | ||||||||||
| schedules={schedulesData ?? []} | ||||||||||
| onMonthChange={(year, month) => { | ||||||||||
| setTimeout(() => { | ||||||||||
| setYear(year); | ||||||||||
| setMonth(month); | ||||||||||
| }, 0); | ||||||||||
| }} | ||||||||||
| isOwner={isOwner} | ||||||||||
| price={activityData.price} | ||||||||||
| /> | ||||||||||
| </div> | ||||||||||
| )} | ||||||||||
|
|
||||||||||
| <div className={`${isOwner ? 'md:col-span-4' : 'md:col-span-2'}`}> | ||||||||||
| <h2 className='mb-4 pb-2 text-2xl font-bold'>체험 장소</h2> | ||||||||||
| <LocationMap address={activityData.address} /> | ||||||||||
| <ReviewTitle /> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
|
|
||
| export function Skeleton({ className = '' }: { className?: string }) { | ||
| return <div className={`animate-pulse bg-gray-200 ${className}`} />; | ||
| } | ||
|
Comment on lines
+2
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 기본적인 스켈레톤 컴포넌트가 잘 구현되었습니다. 로딩 상태를 위한 간단하고 효과적인 구현입니다. 더 나은 className 처리와 유연성을 위해 다음과 같은 개선을 고려해보세요: import { cn } from '@/lib/utils'; // or use clsx
export function Skeleton({
className = '',
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse bg-gray-200", className)}
{...props}
/>
);
}이렇게 하면 더 안전한 className 병합과 추가 HTML 속성 지원이 가능합니다. 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import ActivityDetailForm from './components/ActivityDetailForm'; | ||
|
|
||
| export default function ActivityDetailPage() { | ||
| return <ActivityDetailForm />; | ||
| } | ||
|
Comment on lines
+1
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 깔끔한 페이지 구조가 잘 구현되었습니다. Next.js 페이지 컴포넌트 패턴을 잘 따르고 있으며, 실제 렌더링 로직을 별도 컴포넌트로 분리한 것이 좋습니다. 향후 고려사항으로, ActivityDetailForm에서 발생할 수 있는 에러를 처리하기 위해 Error Boundary를 추가하는 것을 검토해보세요: import { Suspense } from 'react';
import ActivityDetailForm from './components/ActivityDetailForm';
import { Skeleton } from './components/Skeleton';
export default function ActivityDetailPage() {
return (
<Suspense fallback={<Skeleton className="h-screen" />}>
<ActivityDetailForm />
</Suspense>
);
}🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,3 @@ | ||||||||||||||||||||
| export function padMonth(m: number): string { | ||||||||||||||||||||
| return m.toString().padStart(2, '0'); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 월 포맷팅 유틸리티 함수 개선 제안 기본 로직은 올바르지만 개선할 부분이 있습니다. 함수명과 입력 검증을 개선해보세요: -export function padMonth(m: number): string {
- return m.toString().padStart(2, '0');
-}
+export function padMonthWithZero(month: number): string {
+ if (month < 1 || month > 12) {
+ throw new Error('Month must be between 1 and 12');
+ }
+ return month.toString().padStart(2, '0');
+}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import axios, { AxiosError } from 'axios'; | ||
| import { cookies } from 'next/headers'; | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
|
|
||
| export async function GET( | ||
| request: NextRequest, | ||
| { params }: { params: { id: string } }, | ||
| ) { | ||
| const { id } = await params; | ||
| const cookieStore = await cookies(); | ||
| const accessToken = cookieStore.get('accessToken')?.value; | ||
|
|
||
| const { searchParams } = request.nextUrl; | ||
| const year = searchParams.get('year'); | ||
| const month = searchParams.get('month'); | ||
|
|
||
| try { | ||
| const response = await axios.get( | ||
| `${process.env.NEXT_PUBLIC_API_SERVER_URL}/activities/${id}/available-schedule?year=${year}&month=${month}`, | ||
|
||
| { | ||
| headers: { | ||
| Authorization: accessToken ? `Bearer ${accessToken}` : undefined, | ||
| }, | ||
| }, | ||
| ); | ||
| return NextResponse.json(response.data); | ||
| } catch (err) { | ||
| const error = err as AxiosError; | ||
| const message = | ||
| (error.response?.data as any)?.error || '스케줄 데이터 조회 실패'; | ||
| const status = error.response?.status || 500; | ||
| return NextResponse.json({ error: message }, { status }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import axios, { AxiosError } from 'axios'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cookies } from 'next/headers'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request: NextRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { params }: { params: { id: string } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { id } = await params; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cookieStore = await cookies(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const accessToken = cookieStore.get('accessToken')?.value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { selectedTimeId, participants } = await request.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await axios.post( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${process.env.NEXT_PUBLIC_API_SERVER_URL}/activities/${id}/reservations`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scheduleId: Number(selectedTimeId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headCount: Number(participants), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 요청 body 데이터 검증이 필요합니다.
const { selectedTimeId, participants } = await request.json();
+
+ if (!selectedTimeId || !participants) {
+ return NextResponse.json(
+ { error: '필수 데이터가 누락되었습니다.' },
+ { status: 400 }
+ );
+ }
+
+ const scheduleId = Number(selectedTimeId);
+ const headCount = Number(participants);
+
+ if (isNaN(scheduleId) || isNaN(headCount) || headCount < 1) {
+ return NextResponse.json(
+ { error: '잘못된 데이터 형식입니다.' },
+ { status: 400 }
+ );
+ }
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/activities/${id}/reservations`,
{
- scheduleId: Number(selectedTimeId),
- headCount: Number(participants),
+ scheduleId,
+ headCount,
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization: accessToken ? `Bearer ${accessToken}` : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json(response.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const error = err as AxiosError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| //디버그용 전체 에러 뽑아서 넘기기 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('예약 에러:', error.toJSON?.() ?? error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const message = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (error.response?.data as any)?.error || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (error.response?.data as any)?.message || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error.message || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '예약실패'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status = error.response?.status || 500; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: message }, { status }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import axios, { AxiosError } from 'axios'; | ||
| import { cookies } from 'next/headers'; | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { ServerErrorResponse } from '@/types/apiErrorResponseType'; | ||
|
|
||
| export const GET = async ( | ||
| request: NextRequest, | ||
| { params }: { params: Promise<{ id: string }> }, | ||
| ) => { | ||
| const { id } = await params; | ||
| const cookieStore = await cookies(); | ||
| const accessToken = cookieStore.get('accessToken')?.value; | ||
|
|
||
| try { | ||
| const response = await axios.get( | ||
| `${process.env.NEXT_PUBLIC_API_SERVER_URL}/activities/${id}`, | ||
| { | ||
| headers: { | ||
| Authorization: accessToken ? `Bearer ${accessToken}` : undefined, | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| return NextResponse.json(response.data); | ||
| } catch (err) { | ||
| const error = err as AxiosError<ServerErrorResponse>; | ||
| const message = error.response?.data?.error || '활동 상세 데이터 조회실패'; | ||
| const status = error.response?.status || 500; | ||
|
|
||
| return NextResponse.json({ error: message }, { status }); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
외부 이미지 도메인 설정 승인 및 보안 고려사항
S3 버킷의 이미지 최적화를 위한 올바른 설정입니다.
보안과 환경 분리를 위해 환경변수 사용을 고려해보세요:
images: { - domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com'], + domains: [process.env.NEXT_PUBLIC_IMAGE_DOMAIN || 'sprint-fe-project.s3.ap-northeast-2.amazonaws.com'], },📝 Committable suggestion
🤖 Prompt for AI Agents