diff --git a/src/components/reservation/PaymentModal.tsx b/src/components/reservation/PaymentModal.tsx index b7395c3..c24cbaa 100644 --- a/src/components/reservation/PaymentModal.tsx +++ b/src/components/reservation/PaymentModal.tsx @@ -4,6 +4,11 @@ import { formatKrw } from "@/utils/money"; import { Button } from "../ui/button"; import { cn } from "@/lib/utils"; import { X } from "lucide-react"; +import { useMenus } from "@/hooks/useMenus"; +import { useDepositRate } from "@/hooks/useDepositRate"; +import { calcMenuTotal } from "@/utils/menu"; +import { calcDeposit } from "@/utils/payment"; +import { useConfirmClose } from "@/hooks/useConfirmClose"; type PayMethod = "KAKAOPAY" | "TOSSPAY"; @@ -14,7 +19,6 @@ type Props = { restaurant: Restaurant; draft: ReservationDraft; onSuccess: () => void; - onBack: () => void; }; function mockPay(method: PayMethod, amount: number) { @@ -31,12 +35,20 @@ export default function PaymentModal({ restaurant, draft, onSuccess, - onBack, }: Props) { const [method, setMethod] = useState(); const [loading, setLoading] = useState(false); - const amount = useMemo(() => draft.payment.depositAmount, [draft.payment]); + const { menus } = useMenus(restaurant.id); + const { rate } = useDepositRate(restaurant.id); + + const menuTotal = useMemo(() => { + return calcMenuTotal(menus, draft.selectedMenus); + }, [menus, draft.selectedMenus]); + + const amount = useMemo(() => { + return calcDeposit(menuTotal, rate); + }, [menuTotal, rate]); const onClickPay = async () => { if (loading) return; @@ -50,18 +62,7 @@ export default function PaymentModal({ } }; - const handleBack = () => { - onOpenChange(false); - onBack(); - }; - - const handleRequestClose = () => { - const ok = window.confirm( - "예약금이 결제되지 않았습니다. \n 예약화면을 벗어나시겠습니까?", - ); - if (ok) onClose(); - }; - + const handleRequestClose = useConfirmClose(onClose); if (!open) return null; return ( @@ -83,7 +84,7 @@ export default function PaymentModal({

예약금 결제

+ + {/* 메뉴선택UI */} +
+ {activeMenus.length === 0 ? ( +
+ 아직 등록된 메뉴가 없어요 +
+ ) : ( + (["MAIN", "SIDE", "DRINK"] as MenuCategory[]).map((cat) => { + const list = grouped[cat]; + if (list.length === 0) return null; + + return ( +
+
{CategoryLabel[cat]}
+
+ {list.map((menu) => { + const qty = qtyMap.get(menu.id) ?? 0; + const img = + menu.imageUrl && menu.imageUrl.trim().length > 0 + ? menu.imageUrl + : "/modernKoreaRestaurant.jpg"; + return ( +
+
+ {/* 음식사진 */} +
+ {menu.name} + {menu.isSoldOut && ( +
+ + 품절 + +
+ )} +
+ {/* 내용 */} +
+
+
+
+

+ {menu.name} +

+ {menu.description ? ( +

+ {menu.description} +

+ ) : null} +
+ {qty > 0 ? ( + + {qty}개 + + ) : null} +
+

+ {formatKrw(menu.price)}원 +

+
+ {/* 수량 조절 */} +
+
+ + {qty} + +
+
+
+
+
+ ); + })} +
+
+ ); + }) + )} +
+ +
+
+
+
+ 메뉴 총액: {""} + + {formatKrw(totalPrice)} + +
+ +
+ 예약금 ({Math.round(rate * 100)}%): {""} + {formatKrw(depositAmount)} +
+
+
+ + +
+
+

+ 실제 결제는 다음 단계에서 진행됩니다 +

+
+ + + ); +} diff --git a/src/components/reservation/ReservationModal.tsx b/src/components/reservation/ReservationModal.tsx index 70dd20c..8b506ed 100644 --- a/src/components/reservation/ReservationModal.tsx +++ b/src/components/reservation/ReservationModal.tsx @@ -1,6 +1,5 @@ import { SEATS, - type PaymentPolicy, type ReservationDraft, type Restaurant, type SeatLayout, @@ -14,10 +13,11 @@ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Calendar } from "../ui/calendar"; import { Button } from "../ui/button"; import { startOfTodayInKst, toYmd } from "@/utils/date"; -import { formatKrw } from "@/utils/money"; import { getMockLayoutByRestaurantId } from "@/mock/seatLayout"; import { getMockAvailableTableIds } from "@/mock/tableAvailability"; import TableMap from "./TableMap"; +import { useConfirmClose } from "@/hooks/useConfirmClose"; +import { useDepositRate } from "@/hooks/useDepositRate"; type Props = { open: boolean; @@ -25,7 +25,7 @@ type Props = { restaurant: Restaurant; initialDraft?: ReservationDraft; onClickConfirm: (draft: ReservationDraft) => void; - onBack: () => void; + onClose: () => void; }; const PEOPLE = [1, 2, 3, 4, 5, 6, 7, 8]; @@ -54,7 +54,7 @@ export default function ReservationModal({ restaurant, initialDraft, onClickConfirm, - onBack, + onClose, }: Props) { const [people, setPeople] = useState(2); const [date, setDate] = useState(undefined); @@ -63,7 +63,10 @@ export default function ReservationModal({ const [tablePref, setTablePref] = useState("split_ok"); const [selectedTableId, setSelectedTableId] = useState(null); - const policy = restaurant.paymentPolicy; + const { rate: depositRate } = useDepositRate(restaurant.id); + const paymentNotice = + restaurant.paymentPolicy?.notice ?? + "예약 확정을 위해 예약금 결제가 필요합니다."; useEffect(() => { if (!open) return; @@ -91,22 +94,10 @@ export default function ReservationModal({ const todayKst = startOfTodayInKst(); - const paymentDraft: PaymentPolicy = { - depositRate: policy?.depositRate ?? 0.1, - depositAmount: policy?.depositAmount ?? 0, - notice: - policy?.notice ?? - "예약 확정을 위해 예약금 결제가 필요합니다. (노쇼 방지 목적)", - }; - // 레이아웃 mock 넣음. (나중에 API로 교체예정) const layout: SeatLayout | null = useMemo(() => { - // 레스토랑 id없으면 name등 다른걸로 매핑하지말고 실제로 식당id가 있어야함. - // 레스토랑 id가 있다고 가정함. - // return getMockLayoutByRestaurantId(restaurant.id ?? "r-1"); - // }, [restaurant]); - return getMockLayoutByRestaurantId("r-1"); //UI테스트 용으로 고정시켜 놓음. - }, []); + return getMockLayoutByRestaurantId(restaurant.id ?? "1"); + }, [restaurant.id]); const dateYmd = date ? toYmd(date) : ""; const canQueryTables = !!layout && !!date && !!time; @@ -135,6 +126,8 @@ export default function ReservationModal({ return SEATS.filter((s) => seatTypeExists.has(s)); }, [layout, seatTypeExists]); + const handleRequestClose = useConfirmClose(onClose); + if (!open) return null; return ( @@ -160,9 +153,9 @@ export default function ReservationModal({ @@ -219,7 +212,7 @@ export default function ReservationModal({ mode="single" selected={date} onSelect={setDate} - disabled={(d) => d < todayKst} //오늘포함 허용함. 오늘 제외하려면 <=로 변경하기. + disabled={(d) => d <= todayKst} //오늘미포함. /> @@ -294,22 +287,23 @@ export default function ReservationModal({

)} {layout && canQueryTables && ( - <> - - +
+
+ +
{!selectedTableId && (

배치도에서 테이블을 선택해주세요

)} - +
)} @@ -372,23 +366,20 @@ export default function ReservationModal({
사전결제 - 예약금: {formatKrw(paymentDraft.depositAmount)}원 + {Math.round(depositRate * 100)}% 정책

- 예약 확정을 위해 예약금 결제가 필요합니다.{" "} - {paymentDraft.depositRate - ? `(${Math.round(paymentDraft.depositRate * 100)}% 정책 적용)` - : null} -

-

- {paymentDraft.notice} + 예약금은 메뉴 선택 후 메뉴 총액의{" "} + {Math.round(depositRate * 100)}%로 계산됩니다.

+

{paymentNotice}

{/* 예약 확인 모달 이동 */}