Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default function ActivityDetailForm() {
);

return (
<div className='mx-auto max-w-1200 p-4 sm:px-20 lg:p-8'>
<div className='mx-auto mt-30 max-w-1200 p-4 px-20 sm:px-20 lg:p-8'>
<Title
title={activityData.title}
category={activityData.category}
Expand Down
55 changes: 47 additions & 8 deletions src/app/(with-header)/activities/[id]/components/ImageGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,83 @@
import Image from 'next/image';
import React, { useState } from 'react';
import { ImageGridProps } from '@/types/activityDetailType';
import { AnimatePresence, motion } from 'framer-motion';

function ImageGrid({ mainImage, subImages }: ImageGridProps) {
const images = [mainImage, ...subImages];

const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);

const prevSlide = () => {
setDirection(-1);
setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
};

const nextSlide = () => {
setDirection(1);
setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
};

const variants = {
enter: (direction: number) => ({
x: direction > 0 ? 300 : -300,
opacity: 0,
}),
center: {
x: 0,
opacity: 1,
},
exit: (direction: number) => ({
x: direction > 0 ? -300 : 300,
opacity: 0,
}),
};

return (
<>
{/* 모바일 */}
<div className='relative block aspect-square h-[300px] w-full overflow-hidden rounded-lg md:hidden'>
<Image
src={images[currentIndex]}
alt={`슬라이드 이미지 ${currentIndex + 1}`}
fill
className='object-cover hover:animate-pulse'
/>
<AnimatePresence custom={direction} initial={false}>
<motion.div
key={currentIndex}
custom={direction}
variants={variants}
initial='enter'
animate='center'
exit='exit'
transition={{
x: { type: 'spring', stiffness: 300, damping: 30 },
opacity: { duration: 0.2 },
}}
className='absolute inset-0'
>
<Image
src={images[currentIndex]}
alt={` ${currentIndex + 1}`}
fill
className='rounded-lg object-cover'
priority
/>
</motion.div>
</AnimatePresence>

<button
onClick={prevSlide}
aria-label='이전 이미지'
className='absolute top-1/2 left-2 -translate-y-1/2 rounded-full bg-black/50 px-6 py-10 text-white'
>
</button>
Comment on lines 67 to 73
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

버튼 요소에 type 속성 추가 필요

정적 분석 도구가 지적한 대로 버튼 요소에 명시적인 type 속성이 필요합니다. 폼 내부에서 의도하지 않은 제출을 방지하기 위해 type="button"을 추가해야 합니다.

다음과 같이 수정하세요:

        <button
+         type="button"
          onClick={prevSlide}
          aria-label='이전 이미지'
          className='absolute top-1/2 left-2 -translate-y-1/2 rounded-full bg-black/50 px-6 py-10 text-white'
        >
          ‹
        </button>

        <button
+         type="button"
          onClick={nextSlide}
          aria-label='다음 이미지'
          className='absolute top-1/2 right-2 -translate-y-1/2 rounded-full bg-black/50 px-6 py-10 text-white'
        >
          ›
        </button>

Also applies to: 75-81

🧰 Tools
🪛 Biome (2.1.2)

[error] 67-73: 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)

🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ImageGrid.tsx around lines
67 to 73 and also lines 75 to 81, the button elements lack an explicit type
attribute, which can cause unintended form submissions. Add type="button" to
each button element to explicitly define their behavior and prevent accidental
form submissions.


<button
onClick={nextSlide}
aria-label='다음 이미지'
className='absolute top-1/2 right-2 -translate-y-1/2 rounded-full bg-black/50 px-6 py-10 text-white'
>
</button>

<div className='absolute bottom-2 left-1/2 flex -translate-x-1/2 gap-1'>
{images.map((_, i) => (
<div
Expand All @@ -52,7 +91,8 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) {
))}
</div>
</div>
{/* PC 태블릿 */}

{/* PC/태블릿 */}
<div className='hidden h-[500px] grid-cols-4 grid-rows-4 gap-6 md:grid'>
<div className='relative col-span-2 row-span-4 hover:animate-pulse'>
<Image
Expand All @@ -62,7 +102,6 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) {
className='rounded-lg object-cover'
/>
</div>

{subImages.slice(0, 4).map((image, index) => (
<div
key={index}
Expand Down
18 changes: 10 additions & 8 deletions src/app/(with-header)/activities/[id]/components/ReviewSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ function ReviewSection({
<div className='relative min-h-350'>
<ReviewTitle reviewCount={reviewCount} rating={rating} />

<div className='pointer-events-none absolute inset-0 z-10 flex items-center justify-center'>
<div className='flex items-center justify-center font-bold'>
<p>작성된 리뷰가 없습니다</p>
<div className='pointer-events-none absolute inset-0 z-10 flex h-full items-center justify-center select-none'>
<div className='flex max-w-md items-center justify-center rounded-md bg-white px-20 py-20 shadow-2xl ring-1 ring-gray-200 select-none'>
<p className='text-md text-center font-bold text-gray-800'>
작성된 후기가 없습니다
</p>
</div>
</div>
</div>
Expand All @@ -98,20 +100,20 @@ function ReviewSection({
}

if (isError) {
throw new Error('에러발생');
throw new Error('리뷰섹션에서 에러가 발생했습니다.');
}

return (
<div className='mt-10 flex flex-col space-y-8'>
<ReviewTitle reviewCount={reviewCount} rating={rating} />

<div className='relative min-h-350'>
<div className='pointer-events-none relative min-h-350 select-none'>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

사용자 상호작용 제한을 재검토해주세요.

pointer-events-noneselect-none 클래스가 전체 리뷰 컨테이너에 적용되어 로그인한 사용자도 리뷰 내용과 상호작용(텍스트 선택, 클릭 등)할 수 없게 됩니다. 이 제한이 로그인하지 않은 사용자에게만 적용되도록 조건부로 적용하는 것을 고려해보세요.

다음과 같이 수정하는 것을 제안합니다:

-      <div className='pointer-events-none relative min-h-350 select-none'>
+      <div className={`relative min-h-350 ${!user ? 'pointer-events-none select-none' : ''}`}>
📝 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.

Suggested change
<div className='pointer-events-none relative min-h-350 select-none'>
<div className={`relative min-h-350 ${!user ? 'pointer-events-none select-none' : ''}`}>
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ReviewSection.tsx at line
110, the classes 'pointer-events-none' and 'select-none' are applied
unconditionally to the review container, preventing all users from interacting
with the review content. Modify the code to apply these classes conditionally
only when the user is not logged in, allowing logged-in users to interact with
the review section normally.

<div className={user ? '' : 'blur-sm'}>{ReviewComponent()}</div>

{!user && (
<div className='pointer-events-none absolute inset-0 z-10 flex items-center justify-center'>
<div className='rounded-md bg-white/70 px-4 py-2 shadow-md'>
<p className='text-sm font-semibold text-gray-700'>
<div className='pointer-events-none absolute inset-0 z-10 flex h-full items-center justify-center select-none'>
<div className='flex max-w-md items-center justify-center rounded-md bg-white px-20 py-20 shadow-2xl ring-1 ring-gray-200 select-none'>
<p className='text-md text-center font-bold text-gray-800'>
로그인 후 리뷰 전체 내용을 확인할 수 있어요
</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export default function ReviewTitle({

useEffect(() => {
const handleSummary = () => {
if (reviewCount === 0) {
setSummary('작성된 후기가 없습니다');
return;
}

if (rating >= 4.5) {
setSummary('매우 만족');
} else if (rating >= 3) {
Expand Down
4 changes: 0 additions & 4 deletions src/app/(with-header)/activities/[id]/components/Skeleton.tsx

This file was deleted.

9 changes: 5 additions & 4 deletions src/app/(with-header)/activities/[id]/components/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function Title({
mutate(id as string);
setIsPopupOpen(false);
};


return (
<>
Expand All @@ -55,14 +56,14 @@ function Title({
<h1 className='mb-2 text-2xl font-bold text-black md:text-3xl'>
{title}
</h1>
<div className='flex items-center gap-10 text-sm text-gray-600'>
<div className='flex flex-nowrap items-center gap-30 text-sm text-gray-600'>
<div className='flex items-center gap-1'>
<Star />
<span className='font-medium'>
<p className='font-medium'>
{rating.toFixed(1)} ({reviewCount}명)
</span>
</p>
</div>
<span>{address}</span>
<p>{address}</p>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { uploadImage } from '../../utils/uploadImage';
import { ActivityDetailEdit, Schedule } from '@/types/activityDetailType';
import { AxiosError } from 'axios';
import { toast } from 'sonner';
import { notFound } from 'next/navigation';

interface SubImageType {
id?: number;
Expand All @@ -32,14 +33,26 @@ export const useEditActivityForm = () => {
const [originalSchedules, setOriginalSchedules] = useState<Schedule[]>([]);
const [dates, setDates] = useState<Schedule[]>([]);

const { data, isLoading, isError } = useQuery<ActivityDetailEdit, Error>({
const { data, isLoading, status, isError, error } = useQuery<
ActivityDetailEdit,
Error
>({
queryKey: ['edit-activity', id],
queryFn: async () => {
const res = await privateInstance.get(`/activities/${id}`);
return res.data;
},
enabled: !!id,
});
if (status === 'error') {
const axiosError = error as AxiosError;
const httpStatus = axiosError.response?.status;

if (httpStatus === 404) {
console.log('404 에러임');
notFound();
}
}
Comment on lines +47 to +55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

404 에러 처리 로직의 위치를 수정해주세요.

현재 에러 처리 로직이 컴포넌트 본문 외부에 위치하여 렌더링 중에 실행됩니다. 이는 React의 규칙을 위반하고 예상치 못한 동작을 일으킬 수 있습니다.

다음과 같이 수정하여 에러 처리를 useEffect 내부로 이동시키거나 별도의 효과로 분리하는 것을 권장합니다:

-  if (status === 'error') {
-    const axiosError = error as AxiosError;
-    const httpStatus = axiosError.response?.status;
-
-    if (httpStatus === 404) {
-      console.log('404 에러임');
-      notFound();
-    }
-  }
+  useEffect(() => {
+    if (status === 'error') {
+      const axiosError = error as AxiosError;
+      const httpStatus = axiosError.response?.status;
+
+      if (httpStatus === 404) {
+        console.log('Activity not found (404)');
+        notFound();
+      }
+    }
+  }, [status, error]);

추가 개선사항:

  • 디버깅을 위해 콘솔 메시지를 영어로 변경
  • useEffect를 사용하여 부수효과를 적절히 관리
📝 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.

Suggested change
if (status === 'error') {
const axiosError = error as AxiosError;
const httpStatus = axiosError.response?.status;
if (httpStatus === 404) {
console.log('404 에러임');
notFound();
}
}
useEffect(() => {
if (status === 'error') {
const axiosError = error as AxiosError;
const httpStatus = axiosError.response?.status;
if (httpStatus === 404) {
console.log('Activity not found (404)');
notFound();
}
}
}, [status, error]);
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts around
lines 47 to 55, the 404 error handling logic is currently placed outside of a
React effect, causing it to run during rendering which violates React rules.
Move this error handling code inside a useEffect hook that depends on the status
and error variables to properly manage side effects. Also, change the console
log message to English for consistency and clarity.


useEffect(() => {
if (data) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/FloatingBox/BookingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function BookingButton({
{onBooking ? (
<div className='flex items-center justify-center gap-2'>
<span className='h-10 w-10 animate-spin rounded-full border-2 border-white border-t-transparent' />
<p>예약 중...</p>
<p>...</p>
</div>
) : (
children
Expand Down
15 changes: 5 additions & 10 deletions src/components/FloatingBox/BookingInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ export default function BookingInterface({
<h3 className='mb-4 text-lg font-semibold text-gray-900'>날짜</h3>
<button
onClick={() => setIsOpen(true)}
className='w-full rounded-lg border border-gray-300 p-3 py-8 text-left text-black hover:bg-gray-50'
className='flex w-full items-center justify-center rounded-lg border border-gray-300 p-3 py-8 text-left text-black hover:bg-gray-50'
>
{selectedDate && selectedTime ? (
<h2>
<h2 className='animate-pulse'>
{selectedDate instanceof Date
? selectedDate.toLocaleDateString()
: selectedDate}
Expand Down Expand Up @@ -145,18 +145,13 @@ export default function BookingInterface({
</div>

{/* 모바일 */}
<div className='fixed right-0 bottom-0 left-0 z-50 block border border-gray-200 bg-white p-6 md:hidden'>
<div className='fixed right-0 bottom-0 left-0 z-50 block h-150 border border-gray-200 bg-white px-20 md:hidden'>
<div className='mb-6 flex items-start justify-between'>
<div className='flex-1'>
<div className='mb-1 text-xl font-bold text-gray-900'>
₩{price}
<span className='text-sm font-normal text-gray-600'>
/ 총 {participants}인
</span>
</div>
<PriceDisplay price={price} />
<div
onClick={() => setIsOpen(true)}
className='mb-4 text-sm text-gray-600'
className='mb-4 animate-pulse cursor-pointer text-sm text-gray-600'
>
{selectedDate && selectedTime ? (
<h2>
Expand Down
3 changes: 2 additions & 1 deletion src/components/FloatingBox/PriceDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export default function PriceDisplay({ price }: { price: number }) {
return (
<div className='mt-15 mb-6'>
<div className='mb-1 text-2xl font-bold text-black'>
₩{price} <span className='text-xl font-bold text-gray-800'>/ 인</span>
₩{price.toLocaleString('ko-KR')}{' '}
<span className='text-xl font-bold text-gray-800'>/ 인</span>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/LocationMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const LocationMap = ({ address }: LocationMapProps) => {

return (
<>
<div className='flex h-[480px] w-full flex-col overflow-hidden rounded-lg shadow-md lg:max-w-[1200px]'>
<div className='flex h-[400px] w-full flex-col overflow-hidden rounded-lg shadow-md md:h-[480px] lg:max-w-[1200px]'>
{/* 지도 */}
<Map
center={coords}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Modal/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function ModalContent({
const { isOpen } = useModalContext();
const [isMounted, setIsMounted] = useState(false);

const zIndexClass = zIndex ? `z-[${zIndex}]` : 'z-50';
const zIndexClass = zIndex ? `z-[${zIndex}]` : 'z-999';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

z-index 값 관리 체계화 필요

z-index를 50에서 999로 크게 증가시킨 것은 즉각적인 레이어링 문제는 해결할 수 있지만, 향후 유지보수에 어려움을 줄 수 있습니다.

z-index 값들을 체계적으로 관리하는 것을 권장합니다:

  • 기본 컨텐츠: 1-99
  • 드롭다운/툴팁: 100-199
  • 모달/오버레이: 200-299
  • 최상위 알림: 300+
-  const zIndexClass = zIndex ? `z-[${zIndex}]` : 'z-999';
+  const zIndexClass = zIndex ? `z-[${zIndex}]` : 'z-200';
🤖 Prompt for AI Agents
In src/components/Modal/Content.tsx at line 34, the zIndex value is set directly
to 999 or a custom value, which lacks a structured management approach. Refactor
the code to use a defined zIndex scale for different UI layers, such as 1-99 for
base content, 100-199 for dropdowns/tooltips, 200-299 for modals/overlays, and
300+ for top-level notifications. Replace the hardcoded 'z-999' with a constant
or enum representing the modal layer range (200-299) to ensure consistent and
maintainable z-index usage.


useEffect(() => {
setIsMounted(true);
Expand Down
26 changes: 19 additions & 7 deletions src/ui/MobileBookingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import BookingButton from '@/components/FloatingBox/BookingButton';
import ParticipantsSelector from '@/components/FloatingBox/ParticipantSelector';
import TotalPriceDisplay from '@/components/FloatingBox/TotalPriceDisplay';
import { SchedulesProps } from '@/types/activityDetailType';
import { toast } from 'sonner';

export default function MobileModal({
schedules,
Expand All @@ -27,13 +28,23 @@ export default function MobileModal({
);

const next = () => {
if (step === 'date-time' && !selectedTime) {
toast.error('시간을 선택해주세요.');
return;
}

setStep((prev) => (prev === 'date-time' ? 'participants' : 'confirm'));
};

const prev = () => {
setStep((prev) => (prev === 'confirm' ? 'participants' : 'date-time'));
};

const handleConfirm = () => {
setIsOpen(false);
setStep('date-time');
};

// const handleBooking = () => {
// alert('예약이 완료되었습니다!');
// setIsOpen(false);
Expand All @@ -42,7 +53,7 @@ export default function MobileModal({

return (
<Modal isOpen={isOpen} onOpenChange={setIsOpen}>
<Modal.Content zIndex={300}>
<Modal.Content zIndex={9999}>
<Modal.Header>
<Modal.Title>예약하기</Modal.Title>
<Modal.Close />
Expand All @@ -67,22 +78,23 @@ export default function MobileModal({
</div>
<div className={step === 'confirm' ? 'block' : 'hidden'}>
<div className='flex min-h-400 flex-col items-center justify-center gap-20'>
<h3 className='text-xl font-bold'>예약 내역 확인</h3>
<div>
<p className='font-bold'>
날짜 및 시간 {selectedDate?.toLocaleDateString()}{' '}
{selectedTime}{' '}
날짜 및 시간 {selectedDate?.toLocaleDateString()}
{selectedTime}
</p>
</div>
<div>
<p className='font-bold'>인원 {participants}</p>
</div>
<TotalPriceDisplay price={price} />
<div className='flex items-end justify-end'>
<TotalPriceDisplay price={price} />
</div>
</div>
</div>
</Modal.Item>
<Modal.Footer>
<div className='flex justify-between gap-10'>
<div className='flex justify-between gap-20'>
{step !== 'date-time' && (
<button
onClick={prev}
Expand All @@ -99,7 +111,7 @@ export default function MobileModal({
다음
</button>
) : (
<BookingButton onClick={() => setIsOpen(false)}>
<BookingButton onClick={() => handleConfirm()}>
확인
</BookingButton>
)}
Expand Down