diff --git a/env.d.ts b/env.d.ts
index 24c36e94..29029cac 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -2,6 +2,7 @@ interface ImportMetaEnv {
readonly NEXT_PUBLIC_API_BASE_URL: string;
readonly NEXT_PUBLIC_OAUTH_APP_KEY: string;
readonly NEXT_PUBLIC_APP_URL: string;
+ readonly NEXT_PUBLIC_KAKAO_MAP_KEY: string;
}
interface ImportMeta {
diff --git a/public/images/actit-logo.png b/public/images/actit-logo.png
new file mode 100644
index 00000000..a77f5646
Binary files /dev/null and b/public/images/actit-logo.png differ
diff --git a/src/api/types/activities.ts b/src/api/types/activities.ts
index cac00124..9dd136a9 100644
--- a/src/api/types/activities.ts
+++ b/src/api/types/activities.ts
@@ -154,7 +154,7 @@ export interface ActivityRegisterPayload {
price: number;
bannerImageUrl: string;
subImageUrls: string[];
- schedule: ActivityRegisterSchedule[];
+ schedules: ActivityRegisterSchedule[];
}
export interface ActivityRegisterSchedule {
diff --git a/src/api/types/myActivities.ts b/src/api/types/myActivities.ts
index 5f0401c6..0a9b357b 100644
--- a/src/api/types/myActivities.ts
+++ b/src/api/types/myActivities.ts
@@ -1,20 +1,24 @@
// refactoring 필요
-export type Category =
- | '문화 · 예술'
- | '식음료'
- | '스포츠'
- | '투어'
- | '관광'
- | '웰빙';
+export const CATEGORY = [
+ '문화 · 예술',
+ '식음료',
+ '스포츠',
+ '투어',
+ '관광',
+ '웰빙',
+] as const;
+export type Category = (typeof CATEGORY)[number];
// refactoring 필요
// ReservationResponse, ReservationWithActivityResponse 에 사용
-export type ReservationStatus =
- | 'pending'
- | 'confirmed'
- | 'declined'
- | 'canceled'
- | 'completed';
+export const RESERVATION_STATUS = [
+ 'pending',
+ 'confirmed',
+ 'declined',
+ 'canceled',
+ 'completed',
+] as const;
+export type ReservationStatus = (typeof RESERVATION_STATUS)[number];
// refactoring 필요
export interface CreateScheduleBody {
diff --git a/src/app/[activityId]/_components/ActivityDescription.tsx b/src/app/[activityId]/_components/ActivityDescription.tsx
index 50a9c6a3..dbb0e9f6 100644
--- a/src/app/[activityId]/_components/ActivityDescription.tsx
+++ b/src/app/[activityId]/_components/ActivityDescription.tsx
@@ -1,22 +1,15 @@
'use client';
import { useActivityDescription } from '@/app/[activityId]/_hooks/useActivityDescription';
-import {
- getImageColumnWrapperClass,
- getImageContainerClass,
- getSubImageClass,
-} from '@/app/[activityId]/_utils/getImageClass';
type Props = {
activityId: number;
};
export default function ActivityDescription({ activityId }: Props) {
- const { description, bannerImageUrl, subImages, errorMessage, isLoading } =
+ const { description, isLoading, errorMessage } =
useActivityDescription(activityId);
- const isSingleSubImage = subImages.length === 1;
-
if (isLoading) {
return (
@@ -28,50 +21,7 @@ export default function ActivityDescription({ activityId }: Props) {
}
return (
-
-
- {bannerImageUrl && (
-
-

-
- )}
-
-
- {subImages[0] && (
-
-

-
- )}
- {subImages[1] && !isSingleSubImage && (
-
-

-
- )}
-
-
-
+
체험 설명
{description}
diff --git a/src/app/[activityId]/_components/ActivityDetailImage.tsx b/src/app/[activityId]/_components/ActivityDetailImage.tsx
new file mode 100644
index 00000000..64f3f8a7
--- /dev/null
+++ b/src/app/[activityId]/_components/ActivityDetailImage.tsx
@@ -0,0 +1,109 @@
+'use client';
+
+import FallbackImage from '@/app/[activityId]/_components/FallbackImage';
+import { useActivityDescription } from '@/app/[activityId]/_hooks/useActivityDescription';
+import {
+ getImageColumnWrapperClass,
+ getSubImageClass,
+} from '@/app/[activityId]/_utils/getImageClass';
+
+type Props = {
+ activityId: number;
+};
+
+export default function ActivityDetailImage({ activityId }: Props) {
+ const { bannerImageUrl, subImages, errorMessage, isLoading } =
+ useActivityDescription(activityId);
+
+ const isSingleSubImage = subImages.length === 1;
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (errorMessage) {
+ return {errorMessage}
;
+ }
+
+ const showOnlyFallbackImage =
+ !bannerImageUrl && (!subImages || subImages.length === 0);
+
+ return (
+
+ {showOnlyFallbackImage ? (
+
+
+
+ ) : (
+ <>
+ {/* 배너 이미지 */}
+ {bannerImageUrl && (
+
+
+
+ )}
+
+ {/* 서브 이미지 */}
+ {subImages.length > 0 && (
+
+ {subImages[0] && (
+
+
+
+ )}
+ {subImages[1] && !isSingleSubImage && (
+
+
+
+ )}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/src/app/[activityId]/_components/ActivityReviews.tsx b/src/app/[activityId]/_components/ActivityReviews.tsx
index f7cd4d01..c5896a3c 100644
--- a/src/app/[activityId]/_components/ActivityReviews.tsx
+++ b/src/app/[activityId]/_components/ActivityReviews.tsx
@@ -23,7 +23,8 @@ export default function ActivityReviews({ activityId }: Props) {
} = useActivityReviews({ activityId });
const getRatingText = (rating: number) => {
- if (rating >= 0 && rating <= 1) return '매우 불만족';
+ if (rating === 0) return '평점이 아직 없습니다.';
+ if (rating > 0 && rating <= 1) return '매우 불만족';
if (rating > 1 && rating <= 2) return '불만족';
if (rating > 2 && rating <= 3) return '보통';
if (rating > 3 && rating <= 4) return '만족';
@@ -32,7 +33,7 @@ export default function ActivityReviews({ activityId }: Props) {
};
return (
-
+
체험 후기
diff --git a/src/app/[activityId]/_components/ActivitySummary.tsx b/src/app/[activityId]/_components/ActivitySummary.tsx
index 7d6d55cc..12ca727f 100644
--- a/src/app/[activityId]/_components/ActivitySummary.tsx
+++ b/src/app/[activityId]/_components/ActivitySummary.tsx
@@ -71,7 +71,7 @@ export default function ActivitySummary({ activityId }: Props) {
const { averageRating, totalCount } = review;
return (
-
+
{isMyActivity && (
- router.push(`/mypage/add-activity?id={activityId}`),
+ router.push(`/mypage/add-activity?id=${activityId}`),
},
{
text: '삭제하기',
@@ -121,6 +121,7 @@ export default function ActivitySummary({ activityId }: Props) {
{isModalOpen && (
setIsModalOpen(false)}
diff --git a/src/app/[activityId]/_components/FallbackImage.tsx b/src/app/[activityId]/_components/FallbackImage.tsx
new file mode 100644
index 00000000..7c28358b
--- /dev/null
+++ b/src/app/[activityId]/_components/FallbackImage.tsx
@@ -0,0 +1,30 @@
+import { SyntheticEvent } from 'react';
+
+type FallbackImageProps = {
+ src?: string;
+ alt: string;
+ className?: string;
+ fallbackSrc?: string; // 커스텀 fallback 이미지도 허용
+};
+
+export default function FallbackImage({
+ src,
+ alt,
+ className,
+ fallbackSrc = '/images/logo-lg.png',
+}: FallbackImageProps) {
+ const handleError = (e: SyntheticEvent) => {
+ const target = e.currentTarget;
+ target.src = fallbackSrc;
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/[activityId]/_components/MobileReserveModal.tsx b/src/app/[activityId]/_components/MobileReserveModal.tsx
index 9264c96e..3eea77ec 100644
--- a/src/app/[activityId]/_components/MobileReserveModal.tsx
+++ b/src/app/[activityId]/_components/MobileReserveModal.tsx
@@ -5,16 +5,16 @@ import { useEffect, useRef, useState } from 'react';
import activitiesApi from '@/api/activitiesApi';
import { useActivityDetail } from '@/app/[activityId]/_hooks/queries/useActivityDetail';
import { useAvailableSchedule } from '@/app/[activityId]/_hooks/queries/useAvailableSchedule';
+import {
+ getMonthNameEnglish,
+ isSameMonth,
+} from '@/app/[activityId]/_utils/activityDetailDates';
import Button from '@/components/Button';
import Icon from '@/components/Icon';
import { useClickOutside } from '@/hooks/useClickOutside';
import { useUserStore } from '@/stores/userStore';
import { getCalendarDates } from '@/utils/dateUtils';
-const isSameMonth = (base: Date, target: Date) =>
- base.getFullYear() === target.getFullYear() &&
- base.getMonth() === target.getMonth();
-
interface MobileReserveModalProps {
activityId: number;
onClose: () => void;
@@ -29,6 +29,7 @@ export default function MobileReserveModal({
const modalRef = useRef(null);
useClickOutside(modalRef, onClose);
+ const [step, setStep] = useState<1 | 2 | 3>(1);
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(null);
const [selectedTimeId, setSelectedTimeId] = useState(null);
@@ -56,6 +57,18 @@ export default function MobileReserveModal({
? (schedules.find((s) => s.date === selectedDate)?.times ?? [])
: [];
+ const handleGoStep2 = () => {
+ setTimeout(() => {
+ setStep(2);
+ }, 10);
+ };
+
+ const handleGoStep3 = () => {
+ setTimeout(() => {
+ setStep(3);
+ });
+ };
+
const handleReserve = async () => {
if (!selectedTimeId) return;
try {
@@ -68,6 +81,7 @@ export default function MobileReserveModal({
setSelectedDate(null);
setSelectedTimeId(null);
setPeopleCount(1);
+ setStep(1);
} catch (error) {
alert('예약에 실패했습니다.');
console.error(error);
@@ -76,11 +90,6 @@ export default function MobileReserveModal({
}
};
- const getMonthNameEnglish = (date: Date): string =>
- new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long' }).format(
- date,
- );
-
useEffect(() => {
const originalStyle = window.getComputedStyle(document.body).overflow;
document.body.style.overflow = 'hidden';
@@ -95,151 +104,197 @@ export default function MobileReserveModal({
- {/* 날짜 선택 */}
-
날짜
-
-
{getMonthNameEnglish(currentDate)}
-
-
-
-
-
-
- {/* 캘린더 */}
-
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, index) => (
-
- {day}
-
- ))}
- {dates.map((date) => {
- const isCurrent = isSameMonth(currentDate, date);
- const formatted = date.toISOString().split('T')[0];
- const isAvailable = schedules.some((s) => s.date === formatted);
- return (
-
- isCurrent && isAvailable && setSelectedDate(formatted)
- }
- >
- {date.getDate()}
-
- );
- })}
-
-
- {/* 시간 선택 */}
-
-
예약 가능한 시간
- {selectedDate ? (
-
- {timeOptions.map((time) => (
-
-
- {/* 인원 선택 */}
-
-
참여 인원 수
-
- setPeopleCount((p) => Math.max(1, p - 1))}
- >
-
-
- {peopleCount}
- setPeopleCount((p) => Math.min(10, p + 1))}
- >
-
-
-
-
-
- {/* 가격 및 시간 요약 */}
- {price !== null && (
-
-
-
- ₩ {totalPrice?.toLocaleString()}{' '}
-
- / {peopleCount}명
-
-
- {selectedDate && selectedTimeId && (
-
- {selectedDate.replaceAll('-', '/')}{' '}
- {timeOptions.find((t) => t.id === selectedTimeId)?.startTime}{' '}
- ~ {timeOptions.find((t) => t.id === selectedTimeId)?.endTime}
-
+
+ {/* 시간 선택 */}
+
+
예약 가능한 시간
+ {selectedDate ? (
+
+ {timeOptions.map((time) => (
+
+ setSelectedTimeId((prev) =>
+ prev === time.id ? null : time.id,
+ )
+ }
+ >
+ {time.startTime} - {time.endTime}
+
+ ))}
+
+ ) : (
+
+ 날짜를 선택해주세요.
+
)}
-
+
+
+ 다음
+
+ >
+ )}
+
+ {step === 2 && (
+ <>
+ {/* 인원 선택 */}
+
+
setStep(1)}>
+
+
+
인원
+
+
+ 예약할 인원을 선택해주세요.
+
+
+
참여 인원 수
+
+ setPeopleCount((p) => Math.max(1, p - 1))}
+ >
+
+
+ {peopleCount}
+ setPeopleCount((p) => Math.min(10, p + 1))}
+ >
+
+
+
+
+
+
+ 다음
+
+ >
)}
-
- {isLoading ? '예약 중...' : '예약하기'}
-
+ {step === 3 && (
+ <>
+ {/* 가격 및 시간 요약 */}
+ {price !== null && (
+
+
+
+ ₩ {totalPrice?.toLocaleString()}{' '}
+
+ / {peopleCount}명
+
+
+
+ {selectedDate?.replaceAll('-', '/')}{' '}
+ {
+ timeOptions.find((t) => t.id === selectedTimeId)
+ ?.startTime
+ }{' '}
+ ~{' '}
+ {timeOptions.find((t) => t.id === selectedTimeId)?.endTime}
+
+
+
+ )}
+
+
+ {isLoading ? '예약 중...' : '예약하기'}
+
+ >
+ )}
);
diff --git a/src/app/[activityId]/_components/ReserveCalender.tsx b/src/app/[activityId]/_components/ReserveCalender.tsx
index 6daed0e4..f0be0558 100644
--- a/src/app/[activityId]/_components/ReserveCalender.tsx
+++ b/src/app/[activityId]/_components/ReserveCalender.tsx
@@ -6,16 +6,16 @@ import activitiesApi from '@/api/activitiesApi';
import { ApiError } from '@/api/types/auth';
import { useActivityDetail } from '@/app/[activityId]/_hooks/queries/useActivityDetail';
import { useAvailableSchedule } from '@/app/[activityId]/_hooks/queries/useAvailableSchedule';
+import {
+ getMonthNameEnglish,
+ isSameMonth,
+} from '@/app/[activityId]/_utils/activityDetailDates';
import Button from '@/components/Button';
import Icon from '@/components/Icon';
import { useUserStore } from '@/stores/userStore';
import { getCalendarDates } from '@/utils/dateUtils';
import getErrorMessage from '@/utils/getErrorMessage';
-const isSameMonth = (base: Date, target: Date) =>
- base.getFullYear() === target.getFullYear() &&
- base.getMonth() === target.getMonth();
-
interface ReserveCalenderProps {
activityId: number;
onReserved: () => void;
@@ -93,11 +93,6 @@ export default function ReserveCalender({
const isMyActivity = user && detail && user.id === detail.userId;
if (isMyActivity) return null;
- const getMonthNameEnglish = (date: Date): string =>
- new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long' }).format(
- date,
- );
-
return (
diff --git a/src/app/[activityId]/_components/TabletReserveModal.tsx b/src/app/[activityId]/_components/TabletReserveModal.tsx
index 526a9e48..fd80b0c3 100644
--- a/src/app/[activityId]/_components/TabletReserveModal.tsx
+++ b/src/app/[activityId]/_components/TabletReserveModal.tsx
@@ -5,16 +5,16 @@ import { useEffect, useRef, useState } from 'react';
import activitiesApi from '@/api/activitiesApi';
import { useActivityDetail } from '@/app/[activityId]/_hooks/queries/useActivityDetail';
import { useAvailableSchedule } from '@/app/[activityId]/_hooks/queries/useAvailableSchedule';
+import {
+ getMonthNameEnglish,
+ isSameMonth,
+} from '@/app/[activityId]/_utils/activityDetailDates';
import Button from '@/components/Button';
import Icon from '@/components/Icon';
import { useClickOutside } from '@/hooks/useClickOutside';
import { useUserStore } from '@/stores/userStore';
import { getCalendarDates } from '@/utils/dateUtils';
-const isSameMonth = (base: Date, target: Date) =>
- base.getFullYear() === target.getFullYear() &&
- base.getMonth() === target.getMonth();
-
interface TabletReserveModalProps {
activityId: number;
onClose: () => void;
@@ -34,6 +34,7 @@ export default function TabletReserveModal({
const [selectedTimeId, setSelectedTimeId] = useState
(null);
const [peopleCount, setPeopleCount] = useState(1);
const [isLoading, setIsLoading] = useState(false);
+ const [step, setStep] = useState<1 | 2>(1);
const year = String(currentDate.getFullYear());
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
@@ -73,6 +74,12 @@ export default function TabletReserveModal({
}
};
+ const handleGoStep2 = () => {
+ setTimeout(() => {
+ setStep(2);
+ }, 10);
+ };
+
useEffect(() => {
const originalStyle = window.getComputedStyle(document.body).overflow;
document.body.style.overflow = 'hidden';
@@ -89,171 +96,175 @@ export default function TabletReserveModal({
ref={modalRef}
className='max-h-[80vh] w-full overflow-y-auto rounded-t-2xl bg-white p-20'
>
- {/* 달 상단 */}
- 날짜
-
-
- {new Intl.DateTimeFormat('en-US', {
- year: 'numeric',
- month: 'long',
- }).format(currentDate)}
-
-
-
- setCurrentDate(
- new Date(
- currentDate.getFullYear(),
- currentDate.getMonth() - 1,
- 1,
- ),
- )
- }
- >
-
-
-
- setCurrentDate(
- new Date(
- currentDate.getFullYear(),
- currentDate.getMonth() + 1,
- 1,
- ),
- )
- }
- >
-
-
-
-
-
- {/* 캘린더 + 시간/인원 */}
-
- {/* 캘린더 */}
-
-
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, index) => (
-
+ {/* 달 상단 */}
+
날짜
+
+
{getMonthNameEnglish(currentDate)}
+
+
+ setCurrentDate(
+ new Date(
+ currentDate.getFullYear(),
+ currentDate.getMonth() - 1,
+ 1,
+ ),
+ )
+ }
>
- {day}
-
- ))}
- {dates.map((date) => {
- const isCurrent = isSameMonth(currentDate, date);
- const formatted = date.toISOString().split('T')[0];
- const isAvailable = schedules.some((s) => s.date === formatted);
- return (
-
- isCurrent && isAvailable && setSelectedDate(formatted)
- }
- >
- {date.getDate()}
-
- );
- })}
+
+
+
+ setCurrentDate(
+ new Date(
+ currentDate.getFullYear(),
+ currentDate.getMonth() + 1,
+ 1,
+ ),
+ )
+ }
+ >
+
+
+
-
- {/* 시간 / 인원 수 선택 */}
-
-
예약 가능한 시간
- {selectedDate ? (
- <>
-
- {timeOptions.map((time) => (
-
setSelectedTimeId(time.id)}
- >
- {time.startTime} - {time.endTime}
-
+ {/* 캘린더 + 시간/인원 */}
+
+ {/* 캘린더 */}
+
+
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
+
+ {day}
+
))}
+ {dates.map((date) => {
+ const isCurrent = isSameMonth(currentDate, date);
+ const formatted = date.toISOString().split('T')[0];
+ const isAvailable = schedules.some(
+ (s) => s.date === formatted,
+ );
+ return (
+
+ isCurrent &&
+ isAvailable &&
+ setSelectedDate((prev) =>
+ prev === formatted ? null : formatted,
+ )
+ }
+ >
+ {date.getDate()}
+
+ );
+ })}
+
-
- 참여 인원 수
-
- setPeopleCount((p) => Math.max(1, p - 1))}
- >
-
-
- {peopleCount}
- setPeopleCount((p) => Math.min(10, p + 1))}
- >
-
-
-
-
- >
- ) : (
-
날짜를 선택해주세요.
- )}
-
-
+ {/* 시간/인원 */}
+
+
예약 가능한 시간
+ {selectedDate ? (
+ <>
+
+ {timeOptions.map((time) => (
+ setSelectedTimeId(time.id)}
+ >
+ {time.startTime} - {time.endTime}
+
+ ))}
+
- {/* 요약 및 예약 버튼 */}
- {price !== null && (
-
-
-
-
-
- ₩ {totalPrice?.toLocaleString()}{' '}
-
/ {peopleCount} 명
-
- {selectedDate && selectedTimeId && (
-
- {selectedDate.replaceAll('-', '/')}{' '}
- {
- timeOptions.find((t) => t.id === selectedTimeId)
- ?.startTime
- }{' '}
- ~{' '}
- {
- timeOptions.find((t) => t.id === selectedTimeId)
- ?.endTime
- }
-
- )}
-
-
+
+ 참여 인원 수
+
+
+ setPeopleCount((p) => Math.max(1, p - 1))
+ }
+ >
+
+
+ {peopleCount}
+
+ setPeopleCount((p) => Math.min(10, p + 1))
+ }
+ >
+
+
+
+
+ >
+ ) : (
+
날짜를 선택해주세요.
+ )}
+
-
+
+ {/* 다음 버튼 */}
+
+
+ 다음
+
+
+ >
)}
-
-
- {isLoading ? '예약 중...' : '예약하기'}
-
-
+ {step === 2 && (
+ <>
+ {/* 요약 + 예약 */}
+
+
+ ₩ {totalPrice?.toLocaleString()}{' '}
+ / {peopleCount}명
+
+
+ {selectedDate && selectedTimeId && (
+
+ {selectedDate.replaceAll('-', '/')}{' '}
+ {timeOptions.find((t) => t.id === selectedTimeId)?.startTime}{' '}
+ ~ {timeOptions.find((t) => t.id === selectedTimeId)?.endTime}
+
+ )}
+
+
+ {isLoading ? '예약 중...' : '예약하기'}
+
+ >
+ )}
);
diff --git a/src/app/[activityId]/_utils/activityDetailDates.ts b/src/app/[activityId]/_utils/activityDetailDates.ts
new file mode 100644
index 00000000..94d664b1
--- /dev/null
+++ b/src/app/[activityId]/_utils/activityDetailDates.ts
@@ -0,0 +1,10 @@
+// 달 이름 영어로 변경
+export const getMonthNameEnglish = (date: Date): string =>
+ new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long' }).format(
+ date,
+ );
+
+// 달 일치여부 확인
+export const isSameMonth = (base: Date, target: Date) =>
+ base.getFullYear() === target.getFullYear() &&
+ base.getMonth() === target.getMonth();
diff --git a/src/app/[activityId]/_utils/getImageClass.ts b/src/app/[activityId]/_utils/getImageClass.ts
index ab3e84bc..479b4491 100644
--- a/src/app/[activityId]/_utils/getImageClass.ts
+++ b/src/app/[activityId]/_utils/getImageClass.ts
@@ -1,10 +1,12 @@
export const getImageContainerClass = (isSingle: boolean) => {
- const base = 'overflow-hidden aspect-[6/3]';
- return isSingle ? `${base} w-1/2` : `${base} flex-1`;
+ const base = 'overflow-hidden';
+ return isSingle
+ ? `${base} w-1/2 aspect-[6/3]`
+ : `${base} flex-1 aspect-[6/3]`;
};
export const getImageColumnWrapperClass = (isSingle: boolean) => {
- return isSingle ? 'w-1/2' : 'flex-1';
+ return isSingle ? 'w-1/2 h-full' : 'flex-1';
};
export const getSubImageClass = ({
diff --git a/src/app/[activityId]/layout.tsx b/src/app/[activityId]/layout.tsx
index 84898d53..afa93942 100644
--- a/src/app/[activityId]/layout.tsx
+++ b/src/app/[activityId]/layout.tsx
@@ -7,7 +7,7 @@ export default function ActivityDetailLayout({
}) {
return (
-
+
{children}
diff --git a/src/app/[activityId]/page.tsx b/src/app/[activityId]/page.tsx
index 8010a90e..7ce4018b 100644
--- a/src/app/[activityId]/page.tsx
+++ b/src/app/[activityId]/page.tsx
@@ -1,10 +1,8 @@
'use client';
-import { useParams } from 'next/navigation';
-import { notFound } from 'next/navigation';
+import { notFound, useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
-import activitiesDetailApi from '@/api/activitiesApi';
import Button from '@/components/Button';
import Modal from '@/components/Modal/Modal';
import { useMediaQuery } from '@/hooks/useMediaQuery';
@@ -12,21 +10,21 @@ import { useUserStore } from '@/stores/userStore';
import getErrorMessage from '@/utils/getErrorMessage';
import ActivityDescription from './_components/ActivityDescription';
+import ActivityDetailImage from './_components/ActivityDetailImage';
import ActivityReviews from './_components/ActivityReviews';
import ActivitySummary from './_components/ActivitySummary';
import LoadKakaoMap from './_components/LoadKakaoMap';
import MobileReserveModal from './_components/MobileReserveModal';
import ReserveCalender from './_components/ReserveCalender';
import TabletReserveModal from './_components/TabletReserveModal';
+import { useActivityDetail } from './_hooks/queries/useActivityDetail';
export default function ActivityDetail() {
const { activityId } = useParams();
- const [address, setAddress] = useState('');
const [error, setError] = useState('');
- const [price, setPrice] = useState
(null);
const [isReserveModalOpen, setIsReserveModalOpen] = useState(false);
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false);
- const [creatorId, setCreatorId] = useState(null);
+ const [isAlertModal, setIsAlertModal] = useState(false);
const user = useUserStore((state) => state.user);
const myUserId = user?.id;
@@ -35,37 +33,28 @@ export default function ActivityDetail() {
const isTablet = useMediaQuery('tablet');
const isMobile = useMediaQuery('mobile');
- useEffect(() => {
- // activityId가 undefined이거나 배열인 경우 404 처리
- if (!activityId || Array.isArray(activityId)) {
- notFound();
- }
+ if (!activityId || Array.isArray(activityId)) notFound();
+ const id = Number(activityId);
+ if (isNaN(id)) notFound();
+
+ const {
+ data: activity,
+ isLoading,
+ error: queryError,
+ } = useActivityDetail(id);
- const id = Number(activityId);
- if (isNaN(id)) {
- notFound();
+ useEffect(() => {
+ if (queryError) {
+ const message = getErrorMessage(
+ queryError,
+ '체험 정보를 불러오지 못했습니다.',
+ );
+ console.error('실패:', message);
+ setError(message);
}
+ }, [queryError]);
- const fetchData = async () => {
- try {
- const activity = await activitiesDetailApi.getDetail(id);
- setAddress(activity.address);
- setPrice(activity.price);
- setCreatorId(activity.userId); // 작성자 ID 저장
- } catch (error) {
- const message = getErrorMessage(
- error,
- '체험 정보를 불러오지 못했습니다.',
- );
- console.error('실패:', message);
- setError(message);
- }
- };
-
- fetchData();
- }, [activityId]);
-
- const isMyActivity = myUserId !== undefined && myUserId === creatorId;
+ const isMyActivity = myUserId !== undefined && myUserId === activity?.userId;
if (error) {
return (
@@ -75,7 +64,7 @@ export default function ActivityDetail() {
);
}
- if (!address) {
+ if (isLoading || !activity) {
return (
@@ -88,89 +77,85 @@ export default function ActivityDetail() {
-
-
-
- {(isTablet || isMobile) && (
- <>
-
-
-
- ₩ {price?.toLocaleString()}
-
- {' '}
- / 1명
-
-
-
{
- if (isMyActivity) {
- alert('내가 생성한 체험은 예약할 수 없습니다.');
- } else {
- setIsReserveModalOpen(true);
- }
- }}
- >
- 날짜 선택하기
-
-
-
- 예약하기
-
+ {isTablet || isMobile ? (
+ <>
+
+
+
+
+
+
+
+ ₩ {activity.price.toLocaleString()}
+
+ {' '}
+ / 1명
+
+
+
{
+ if (isMyActivity) {
+ setIsAlertModal(true);
+ } else {
+ setIsReserveModalOpen(true);
+ }
+ }}
+ >
+ 날짜 선택하기
+
-
- {isReserveModalOpen && (
- <>
- {isTablet ? (
-
setIsReserveModalOpen(false)}
- onReserved={() => {
- setIsReserveModalOpen(false);
- setIsSuccessModalOpen(true);
- }}
- />
- ) : (
- setIsReserveModalOpen(false)}
- onReserved={() => {
- setIsReserveModalOpen(false);
- setIsSuccessModalOpen(true);
- }}
- />
- )}
- >
- )}
- >
- )}
-
-
-
-
-
+
+ 예약하기
+
+
+
+ {isReserveModalOpen &&
+ (isTablet ? (
+
setIsReserveModalOpen(false)}
+ onReserved={() => {
+ setIsReserveModalOpen(false);
+ setIsSuccessModalOpen(true);
+ }}
+ />
+ ) : (
+ setIsReserveModalOpen(false)}
+ onReserved={() => {
+ setIsReserveModalOpen(false);
+ setIsSuccessModalOpen(true);
+ }}
+ />
+ ))}
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
-
+ )}
- {/* 예약 완료 시 띄우는 모달 */}
{isSuccessModalOpen && (
setIsSuccessModalOpen(false)}
/>
)}
+ {isAlertModal && (
+ setIsAlertModal(false)}
+ />
+ )}
>
);
}
diff --git a/src/app/_components/MainPage.tsx b/src/app/_components/MainPage.tsx
index 359cf5eb..ee1e1869 100644
--- a/src/app/_components/MainPage.tsx
+++ b/src/app/_components/MainPage.tsx
@@ -18,7 +18,7 @@ export default function MainPage() {
const handleSearch = (query: string) => {
if (query.trim() === '') return;
const encoded = encodeURIComponent(query);
- router.push(`/?search=${encoded}`);
+ router.push(`/?search=${encoded}`, { scroll: false });
};
return (
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
new file mode 100644
index 00000000..c4c785cf
Binary files /dev/null and b/src/app/favicon.ico differ
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f4554d0d..a09b4690 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,6 @@
import './globals.css';
+import { Metadata } from 'next';
import localFont from 'next/font/local';
import QueryProvider from '@/app/provider/QueryProvider';
@@ -13,6 +14,17 @@ const pretendard = localFont({
variable: '--font-pretendard',
});
+export const metadata: Metadata = {
+ title: '글로벌 노마드',
+ description: '언제 어디서든 원하는 체험을 예약하세요',
+ metadataBase: new URL('https://global-nomad-omega.vercel.app'),
+ openGraph: {
+ title: '글로벌 노마드',
+ description: '언제 어디서든 원하는 체험을 예약하세요',
+ images: ['/images/actit-logo.png'],
+ },
+};
+
export default function RootLayout({
children,
}: {
@@ -26,6 +38,10 @@ export default function RootLayout({
{children}
+