-
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
Conversation
|
""" Walkthrough이 변경사항은 액티비티 상세 페이지의 전체 구조를 동적으로 재구성하여, 실제 API 연동을 통한 데이터 조회 및 예약 기능을 도입합니다. 기존 mock 데이터 기반의 상세 페이지와 컴포넌트가 제거되고, API 기반의 예약·상세·스케줄 관리 로직과 타입 정의, 관련 UI 컴포넌트가 새롭게 추가 및 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ActivityDetailPage
participant ActivityDetailForm
participant API_Route
participant ExternalAPI
User->>ActivityDetailPage: 상세 페이지 접근
ActivityDetailPage->>ActivityDetailForm: 컴포넌트 렌더링
ActivityDetailForm->>API_Route: GET /api/activities/:id
API_Route->>ExternalAPI: GET /activities/:id (with accessToken)
ExternalAPI-->>API_Route: 상세 데이터 반환
API_Route-->>ActivityDetailForm: JSON 상세 데이터 반환
ActivityDetailForm->>API_Route: GET /api/activities/:id/available-schedule?year&month
API_Route->>ExternalAPI: GET /activities/:id/available-schedule
ExternalAPI-->>API_Route: 스케줄 데이터 반환
API_Route-->>ActivityDetailForm: JSON 스케줄 데이터 반환
User->>ActivityDetailForm: 예약 시도 (예약 버튼 클릭)
ActivityDetailForm->>API_Route: POST /api/activities/:id/reservation (selectedTimeId, participants)
API_Route->>ExternalAPI: POST /activities/:id/reservation
ExternalAPI-->>API_Route: 예약 결과 반환
API_Route-->>ActivityDetailForm: 예약 결과 반환
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Suggested reviewers
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Actionable comments posted: 26
🔭 Outside diff range comments (2)
src/ui/MobileBookingModal.tsx (1)
35-39: 주석 처리된 코드를 정리해주세요.더 이상 사용하지 않는 handleBooking 함수가 주석 처리되어 있습니다. 향후 사용 계획이 없다면 제거하는 것이 좋겠습니다.
- // const handleBooking = () => { - // alert('예약이 완료되었습니다!'); - // setIsOpen(false); - // setStep('date-time'); - // };src/components/FloatingBox/BookingInterface.tsx (1)
114-119: 하드코딩된 가격을 props로 전달받은 값으로 수정하세요.모바일 섹션에서 ₩ 10,000으로 하드코딩되어 있는데, props로 전달받은
price값을 사용해야 합니다.<div className='mb-1 text-xl font-bold text-gray-900'> - ₩ 10,000{' '} + ₩ {price.toLocaleString()}{' '} <span className='text-sm font-normal text-gray-600'> / 총 {participants}인 </span> </div>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (23)
next.config.ts(1 hunks)src/app/(with-header)/activities/[activitiesId]/page.tsx(0 hunks)src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx(1 hunks)src/app/(with-header)/activities/[id]/components/ImageGrid.tsx(1 hunks)src/app/(with-header)/activities/[id]/components/Skeleton.tsx(1 hunks)src/app/(with-header)/activities/[id]/components/Title.tsx(2 hunks)src/app/(with-header)/activities/[id]/page.tsx(1 hunks)src/app/(with-header)/activities/[id]/utils/MonthFormatChange.ts(1 hunks)src/app/api/activities/[id]/available-schedule/route.ts(1 hunks)src/app/api/activities/[id]/reservation/route.ts(1 hunks)src/app/api/activities/[id]/route.ts(1 hunks)src/components/DatePicker/CalendarBody.tsx(1 hunks)src/components/DatePicker/DatePicker.tsx(1 hunks)src/components/FloatingBox/BookingInterface.tsx(3 hunks)src/components/FloatingBox/PriceDisplay.tsx(1 hunks)src/components/FloatingBox/TimeSelector.tsx(1 hunks)src/components/FloatingBox/TotalPriceDisplay.tsx(1 hunks)src/components/LocationMap.tsx(1 hunks)src/types/activityDetailType.ts(1 hunks)src/types/apiErrorResponseType.ts(1 hunks)src/ui/BookingModal.tsx(1 hunks)src/ui/MobileBookingModal.tsx(2 hunks)src/ui/TabletBookingModal.tsx(2 hunks)
💤 Files with no reviewable changes (1)
- src/app/(with-header)/activities/[activitiesId]/page.tsx
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/app/(with-header)/activities/[id]/components/ImageGrid.tsx (1)
src/app/(with-header)/activities/[activitiesId]/components/ImageGrid.tsx (2)
ImageGrid(7-86)function(21-21)
src/ui/TabletBookingModal.tsx (2)
src/types/activityDetailType.ts (1)
SchedulesProps(40-40)src/components/DatePicker/DatePicker.tsx (1)
DatePicker(20-94)
src/ui/MobileBookingModal.tsx (2)
src/types/activityDetailType.ts (1)
SchedulesProps(40-40)src/components/DatePicker/DatePicker.tsx (1)
DatePicker(20-94)
src/ui/BookingModal.tsx (4)
src/types/activityDetailType.ts (1)
SchedulesProps(40-40)src/hooks/useDeviceSize.ts (1)
useDeviceSize(7-30)src/ui/MobileBookingModal.tsx (1)
MobileModal(13-109)src/ui/TabletBookingModal.tsx (1)
TabletModal(10-51)
src/app/api/activities/[id]/route.ts (2)
src/app/api/activities/[id]/available-schedule/route.ts (1)
GET(5-34)src/types/apiErrorResponseType.ts (1)
ServerErrorResponse(1-3)
src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx (2)
src/app/(with-header)/activities/[id]/utils/MonthFormatChange.ts (1)
padMonth(1-3)src/components/FloatingBox/BookingInterface.tsx (1)
BookingInterface(16-149)
src/components/DatePicker/DatePicker.tsx (3)
src/types/activityDetailType.ts (1)
SchedulesProps(40-40)src/types/datePickerTypes.ts (2)
CalendarHeaderProps(11-14)CalendarBodyProps(3-9)src/components/DatePicker/CalendarHeader.tsx (1)
CalendarHeader(5-35)
src/types/activityDetailType.ts (3)
src/app/(with-header)/activities/[activitiesId]/page.tsx (1)
ActivityDetailPage(11-79)src/types/bookingStoreTypes.ts (1)
BookingState(1-23)src/app/(with-header)/activities/[activitiesId]/components/ReviewCard.tsx (1)
ReviewCard(4-25)
🪛 Biome (2.1.2)
src/app/(with-header)/activities/[id]/components/Title.tsx
[error] 34-35: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🔇 Additional comments (28)
src/components/LocationMap.tsx (1)
79-79: 레이아웃 개선을 위한 최대 너비 증가 승인액티비티 상세 페이지의 향상된 UI와 잘 맞는 변경사항입니다. 800px에서 1200px로 증가한 것은 더 넓은 화면에서 더 나은 사용자 경험을 제공할 것입니다.
src/ui/MobileBookingModal.tsx (2)
11-17: 일관된 props 전달 구조가 잘 구현되었습니다.스케줄 데이터를 DatePicker로 전달하는 prop threading이 올바르게 구현되어 있고, 타입 정의도 적절합니다.
52-52: schedules prop이 DatePicker에 올바르게 전달되었습니다.DatePicker 컴포넌트가 schedules를 받도록 업데이트된 것과 일치하여 데이터 흐름이 적절합니다.
src/components/FloatingBox/TimeSelector.tsx (1)
16-17: 안전한 null 체크 로직이 추가되었습니다.
availableDates가 null이나 undefined일 때 발생할 수 있는 런타임 에러를 방지하는 좋은 방어적 프로그래밍 사례입니다. nullish coalescing과 optional chaining을 적절히 사용했습니다.src/ui/TabletBookingModal.tsx (2)
8-14: 일관된 props 구조가 잘 유지되었습니다.MobileBookingModal과 동일한 패턴으로 schedules prop을 추가하여 일관성 있는 구현을 보여줍니다.
31-31: DatePicker에 schedules가 올바르게 전달되었습니다.DatePicker 컴포넌트의 업데이트된 인터페이스와 일치하는 적절한 prop 전달입니다.
src/app/(with-header)/activities/[id]/components/ImageGrid.tsx (1)
7-7: 리팩토링 승인함수 시그니처에서 매개변수 구조 분해를 인라인으로 이동한 것은 코드를 더 간결하고 읽기 쉽게 만드는 좋은 개선사항입니다.
src/components/FloatingBox/PriceDisplay.tsx (1)
1-1: 동적 가격 표시로의 개선 승인하드코딩된 가격에서
priceprop을 받는 방식으로 변경한 것은 컴포넌트의 재사용성을 크게 향상시키는 좋은 개선사항입니다.src/ui/BookingModal.tsx (1)
3-3: 스케줄 prop 추가 승인
SchedulesProps타입을 추가하고 하위 모달 컴포넌트들에게 스케줄 데이터를 전달하는 구조가 API 연동 패턴과 일치합니다.Also applies to: 8-12
src/components/FloatingBox/TotalPriceDisplay.tsx (1)
3-4: 동적 가격 계산으로의 개선 승인하드코딩된
pricePerPerson값을priceprop으로 외부화한 것은 컴포넌트의 유연성을 크게 향상시키는 훌륭한 개선사항입니다.src/app/(with-header)/activities/[id]/components/Title.tsx (1)
13-14: 소유자 기반 조건부 렌더링 승인
isDropDown에서isOwner로 변경하여 액티비티 소유자 기반의 UI 제어로 개선한 것은 비즈니스 로직에 더 적합한 접근방식입니다.src/app/api/activities/[id]/route.ts (1)
6-32: API 라우트 구현이 잘 되어 있습니다.인증, 에러 처리, 응답 형식이 일관성 있게 구현되어 있고, Next.js 15의 async params 패턴을 올바르게 사용하고 있습니다. 다른 API 라우트들과 동일한 패턴을 따라 유지보수성이 좋습니다.
src/app/api/activities/[id]/available-schedule/route.ts (1)
5-34: API 라우트 구현이 잘 되어 있습니다.인증, 에러 처리, URL 구성이 올바르게 구현되어 있고, 다른 API 라우트들과 일관된 패턴을 따르고 있습니다.
src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx (2)
104-109: setTimeout(0) 사용 목적을 명확히 하거나 제거하세요.0ms 딜레이의 setTimeout이 사용된 목적이 명확하지 않습니다. 상태 업데이트를 다음 틱으로 연기할 필요가 있다면 주석으로 설명하거나, 불필요하다면 제거하세요.
이 setTimeout이 정말 필요한지 확인해보세요. React의 상태 업데이트는 이미 비동기적으로 배치 처리됩니다.
onMonthChange={(year, month) => { - setTimeout(() => { setYear(year); setMonth(month); - }, 0); }}
22-29: 전반적인 컴포넌트 구조가 잘 설계되어 있습니다.React Query 사용, 조건부 렌더링, props 전달 방식이 모두 적절하게 구현되어 있습니다. 특히 ownership 로직과 그에 따른 UI 조건부 렌더링이 잘 되어 있습니다.
Also applies to: 82-123
src/components/FloatingBox/BookingInterface.tsx (2)
16-45: props 기반 리팩토링과 API 연동이 잘 구현되어 있습니다.컴포넌트가 props를 받도록 리팩토링되었고, 비동기 예약 처리와 포괄적인 에러 핸들링이 잘 구현되어 있습니다. 소유자 기반 예약 제한 로직도 적절합니다.
52-56: 예약 가능 조건에 소유자 체크가 잘 추가되었습니다.
!isOwner조건이 추가되어 활동 소유자가 자신의 활동을 예약할 수 없도록 적절히 제한되어 있습니다.src/components/DatePicker/DatePicker.tsx (6)
13-13: 새로운 타입 import 확인
SchedulesProps타입이 올바르게 import되어 있습니다.
32-34: viewDate 초기화 로직 확인
selectedDate가 있을 때 dayjs로 변환하는 로직이 올바르게 구현되어 있습니다.
65-72: 월 변경 콜백 함수 구현 확인
onMonthChange콜백에서 월을 1-based로 전달하는 로직이 올바르게 구현되어 있습니다.
76-78: 날짜 선택 핸들러 구현 확인날짜 선택 시 Date 객체로 변환하는 로직이 올바릅니다.
80-80: null 체크 안전장치 확인
availableDates에 대한 null 체크가 올바르게 구현되어 있습니다.
40-51: 자동 날짜 선택 로직의 의존성 배열 문제
setSelectedDate가 의존성 배열에 누락되어 있고, 무한 루프 가능성이 있습니다.useEffect(() => { if (!selectedDate && schedules.length > 0) { const firstSchedule = schedules.find( (item) => dayjs(item.date).year() === viewDate.year() && dayjs(item.date).month() === viewDate.month(), ); if (firstSchedule) { setSelectedDate(dayjs(firstSchedule.date).toDate()); } } - }, [schedules, viewDate]); + }, [schedules, viewDate, selectedDate, setSelectedDate]);Likely an incorrect or invalid review comment.
src/types/activityDetailType.ts (5)
22-27: ActivitySchedule 인터페이스 정의 확인스케줄 정보를 위한 기본 인터페이스가 올바르게 정의되어 있습니다.
29-33: TimeSlot 타입 정의 확인시간대 정보를 위한 타입이 적절하게 정의되어 있습니다.
35-38: GroupedSchedule 타입 정의 확인날짜별로 그룹화된 스케줄 구조가 잘 설계되어 있습니다.
40-40: SchedulesProps 타입 별칭 확인
GroupedSchedule배열에 대한 타입 별칭이 명확합니다.
42-44: ActivitySubImage 인터페이스 정의 확인서브 이미지를 위한 간단한 인터페이스가 적절합니다.
| images: { | ||
| domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com'], | ||
| }, |
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
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| images: { | |
| domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com'], | |
| }, | |
| images: { | |
| domains: [process.env.NEXT_PUBLIC_IMAGE_DOMAIN || 'sprint-fe-project.s3.ap-northeast-2.amazonaws.com'], | |
| }, |
🤖 Prompt for AI Agents
In next.config.ts around lines 6 to 8, the external image domain is hardcoded
for S3 bucket access. To improve security and environment separation, replace
the hardcoded domain with an environment variable. Define the domain in an
environment variable and reference it in the images.domains array to allow
different domains per environment and avoid exposing sensitive info in code.
| const [year, setYear] = useState(2025); | ||
| const [month, setMonth] = useState(7); |
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.
하드코딩된 년도와 월을 현재 날짜로 초기화하세요.
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
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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); |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx
around lines 16 to 17, the year and month state variables are hardcoded to 2025
and 7. Replace these hardcoded values by initializing year and month with the
current date's year and month using JavaScript's Date object to ensure the
component uses the current date dynamically.
| useEffect(() => { | ||
| if (currentUserId && currentUserId === userId) { | ||
| setIsOwner(true); | ||
| console.log('니가 작성한 체험임'); |
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)
디버그용 console.log를 제거하세요.
프로덕션 코드에서 디버그 로그는 제거해야 합니다.
- console.log('니가 작성한 체험임');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log('니가 작성한 체험임'); |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx at
line 40, remove the console.log statement used for debugging to ensure no debug
logs remain in the production code.
| 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, | ||
| }); |
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)
복잡한 스케줄 조회 로직을 별도 함수로 분리하는 것을 고려해보세요.
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
In src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx
around lines 46 to 72, the logic for fetching schedules for three months is
complex and embedded directly in the useQuery hook. To improve readability and
reusability, extract this logic into a separate utility function, for example in
a utils/scheduleUtils.ts file. Move the calculation of previous, current, and
next month/year and the Promise.allSettled API calls into this function, then
call this utility function inside the queryFn of useQuery.
| export function Skeleton({ className = '' }: { className?: string }) { | ||
| return <div className={`animate-pulse bg-gray-200 ${className}`} />; | ||
| } |
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)
기본적인 스켈레톤 컴포넌트가 잘 구현되었습니다.
로딩 상태를 위한 간단하고 효과적인 구현입니다.
더 나은 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
In src/app/(with-header)/activities/[id]/components/Skeleton.tsx lines 2 to 4,
improve the Skeleton component by replacing the manual className string
concatenation with a utility function like cn or clsx for safer class merging,
and extend the component props to accept all standard HTML div attributes by
typing with React.HTMLAttributes<HTMLDivElement> and spreading ...props onto the
div element to support additional HTML properties.
| export default function TotalPriceDisplay({ price }: { price: number }) { | ||
| const participants = useBookingStore((state) => state.participants); | ||
| const pricePerPerson = 1000; | ||
|
|
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)
불필요한 빈 줄 제거
코드 일관성을 위해 불필요한 빈 줄을 제거해주세요.
const participants = useBookingStore((state) => state.participants);
-Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/components/FloatingBox/TotalPriceDisplay.tsx at line 5, there is an
unnecessary blank line that should be removed to maintain code consistency.
Delete the empty line to keep the code clean and uniform.
| export interface ActivityDetail { | ||
| id: number; | ||
| isOwner : boolean; | ||
| userId: number; | ||
| title: string; | ||
| description: string; | ||
| category: string; | ||
| price: number; | ||
| address: string | undefined; | ||
| bannerImageUrl: string; | ||
| rating: number; | ||
| reviewCount: number; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| subImages: ActivitySubImage[]; | ||
| schedules: ActivitySchedule[]; | ||
| } |
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)
ActivityDetail 인터페이스의 공백 문제
isOwner 속성 정의에 불필요한 공백이 있습니다.
- isOwner : boolean;
+ isOwner: boolean;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export interface ActivityDetail { | |
| id: number; | |
| isOwner : boolean; | |
| userId: number; | |
| title: string; | |
| description: string; | |
| category: string; | |
| price: number; | |
| address: string | undefined; | |
| bannerImageUrl: string; | |
| rating: number; | |
| reviewCount: number; | |
| createdAt: string; | |
| updatedAt: string; | |
| subImages: ActivitySubImage[]; | |
| schedules: ActivitySchedule[]; | |
| } | |
| export interface ActivityDetail { | |
| id: number; | |
| isOwner: boolean; | |
| userId: number; | |
| title: string; | |
| description: string; | |
| category: string; | |
| price: number; | |
| address: string | undefined; | |
| bannerImageUrl: string; | |
| rating: number; | |
| reviewCount: number; | |
| createdAt: string; | |
| updatedAt: string; | |
| subImages: ActivitySubImage[]; | |
| schedules: ActivitySchedule[]; | |
| } |
🤖 Prompt for AI Agents
In src/types/activityDetailType.ts between lines 46 and 62, remove the
unnecessary space before the colon in the isOwner property definition to ensure
consistent formatting and style in the ActivityDetail interface.
💡 Verification agent
🧩 Analysis chain
ActivityDetail 인터페이스의 address 타입 검토
address가 string | undefined로 정의되어 있는데, 이는 API 응답에서 주소가 선택적일 수 있음을 나타냅니다. 하지만 UI에서 주소를 표시할 때 적절한 처리가 필요합니다.
다음 스크립트로 address 필드 사용을 확인해보겠습니다:
🏁 Script executed:
#!/bin/bash
# address 필드가 undefined일 때의 처리 방법 확인
rg -A 5 -B 5 "address.*undefined|address\?" --type tsx --type tsLength of output: 94
아직 address 사용처가 확인되지 않아, tsx 파일을 glob 패턴으로 검색해 다시 확인해보겠습니다.
🏁 Script executed:
#!/bin/bash
# ts, tsx 확장자 내 'address' 프로퍼티 사용 위치 탐색
rg -n "\.address\b" -g '*.ts' -g '*.tsx'Length of output: 175
LocationMap 컴포넌트가 address가 undefined일 때 어떻게 동작하는지 확인해보겠습니다.
🏁 Script executed:
#!/bin/bash
# ActivityDetailForm.tsx 내 LocationMap 사용 부분 확인
sed -n '110,130p' 'src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx'
# LocationMap 컴포넌트 정의 및 처리 로직 검색
rg -A5 -B5 "LocationMap" -g '*.tsx'Length of output: 4108
LocationMap에 전달되는 address 타입 불일치 확인 및 처리 필요
ActivityDetail 인터페이스에는
address: string | undefined;로 정의되어 있지만,
LocationMap 컴포넌트는
interface LocationMapProps {
address: string;
}로 string만 받도록 선언되어 있어, undefined가 전달될 경우 타입 불일치 또는 런타임 에러가 발생할 수 있습니다.
확인 지점:
src/types/activityDetailType.ts–address: string | undefined;src/components/LocationMap.tsx–interface LocationMapProps { address: string; }src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx(~118행)
<LocationMap address={activityData.address} />
제안
- 렌더링 전에
activityData.address존재 여부 검사 (address && <LocationMap .../>) - 또는
LocationMapProps를address?: string으로 변경하고 내부에서undefined처리 로직 추가
🤖 Prompt for AI Agents
In src/types/activityDetailType.ts lines 46-62, the address property is defined
as string | undefined, but in src/components/LocationMap.tsx the
LocationMapProps interface expects address as a string only. To fix this, either
update the LocationMapProps interface to make address optional (address?:
string) and handle undefined inside LocationMap, or in
src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx around
line 118, add a check to render LocationMap only if activityData.address exists
(e.g., address && <LocationMap address={address} />) to prevent passing
undefined.
|
|
||
| export interface BookinDateProps { | ||
| schedules: ActivitySchedule[]; |
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.
BookinDateProps 인터페이스명 오타
인터페이스명에 오타가 있습니다.
-export interface BookinDateProps {
+export interface BookingDateProps {이 인터페이스를 사용하는 다른 파일에서도 함께 수정이 필요할 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export interface BookinDateProps { | |
| schedules: ActivitySchedule[]; | |
| } | |
| export interface BookingDateProps { | |
| schedules: ActivitySchedule[]; | |
| } |
🤖 Prompt for AI Agents
In src/types/activityDetailType.ts around lines 64 to 66, the interface name
BookinDateProps contains a typo. Rename the interface to BookingDateProps and
update all references to this interface in other files accordingly to maintain
consistency and avoid errors.
| export type ServerErrorResponse = { | ||
| error?: string; | ||
| }; |
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)
API 에러 응답 타입의 일관성 확보
API 전반에서 일관된 에러 응답 형식을 제공하는 좋은 접근입니다.
향후 확장성을 위해 더 포괄적인 에러 타입을 고려해보세요:
-export type ServerErrorResponse = {
- error?: string;
-};
+export type ServerErrorResponse = {
+ error?: string;
+ code?: string;
+ details?: Record<string, any>;
+};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export type ServerErrorResponse = { | |
| error?: string; | |
| }; | |
| export type ServerErrorResponse = { | |
| error?: string; | |
| code?: string; | |
| details?: Record<string, any>; | |
| }; |
🤖 Prompt for AI Agents
In src/types/apiErrorResponseType.ts at lines 1 to 3, the current
ServerErrorResponse type only includes an optional error string, which may limit
future extensibility. To fix this, expand the type to include additional fields
such as error codes, messages, or metadata that can accommodate more detailed
error information consistently across the API. This will ensure the error
response type is more comprehensive and scalable for future needs.
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.
Actionable comments posted: 9
🔭 Outside diff range comments (1)
src/components/FloatingBox/BookingInterface.tsx (1)
122-127: 모바일 뷰에서 하드코딩된 가격을 동적 가격으로 변경하세요데스크톱과 태블릿 뷰에서는
priceprop을 사용하지만, 모바일 뷰에서는 여전히 하드코딩된 "₩ 10,000"을 표시하고 있습니다.<div className='mb-1 text-xl font-bold text-gray-900'> - ₩ 10,000{' '} + ₩ {price.toLocaleString()}{' '} <span className='text-sm font-normal text-gray-600'> / 총 {participants}인 </span> </div>
♻️ Duplicate comments (2)
src/app/api/activities/[id]/reservation/route.ts (1)
14-14: 요청 body 데이터 검증이 필요합니다.
selectedTimeId와participants에 대한 유효성 검사 없이Number()로 변환하고 있어, 잘못된 데이터가 전달될 경우 NaN이 될 수 있습니다.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 } + ); + }src/app/api/activities/[id]/available-schedule/route.ts (1)
16-17: 쿼리 파라미터 유효성 검사를 고려해보세요.
year와month파라미터가 null일 수 있는데, URL에 직접 사용하고 있습니다. API 호출 전에 유효성 검사를 추가하는 것을 권장합니다.const year = searchParams.get('year'); const month = searchParams.get('month'); + if (!year || !month) { + return NextResponse.json( + { error: '년도와 월 정보가 필요합니다.' }, + { status: 400 } + ); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (15)
src/app/(with-header)/activities/[id]/components/Title.tsx(2 hunks)src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx(1 hunks)src/app/api/activities/[id]/available-schedule/route.ts(1 hunks)src/app/api/activities/[id]/reservation/route.ts(1 hunks)src/components/ActivityDropdown/Dropdown.tsx(1 hunks)src/components/ActivityDropdown/DropdownContext.tsx(1 hunks)src/components/ActivityDropdown/Item.tsx(1 hunks)src/components/ActivityDropdown/index.ts(1 hunks)src/components/ActivityDropdown/menu.tsx(1 hunks)src/components/ActivityDropdown/trigger.tsx(1 hunks)src/components/DatePicker/CalendarBody.tsx(0 hunks)src/components/FloatingBox/BookingInterface.tsx(3 hunks)src/components/FloatingBox/TabletPopup.tsx(1 hunks)src/ui/BookingModal.tsx(1 hunks)src/ui/MobileBookingModal.tsx(3 hunks)
💤 Files with no reviewable changes (1)
- src/components/DatePicker/CalendarBody.tsx
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/ui/MobileBookingModal.tsx (3)
src/types/activityDetailType.ts (1)
SchedulesProps(40-40)src/components/DatePicker/DatePicker.tsx (1)
DatePicker(20-94)src/components/FloatingBox/TotalPriceDisplay.tsx (1)
TotalPriceDisplay(3-12)
src/components/ActivityDropdown/Dropdown.tsx (2)
src/components/ActivityDropdown/DropdownContext.tsx (1)
DropdownContext(11-11)src/components/Dropdown.tsx (1)
Dropdown(29-205)
src/components/ActivityDropdown/menu.tsx (1)
src/components/ActivityDropdown/DropdownContext.tsx (1)
useDropdownContext(13-19)
src/components/FloatingBox/TabletPopup.tsx (1)
src/ui/TabletBookingModal.tsx (1)
TabletModal(9-46)
src/components/ActivityDropdown/Item.tsx (1)
src/components/ActivityDropdown/DropdownContext.tsx (1)
useDropdownContext(13-19)
🪛 GitHub Check: Code Quality Check
src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx
[failure] 11-11:
Cannot find module '../../utils/uploadImage' or its corresponding type declarations.
🪛 GitHub Actions: CI
src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx
[error] 11-11: TypeScript error TS2307: Cannot find module '../../utils/uploadImage' or its corresponding type declarations.
🔇 Additional comments (25)
src/ui/MobileBookingModal.tsx (3)
11-19: Props 추가가 적절하게 구현되었습니다.
schedules와priceprops가 올바르게 타입 정의되고 하위 컴포넌트들에게 전달되고 있습니다. 타입 임포트와 컴포넌트 간의 데이터 흐름이 일관성 있게 처리되었습니다.
54-54: 일관된 props 전달이 잘 구현되었습니다.
DatePicker에schedulesprop이 올바르게 전달되어 동적 스케줄 데이터를 지원합니다.
80-80: 가격 정보 전달이 적절합니다.
TotalPriceDisplay에priceprop이 올바르게 전달되어 동적 가격 계산을 지원합니다.src/ui/BookingModal.tsx (1)
8-19: Device별 props 전달이 올바르게 구현되었습니다.
schedules와priceprops가 적절하게 타입 정의되고, 디바이스 유형에 따라 올바른 하위 컴포넌트에 전달되고 있습니다. MobileModal에는 두 props 모두, TabletModal에는 schedules만 전달하는 것이 적절합니다.src/app/api/activities/[id]/reservation/route.ts (2)
27-40: API 호출 구현이 적절합니다.인증 헤더 설정과 요청 데이터 구조가 올바르게 구현되어 있고, 응답 처리도 적절합니다.
41-51: 에러 처리가 체계적으로 구현되었습니다.다양한 에러 응답 형태를 고려한 메시지 추출 로직과 상태 코드 처리가 잘 구현되어 있습니다.
src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx (3)
22-30: React Query를 이용한 데이터 페칭이 적절합니다.액티비티 데이터를 가져오는 로직이 올바르게 구현되어 있고, 에러 핸들링과 로딩 상태 관리가 적절합니다.
62-88: 이미지 업로드 로직이 체계적으로 구현되었습니다.메인 이미지와 서브 이미지 업로드 처리, 에러 핸들링, 용량 제한 로직이 잘 구현되어 있습니다. 업로드 실패 시 사용자에게 적절한 피드백을 제공합니다.
94-143: 폼 검증과 제출 로직이 포괄적으로 구현되었습니다.필수 필드 검증, 에러 처리, 성공/실패 시 사용자 피드백이 체계적으로 처리되어 있습니다. Alert 사용에 대한 개선 계획이 주석으로 명시되어 있어 향후 리팩토링이 계획되어 있음을 확인할 수 있습니다.
src/app/api/activities/[id]/available-schedule/route.ts (2)
29-38: API 호출과 응답 처리가 적절합니다.외부 API 호출을 위한 URL 구성, 인증 헤더 설정, 응답 데이터 반환이 올바르게 구현되어 있습니다.
39-51: 에러 처리 패턴이 일관성 있게 구현되었습니다.다른 API 라우트와 동일한 에러 처리 패턴을 사용하여 일관성을 유지하고 있으며, 적절한 에러 메시지와 상태 코드를 반환합니다.
src/components/FloatingBox/BookingInterface.tsx (2)
59-64: 소유자 예약 방지 로직이 잘 구현되었습니다
isOwnerprop을 활용하여 체험 등록자가 자신의 체험을 예약할 수 없도록 하는 로직이 적절히 구현되었습니다.Also applies to: 77-79, 110-112
28-53:id변수가 정의되기 전에 사용되고 있습니다
handleBooking함수(30번 줄)에서id를 사용하고 있지만,useParams에서id를 추출하는 코드는 57번 줄에 있습니다. 이로 인해 참조 에러가 발생합니다.export default function BookingInterface({ schedules, onMonthChange, isOwner, price, }: { schedules: SchedulesProps; onMonthChange?: (year: number, month: number) => void; isOwner: boolean; price: number; }) { + const { id } = useParams(); + const setIsOpen = useBookingStore((state) => state.setIsOpen); + const { selectedDate, selectedTime, participants, selectedTimeId } = + useBookingStore(); + const handleBooking = async () => { try { await privateInstance.post(`/activities/${id}/reservation`, { selectedTimeId, participants, }); alert('예약이 완료되었습니다!'); setIsOpen(false); } catch (err) { const error = err as AxiosError; const responseData = error.response?.data as | { error?: string; message?: string } | undefined; console.error('전체 에러:', error); alert( responseData?.error || responseData?.message || error.message || '예약에 실패했습니다.', ); } }; - const setIsOpen = useBookingStore((state) => state.setIsOpen); - const { selectedDate, selectedTime, participants, selectedTimeId } = - useBookingStore(); - const { id } = useParams();Likely an incorrect or invalid review comment.
src/components/ActivityDropdown/trigger.tsx (2)
23-24: 컨텍스트 사용이 적절합니다useDropdownContext 훅을 통해 드롭다운 상태에 올바르게 접근하고 있으며, 컴포넌트 간 상태 공유가 잘 구현되어 있습니다.
27-40: 키보드 접근성 향상을 위한 개선 제안현재 구현은 마우스 클릭 이벤트만 처리하고 있습니다. 완전한 접근성을 위해 키보드 이벤트 처리를 추가하는 것을 고려해보세요.
다음과 같이 키보드 이벤트 핸들링을 추가할 수 있습니다:
<button aria-expanded={isOpen} aria-haspopup='true' className='cursor-pointer' type='button' onClick={(e) => { e.preventDefault(); e.stopPropagation(); setIsOpen((prev) => !prev); }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + setIsOpen((prev) => !prev); + } + }} >Likely an incorrect or invalid review comment.
src/components/ActivityDropdown/index.ts (1)
27-31: 컴파운드 컴포넌트 패턴이 잘 구현되었습니다Object.assign을 사용한 컴파운드 컴포넌트 패턴이 적절하게 구현되어 있습니다. 이 패턴은 사용자가
<ActivityDropdown.Trigger>,<ActivityDropdown.Menu>등으로 직관적으로 사용할 수 있게 해줍니다.더 현대적인 방식을 선호한다면 다음과 같이 구현할 수도 있습니다:
-const ActivityDropdown = Object.assign(Wrapper, { - Trigger: Trigger, - Menu: Menu, - Item: Item, -}); +const ActivityDropdown = Object.assign(Wrapper, { + Trigger, + Menu, + Item, +} as const);src/components/ActivityDropdown/Dropdown.tsx (2)
10-21: 외부 클릭 감지 로직이 잘 구현되었습니다useEffect를 사용한 외부 클릭 감지와 이벤트 리스너 정리가 적절하게 구현되어 있습니다. 메모리 누수 방지를 위한 cleanup 함수도 올바르게 설정되어 있습니다.
1-30: 기존 Dropdown과 ActivityDropdown은 중복되지 않습니다
두 컴포넌트는 목적과 구현 방식이 완전히 다릅니다.
src/components/Dropdown.tsx는 일반적인 옵션 선택용 제네릭 드롭다운으로, 옵션 리스트 렌더링, 애니메이션, 선택 관리(onChange 등)를 포함합니다.src/components/ActivityDropdown/Dropdown.tsx는 드롭다운 상태(isOpen)와 외부 클릭 감지를 위한 Context 제공용 래퍼 역할만 합니다.따라서 공통 로직을 추출하거나 통합할 필요가 없으며, 원래 제안된 중복성 검토는 해당되지 않습니다.
Likely an incorrect or invalid review comment.
src/components/ActivityDropdown/menu.tsx (1)
36-38: 조건부 렌더링이 적절하게 구현되었습니다isOpen 상태에 따른 조건부 렌더링이 올바르게 구현되어 있어 성능상 이점을 제공합니다.
src/components/ActivityDropdown/DropdownContext.tsx (3)
5-9: TypeScript 인터페이스가 잘 정의되었습니다DropdownContextType 인터페이스가 드롭다운에 필요한 모든 속성을 명확하게 정의하고 있습니다. 타입 안전성이 잘 보장되어 있습니다.
13-19: 커스텀 훅의 에러 처리가 적절합니다useDropdownContext 훅에서 컨텍스트가 null인 경우 명확한 에러를 발생시키는 것이 좋습니다. 이는 개발자가 올바른 컴포넌트 구조를 사용하도록 도움을 줍니다.
더 구체적인 에러 메시지로 개선할 수 있습니다:
if (!context) { - throw new Error('요소들은 <Dropdown> 내부에서만 사용되야합니다'); + throw new Error('Dropdown 관련 컴포넌트들은 <ActivityDropdown> 내부에서만 사용할 수 있습니다'); }
11-11: 컨텍스트 초기값 설정이 적절합니다null을 기본값으로 설정하여 Provider 없이 사용하는 경우를 감지할 수 있도록 한 것이 좋은 패턴입니다.
src/components/ActivityDropdown/Item.tsx (3)
39-44: 이벤트 핸들링 로직 검증 완료이벤트 처리 로직이 올바르게 구현되었습니다.
preventDefault()와stopPropagation()을 통해 의도하지 않은 동작을 방지하고, 드롭다운을 자동으로 닫는 로직도 적절합니다.
47-57: 버튼 요소 구현 우수버튼 요소에 명시적인
type="button"속성이 포함되어 있어 접근성 요구사항을 충족하며, 반응형 디자인과 호버 효과도 적절히 구현되었습니다.
9-27: JSDoc 문서화 우수컴포넌트의 목적, 사용법, 매개변수에 대한 상세한 문서화가 잘 되어 있습니다. 예제 코드도 포함되어 있어 사용법을 쉽게 이해할 수 있습니다.
| import { ActivityDetail } from '@/types/activityDetailType'; | ||
| import ActivityDropdown from '@/components/ActivityDropdown'; | ||
| import Menu from '@/components/ActivityDropdown/menu'; | ||
| import Item from '@/components/ActivityDropdown/Item'; | ||
| import Trigger from '@/components/ActivityDropdown/trigger'; |
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)
Import 구조 개선 가능
모든 ActivityDropdown 관련 컴포넌트들을 개별적으로 import하는 대신, 더 깔끔한 구조로 정리할 수 있습니다.
-import { ActivityDetail } from '@/types/activityDetailType';
-import ActivityDropdown from '@/components/ActivityDropdown';
-import Menu from '@/components/ActivityDropdown/menu';
-import Item from '@/components/ActivityDropdown/Item';
-import Trigger from '@/components/ActivityDropdown/trigger';
+import { ActivityDetail } from '@/types/activityDetailType';
+import ActivityDropdown from '@/components/ActivityDropdown';그리고 사용 시에는:
- <ActivityDropdown>
- <Trigger>
+ <ActivityDropdown>
+ <ActivityDropdown.Trigger>
<IconDropdown />
- </Trigger>
- <Menu>
- <Item onClick={() => alert('수정')}>수정하기</Item>
- <Item onClick={() => alert('삭제')}>삭제하기</Item>
- </Menu>
+ </ActivityDropdown.Trigger>
+ <ActivityDropdown.Menu>
+ <ActivityDropdown.Item onClick={() => alert('수정')}>수정하기</ActivityDropdown.Item>
+ <ActivityDropdown.Item onClick={() => alert('삭제')}>삭제하기</ActivityDropdown.Item>
+ </ActivityDropdown.Menu>
</ActivityDropdown>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { ActivityDetail } from '@/types/activityDetailType'; | |
| import ActivityDropdown from '@/components/ActivityDropdown'; | |
| import Menu from '@/components/ActivityDropdown/menu'; | |
| import Item from '@/components/ActivityDropdown/Item'; | |
| import Trigger from '@/components/ActivityDropdown/trigger'; | |
| // Before, we had multiple imports for each sub-component: | |
| -import { ActivityDetail } from '@/types/activityDetailType'; | |
| -import ActivityDropdown from '@/components/ActivityDropdown'; | |
| -import Menu from '@/components/ActivityDropdown/menu'; | |
| -import Item from '@/components/ActivityDropdown/Item'; | |
| -import Trigger from '@/components/ActivityDropdown/trigger'; | |
| // Apply these changes: | |
| +import { ActivityDetail } from '@/types/activityDetailType'; | |
| +import ActivityDropdown from '@/components/ActivityDropdown'; | |
| // Later in the JSX, replace individual sub-component tags: | |
| -<ActivityDropdown> | |
| - <Trigger> | |
| - <IconDropdown /> | |
| - </Trigger> | |
| - <Menu> | |
| - <Item onClick={() => alert('수정')}>수정하기</Item> | |
| - <Item onClick={() => alert('삭제')}>삭제하기</Item> | |
| - </Menu> | |
| -</ActivityDropdown> | |
| +<ActivityDropdown> | |
| + <ActivityDropdown.Trigger> | |
| + <IconDropdown /> | |
| + </ActivityDropdown.Trigger> | |
| + <ActivityDropdown.Menu> | |
| + <ActivityDropdown.Item onClick={() => alert('수정')}> | |
| + 수정하기 | |
| + </ActivityDropdown.Item> | |
| + <ActivityDropdown.Item onClick={() => alert('삭제')}> | |
| + 삭제하기 | |
| + </ActivityDropdown.Item> | |
| + </ActivityDropdown.Menu> | |
| +</ActivityDropdown> |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/Title.tsx around lines 4 to
8, the imports for ActivityDropdown and its subcomponents are done individually,
which can be simplified. Refactor the imports by creating an index file inside
the ActivityDropdown folder that exports all related components, then import
them collectively from that index file to improve code cleanliness and
maintainability.
| <Item onClick={() => alert('수정')}>수정하기</Item> | ||
| <Item onClick={() => alert('삭제')}>삭제하기</Item> |
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)
임시 alert 핸들러를 실제 구현으로 교체 필요
현재 수정/삭제 기능이 alert로만 구현되어 있습니다. 실제 기능 구현이 필요합니다.
실제 수정/삭제 API 호출 로직을 구현하시겠습니까? 다음과 같은 구조를 제안드립니다:
const handleEdit = () => {
// 수정 페이지로 라우팅 또는 모달 열기
router.push(`/activities/${activityId}/edit`);
};
const handleDelete = async () => {
if (confirm('정말 삭제하시겠습니까?')) {
try {
await deleteActivity(activityId);
router.push('/activities');
} catch (error) {
// 에러 처리
}
}
};🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/Title.tsx at lines 42-43,
replace the temporary alert handlers for 수정하기 and 삭제하기 with actual
implementations. Implement a handleEdit function that routes to the edit page
for the current activity using router.push. Implement a handleDelete async
function that confirms deletion, calls the deleteActivity API with the
activityId, and on success routes back to the activities list; include error
handling for the API call. Update the onClick handlers of the Items to call
these new functions instead of alert.
| import { ScheduleSelectForm } from '../../components/ScheduleSelectForm'; | ||
| import { ImageSection } from '../../components/ImageSection'; | ||
| import Button from '@/components/Button'; | ||
| import { uploadImage } from '../../utils/uploadImage'; |
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.
💡 Verification agent
🧩 Analysis chain
누락된 모듈 임포트로 인한 빌드 실패
uploadImage 모듈을 찾을 수 없어 TypeScript 컴파일 오류가 발생하고 있습니다. 해당 유틸리티 함수가 구현되어 있는지 확인하고 올바른 경로로 임포트해주세요.
🏁 Script executed:
#!/bin/bash
# uploadImage 모듈 존재 여부 확인
fd -t f "uploadImage" --exec echo "Found: {}"
# utils 디렉토리 구조 확인
fd -t d "utils" --exec ls -la {}Length of output: 438
uploadImage 유틸리티 파일 부재로 인한 빌드 오류
컴파일 시 uploadImage 모듈을 찾을 수 없어 빌드가 실패하고 있습니다. 다음을 확인 및 조치해주세요.
- 위치:
src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx(11번 라인) - 현재
src/app/utils디렉토리에uploadImage.ts파일이 없으며(validateInput.ts,MonthFormatChange.ts만 확인됨)
해결 방안:
src/app/utils/uploadImage.ts파일을 생성하고uploadImage함수를 구현하거나- 올바른 경로에 있는 파일로 임포트 경로를 수정
🧰 Tools
🪛 GitHub Check: Code Quality Check
[failure] 11-11:
Cannot find module '../../utils/uploadImage' or its corresponding type declarations.
🪛 GitHub Actions: CI
[error] 11-11: TypeScript error TS2307: Cannot find module '../../utils/uploadImage' or its corresponding type declarations.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx at line
11, the import statement for uploadImage causes a build error because the file
src/app/utils/uploadImage.ts does not exist. To fix this, either create the
uploadImage.ts file in the src/app/utils directory with the appropriate
uploadImage function implementation or update the import path to point to the
correct existing file that exports uploadImage.
| useEffect(() => { | ||
| const handleClickOutside = (e: MouseEvent) => { | ||
| if ( | ||
| dropdownRef.current && | ||
| !dropdownRef.current.contains(e.target as Node) | ||
| ) { | ||
| setIsOpen(false); | ||
| } | ||
| }; | ||
| document.addEventListener('mousedown', handleClickOutside); | ||
| return () => document.removeEventListener('mousedown', handleClickOutside); | ||
| }, []); |
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)
키보드 접근성을 위한 Escape 키 처리 추가 제안
현재 외부 클릭으로만 드롭다운을 닫을 수 있습니다. 접근성 향상을 위해 Escape 키 처리를 추가하는 것을 고려해보세요.
다음과 같이 키보드 이벤트 핸들러를 추가할 수 있습니다:
useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setIsOpen(false);
+ }
+ };
+
const handleClickOutside = (e: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
+ document.addEventListener('keydown', handleKeyDown);
- return () => document.removeEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ document.removeEventListener('keydown', handleKeyDown);
+ };
}, []);🤖 Prompt for AI Agents
In src/components/ActivityDropdown/Dropdown.tsx around lines 10 to 21, the
dropdown only closes on outside mouse clicks, lacking keyboard accessibility.
Add a keydown event listener to detect the Escape key press and close the
dropdown by setting isOpen to false. Ensure to clean up this event listener in
the useEffect cleanup function along with the existing mouse event listener.
| }: { | ||
| children: string | ReactNode; | ||
| onClick: () => void; | ||
| itemClassName?: string; | ||
| }) { |
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)
props 타입 정의를 별도 인터페이스로 분리 권장
현재 inline으로 정의된 props 타입을 별도 인터페이스로 분리하면 재사용성과 가독성이 향상됩니다.
+interface ItemProps {
+ children: string | ReactNode;
+ onClick: () => void;
+ itemClassName?: string;
+}
+
export default function Item({
children,
onClick,
itemClassName,
-}: {
- children: string | ReactNode;
- onClick: () => void;
- itemClassName?: string;
-}) {
+}: ItemProps) {🤖 Prompt for AI Agents
In src/components/ActivityDropdown/Item.tsx around lines 32 to 36, the props
type is currently defined inline. To improve reusability and readability,
extract the props definition into a separate interface. Define an interface with
the props structure and use it to type the component's props instead of inline
typing.
| return ( | ||
| <div | ||
| className={cn( | ||
| 'absolute top-37 right-0 z-9999 flex flex-col content-center justify-around rounded-md border border-gray-300 bg-white p-2 shadow-lg', | ||
| menuClassName, | ||
| )} | ||
| > | ||
| {children} | ||
| </div> | ||
| ); |
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)
z-index 값이 과도하게 높습니다
현재 z-9999로 설정된 z-index 값이 매우 높습니다. 이는 다른 UI 요소들과의 레이어링 충돌을 야기할 수 있습니다. 더 적절한 z-index 값을 사용하는 것을 고려해보세요.
className={cn(
- 'absolute top-37 right-0 z-9999 flex flex-col content-center justify-around rounded-md border border-gray-300 bg-white p-2 shadow-lg',
+ 'absolute top-37 right-0 z-50 flex flex-col content-center justify-around rounded-md border border-gray-300 bg-white p-2 shadow-lg',
menuClassName,
)}또는 CSS 변수나 테일윈드 설정에서 드롭다운 전용 z-index 값을 정의하여 일관성을 유지할 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return ( | |
| <div | |
| className={cn( | |
| 'absolute top-37 right-0 z-9999 flex flex-col content-center justify-around rounded-md border border-gray-300 bg-white p-2 shadow-lg', | |
| menuClassName, | |
| )} | |
| > | |
| {children} | |
| </div> | |
| ); | |
| return ( | |
| <div | |
| className={cn( | |
| 'absolute top-37 right-0 z-50 flex flex-col content-center justify-around rounded-md border border-gray-300 bg-white p-2 shadow-lg', | |
| menuClassName, | |
| )} | |
| > | |
| {children} | |
| </div> | |
| ); |
🤖 Prompt for AI Agents
In src/components/ActivityDropdown/menu.tsx around lines 40 to 49, the z-index
value is set excessively high at z-9999, which may cause layering conflicts with
other UI elements. Reduce the z-index to a more reasonable value that still
ensures the dropdown appears above other components, such as a value within the
typical Tailwind z-index scale (e.g., z-50 or z-40). Alternatively, define a
dedicated CSS variable or Tailwind configuration for dropdown z-index to
maintain consistency across the app.
🧹 Nitpick (assertive)
UX 개선을 위한 애니메이션 추가 제안
현재 메뉴가 즉시 나타나고 사라집니다. 더 나은 사용자 경험을 위해 페이드 인/아웃 애니메이션을 추가하는 것을 고려해보세요.
Framer Motion을 사용한 애니메이션 예시:
+'use client';
+
+import { motion, AnimatePresence } from 'framer-motion';
import { ReactNode } from 'react';
export default function Menu({
children,
menuClassName,
}: {
children: ReactNode;
menuClassName?: string;
}) {
const { isOpen } = useDropdownContext();
- if (!isOpen) return null;
-
return (
- <div
- className={cn(
- 'absolute top-37 right-0 z-50 flex flex-col content-center justify-around rounded-md border border-gray-300 bg-white p-2 shadow-lg',
- menuClassName,
- )}
- >
- {children}
- </div>
+ <AnimatePresence>
+ {isOpen && (
+ <motion.div
+ initial={{ opacity: 0, y: -10 }}
+ animate={{ opacity: 1, y: 0 }}
+ exit={{ opacity: 0, y: -10 }}
+ transition={{ duration: 0.2 }}
+ className={cn(
+ 'absolute top-37 right-0 z-50 flex flex-col content-center justify-around rounded-md border border-gray-300 bg-white p-2 shadow-lg',
+ menuClassName,
+ )}
+ >
+ {children}
+ </motion.div>
+ )}
+ </AnimatePresence>
);
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/components/ActivityDropdown/menu.tsx around lines 40 to 49, the dropdown
menu currently appears and disappears instantly, which can be jarring for users.
To improve UX, wrap the div containing the menu with a Framer Motion component
like motion.div and add fade in/out animations using initial, animate, and exit
props. This will create smooth fade transitions when the menu mounts and
unmounts.
| } catch (err) { | ||
| const error = err as AxiosError; | ||
|
|
||
| const responseData = error.response?.data as | ||
| | { error?: string; message?: string } | ||
| | undefined; | ||
|
|
||
| console.error('전체 에러:', error); | ||
|
|
||
| alert( | ||
| responseData?.error || | ||
| responseData?.message || | ||
| error.message || | ||
| '예약에 실패했습니다.', | ||
| ); | ||
| } |
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)
프로덕션 환경에서의 에러 로깅 개선 필요
현재 console.error로 전체 에러를 로깅하고 있습니다. 프로덕션 환경에서는 적절한 에러 로깅 서비스(예: Sentry)를 사용하는 것이 좋습니다.
추가로, PR 목표에 언급된 대로 추후 toast 알림으로 전환할 계획이라면, TODO 주석을 추가하여 명시하는 것이 좋습니다:
console.error('전체 에러:', error);
+ // TODO: toast 알림으로 전환 예정
alert(
responseData?.error ||
responseData?.message ||
error.message ||
'예약에 실패했습니다.',
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (err) { | |
| const error = err as AxiosError; | |
| const responseData = error.response?.data as | |
| | { error?: string; message?: string } | |
| | undefined; | |
| console.error('전체 에러:', error); | |
| alert( | |
| responseData?.error || | |
| responseData?.message || | |
| error.message || | |
| '예약에 실패했습니다.', | |
| ); | |
| } | |
| } catch (err) { | |
| const error = err as AxiosError; | |
| const responseData = error.response?.data as | |
| | { error?: string; message?: string } | |
| | undefined; | |
| console.error('전체 에러:', error); | |
| // TODO: toast 알림으로 전환 예정 | |
| alert( | |
| responseData?.error || | |
| responseData?.message || | |
| error.message || | |
| '예약에 실패했습니다.', | |
| ); | |
| } |
🤖 Prompt for AI Agents
In src/components/FloatingBox/BookingInterface.tsx around lines 37 to 52,
replace the console.error call with a call to the configured production error
logging service such as Sentry to properly capture errors in production. Also,
add a TODO comment near the alert call indicating that this alert should be
replaced with a toast notification in the future as per the PR goals.
| // 'use client'; | ||
|
|
||
| import useBookingStore from '@/stores/Booking/useBookingStore'; | ||
| import IconClose from '@assets/svg/close'; | ||
| import DatePicker from '../DatePicker/DatePicker'; | ||
| import TimeSelector from './TimeSelector'; | ||
| // import useBookingStore from '@/stores/Booking/useBookingStore'; | ||
| // import IconClose from '@assets/svg/close'; | ||
| // import DatePicker from '../DatePicker/DatePicker'; | ||
| // import TimeSelector from './TimeSelector'; | ||
| // import { SchedulesProps } from '@/types/activityDetailType'; | ||
|
|
||
| export default function TabletPopup({}) { | ||
| const isOpen = useBookingStore((state) => state.isOpen); | ||
| const setIsOpen = useBookingStore((state) => state.setIsOpen); | ||
| // export default function TabletPopup({schedules}:SchedulesProps) { | ||
| // const isOpen = useBookingStore((state) => state.isOpen); | ||
| // const setIsOpen = useBookingStore((state) => state.setIsOpen); | ||
|
|
||
| if (!isOpen) return null; | ||
| // if (!isOpen) return null; | ||
|
|
||
| return ( | ||
| <div className='absolute z-50 flex flex-col items-center justify-center'> | ||
| <div className='flex justify-between'> | ||
| <h2>날짜</h2> | ||
| <button onClick={() => setIsOpen(false)}> | ||
| <IconClose /> | ||
| </button> | ||
| </div> | ||
| // return ( | ||
| // <div className='absolute z-50 flex flex-col items-center justify-center'> | ||
| // <div className='flex justify-between'> | ||
| // <h2>날짜</h2> | ||
| // <button onClick={() => setIsOpen(false)}> | ||
| // <IconClose /> | ||
| // </button> | ||
| // </div> | ||
|
|
||
| <DatePicker /> | ||
| // <DatePicker schedules={schedules}/> | ||
|
|
||
| <TimeSelector /> | ||
| </div> | ||
| ); | ||
| } | ||
| // <TimeSelector /> | ||
| // </div> | ||
| // ); | ||
| // } |
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.
🛠️ Refactor suggestion
주석 처리된 코드를 제거하세요
전체 파일이 주석 처리되어 있습니다. TabletBookingModal.tsx가 이 컴포넌트를 대체하는 것으로 보입니다. 사용하지 않는 코드는 주석으로 남겨두기보다 완전히 제거하는 것이 좋습니다.
이 파일을 삭제하거나, 향후 사용 계획이 있다면 그 이유를 명시하는 주석을 추가하세요.
🤖 Prompt for AI Agents
In src/components/FloatingBox/TabletPopup.tsx lines 1 to 29, the entire code is
commented out and appears unused since TabletBookingModal.tsx replaces this
component. Remove all the commented-out code entirely to clean up the file. If
you plan to keep this file for future use, add a clear comment at the top
explaining the reason for retaining it; otherwise, consider deleting the file
altogether.
LeeCh0129
left a comment
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.
체험 상세페이지 API 연결 작업을 완성도 높게 해주셨네요! 라우트 구조부터, useQuery 활용이나 조건부 쿼리 등을 이용해서 체계적으로 잘 구현해주셨네요! 덕분에 많이 많이 배워갑니다 🙏
minimo-9
left a comment
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.
고생많으셨습니다! 구현 너무 잘해주셨네요!
BokyungCodes
left a comment
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.
Booking 인터페이스의 디바이스별 UI가 명확하게 분리되어 있어 유지보수하기 좋아 보입니다!
console 로그들은 나중에 삭제하는게 성능에 더 좋을 것 같습니다. 👍
📌 변경 사항 개요
-체험 상세페이지 리뷰 페이지네이션 제외 api 연결
📝 상세 내용
🔗 관련 이슈
🖼️ 스크린샷(선택사항)
2025-07-28.02-12-07.mp4
💡 참고 사항
Summary by CodeRabbit
신규 기능
버그 수정
개선 및 리팩터링
기타