diff --git a/index.html b/index.html index e3a9bcfc..f923128b 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,10 @@ href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" /> 퍼센트 호텔 + + + ); + }, +); diff --git a/src/components/carousel/Carousel.style.ts b/src/components/carousel/Carousel.style.ts index 3595b001..d4fe0472 100644 --- a/src/components/carousel/Carousel.style.ts +++ b/src/components/carousel/Carousel.style.ts @@ -1,16 +1,10 @@ import { PiCaretLeftBold, PiCaretRightBold } from "react-icons/pi"; import styled, { css } from "styled-components"; -export const CarouselContainer = styled.div<{ - $height: number; -}>` +export const CarouselContainer = styled.div` position: relative; - min-height: ${(props) => `${props.$height}px`}; - height: ${(props) => `${props.$height}px`}; - overflow: hidden; - cursor: grab; touch-action: pan-y; `; diff --git a/src/components/carousel/Carousel.tsx b/src/components/carousel/Carousel.tsx index f06376c2..f5007000 100644 --- a/src/components/carousel/Carousel.tsx +++ b/src/components/carousel/Carousel.tsx @@ -1,9 +1,9 @@ -import { useCarousel } from "@/hooks/common/useCarousel"; -import { useCarouselSize } from "@/hooks/common/useCarouselSize"; - import * as S from "./Carousel.style.ts"; import ProgressiveImg from "../progressiveImg/ProgressiveImg.tsx"; +import { useCarousel } from "@/hooks/common/useCarousel"; +import { useCarouselSize } from "@/hooks/common/useCarouselSize"; + interface CarouselProps { images: string[]; height?: number; @@ -41,7 +41,7 @@ const Carousel = ({ }); console.log("currentIndex", currentIndex); return ( - + { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${location.pathname}}`, + }, + }; + + return ( + <> + + + + ); +}; + +export default Outlet; diff --git a/src/hooks/api/useSearchQuery.ts b/src/hooks/api/useSearchQuery.ts new file mode 100644 index 00000000..ab9ee45f --- /dev/null +++ b/src/hooks/api/useSearchQuery.ts @@ -0,0 +1,45 @@ +import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; + +import type { ISearchFilterInfo } from "@/types/searchFilterInfo"; + +import { fetchSearchList } from "@/apis/fetchSeachList"; + +export const useInfiniteSearchQuery = (searchInfo: ISearchFilterInfo) => { + const pageSize = 10; + + return useSuspenseInfiniteQuery({ + queryKey: [ + "searchItems", + searchInfo.location, + searchInfo.checkIn, + searchInfo.checkOut, + searchInfo.quantityPeople, + searchInfo.sorted, + searchInfo.brunch, + searchInfo.pool, + searchInfo.oceanView, + ], + queryFn: ({ pageParam = 0 }) => + fetchSearchList( + searchInfo.location, + searchInfo.checkIn, + searchInfo.checkOut, + searchInfo.quantityPeople, + searchInfo.sorted, + searchInfo.brunch, + searchInfo.pool, + searchInfo.oceanView, + pageParam, + pageSize, + ), + refetchOnMount: false, + refetchOnWindowFocus: false, + initialPageParam: 0, + getNextPageParam: (lastPage) => { + const lastData = lastPage?.content; + return lastData && lastData.length === pageSize + ? lastPage?.number + 1 + : undefined; + }, + }); +}; diff --git a/src/pages/alarmPage/AlarmPage.tsx b/src/pages/alarmPage/AlarmPage.tsx index b33405ec..56d61eca 100644 --- a/src/pages/alarmPage/AlarmPage.tsx +++ b/src/pages/alarmPage/AlarmPage.tsx @@ -8,8 +8,9 @@ import { fetchUserInfo } from "@/apis/fetchUserInfo"; import NoResult from "@/components/noResult/NoResult"; import { PATH } from "@/constants/path"; import { isMobileSafari } from "@/utils/isMobileSafari"; +import { HelmetTag } from "@/components/Helmet/Helmet"; -const AlarmPage = () => { +const AlarView = () => { const mobileSafari = isMobileSafari(); const { data: userData } = useSuspenseQuery({ @@ -66,4 +67,21 @@ const AlarmPage = () => { } }; +const AlarmPage = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${location.pathname}}`, + }, + }; + + return ( + <> + + + + ); +}; + export default AlarmPage; diff --git a/src/pages/connectYanoljaPage/IntroPage/IntroPage.tsx b/src/pages/connectYanoljaPage/IntroPage/IntroPage.tsx index 022d5726..e5bc4cda 100644 --- a/src/pages/connectYanoljaPage/IntroPage/IntroPage.tsx +++ b/src/pages/connectYanoljaPage/IntroPage/IntroPage.tsx @@ -1,12 +1,12 @@ -import XIcon from "@/assets/icons/ic_x.png"; -import PercentHotelLogo from "@/assets/logos/Percent-hotel_logo_b.png"; -import YanoljaLogo from "@/assets/logos/Yanolja_CI.png"; -import { PATH } from "@/constants/path"; import { useNavigate } from "react-router-dom"; import * as S from "./IntroPage.style.ts"; +import XIcon from "@/assets/icons/ic_x.png"; +import PercentHotelLogo from "@/assets/logos/Percent-hotel_logo_b.png"; +import YanoljaLogo from "@/assets/logos/Yanolja_CI.png"; import ProgressiveImg from "@/components/progressiveImg/ProgressiveImg.tsx"; +import { PATH } from "@/constants/path"; const IntroPage = () => { const navigate = useNavigate(); diff --git a/src/pages/connectYanoljaPage/successPage/SuccessPage.tsx b/src/pages/connectYanoljaPage/successPage/SuccessPage.tsx index cccf79c9..7af6789f 100644 --- a/src/pages/connectYanoljaPage/successPage/SuccessPage.tsx +++ b/src/pages/connectYanoljaPage/successPage/SuccessPage.tsx @@ -1,11 +1,11 @@ -import PercentHotelLogo from "@/assets/logos/Percent-hotel_logo_b.png"; -import { PATH } from "@/constants/path"; import { useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import * as S from "./SuccessPage.style.ts"; +import PercentHotelLogo from "@/assets/logos/Percent-hotel_logo_b.png"; import Success from "@/components/lottie/success/Success"; +import { PATH } from "@/constants/path"; const SuccessPage = () => { const navigate = useNavigate(); diff --git a/src/pages/connectYanoljaPage/verificationPage/VerificationPage.tsx b/src/pages/connectYanoljaPage/verificationPage/VerificationPage.tsx index ed00151c..362e5acd 100644 --- a/src/pages/connectYanoljaPage/verificationPage/VerificationPage.tsx +++ b/src/pages/connectYanoljaPage/verificationPage/VerificationPage.tsx @@ -1,10 +1,11 @@ +import { FormProvider, useForm } from "react-hook-form"; + +import * as S from "./VerificationPage.style"; + import YanoljaLogo from "@/assets/logos/Yanolja_CI.png"; import SubmitButton from "@/pages/connectYanoljaPage/verificationPage/components/submitButton/SubmitButton"; import TermsAgreementSection from "@/pages/connectYanoljaPage/verificationPage/components/termsAgreementSection/TermsAgreementSection"; import VerificationSection from "@/pages/connectYanoljaPage/verificationPage/components/verificationSection/VerificationSection"; -import { FormProvider, useForm } from "react-hook-form"; - -import * as S from "./VerificationPage.style"; const VerificationPage = () => { const methods = useForm({ diff --git a/src/pages/homePage/Home.tsx b/src/pages/homePage/Home.tsx index 2b5bad15..1c0e7ad1 100644 --- a/src/pages/homePage/Home.tsx +++ b/src/pages/homePage/Home.tsx @@ -12,7 +12,8 @@ import WeekendCarousel from "./weekendCarousel/WeekendCarousel"; import { fetchMainItem } from "@/apis/fetchMainItems"; import secondMonth from "@/assets/EventImages/secondMonth.png"; -import { LocaleItem, WeekendItem } from "@/types/saleSection"; +import { HelmetTag } from "@/components/Helmet/Helmet"; +import { LocaleItem, LocaleItemsType, WeekendItem } from "@/types/saleSection"; interface EventItem { id: number; image: string; @@ -20,12 +21,12 @@ interface EventItem { title2: string; } -const Home = () => { - const { data: mainData } = useSuspenseQuery({ - queryKey: ["main"], - queryFn: fetchMainItem, - }); - const [localeProds, weekendProds] = mainData; +const HomeView = ({ + data, +}: { + data: [LocaleItemsType, WeekendItem[] | null]; +}) => { + const [localeProds, weekendProds] = data; const EventCarouselContents: EventItem[] = [ { id: 1, @@ -73,11 +74,9 @@ const Home = () => { 프리미엄 호캉스 퍼센특가로 만나는 4-5성급 호텔 모음 - { )} - 지역 + 지역 - 할인 호텔 + 할인 호텔
{ ); }; +const Home = () => { + const { data } = useSuspenseQuery({ + queryKey: ["main"], + queryFn: fetchMainItem, + }); + + const [localeProds, weekendProds] = data; + + const schemaData = { + areaServed: Object.keys(localeProds), + makesOffer: [ + { + "@type": "Offer", + itemOffered: { + "@type": "TravelService", + name: "지역별 할인 호텔", + description: `${Object.keys(localeProds).length}개 지역의 할인 호텔 상품`, + }, + }, + { + "@type": "Offer", + itemOffered: { + "@type": "TravelService", + name: "프리미엄 호캉스", + description: `${weekendProds?.length || 0}개의 프리미엄 호캉스 상품`, + }, + }, + ], + }; + + return ( + <> + + + + ); +}; + export default Home; diff --git a/src/pages/homePage/itemCarousel/ItemCarousel.tsx b/src/pages/homePage/itemCarousel/ItemCarousel.tsx index e8f527ed..9144e340 100644 --- a/src/pages/homePage/itemCarousel/ItemCarousel.tsx +++ b/src/pages/homePage/itemCarousel/ItemCarousel.tsx @@ -1,11 +1,12 @@ -import { useAnimateCarousel } from "@/hooks/common/useAnimateCarousel"; -import { useCarouselSize } from "@/hooks/common/useCarouselSize"; -import { type LocaleItem } from "@/types/saleSection"; import { useEffect } from "react"; import * as S from "./ItemCarousel.style"; import ItemCarouselUnit from "./itemCarouselUnit/ItemCarouselUnit.tsx"; +import { useAnimateCarousel } from "@/hooks/common/useAnimateCarousel"; +import { useCarouselSize } from "@/hooks/common/useCarouselSize"; +import { type LocaleItem } from "@/types/saleSection"; + interface CarouselProps { currentLocale: [number, string, LocaleItem[]]; localeAndHotel: [number, string, LocaleItem[]][]; diff --git a/src/pages/homePage/weekendCarousel/WeekendCarousel.style.ts b/src/pages/homePage/weekendCarousel/WeekendCarousel.style.ts index 2532dee3..90f5dc41 100644 --- a/src/pages/homePage/weekendCarousel/WeekendCarousel.style.ts +++ b/src/pages/homePage/weekendCarousel/WeekendCarousel.style.ts @@ -1,20 +1,13 @@ import { PiCaretLeftBold, PiCaretRightBold } from "react-icons/pi"; import styled, { css } from "styled-components"; -export const CarouselContainer = styled.div<{ - $height: number; -}>` +export const CarouselContainer = styled.div` position: relative; - width: 100%; - min-height: ${(props) => `${props.$height}px`}; - height: ${(props) => `${props.$height}px`}; background-color: white; display: flex; align-items: center; gap: 8px; - - cursor: grab; touch-action: pan-y; `; diff --git a/src/pages/homePage/weekendCarousel/WeekendCarousel.tsx b/src/pages/homePage/weekendCarousel/WeekendCarousel.tsx index 3d23edb4..dd1a02ad 100644 --- a/src/pages/homePage/weekendCarousel/WeekendCarousel.tsx +++ b/src/pages/homePage/weekendCarousel/WeekendCarousel.tsx @@ -11,7 +11,6 @@ interface CarouselProps { onChangeLocale: React.Dispatch< React.SetStateAction<[number, string, LocaleItem[]]> >; - height?: number; arrows?: boolean; infinite?: boolean; draggable?: boolean; @@ -20,7 +19,6 @@ interface CarouselProps { const WeekendCarousel = ({ weekendHotels, - height = 300, arrows = true, infinite = false, draggable = false, @@ -41,7 +39,7 @@ const WeekendCarousel = ({ }); return ( - + { - const pageSize = 10; - const [selectedRegion, setSelectedRegion] = useState("전체"); - const [scrollPosition, setScrollPosition] = useState(0); +interface SearchPage { + content: ISearchList[]; + empty: boolean; + first: boolean; + last: boolean; + number: number; + numberOfElements: number; + size: number; + totalElements: number; + totalPages: number; +} + +interface MainDetailViewProps { + data: InfiniteData; + fetchNextPage: () => void; + isLoading: boolean; + hasNextPage: boolean | undefined; + selectedRegion: string; + setSelectedRegion: React.Dispatch>; +} +const MainDetailView = ({ + data, + fetchNextPage, + isLoading, + hasNextPage, + selectedRegion, + setSelectedRegion, +}: MainDetailViewProps) => { const [isTopButtonVisible, setIsTopButtonVisible] = useState(false); - const searchInfo = { - brunch: null, - checkIn: null, - checkOut: null, - location: selectedRegion === "전체" ? null : selectedRegion, - oceanView: null, - pool: null, - quantityPeople: 0, - sorted: null, - }; - const { data, fetchNextPage, isLoading, hasNextPage } = useInfiniteQuery({ - queryKey: [ - "searchItems", - searchInfo.location, - searchInfo.checkIn, - searchInfo.checkOut, - searchInfo.quantityPeople, - searchInfo.sorted, - searchInfo.brunch, - searchInfo.pool, - searchInfo.oceanView, - ], - queryFn: ({ pageParam = 0 }) => - fetchSearchList( - searchInfo.location, - searchInfo.checkIn, - searchInfo.checkOut, - searchInfo.quantityPeople, - searchInfo.sorted, - searchInfo.brunch, - searchInfo.pool, - searchInfo.oceanView, - pageParam, - pageSize, - ), - refetchOnMount: false, - refetchOnWindowFocus: false, - initialPageParam: 0, - getNextPageParam: (lastPage) => { - const lastData = lastPage?.content; - return lastData && lastData.length === pageSize - ? lastPage?.number + 1 - : undefined; - }, - }); const handleIntersect = (isIntersecting: boolean) => { if (isIntersecting && hasNextPage) { fetchNextPage(); } }; + const { ref } = useIntersectionObserver({ onChange: handleIntersect, threshold: 0.5, @@ -75,23 +57,18 @@ const MainDetail = () => { const handleScroll = () => { const currentPosition = window.scrollY; - setScrollPosition(currentPosition); setIsTopButtonVisible(currentPosition > 500); }; useEffect(() => { window.addEventListener("scroll", handleScroll); - return () => { - window.removeEventListener("scroll", handleScroll); - }; - }, [scrollPosition]); - - const MoveToTop = () => { - window.scroll({ - top: 0, - behavior: "smooth", - }); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + const moveToTop = () => { + window.scroll({ top: 0, behavior: "smooth" }); }; + return ( @@ -99,9 +76,8 @@ const MainDetail = () => { selectedRegion={selectedRegion} setSelectedRegion={setSelectedRegion} /> - - {!isLoading && data && !data?.pages?.[0]?.content?.length && ( + {!isLoading && data && !data.pages[0]?.content.length && ( 검색 조건에 맞는 호텔이 없어요 @@ -110,19 +86,17 @@ const MainDetail = () => { )} - {data && - data.pages?.length > 0 && - data.pages.map((page) => - page?.content.map((item: ISearchList) => ( - - )), - )} + {data?.pages.map((page) => + page?.content.map((item: ISearchList) => ( + + )), + )}
@@ -131,4 +105,78 @@ const MainDetail = () => { ); }; + +const MainDetail: React.FC = () => { + const [selectedRegion, setSelectedRegion] = useState("전체"); + + const searchInfo = { + brunch: null, + checkIn: null, + checkOut: null, + location: selectedRegion === "전체" ? null : selectedRegion, + oceanView: null, + pool: null, + quantityPeople: 0, + sorted: null, + }; + + const { data, fetchNextPage, isLoading, hasNextPage } = + useInfiniteSearchQuery(searchInfo); + + const schemaData = { + "@type": "ItemList", + name: "황금 연휴 호캉스 추천", + description: "성수기 숙소 예약 놓쳤다면? 황금연휴 호캉스 추천", + itemListElement: + data?.pages.flatMap((page, pageIndex) => + page?.content.map((item: ISearchList, index: number) => ({ + "@type": "ListItem", + position: pageIndex * 10 + index + 1, + item: { + "@type": "Hotel", + name: item.name, + image: item.imageUrl, + offers: { + "@type": "Offer", + price: item.salePrice, + priceCurrency: "KRW", + priceValidUntil: item.checkOut, + availability: "https://schema.org/InStock", + }, + checkinTime: item.checkIn, + checkoutTime: item.checkOut, + starRating: { + "@type": "Rating", + ratingValue: item.hotelRate, + }, + aggregateRating: { + "@type": "AggregateRating", + ratingValue: item.reviewRate, + bestRating: "5", + worstRating: "1", + }, + }, + })), + ) || [], + }; + + return ( + <> + + + + ); +}; + export default MainDetail; diff --git a/src/pages/myPage/MyPage.tsx b/src/pages/myPage/MyPage.tsx index 94fcfa20..c7d1beb7 100644 --- a/src/pages/myPage/MyPage.tsx +++ b/src/pages/myPage/MyPage.tsx @@ -3,6 +3,7 @@ import { Outlet, useNavigate } from "react-router-dom"; import MyPageNav from "./components/myPageNav/MyPageNav"; import * as S from "./MyPage.style"; +import { HelmetTag } from "@/components/Helmet/Helmet"; import { PATH } from "@/constants/path"; import { useLoadUserInfo } from "@/hooks/common/useLoadUserInfo"; import useAuthStore from "@/store/authStore"; @@ -17,25 +18,36 @@ const MyPage = () => { navigate(PATH.YANOLJA_ACCOUNT); }; + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${location.pathname}}`, + }, + }; + return ( - - - {isLoggedIn && userInfo ? ( -

{userInfo.email} 님

- ) : ( -

로그인 후 판매글을 작성해보세요

- )} - {!isLoggedIn ? ( - - ) : isConnected ? ( - 야놀자와 연동된 계정입니다 - ) : ( - - )} -
- - -
+ <> + + + + {isLoggedIn && userInfo ? ( +

{userInfo.email} 님

+ ) : ( +

로그인 후 판매글을 작성해보세요

+ )} + {!isLoggedIn ? ( + + ) : isConnected ? ( + 야놀자와 연동된 계정입니다 + ) : ( + + )} +
+ + +
+ ); }; diff --git a/src/pages/myPage/setting/Setting.tsx b/src/pages/myPage/setting/Setting.tsx index 9584eaef..b9bc9992 100644 --- a/src/pages/myPage/setting/Setting.tsx +++ b/src/pages/myPage/setting/Setting.tsx @@ -2,12 +2,25 @@ import * as S from "./Setting.style"; import Info from "../info/Info"; import Manage from "../manage/Manage"; +import { HelmetTag } from "@/components/Helmet/Helmet"; + const Setting = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${location.pathname}}`, + }, + }; + return ( - - - - + <> + + + + + + ); }; diff --git a/src/pages/passwordResetPage/PasswordReset.tsx b/src/pages/passwordResetPage/PasswordReset.tsx index 87224bc7..f928f458 100644 --- a/src/pages/passwordResetPage/PasswordReset.tsx +++ b/src/pages/passwordResetPage/PasswordReset.tsx @@ -4,7 +4,10 @@ import FieldValues from "./components/fieldValues/FieldValues"; import PasswordResetSubmitBtn from "./components/passwordResetSubmitBtn.tsx/PasswordResetSubmitBtn"; import * as S from "./PasswordReset.style"; -const PasswordReset = () => { +import { HelmetTag } from "@/components/Helmet/Helmet"; +import { PATH } from "@/constants/path"; + +const PasswordResetView = () => { const method = useForm({ mode: "onChange", shouldUnregister: false, @@ -24,4 +27,21 @@ const PasswordReset = () => { ); }; +const PasswordReset = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${PATH.PASSWORD_RESET}`, + }, + }; + + return ( + <> + + + + ); +}; + export default PasswordReset; diff --git a/src/pages/purchaseDetailPage/PurchaseDetail.tsx b/src/pages/purchaseDetailPage/PurchaseDetail.tsx index 5574c512..96cf823b 100644 --- a/src/pages/purchaseDetailPage/PurchaseDetail.tsx +++ b/src/pages/purchaseDetailPage/PurchaseDetail.tsx @@ -5,12 +5,13 @@ import { useNavigate, useSearchParams } from "react-router-dom"; import * as S from "./PurchaseDetail.style"; import { fetchPurchaseDetail } from "../../apis/fetchPurchaseDetail"; +import { HelmetTag } from "@/components/Helmet/Helmet"; import ProgressiveImg from "@/components/progressiveImg/ProgressiveImg"; import { PATH } from "@/constants/path"; import { IPurchaseData } from "@/types/purchaseDetail"; import { formatDateString } from "@/utils/dateFormatter"; -const PurchaseDetail = () => { +const PurchaseDetailView = () => { const [searchParams] = useSearchParams(); const id = searchParams.get("id"); if (!id) throw new Error("존재하지 않는 roomId 입니다."); @@ -124,4 +125,21 @@ const PurchaseDetail = () => { ); }; +const PurchaseDetail = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${location.pathname}}`, + }, + }; + + return ( + <> + + + + ); +}; + export default PurchaseDetail; diff --git a/src/pages/roomDetailPage/RoomDetail.tsx b/src/pages/roomDetailPage/RoomDetail.tsx index 10c18fb3..773f7dee 100644 --- a/src/pages/roomDetailPage/RoomDetail.tsx +++ b/src/pages/roomDetailPage/RoomDetail.tsx @@ -3,6 +3,8 @@ import { useParams } from "react-router-dom"; import * as S from "./RoomDetail.style"; +import type { RoomData } from "@/types/room"; + import Carousel from "@/components/carousel/Carousel"; import { HelmetTag } from "@/components/Helmet/Helmet"; import { useRoomQuery } from "@/hooks/api/useRoomQuery"; @@ -12,40 +14,130 @@ import RoomInfo from "@/pages/roomDetailPage/components/roomInfo/RoomInfo"; import RoomNavBar from "@/pages/roomDetailPage/components/roomNavBar/RoomNavBar"; import useAuthStore from "@/store/authStore"; -const RoomDetail = () => { - const { productId } = useParams(); - - if (!productId) throw new Error("존재하지 않는 roomId 입니다."); - - const isLoggedIn = useAuthStore((state) => state.isLoggedIn); +interface RoomDetailViewProps { + roomData: RoomData; + discountRate: string; + productId: string; +} - const { data } = useRoomQuery(productId, isLoggedIn); - const { rawData, discountRate } = data; +const RoomDetailView = ({ + roomData, + discountRate, + productId, +}: RoomDetailViewProps) => { const { handleToast } = useToastConfig(); useEffect(() => { - if (rawData.isSeller) { + if (roomData.isSeller) { handleToast(false, ["내가 판매 중인 상품입니다"]); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rawData.isSeller]); + }, [roomData.isSeller]); return ( - - + - - + + ); }; +const RoomDetail = () => { + const { productId } = useParams(); + + if (!productId) throw new Error("존재하지 않는 roomId 입니다."); + + const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const { data } = useRoomQuery(productId, isLoggedIn); + const { rawData, discountRate } = data; + + const amenities = [ + rawData.roomTheme.parkingZone && "주차장", + rawData.roomTheme.breakfast && "조식", + rawData.roomTheme.pool && "수영장", + rawData.roomTheme.oceanView && "오션뷰", + ].filter(Boolean); + + const schemaData = { + "@type": "Hotel", + name: rawData.hotelName, + image: rawData.hotelImageUrlList, + address: { + "@type": "PostalAddress", + streetAddress: rawData.hotelAddress, + addressCountry: "KR", + }, + starRating: { + "@type": "Rating", + ratingValue: rawData.hotelLevel, + }, + aggregateRating: { + "@type": "AggregateRating", + ratingValue: rawData.roomAllRating, + bestRating: "5", + worstRating: "1", + }, + amenityFeature: amenities.map((amenity) => ({ + "@type": "LocationFeatureSpecification", + name: amenity, + })), + url: rawData.hotelInfoUrl, + offers: { + "@type": "Offer", + name: rawData.roomName, + price: rawData.sellingPrice, + priceCurrency: "KRW", + availability: rawData.saleStatus + ? "https://schema.org/InStock" + : "https://schema.org/OutOfStock", + validFrom: rawData.checkIn, + validThrough: rawData.checkOut, + priceValidUntil: rawData.checkOut, + }, + containsPlace: { + "@type": "HotelRoom", + name: rawData.roomName, + occupancy: { + "@type": "QuantitativeValue", + minValue: rawData.standardPeople, + maxValue: rawData.maxPeople, + }, + bed: { + "@type": "BedDetails", + typeOfBed: rawData.bedType, + }, + amenityFeature: [ + { + "@type": "LocationFeatureSpecification", + name: rawData.facilityInformation, + }, + ], + }, + }; + + return ( + <> + + + + ); +}; + export default RoomDetail; diff --git a/src/pages/saleDetailPage/SaleDetail.tsx b/src/pages/saleDetailPage/SaleDetail.tsx index 3ed8fd7a..37589d28 100644 --- a/src/pages/saleDetailPage/SaleDetail.tsx +++ b/src/pages/saleDetailPage/SaleDetail.tsx @@ -1,8 +1,3 @@ -import Caption from "@/components/caption/Caption"; -import Card from "@/components/card/Card"; -import CardItem from "@/components/cardItem/CardItem"; -import { CaptionWrapper } from "@/pages/paymentPage/Payment.style"; -import { calculateFee } from "@/utils/calculator"; import { parse, sub, format } from "date-fns"; import { useParams, useSearchParams } from "react-router-dom"; @@ -10,10 +5,16 @@ import SaleButton from "./saleButton/SaleButton"; import * as S from "./SaleDetail.style"; import SaleInfo from "./saleInfo/SaleInfo"; +import Caption from "@/components/caption/Caption"; +import Card from "@/components/card/Card"; +import CardItem from "@/components/cardItem/CardItem"; +import { HelmetTag } from "@/components/Helmet/Helmet"; import { useSaleDetailQuery } from "@/hooks/api/useSaleQuery"; +import { CaptionWrapper } from "@/pages/paymentPage/Payment.style"; +import { calculateFee } from "@/utils/calculator"; import { formatDateString } from "@/utils/dateFormatter"; -const SaleDetail = () => { +const SaleDetailView = () => { const { saleId } = useParams(); const [searchParams] = useSearchParams(); const isPaymentId = searchParams.get("isPaymentId"); @@ -269,4 +270,21 @@ const SaleDetail = () => { ); }; +const SaleDetail = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${location.pathname}}`, + }, + }; + + return ( + <> + + + + ); +}; + export default SaleDetail; diff --git a/src/pages/searchFilterPage/SearchFilter.tsx b/src/pages/searchFilterPage/SearchFilter.tsx index cbba440f..829c06ee 100644 --- a/src/pages/searchFilterPage/SearchFilter.tsx +++ b/src/pages/searchFilterPage/SearchFilter.tsx @@ -6,11 +6,12 @@ import PeopleCounter from "./components/peopleCounter/PeopleCounter"; import RegionModal from "./components/regionModal/RegionModal"; import * as S from "./SearchFilter.style"; +import { HelmetTag } from "@/components/Helmet/Helmet"; import { PATH } from "@/constants/path"; import { useSearchFilterInfoStore } from "@/store/store"; import { formatDateMonthAndDay } from "@/utils/dateFomaterMonthDay"; -const SearchFilter = () => { +const SearchFilterView = () => { const searchInfo = useSearchFilterInfoStore((state) => state.searchInfo); const setSearchInfo = useSearchFilterInfoStore( (state) => state.setSearchInfo, @@ -113,4 +114,57 @@ const SearchFilter = () => { ); }; +const SearchFilter = () => { + const schemaData = { + "@type": "WebPage", + name: "퍼센트호텔 검색 필터", + description: + "퍼센트호텔에서 원하는 조건으로 호텔을 검색하세요. 지역, 날짜, 인원 수를 선택할 수 있습니다.", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${PATH.SEARCH_FILTER}`, + }, + potentialAction: { + "@type": "SearchAction", + target: { + "@type": "EntryPoint", + urlTemplate: `${location.origin}/search?location={location}&checkIn={checkIn}&checkOut={checkOut}&people={people}`, + }, + "query-input": [ + { + "@type": "PropertyValueSpecification", + valueRequired: true, + valueName: "location", + }, + { + "@type": "PropertyValueSpecification", + valueRequired: true, + valueName: "checkIn", + }, + { + "@type": "PropertyValueSpecification", + valueRequired: true, + valueName: "checkOut", + }, + { + "@type": "PropertyValueSpecification", + valueRequired: true, + valueName: "people", + }, + ], + }, + }; + + return ( + <> + + + + ); +}; + export default SearchFilter; diff --git a/src/pages/searchPage/Search.tsx b/src/pages/searchPage/Search.tsx index 17a1563f..80f3cca9 100644 --- a/src/pages/searchPage/Search.tsx +++ b/src/pages/searchPage/Search.tsx @@ -1,4 +1,3 @@ -import { useInfiniteQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import SearchBar from "./components/searchBar/SearchBar"; @@ -6,53 +5,19 @@ import SearchItem from "./components/searchItem/SearchItem"; import SearchNav from "./components/searchNav/SearchNav"; import * as S from "./Search.style"; +import type { ISearchFilterInfo } from "@/types/searchFilterInfo"; import type { ISearchList } from "@/types/searchList"; -import { fetchSearchList } from "@/apis/fetchSeachList"; import ArrowIcon from "@/assets/icons/ic_arrow.svg?react"; +import { HelmetTag } from "@/components/Helmet/Helmet"; +import { useInfiniteSearchQuery } from "@/hooks/api/useSearchQuery"; import { useIntersectionObserver } from "@/hooks/common/useIntersectionObserver"; import { useSearchFilterInfoStore } from "@/store/store"; -const Search = () => { +const SearchView = ({ searchInfo }: { searchInfo: ISearchFilterInfo }) => { const [isTopButtonVisible, setIsTopButtonVisible] = useState(false); - const searchInfo = useSearchFilterInfoStore((state) => state.searchInfo); - - const pageSize = 10; - const { data, fetchNextPage, isLoading, hasNextPage } = useInfiniteQuery({ - queryKey: [ - "searchItems", - searchInfo.location, - searchInfo.checkIn, - searchInfo.checkOut, - searchInfo.quantityPeople, - searchInfo.sorted, - searchInfo.brunch, - searchInfo.pool, - searchInfo.oceanView, - ], - queryFn: ({ pageParam = 0 }) => - fetchSearchList( - searchInfo.location, - searchInfo.checkIn, - searchInfo.checkOut, - searchInfo.quantityPeople, - searchInfo.sorted, - searchInfo.brunch, - searchInfo.pool, - searchInfo.oceanView, - pageParam, - pageSize, - ), - refetchOnMount: false, - refetchOnWindowFocus: false, - initialPageParam: 0, - getNextPageParam: (lastPage) => { - const lastData = lastPage?.content; - return lastData && lastData.length === pageSize - ? lastPage?.number + 1 - : undefined; - }, - }); + const { data, fetchNextPage, isLoading, hasNextPage } = + useInfiniteSearchQuery(searchInfo); const handleIntersect = (isIntersecting: boolean) => { if (isIntersecting && hasNextPage) { @@ -119,4 +84,48 @@ const Search = () => { ); }; +const Search = () => { + const searchInfo = useSearchFilterInfoStore((state) => state.searchInfo); + + const schemaData = { + "@type": "SearchResultsPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}/search`, + }, + about: { + "@type": "HotelSearch", + checkinTime: searchInfo.checkIn, + checkoutTime: searchInfo.checkOut, + location: { + "@type": "Place", + name: searchInfo.location, + }, + occupancy: { + "@type": "QuantitativeValue", + value: searchInfo.quantityPeople, + }, + }, + potentialAction: { + "@type": "SearchAction", + target: { + "@type": "EntryPoint", + urlTemplate: `${location.origin}/search?q={search_term_string}`, + }, + "query-input": "required name=search_term_string", + }, + }; + + return ( + <> + + + + ); +}; + export default Search; diff --git a/src/pages/signInPage/SignIn.tsx b/src/pages/signInPage/SignIn.tsx index 038c4c18..2c84633d 100644 --- a/src/pages/signInPage/SignIn.tsx +++ b/src/pages/signInPage/SignIn.tsx @@ -5,6 +5,7 @@ import { Link, useNavigate, useSearchParams } from "react-router-dom"; import * as S from "./SignIn.style"; import { postLogin } from "@/apis/fetchLogin"; +import { HelmetTag } from "@/components/Helmet/Helmet"; import { PATH } from "@/constants/path"; import useToastConfig from "@/hooks/common/useToastConfig"; import { useUserInfoStore } from "@/store/store"; @@ -15,7 +16,7 @@ type FormValues = { password: string; }; -const SignIn = () => { +const SignInView = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const redirectUrl = searchParams.get("redirect"); @@ -114,4 +115,21 @@ const SignIn = () => { ); }; +const SignIn = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${PATH.LOGIN}`, + }, + }; + + return ( + <> + + + + ); +}; + export default SignIn; diff --git a/src/pages/signUpPage/SignUp.tsx b/src/pages/signUpPage/SignUp.tsx index c989b60f..2e1f683c 100644 --- a/src/pages/signUpPage/SignUp.tsx +++ b/src/pages/signUpPage/SignUp.tsx @@ -1,11 +1,14 @@ -import TermsAgreementSection from "@/pages/connectYanoljaPage/verificationPage/components/termsAgreementSection/TermsAgreementSection"; import { FormProvider, useForm } from "react-hook-form"; import FieldValues from "./components/fieldValues/FieldValues"; import SignUpSubmitBtn from "./components/signUpSubmitBtn/SignUpSubmitBtn"; import * as S from "./SignUp.style"; -const SignUp = () => { +import { HelmetTag } from "@/components/Helmet/Helmet"; +import { PATH } from "@/constants/path"; +import TermsAgreementSection from "@/pages/connectYanoljaPage/verificationPage/components/termsAgreementSection/TermsAgreementSection"; + +const SignUpView = () => { const methods = useForm({ mode: "onChange", shouldUnregister: false, @@ -27,4 +30,27 @@ const SignUp = () => { ); }; +const SignUp = () => { + const schemaData = { + "@type": "WebPage", + description: + "퍼센트호텔 회원가입 페이지입니다. 가입하시면 다양한 혜택을 누리실 수 있습니다.", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${PATH.SIGNUP}`, + }, + }; + + return ( + <> + + + + ); +}; + export default SignUp; diff --git a/src/pages/transferWritingPage/TransferWriting.tsx b/src/pages/transferWritingPage/TransferWriting.tsx index 4805c9f1..1ab59424 100644 --- a/src/pages/transferWritingPage/TransferWriting.tsx +++ b/src/pages/transferWritingPage/TransferWriting.tsx @@ -8,13 +8,14 @@ import * as S from "./TransferWriting.style"; import { fetchTransferItems } from "@/apis/fetchTransferItems"; import { fetchUserInfo } from "@/apis/fetchUserInfo.ts"; import { ResponseError } from "@/components/error/Error"; +import { HelmetTag } from "@/components/Helmet/Helmet"; import NoResult from "@/components/noResult/NoResult"; import { ERROR_CODE } from "@/constants/api"; import { PATH } from "@/constants/path"; import useToastConfig from "@/hooks/common/useToastConfig"; import useAuthStore from "@/store/authStore"; -const TransferWriting = () => { +const TransferWritingView = () => { const { handleToast } = useToastConfig(); const isLoggedIn = useAuthStore((state) => state.isLoggedIn); @@ -110,4 +111,21 @@ const TransferWriting = () => { ); }; +const TransferWriting = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${PATH.WRITE_TRANSFER}`, + }, + }; + + return ( + <> + + + + ); +}; + export default TransferWriting; diff --git a/src/pages/transferWritingPricePage/TransferWritingPrice.tsx b/src/pages/transferWritingPricePage/TransferWritingPrice.tsx index 5f829186..acdc9b71 100644 --- a/src/pages/transferWritingPricePage/TransferWritingPrice.tsx +++ b/src/pages/transferWritingPricePage/TransferWritingPrice.tsx @@ -17,6 +17,7 @@ import useChangePage from "../../hooks/common/useChangePage"; import useReadyToSubmit from "../../hooks/common/useReadyToSubmit"; import useSubmitHandler from "../../hooks/common/useSubmitHandler"; +import { HelmetTag } from "@/components/Helmet/Helmet"; import { useUserInfoQuery } from "@/hooks/api/useUserInfoQuery"; import usePreventLeave from "@/hooks/common/usePreventLeave"; import useTransferNavigation from "@/hooks/common/useTransferNavigation"; @@ -34,7 +35,7 @@ import { type SellerCommentType } from "@/types/sellerComments"; // name: "Bumang", // }; -const TransferWritingPrice = () => { +const TransferWritingPriceView = () => { // 현재 선택된 숙박 const selectedItem = useSelectedItemStore((state) => state.selectedItem); @@ -256,4 +257,21 @@ const TransferWritingPrice = () => { ); }; +const TransferWritingPrice = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${location.pathname}}`, + }, + }; + + return ( + <> + + + + ); +}; + export default TransferWritingPrice; diff --git a/src/pages/transferWritingSuccessPage/TransferWritingSuccess.tsx b/src/pages/transferWritingSuccessPage/TransferWritingSuccess.tsx index 7ba6d0f3..a6e2775d 100644 --- a/src/pages/transferWritingSuccessPage/TransferWritingSuccess.tsx +++ b/src/pages/transferWritingSuccessPage/TransferWritingSuccess.tsx @@ -1,14 +1,15 @@ -import PercentHotelLogo from "@/assets/logos/Percent-hotel_logo_b.png"; -import { PATH } from "@/constants/path"; import { useEffect, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import AccountBottomSheet from "./accountBottomSheet/AccountBottomSheet"; import * as S from "./TransferWritingSuccess.style"; +import PercentHotelLogo from "@/assets/logos/Percent-hotel_logo_b.png"; +import { HelmetTag } from "@/components/Helmet/Helmet"; import Success from "@/components/lottie/success/Success"; +import { PATH } from "@/constants/path"; -const TransferWritingSuccess = () => { +const TransferWritingSuccessView = () => { const [content, setContent] = useState< "default" | "agreement" | "firstlyNoAccount" >("default"); @@ -52,4 +53,21 @@ const TransferWritingSuccess = () => { ); }; +const TransferWritingSuccess = () => { + const schemaData = { + "@type": "WebPage", + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${location.origin}${location.pathname}}`, + }, + }; + + return ( + <> + + + + ); +}; + export default TransferWritingSuccess; diff --git a/src/routes/router.tsx b/src/routes/router.tsx index 08859172..d2305fc4 100644 --- a/src/routes/router.tsx +++ b/src/routes/router.tsx @@ -1,13 +1,17 @@ import { Suspense } from "react"; -import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom"; +import { + createBrowserRouter, + Navigate, + RouterProvider, +} from "react-router-dom"; import * as Lazy from "./lazy.ts"; import App from "@/App.tsx"; import LoadingFallback from "@/components/deferredComponent/LoadingFallback.tsx"; import ApiErrorBoundary from "@/components/errorBoundary/ErrorBoundary.tsx"; -import { HelmetTag } from "@/components/Helmet/Helmet.tsx"; import Layout from "@/components/layout/Layout.tsx"; +import Outlet from "@/components/layout/Outlet.tsx"; import Redirect from "@/components/redirect/Redirect.tsx"; import { PATH } from "@/constants/path.ts"; import NotFound from "@/pages/notFoundPage"; @@ -24,23 +28,9 @@ const AppRouter = () => { ), children: [ { - path: "", + index: true, element: ( - - - }> - - - - - ), - }, - { - path: "", - element: ( - - }> @@ -53,7 +43,6 @@ const AppRouter = () => { path: PATH.LOGIN, element: ( - }> @@ -66,7 +55,6 @@ const AppRouter = () => { path: PATH.SIGNUP, element: ( - }> @@ -79,7 +67,6 @@ const AppRouter = () => { path: PATH.PASSWORD_RESET, element: ( - }> @@ -90,74 +77,82 @@ const AppRouter = () => { }, { path: PATH.SEARCHLIST, - element: ( - - - - }> - - - - - ), - }, - { - path: PATH.SEARCH_FILTER, - element: ( - - - - }> - - - - - ), - }, - { - path: PATH.WRITE_TRANSFER, - element: ( - - - - }> - - - - - ), - }, - { - path: PATH.WRITE_TRANSFER_PRICE + `/:id`, - element: ( - - - - }> - - - - - ), + children: [ + { + index: true, + element: ( + + + }> + + + + + ), + }, + { + path: PATH.SEARCH_FILTER, + element: ( + + + }> + + + + + ), + }, + ], }, { - path: PATH.WRITE_TRANSFER_SUCCESS, - element: ( - - - - }> - - - - - ), + path: "transfer", + children: [ + { + index: true, + element: , + }, + { + path: PATH.WRITE_TRANSFER, + element: ( + + + }> + + + + + ), + }, + { + path: PATH.WRITE_TRANSFER_PRICE + `/:id`, + element: ( + + + }> + + + + + ), + }, + { + path: PATH.WRITE_TRANSFER_SUCCESS, + element: ( + + + }> + + + + + ), + }, + ], }, { path: PATH.MY_PAGE, element: ( - }> @@ -170,7 +165,6 @@ const AppRouter = () => { index: true, element: ( <> - ), @@ -180,7 +174,6 @@ const AppRouter = () => { path: PATH.SALE_LIST, element: ( <> - ), @@ -188,52 +181,86 @@ const AppRouter = () => { ], }, { - path: PATH.SETTING, - element: ( - - - }> - - - - - ), - }, - { - path: PATH.MANAGE_PROFILE, - element: ( - - - }> - - - - - ), - }, - { - path: PATH.MANAGE_ACCOUNT, - element: ( - - - }> - - - - - ), - }, - { - path: PATH.ACCOUNT_EDIT, - element: ( - - - }> - - - - - ), + path: PATH.MY_PAGE, + children: [ + { + path: PATH.SETTING, + children: [ + { + index: true, + element: ( + + + }> + + + + + ), + }, + { + path: PATH.MANAGE_PROFILE, + element: ( + + + }> + + + + + ), + }, + { + path: PATH.MANAGE_ACCOUNT, + element: ( + + + }> + + + + + ), + }, + { + path: PATH.ACCOUNT_EDIT, + element: ( + + + }> + + + + + ), + }, + ], + }, + { + path: PATH.PURCAHSE_DETAIL, + element: ( + + + }> + + + + + ), + }, + { + path: PATH.SALE_DETAIL + "/:saleId", + element: ( + + + }> + + + + + ), + }, + ], }, { path: PATH.DETAIL_ROOM(), @@ -251,7 +278,6 @@ const AppRouter = () => { path: PATH.ALARM, element: ( - }> @@ -260,81 +286,57 @@ const AppRouter = () => { ), }, - { - path: PATH.PURCAHSE_DETAIL, - element: ( - - - - }> - - - - - ), - }, - { - path: PATH.SALE_DETAIL + "/:saleId", - element: ( - - - - }> - - - - - ), - }, { path: PATH.YANOLJA_ACCOUNT, - element: ( - - - - }> - - - - - ), - }, - { - path: PATH.YANOLJA_ACCOUNT_VERIFICATION, - element: ( - - - - - }> - - - - - - ), - }, - { - path: PATH.YANOLJA_ACCOUNT_VERIFICATION_SUCCESS, - element: ( - - - - }> - - - - - ), + element: , + children: [ + { + index: true, + element: ( + + + }> + + + + + ), + }, + { + path: PATH.YANOLJA_ACCOUNT_VERIFICATION, + element: ( + + + + }> + + + + + + ), + }, + { + path: PATH.YANOLJA_ACCOUNT_VERIFICATION_SUCCESS, + element: ( + + + }> + + + + + ), + }, + ], }, { path: PATH.PAYMENT(":productId"), element: ( - }> - + @@ -346,12 +348,7 @@ const AppRouter = () => { }, { path: "success", - element: ( - <> - - - - ), + element: , }, { path: "ready", @@ -367,7 +364,6 @@ const AppRouter = () => { path: PATH.MAIN_DETAIL, element: ( - }> @@ -378,16 +374,19 @@ const AppRouter = () => { }, { path: PATH.WISHLIST, - element: ( - <> - - - }> - - - - - ), + element: , + children: [ + { + index: true, + element: ( + + }> + + + + ), + }, + ], }, ], }, diff --git a/src/styles/breakpoints.ts b/src/styles/breakpoints.ts index 8e60d5ac..db2aec15 100644 --- a/src/styles/breakpoints.ts +++ b/src/styles/breakpoints.ts @@ -1,10 +1,10 @@ const breakpoints = { base: "0em", xs: "24em", // 384px - sm: "36em", // 576px - md: "48em", // 768 px - lg: "62em", // 992 px - xl: "80em", // 1280px + sm: "36em", // 576px Mobile + md: "48em", // 768 px Tablet + lg: "62em", // 992 px Desktop + xl: "80em", // 1280px Desktop }; export default breakpoints;