diff --git a/apps/what-today/src/components/.gitkeep b/apps/what-today/src/components/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/what-today/src/components/activities/reservation/DesktopReservation.tsx b/apps/what-today/src/components/activities/reservation/DesktopReservation.tsx deleted file mode 100644 index 3c5b2f81..00000000 --- a/apps/what-today/src/components/activities/reservation/DesktopReservation.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useToast } from '@what-today/design-system'; - -import { useCreateReservation } from '@/hooks/activityDetail'; - -import Divider from '../Divider'; -import ReservationForm from './ReservationForm'; -import type { DesktopReservationProps, ReservationRequest } from './types'; - -export default function DesktopReservation({ - activityId, - price, - schedules, - isAuthor = false, - isLoggedIn = true, -}: DesktopReservationProps) { - const { toast } = useToast(); - const createReservationMutation = useCreateReservation(activityId); - - const handleSubmit = async (request: ReservationRequest) => { - createReservationMutation.mutate(request, { - onSuccess: (data) => { - toast({ - title: '예약 완료', - description: `예약 ID: ${data.id}`, - type: 'success', - }); - }, - onError: (error) => { - console.error('예약 중 오류 발생:', error); - const errorMessage = error instanceof Error ? error.message : '예약 중 오류가 발생했습니다.'; - toast({ - title: '예약 실패', - description: errorMessage, - type: 'error', - }); - }, - }); - }; - - return ( -
- - - -
- ); -} diff --git a/apps/what-today/src/components/activities/reservation/MobileReservationSheet.tsx b/apps/what-today/src/components/activities/reservation/MobileReservationSheet.tsx index 6e6e038d..7d060aed 100644 --- a/apps/what-today/src/components/activities/reservation/MobileReservationSheet.tsx +++ b/apps/what-today/src/components/activities/reservation/MobileReservationSheet.tsx @@ -10,6 +10,8 @@ import type { TabletReservationSheetProps } from './types'; type MobileStep = 'dateTime' | 'headCount'; +type MobileReservationSheetProps = TabletReservationSheetProps; + export default function MobileReservationSheet({ schedules, price, @@ -18,7 +20,7 @@ export default function MobileReservationSheet({ onConfirm, isAuthor = false, isLoggedIn = true, -}: TabletReservationSheetProps) { +}: MobileReservationSheetProps) { const [currentStep, setCurrentStep] = useState('dateTime'); const { @@ -52,16 +54,17 @@ export default function MobileReservationSheet({ const handleConfirm = () => { if (selectedScheduleId && selectedDate) { - const selectedSchedule = schedules.find((s) => s.id === selectedScheduleId); - if (selectedSchedule) { + const selectedTime = availableTimes.find((t) => t.id === selectedScheduleId); + if (selectedTime) { onConfirm({ - date: selectedSchedule.date, - startTime: selectedSchedule.startTime, - endTime: selectedSchedule.endTime, + date: selectedDate, + startTime: selectedTime.startTime, + endTime: selectedTime.endTime, headCount, scheduleId: selectedScheduleId, }); - setCurrentStep('dateTime'); // 완료 후 초기 단계로 리셋 + setCurrentStep('dateTime'); // 첫 번째 단계로 리셋 + onClose(); // 시트 닫기 } } }; @@ -70,7 +73,8 @@ export default function MobileReservationSheet({ let buttonText = ''; if (!isLoggedIn) buttonText = '로그인 필요'; else if (isAuthor) buttonText = '예약 불가'; - else buttonText = '확인'; + else if (currentStep === 'dateTime') buttonText = '다음'; + else buttonText = '다음'; return ( diff --git a/apps/what-today/src/components/activities/reservation/ReservationForm.tsx b/apps/what-today/src/components/activities/reservation/ReservationForm.tsx index 8ca1064b..8e6d9714 100644 --- a/apps/what-today/src/components/activities/reservation/ReservationForm.tsx +++ b/apps/what-today/src/components/activities/reservation/ReservationForm.tsx @@ -1,4 +1,5 @@ -import { Button } from '@what-today/design-system'; +import { useQueryClient } from '@tanstack/react-query'; +import { Button, useToast } from '@what-today/design-system'; import CalendarSelector from './CalendarSelector'; import HeadCountSelector from './HeadCountSelector'; @@ -7,17 +8,40 @@ import TimeSelector from './TimeSelector'; import type { ReservationFormProps } from './types'; export default function ReservationForm({ + activityId, schedules, price, onReservationChange, onSubmit, - showSubmitButton = false, + showSubmitButton = true, isSubmitting: externalIsSubmitting, isAuthor = false, isLoggedIn = true, }: ReservationFormProps) { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const reservation = useReservation(schedules, price, { onReservationChange, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['reservations'], + }); + + toast({ + title: '예약 완료', + description: '마이페이지에서 예약을 확인해보세요!', + type: 'success', + }); + }, + onError: (error) => { + const errorMessage = error instanceof Error ? error.message : '예약 중 오류가 발생했습니다.'; + toast({ + title: '예약 실패', + description: errorMessage, + type: 'error', + }); + }, }); const { @@ -33,18 +57,25 @@ export default function ReservationForm({ totalPrice, isReadyToReserve, isSubmitting: internalIsSubmitting, + submitReservation, } = reservation; // 외부에서 전달받은 isSubmitting을 우선시 const isSubmitting = externalIsSubmitting ?? internalIsSubmitting; const handleSubmit = async () => { - if (!onSubmit || !selectedScheduleId) return; + if (!selectedScheduleId) return; - await onSubmit({ - scheduleId: selectedScheduleId, - headCount, - }); + // onSubmit이 있으면 기존 방식 사용 (하위 호환성) + if (onSubmit) { + await onSubmit({ + scheduleId: selectedScheduleId, + headCount, + }); + } else { + // 기본 방식: submitReservation 사용 (자동 초기화) + await submitReservation(activityId); + } }; // 버튼 텍스트 결정 @@ -54,7 +85,7 @@ export default function ReservationForm({ else if (isAuthor) buttonText = '예약 불가'; else buttonText = '예약하기'; - return ( + const content = (
{/* 가격 표시 */}

@@ -98,4 +129,10 @@ export default function ReservationForm({

); + + return ( +
+ {content} +
+ ); } diff --git a/apps/what-today/src/components/activities/reservation/TabletReservationSheet.tsx b/apps/what-today/src/components/activities/reservation/TabletReservationSheet.tsx index 105e6407..5047baca 100644 --- a/apps/what-today/src/components/activities/reservation/TabletReservationSheet.tsx +++ b/apps/what-today/src/components/activities/reservation/TabletReservationSheet.tsx @@ -33,7 +33,7 @@ export default function TabletReservationSheet({ let buttonText = ''; if (!isLoggedIn) buttonText = '로그인 필요'; else if (isAuthor) buttonText = '예약 불가'; - else buttonText = '확인'; + else buttonText = '다음'; return ( @@ -80,15 +80,16 @@ export default function TabletReservationSheet({ variant='fill' onClick={() => { if (selectedScheduleId && selectedDate) { - const selectedSchedule = schedules.find((s) => s.id === selectedScheduleId); - if (selectedSchedule) { + const selectedTime = availableTimes.find((t) => t.id === selectedScheduleId); + if (selectedTime) { onConfirm({ - date: selectedSchedule.date, - startTime: selectedSchedule.startTime, - endTime: selectedSchedule.endTime, + date: selectedDate, + startTime: selectedTime.startTime, + endTime: selectedTime.endTime, headCount, scheduleId: selectedScheduleId, }); + onClose(); // 시트 닫기 } } }} diff --git a/apps/what-today/src/components/activities/reservation/types/index.ts b/apps/what-today/src/components/activities/reservation/types/index.ts index e8a070b1..d2465ebc 100644 --- a/apps/what-today/src/components/activities/reservation/types/index.ts +++ b/apps/what-today/src/components/activities/reservation/types/index.ts @@ -50,6 +50,7 @@ export interface UseReservationReturn { } export interface ReservationFormProps { + activityId: number; schedules: Schedule[]; price: number; onReservationChange?: (summary: ReservationSummary | null) => void; @@ -60,14 +61,6 @@ export interface ReservationFormProps { isLoggedIn?: boolean; } -export interface DesktopReservationProps { - activityId: number; - price: number; - schedules: Schedule[]; - isAuthor?: boolean; - isLoggedIn?: boolean; -} - export interface TabletReservationSheetProps { schedules: Schedule[]; price: number; @@ -82,7 +75,7 @@ export interface ReservationBottomBarProps { price: number; reservation: Pick | null; onSelectDate: () => void; - onReserve: () => void; + onReserve?: () => void; isSubmitting?: boolean; isAuthor?: boolean; isLoggedIn?: boolean; diff --git a/apps/what-today/src/layouts/DefaultLayout.tsx b/apps/what-today/src/layouts/DefaultLayout.tsx index 5905495b..7fde4502 100644 --- a/apps/what-today/src/layouts/DefaultLayout.tsx +++ b/apps/what-today/src/layouts/DefaultLayout.tsx @@ -13,7 +13,7 @@ export default function DefaultLayout() { const footerMarginBottom = isActivityDetailPage && !isDesktop ? 'w-full mb-125' : 'w-full'; // FloatingTranslateButton의 bottom 위치 조건부 설정 - const floatingButtonClass = !isDesktop && isActivityDetailPage ? 'bottom-140' : undefined; + const floatingButtonClass = !isDesktop && isActivityDetailPage ? 'bottom-160' : undefined; return (
diff --git a/apps/what-today/src/pages/activities/index.tsx b/apps/what-today/src/pages/activities/index.tsx index 8552168f..31144763 100644 --- a/apps/what-today/src/pages/activities/index.tsx +++ b/apps/what-today/src/pages/activities/index.tsx @@ -1,79 +1,95 @@ -import { useToast } from '@what-today/design-system'; +import { useQueryClient } from '@tanstack/react-query'; +import { SpinIcon, useToast } from '@what-today/design-system'; import { useState } from 'react'; import { useParams } from 'react-router-dom'; +import { createReservation } from '@/apis/activityDetail'; import ActivitiesDescription from '@/components/activities/ActivitiesDescription'; import ActivitiesInformation from '@/components/activities/ActivitiesInformation'; import ActivitiesMap from '@/components/activities/ActivitiesMap'; import ActivityImages from '@/components/activities/ActivityImages'; import Divider from '@/components/activities/Divider'; -import DesktopReservation from '@/components/activities/reservation/DesktopReservation'; import MobileReservationSheet from '@/components/activities/reservation/MobileReservationSheet'; +import ReservationForm from '@/components/activities/reservation/ReservationForm'; import TabletReservationSheet from '@/components/activities/reservation/TabletReservationSheet'; import type { ReservationSummary } from '@/components/activities/reservation/types'; import ReservationBottomBar from '@/components/activities/ReservationBottomBar'; import ReviewSection from '@/components/activities/ReviewSection'; -import { useActivityDetail, useCreateReservation } from '@/hooks/activityDetail'; +import { useActivityDetail } from '@/hooks/activityDetail'; import { useResponsive } from '@/hooks/useResponsive'; +import NotFoundPage from '@/pages/not-found-page'; import { useWhatTodayStore } from '@/stores'; export default function ActivityDetailPage() { + const queryClient = useQueryClient(); const { id } = useParams<{ id: string }>(); const { toast } = useToast(); const { user } = useWhatTodayStore(); const [isTabletSheetOpen, setIsTabletSheetOpen] = useState(false); const [isMobileSheetOpen, setIsMobileSheetOpen] = useState(false); - const [reservationSummary, setReservationSummary] = useState(null); + const [reservation, setReservation] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [sheetKey, setSheetKey] = useState(0); const { isMobile, isTablet, isDesktop } = useResponsive(); const { data: activity, isLoading: loading, error } = useActivityDetail(id); - const createReservationMutation = useCreateReservation(Number(id)); + if (loading) + return ( +
+ +
+ ); + if (error) return ; + if (!activity) return ; - if (loading) return

로딩 중...

; - if (error) - return

오류: {error instanceof Error ? error.message : '활동 정보를 불러오는 중 오류가 발생했습니다.'}

; - if (!activity) return

데이터 없음

; + const handleReservationSuccess = () => { + queryClient.invalidateQueries({ + queryKey: ['reservations'], + }); + toast({ + title: '예약 완료', + description: '마이페이지에서 예약을 확인해보세요!', + type: 'success', + }); - const handleConfirmTabletReservation = (reservation: ReservationSummary) => { - setReservationSummary(reservation); + setReservation(null); setIsTabletSheetOpen(false); + setIsMobileSheetOpen(false); + setSheetKey((prev) => prev + 1); }; - const handleConfirmMobileReservation = (reservation: ReservationSummary) => { - setReservationSummary(reservation); - setIsMobileSheetOpen(false); + const handleReservationError = (error: Error) => { + const errorMessage = error instanceof Error ? error.message : '예약 중 오류가 발생했습니다.'; + toast({ + title: '예약 실패', + description: errorMessage, + type: 'error', + }); }; - const handleSubmitReservation = async () => { - if (!reservationSummary) return; - - createReservationMutation.mutate( - { - scheduleId: reservationSummary.scheduleId, - headCount: reservationSummary.headCount, - }, - { - onSuccess: (data) => { - toast({ - title: '예약 완료', - description: `예약 ID: ${data.id}`, - type: 'success', - }); - setReservationSummary(null); // 예약 완료 후 상태 초기화 - }, - onError: (error) => { - const errorMessage = error instanceof Error ? error.message : '예약 중 오류가 발생했습니다.'; - toast({ - title: '예약 실패', - description: errorMessage, - type: 'error', - }); - }, - }, - ); + const handleReservationConfirm = (reservationSummary: ReservationSummary) => { + setReservation(reservationSummary); + }; + + const handleReservationSubmit = async () => { + if (!reservation || !activity) return; + + setIsSubmitting(true); + try { + await createReservation(activity.id, { + scheduleId: reservation.scheduleId, + headCount: reservation.headCount, + }); + handleReservationSuccess(); + setReservation(null); // 예약 상태 초기화 + } catch (error) { + handleReservationError(error as Error); + } finally { + setIsSubmitting(false); + } }; return ( @@ -99,7 +115,7 @@ export default function ActivityDetailPage() { reviewCount={activity.reviewCount} title={activity.title} /> - { if (isMobile) setIsMobileSheetOpen(true); else if (isTablet) setIsTabletSheetOpen(true); @@ -158,26 +165,28 @@ export default function ActivityDetailPage() { {/* 태블릿 바텀시트 */} {isTablet && ( setIsTabletSheetOpen(false)} - onConfirm={handleConfirmTabletReservation} + onConfirm={handleReservationConfirm} /> )} {/* 모바일 바텀시트 */} {isMobile && ( setIsMobileSheetOpen(false)} - onConfirm={handleConfirmMobileReservation} + onConfirm={handleReservationConfirm} /> )}