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] && ( -
- 서브 이미지 1 -
- )} - {subImages[1] && !isSingleSubImage && ( -
- 서브 이미지 2 -
- )} -
-
- +
체험 설명

{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 ( + {alt} + ); +} 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) => ( - + + + +
+
+ + {/* 캘린더 */} +
+ {['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); + const isSelected = selectedDate === formatted; + + return ( +
+ isCurrent && + isAvailable && + setSelectedDate((prev) => + prev === formatted ? null : formatted, + ) + } + > + {date.getDate()} +
+ ); + })}
- ) : ( -

- 날짜를 선택해주세요. -

- )} -
- - {/* 인원 선택 */} -
-

참여 인원 수

-
- - {peopleCount} - -
-
- - {/* 가격 및 시간 요약 */} - {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) => ( + + ))} +
+ ) : ( +

+ 날짜를 선택해주세요. +

)}
-
+ + + + )} + + {step === 2 && ( + <> + {/* 인원 선택 */} +
+ +

인원

+
+

+ 예약할 인원을 선택해주세요. +

+
+

참여 인원 수

+
+ + {peopleCount} + +
+
+ + + )} - + {step === 3 && ( + <> + {/* 가격 및 시간 요약 */} + {price !== null && ( +
+
+ + ₩ {totalPrice?.toLocaleString()}{' '} + + / {peopleCount}명 + + + + {selectedDate?.replaceAll('-', '/')}{' '} + { + timeOptions.find((t) => t.id === selectedTimeId) + ?.startTime + }{' '} + ~{' '} + {timeOptions.find((t) => t.id === selectedTimeId)?.endTime} + +
+
+ )} + + + + )}
); 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)} -

-
- - -
-
- - {/* 캘린더 + 시간/인원 */} -
- {/* 캘린더 */} -
-
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, index) => ( -
+ {/* 달 상단 */} +
날짜
+
+

{getMonthNameEnglish(currentDate)}

+
+
- ))} - {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) => ( - + {/* 캘린더 + 시간/인원 */} +
+ {/* 캘린더 */} +
+
+ {['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()} +
+ ); + })}
+
-
- 참여 인원 수 - - - {peopleCount} - - -
- - ) : ( -

날짜를 선택해주세요.

- )} -
-
+ {/* 시간/인원 */} +
+
예약 가능한 시간
+ {selectedDate ? ( + <> +
+ {timeOptions.map((time) => ( + + ))} +
- {/* 요약 및 예약 버튼 */} - {price !== null && ( -
-
- -
-
- ₩ {totalPrice?.toLocaleString()}{' '} -

/ {peopleCount} 명

-
- {selectedDate && selectedTimeId && ( - - {selectedDate.replaceAll('-', '/')}{' '} - { - timeOptions.find((t) => t.id === selectedTimeId) - ?.startTime - }{' '} - ~{' '} - { - timeOptions.find((t) => t.id === selectedTimeId) - ?.endTime - } - - )} -
-
+
+ 참여 인원 수 + + + {peopleCount} + + +
+ + ) : ( +

날짜를 선택해주세요.

+ )} +
-
+ + {/* 다음 버튼 */} +
+ +
+ )} -
- -
+ {step === 2 && ( + <> + {/* 요약 + 예약 */} +
+ + ₩ {totalPrice?.toLocaleString()}{' '} + / {peopleCount}명 + + + {selectedDate && selectedTimeId && ( +

+ {selectedDate.replaceAll('-', '/')}{' '} + {timeOptions.find((t) => t.id === selectedTimeId)?.startTime}{' '} + ~ {timeOptions.find((t) => t.id === selectedTimeId)?.endTime} +

+ )} +
+ + + )}
); 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명 - -

- -
- + {isTablet || isMobile ? ( + <> + + + + +
+
+

+ ₩ {activity.price.toLocaleString()} + + {' '} + / 1명 + +

+
- - {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}