diff --git a/src/apis/fetchRoom.ts b/src/apis/fetchRoom.ts index 5afe5a57..c4ee6b71 100644 --- a/src/apis/fetchRoom.ts +++ b/src/apis/fetchRoom.ts @@ -1,7 +1,11 @@ import { axiosInstance } from "@apis/axiosInstance"; import { END_POINTS } from "@constants/api"; +import type { ResponseData } from "@type/responseType"; +import type { RoomData } from "@type/room"; -export const getRoom = async (roomId: string) => { - const { data } = await axiosInstance.get(END_POINTS.ROOM(roomId)); +export const getRoom = async (roomId: string): Promise => { + const { data } = await axiosInstance.get>( + END_POINTS.ROOM(roomId), + ); return data.data; }; diff --git a/src/components/carousel/Carousel.style.ts b/src/components/carousel/Carousel.style.ts index 0820b746..3595b001 100644 --- a/src/components/carousel/Carousel.style.ts +++ b/src/components/carousel/Carousel.style.ts @@ -3,11 +3,9 @@ import styled, { css } from "styled-components"; export const CarouselContainer = styled.div<{ $height: number; - $width: number; }>` position: relative; - width: ${(props) => `${props.$width}px`}; min-height: ${(props) => `${props.$height}px`}; height: ${(props) => `${props.$height}px`}; diff --git a/src/components/carousel/Carousel.tsx b/src/components/carousel/Carousel.tsx index 61ea6f3c..f7d102e5 100644 --- a/src/components/carousel/Carousel.tsx +++ b/src/components/carousel/Carousel.tsx @@ -4,7 +4,6 @@ import * as S from "./Carousel.style.ts"; interface CarouselProps { images: string[]; - width?: number; height?: number; arrows?: boolean; infinite?: boolean; @@ -14,7 +13,6 @@ interface CarouselProps { const Carousel = ({ height = 300, - width = 300, images, arrows = true, infinite = false, @@ -41,7 +39,7 @@ const Carousel = ({ }); return ( - + ` `; const labelStyles = { - title: (theme: DefaultTheme) => ` + title: (theme: DefaultTheme) => css` color: ${theme.color.greyScale1}; } `, - caption: (theme: DefaultTheme) => ` + caption: (theme: DefaultTheme) => css` color: ${theme.color.greyScale3}; } `, @@ -140,8 +140,6 @@ export const LabelText = styled.span.withConfig({ ${({ theme }) => theme.typo.caption1} - ${({ variant, theme }) => variant && labelStyles[variant](theme)}; - margin-inline-start: 0.5rem; user-select: none; @@ -150,4 +148,6 @@ export const LabelText = styled.span.withConfig({ text-underline-offset: 2px; color: inherit; } + + ${({ variant, theme }) => variant && labelStyles[variant](theme)}; `; diff --git a/src/components/toast/Toast.style.ts b/src/components/toast/Toast.style.ts index da782af1..74c1b9a4 100644 --- a/src/components/toast/Toast.style.ts +++ b/src/components/toast/Toast.style.ts @@ -16,7 +16,7 @@ export const ToastContainer = styled(motion.div)<{ $isError: boolean }>` justify-content: center; align-items: center; - position: absolute; + position: fixed; left: 0; right: 0; top: 80px; diff --git a/src/hooks/api/mutation/useValidateEmailMutation.ts b/src/hooks/api/mutation/useValidateEmailMutation.ts index 8bebda67..5d11f5db 100644 --- a/src/hooks/api/mutation/useValidateEmailMutation.ts +++ b/src/hooks/api/mutation/useValidateEmailMutation.ts @@ -1,10 +1,13 @@ import { postValidateEmail } from "@apis/fetchLogin"; import { useMutation } from "@tanstack/react-query"; +import { isAxiosError } from "axios"; export const useValidateEmailMutation = () => { const validateEmailMutation = useMutation({ mutationFn: ({ email }: { email: string }) => postValidateEmail(email), - throwOnError: true, + throwOnError: (error) => { + return !(isAxiosError(error) && error.response); + }, }); return validateEmailMutation; diff --git a/src/hooks/api/useRoomQuery.ts b/src/hooks/api/useRoomQuery.ts new file mode 100644 index 00000000..a846728c --- /dev/null +++ b/src/hooks/api/useRoomQuery.ts @@ -0,0 +1,28 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { getRoom } from "@apis/fetchRoom"; +import { calculateDiscount } from "@utils/calculator"; + +import type { RoomData } from "@type/room"; +import type { AxiosError } from "axios"; + +interface RoomQueryData { + rawData: RoomData; + discountRate: string; +} + +export const useRoomQuery = (roomId: string) => { + return useSuspenseQuery({ + queryKey: ["room", roomId], + queryFn: () => getRoom(roomId), + select: (data) => { + const discountRate = calculateDiscount( + data.originalPrice, + data.sellingPrice, + ); + return { + rawData: data, + discountRate, + }; + }, + }); +}; diff --git a/src/hooks/common/useIsVisible.ts b/src/hooks/common/useIsVisible.ts index f0d6a7a5..582ebfb9 100644 --- a/src/hooks/common/useIsVisible.ts +++ b/src/hooks/common/useIsVisible.ts @@ -17,12 +17,13 @@ const useIsVisible = (props: UseIsVisibleProps): UseIsVisibleReturnType => { useEffect(() => { const observer = new IntersectionObserver(([entry]) => { - setIsVisible(entry.isIntersecting); + if (entry.isIntersecting !== isVisible) { + setIsVisible(entry.isIntersecting); + } }, options); if (visibleRef) { observer.observe(visibleRef); - return; } return () => { @@ -30,7 +31,7 @@ const useIsVisible = (props: UseIsVisibleProps): UseIsVisibleReturnType => { observer.unobserve(visibleRef); } }; - }, [options, visibleRef]); + }, [options, visibleRef, isVisible]); const setRefCallback: RefCallback = (node) => { setVisibleRef(node); diff --git a/src/main.tsx b/src/main.tsx index 857ae0d2..b2fe260a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,9 @@ -import React, { Suspense } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import ReactDOM from "react-dom/client"; import { RouterProvider } from "react-router-dom"; import { ThemeProvider } from "styled-components"; -import { worker } from "./mocks/broswer.ts"; +import { worker } from "./mocks/broswer"; import { router } from "./routes/router"; import { GlobalStyle } from "./styles/globalStyle"; import { theme } from "./styles/theme"; @@ -15,15 +14,11 @@ if (process.env.NODE_ENV === "development") { } ReactDOM.createRoot(document.getElementById("root")!).render( - - - - - {/* Global Loading... */}}> - - - - - - , + + + + + + + , ); diff --git a/src/mocks/data/dummyRoomDetail.json b/src/mocks/data/dummyRoomDetail.json index 23e03027..263b10e7 100644 --- a/src/mocks/data/dummyRoomDetail.json +++ b/src/mocks/data/dummyRoomDetail.json @@ -21,7 +21,8 @@ }, "hotelAddress": "서울특별시 강남구 테헤란로 99길 9", "hotelInfoUrl": "https://place-site.yanolja.com/places/3001615", - "saleStatus": false + "saleStatus": false, + "isSeller": true }, "message": "상품 조회에 성공했습니다." } diff --git a/src/pages/paymentPage/Payment.tsx b/src/pages/paymentPage/Payment.tsx index 26d81629..1d762265 100644 --- a/src/pages/paymentPage/Payment.tsx +++ b/src/pages/paymentPage/Payment.tsx @@ -4,17 +4,17 @@ import PaymentMethodSection from "@/pages/paymentPage/components/paymentMethodSe import TermsAgreementSection from "@/pages/paymentPage/components/termsAgreementSection/TermsAgreementSection"; import UserInfoSection from "@/pages/paymentPage/components/userInfoSection/UserInfoSection"; import { usePaymentQuery } from "@hooks/api/query/usePaymentQuery"; -import { useSearchParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import PaymentButton from "./components/paymentButton/PaymentButton"; import { FormProvider, useForm } from "react-hook-form"; import Caption from "@components/caption/Caption"; const Payment = () => { - const [searchParams] = useSearchParams(); - const product = searchParams.get("product") ?? ""; + const { productId } = useParams(); + if (!productId) throw Error("존재하지 않는 productId 입니다."); - const { data } = usePaymentQuery(product); + const { data } = usePaymentQuery(productId); const methods = useForm({ mode: "onChange", @@ -47,7 +47,7 @@ const Payment = () => { - + diff --git a/src/pages/paymentPage/components/paymentButton/PaymentButton.tsx b/src/pages/paymentPage/components/paymentButton/PaymentButton.tsx index 6f968af7..b90473d5 100644 --- a/src/pages/paymentPage/components/paymentButton/PaymentButton.tsx +++ b/src/pages/paymentPage/components/paymentButton/PaymentButton.tsx @@ -2,14 +2,13 @@ import { useFormContext } from "react-hook-form"; import { usePaymentMutation } from "@hooks/api/mutation/usePaymentMutation"; import * as S from "./PaymentButton.style"; -import type { PaymentData } from "@type/payment"; interface PaymentButtonProps { productId: string; - payment: Pick; + price: number; } -const PaymentButton = ({ productId, payment }: PaymentButtonProps) => { +const PaymentButton = ({ productId, price }: PaymentButtonProps) => { const { handleSubmit, getValues, @@ -55,7 +54,7 @@ const PaymentButton = ({ productId, payment }: PaymentButtonProps) => { data-disabled={isValid ? null : ""} aria-label="결제하기" > - {payment.salePrice.toLocaleString("ko-KR")}원 결제하기 + {price.toLocaleString("ko-KR")}원 결제하기 ); }; diff --git a/src/pages/paymentSuccessPage/PaymentSuccess.tsx b/src/pages/paymentSuccessPage/PaymentSuccess.tsx index 93a52337..9f0667a0 100644 --- a/src/pages/paymentSuccessPage/PaymentSuccess.tsx +++ b/src/pages/paymentSuccessPage/PaymentSuccess.tsx @@ -1,4 +1,4 @@ -import { useSearchParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { usePurchaseDetailQuery } from "@hooks/api/query/usePurchaseQuery"; import PaymentSuccessInfo from "@pages/paymentSuccessPage/components/PaymentSuccessInfo/PaymentSuccessInfo"; @@ -9,10 +9,10 @@ import CardItem from "@components/cardItem/CardItem"; import * as S from "./PaymentSuccess.style"; const PaymentSuccess = () => { - const [searchParams] = useSearchParams(); - const product = searchParams.get("product") ?? ""; + const { productId } = useParams(); + if (!productId) throw Error("존재하지 않는 productId 입니다."); - const { data } = usePurchaseDetailQuery(product); + const { data } = usePurchaseDetailQuery(productId); return ( @@ -63,7 +63,7 @@ const PaymentSuccess = () => { - + ); diff --git a/src/pages/roomDetailPage/RoomDetail.tsx b/src/pages/roomDetailPage/RoomDetail.tsx index ffdc7931..de7b00f0 100644 --- a/src/pages/roomDetailPage/RoomDetail.tsx +++ b/src/pages/roomDetailPage/RoomDetail.tsx @@ -1,38 +1,44 @@ -import { getRoom } from "@apis/fetchRoom"; - import Carousel from "@components/carousel/Carousel"; import RoomHeader from "@pages/roomDetailPage/components/roomHeader/RoomHeader"; import RoomInfo from "@pages/roomDetailPage/components/roomInfo/RoomInfo"; import RoomNavBar from "@pages/roomDetailPage/components/roomNavBar/RoomNavBar"; -import { useSuspenseQuery } from "@tanstack/react-query"; -import type { RoomData } from "@type/room"; -import type { AxiosError } from "axios"; + +import useToastConfig from "@hooks/common/useToastConfig"; +import { useRoomQuery } from "@hooks/api/useRoomQuery"; import { useParams } from "react-router-dom"; import * as S from "./RoomDetail.style"; +import { useEffect } from "react"; const RoomDetail = () => { const { roomId } = useParams(); if (!roomId) throw new Error("존재하지 않는 roomId 입니다."); - const { data } = useSuspenseQuery({ - queryKey: ["room"], - queryFn: () => getRoom(roomId), - }); + const { data } = useRoomQuery(roomId); + const { rawData, discountRate } = data; + + const { handleToast } = useToastConfig(); + + useEffect(() => { + if (rawData.isSeller) { + handleToast(false, ["내가 판매 중인 상품입니다"]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rawData.isSeller]); return ( - + - - + + ); }; diff --git a/src/pages/roomDetailPage/components/roomHeader/RoomHeader.style.ts b/src/pages/roomDetailPage/components/roomHeader/RoomHeader.style.ts index e850c2bb..5471ab90 100644 --- a/src/pages/roomDetailPage/components/roomHeader/RoomHeader.style.ts +++ b/src/pages/roomDetailPage/components/roomHeader/RoomHeader.style.ts @@ -8,22 +8,25 @@ export const ScrollObserver = styled.div` `; export const HeaderContainer = styled.header<{ $visible: boolean }>` - display: flex; - align-items: center; position: fixed; top: 0; + + display: flex; + align-items: center; width: 100%; max-width: 768px; height: 56px; - z-index: 2; + background-color: ${({ $visible, theme }) => - $visible ? "unset" : theme.color.white}; + $visible ? "transparent" : theme.color.white}; border-bottom: ${({ $visible, theme }) => - $visible ? "none" : `1px solid ${theme.color.greyScale7}`}; + $visible ? "0" : `1px solid ${theme.color.greyScale7}`}; transition: border-bottom, background-color 0.5s ease-in; + + z-index: 2; `; export const Wrapper = styled.div` @@ -61,6 +64,7 @@ export const TitleWrapper = styled.div` overflow: hidden; text-overflow: ellipsis; text-align: center; + width: 100%; `; export const Title = styled.p<{ $visible: boolean }>` diff --git a/src/pages/roomDetailPage/components/roomHeader/RoomHeader.tsx b/src/pages/roomDetailPage/components/roomHeader/RoomHeader.tsx index 02413f20..7bea78ee 100644 --- a/src/pages/roomDetailPage/components/roomHeader/RoomHeader.tsx +++ b/src/pages/roomDetailPage/components/roomHeader/RoomHeader.tsx @@ -1,5 +1,7 @@ import useIsVisible from "@hooks/common/useIsVisible"; + import * as S from "./RoomHeader.style"; +import { useNavigate } from "react-router-dom"; interface RoomHeaderProps { title: string; @@ -11,15 +13,22 @@ const RoomHeader = ({ title }: RoomHeaderProps) => { rootMargin: "0px", threshold: 1.0, }, - initialVisible: false, + initialVisible: true, }); + const navigate = useNavigate(); + return ( <>
-
diff --git a/src/pages/roomDetailPage/components/roomInfo/RoomInfo.tsx b/src/pages/roomDetailPage/components/roomInfo/RoomInfo.tsx index 6d8d2763..199e38e5 100644 --- a/src/pages/roomDetailPage/components/roomInfo/RoomInfo.tsx +++ b/src/pages/roomDetailPage/components/roomInfo/RoomInfo.tsx @@ -1,19 +1,18 @@ import RoomThemeOption from "@pages/roomDetailPage/components/roomThemeOption/RoomThemeOption"; -import * as S from "@pages/roomDetailPage/RoomDetail.style"; import type { RoomData } from "@type/room"; -import { calculateDiscount } from "@utils/calculator"; import { formatDate } from "@utils/dateFormatter"; import IconBed from "@assets/icons/ic_bed.svg?react"; import IconCaretRight from "@assets/icons/ic_caret_right.svg?react"; import IconUser from "@assets/icons/ic_users.svg?react"; +import * as S from "@pages/roomDetailPage/RoomDetail.style"; + interface RoomInfoProps { room: RoomData; + discount: string; } -const RoomInfo = ({ room }: RoomInfoProps) => { - const discountRate = calculateDiscount(room.originalPrice, room.sellingPrice); - +const RoomInfo = ({ room, discount }: RoomInfoProps) => { const checkInDate = formatDate(room.checkIn); const checkOutDate = formatDate(room.checkOut); return ( @@ -35,7 +34,7 @@ const RoomInfo = ({ room }: RoomInfoProps) => { 판매가 - {discountRate}% + {discount}% {room.sellingPrice.toLocaleString()}원 @@ -88,6 +87,7 @@ const RoomInfo = ({ room }: RoomInfoProps) => { diff --git a/src/pages/roomDetailPage/components/roomNavBar/RoomNavBar.style.ts b/src/pages/roomDetailPage/components/roomNavBar/RoomNavBar.style.ts index 6d56ed74..b895cc6b 100644 --- a/src/pages/roomDetailPage/components/roomNavBar/RoomNavBar.style.ts +++ b/src/pages/roomDetailPage/components/roomNavBar/RoomNavBar.style.ts @@ -33,7 +33,6 @@ export const Row2 = styled(Flex)` gap: 0.5rem; `; -// FIXME: Button 컴포넌트 만들기 export const Button = styled.button<{ $status: boolean }>` ${({ theme }) => theme.typo.button2} padding: 0.7rem 3rem; @@ -41,7 +40,7 @@ export const Button = styled.button<{ $status: boolean }>` border-radius: 8px; background-color: ${({ $status, theme }) => $status ? theme.color.percentOrange : theme.color.greyScale5}; - transition: background-color 0.5s ease-in; + transition: background-color 0.2s ease-in; &:hover { background-color: ${({ $status, theme }) => diff --git a/src/pages/roomDetailPage/components/roomNavBar/RoomNavBar.tsx b/src/pages/roomDetailPage/components/roomNavBar/RoomNavBar.tsx index 831d2dc5..59ddd833 100644 --- a/src/pages/roomDetailPage/components/roomNavBar/RoomNavBar.tsx +++ b/src/pages/roomDetailPage/components/roomNavBar/RoomNavBar.tsx @@ -1,14 +1,30 @@ -import { RoomNavBarData } from "@type/room"; -import { calculateDiscount } from "@utils/calculator"; +import { useNavigate } from "react-router-dom"; +import type { RoomNavBarData } from "@type/room"; +import { PATH } from "@constants/path"; +import useToastConfig from "@hooks/common/useToastConfig"; + import * as S from "./RoomNavBar.style"; interface RoomNavBarProps { room: RoomNavBarData; + roomId: string; + discount: string; } -const RoomNavBar = ({ room }: RoomNavBarProps) => { - // FIXME: 패칭 후 가공단계에서 할인율 계산 - const discountRate = calculateDiscount(room.originalPrice, room.sellingPrice); +const RoomNavBar = ({ room, roomId, discount }: RoomNavBarProps) => { + const navigate = useNavigate(); + const { handleToast } = useToastConfig(); + + const handlePurchaseClick = () => { + if (room.isSeller) { + handleToast(true, [<>내가 판매하는 상품은 구매가 불가합니다]); + return; + } else if (!room.saleStatus) { + return; + } + + navigate(`${PATH.PAYMENT}/${roomId}`); + }; return ( @@ -18,14 +34,19 @@ const RoomNavBar = ({ room }: RoomNavBarProps) => { - {discountRate}% + {discount}% {room.sellingPrice.toLocaleString()}원 - + 구매하기
diff --git a/src/routes/router.tsx b/src/routes/router.tsx index a8c3d313..15ef26f3 100644 --- a/src/routes/router.tsx +++ b/src/routes/router.tsx @@ -186,11 +186,11 @@ export const router = createBrowserRouter([ ), children: [ { - index: true, + path: ":productId", element: , }, { - path: "success", + path: "success/:productId", element: , }, ], diff --git a/src/types/room.ts b/src/types/room.ts index 5616a5bb..947a7722 100644 --- a/src/types/room.ts +++ b/src/types/room.ts @@ -1,7 +1,7 @@ export type RoomData = { hotelName: string; roomName: string; - hotelImageUrl: string[]; + hotelImageUrlList: string[]; checkIn: string; checkOut: string; originalPrice: number; @@ -13,6 +13,7 @@ export type RoomData = { hotelAddress: string; hotelInfoUrl: string; saleStatus: boolean; + isSeller: boolean; }; type RoomTheme = { parkingZone: boolean; @@ -23,5 +24,5 @@ type RoomTheme = { export type RoomNavBarData = Pick< RoomData, - "originalPrice" | "sellingPrice" | "saleStatus" + "originalPrice" | "sellingPrice" | "saleStatus" | "isSeller" >;