diff --git a/src/@types/trips.types.ts b/src/@types/trips.types.ts index cda8b28b..93c3172c 100644 --- a/src/@types/trips.types.ts +++ b/src/@types/trips.types.ts @@ -1,4 +1,4 @@ -interface TripRequest { +export interface TripRequest { tripName: string; numberOfPeople: number; startDate: string; @@ -6,3 +6,13 @@ interface TripRequest { area: string; subarea: string; } + +export interface MyTripType { + tripId: number; + tripName: string; + startDate: string; + endDate: string; + numberOfTripMembers: number; + tripStatus: string; + tripThumbnailUrl: string; +} diff --git a/src/api/member.ts b/src/api/member.ts index 1e355324..0a5e6093 100644 --- a/src/api/member.ts +++ b/src/api/member.ts @@ -44,9 +44,13 @@ export const deleteMember = async () => { }; // 나의 여정 조회 -export const getMemberTrips = async () => { - const res = await authClient.get(`member/trips`); - return res; +export const getMemberTrips = async (page?: number, size?: number) => { + try { + const res = await authClient.get(`trips?&page=${page}&size=${size}`); + return res.data; + } catch (e) { + console.error(e); + } }; // 나의 관심 여행지 조회 diff --git a/src/components/DetailSectionTop/DetailTopButton.tsx b/src/components/DetailSectionTop/DetailTopButton.tsx index c1ffe145..225c4fc6 100644 --- a/src/components/DetailSectionTop/DetailTopButton.tsx +++ b/src/components/DetailSectionTop/DetailTopButton.tsx @@ -1,41 +1,51 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { TopIcon } from '@components/common/icons/Icons'; -export default function DetailTopButton() { - const [showButton, setShowButton] = useState(true); +export default function DetailTopButton({ parentRef }: any) { + const [isVisible, setIsVisible] = useState(false); + const [scrollPosition, setScrollPosition] = useState(0); + const [viewportHeight, setViewportHeight] = useState(0); - const scrollToTop = () => { - window.scroll({ - top: 0, - behavior: 'smooth', - }); - }; + const scrollButtonRef = useRef(null); useEffect(() => { - const handleShowButton = () => { - if (window.scrollY > 400) { - setShowButton(true); - } else { - setShowButton(false); + const handleScroll = () => { + if (scrollButtonRef.current && parentRef.current) { + setViewportHeight(screen.height); + + // 부모 요소의 높이보다 적을 때까지 스크롤 허용 + if (window.scrollY < parentRef.current.clientHeight - 50) { + // 기기 높이의 절반 이상 스크롤 했을 때 + if (window.scrollY >= viewportHeight / 2) { + setIsVisible(true); + setScrollPosition(window.scrollY); + } else { + setIsVisible(false); + } + } } }; - window.addEventListener('scroll', handleShowButton); + window.addEventListener('scroll', handleScroll); return () => { - window.removeEventListener('scroll', handleShowButton); + window.removeEventListener('scroll', handleScroll); }; - }, []); + }, [parentRef, scrollPosition, setScrollPosition]); + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; return ( - showButton && ( -
-
- -
-
- ) +
+ +
); } diff --git a/src/components/MyTrip/MyTrip.tsx b/src/components/MyTrip/MyTrip.tsx new file mode 100644 index 00000000..549a3186 --- /dev/null +++ b/src/components/MyTrip/MyTrip.tsx @@ -0,0 +1,58 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { getMemberTrips } from '@api/member'; +import NoDataMessage from '@components/common/noData/NoDataMessage'; +import MyTripList from './MyTripList'; +import { PenIcon } from '@components/common/icons/Icons'; + +const MyTrip = () => { + const { fetchNextPage, hasNextPage, data, isLoading, error } = + useInfiniteQuery({ + queryKey: ['wishList'], + queryFn: ({ pageParam = 0 }) => getMemberTrips(pageParam, 10), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + if ( + lastPage && + lastPage.data && + lastPage.data && + lastPage.data.pageable + ) { + const currentPage = lastPage.data.pageable.pageNumber; + const totalPages = lastPage.data.totalPages; + + if (currentPage < totalPages - 1) { + return currentPage + 1; + } + } + return undefined; + }, + }); + + if (error) { + return
데이터를 불러오는 중 오류가 발생했습니다.
; + } + + return ( +
+
+

나의 여정

+
+ {data?.pages[0].data.content.length > 0 ? ( + + ) : ( + } + /> + )} +
+ ); +}; + +export default MyTrip; diff --git a/src/components/MyTrip/MyTripItem.tsx b/src/components/MyTrip/MyTripItem.tsx new file mode 100644 index 00000000..52e4bcd0 --- /dev/null +++ b/src/components/MyTrip/MyTripItem.tsx @@ -0,0 +1,64 @@ +import { MyTripType } from '@/@types/trips.types'; +import { MoreIcon } from '@components/common/icons/Icons'; + +interface MyTripItemProps { + myTripList: MyTripType; +} + +const MyTripItem: React.FC = ({ myTripList }) => { + const { + tripName, + startDate, + endDate, + numberOfTripMembers, + tripStatus, + tripThumbnailUrl, + } = myTripList; + + const ACTIVE_TRIP = '여행 중'; + + return ( +
+
+
+ 여행지 이미지 +
+ +
+
+
+
+
+ + {tripStatus} + +
+
+ {tripName} +
+
+ {startDate} ~ {endDate} +
+
+
+ {numberOfTripMembers}명과 공유중 +
+
+
+
+
+
+ ); +}; + +export default MyTripItem; diff --git a/src/components/MyTrip/MyTripList.tsx b/src/components/MyTrip/MyTripList.tsx new file mode 100644 index 00000000..fbe9a401 --- /dev/null +++ b/src/components/MyTrip/MyTripList.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { MyTripType } from '@/@types/trips.types'; +import InfiniteScroll from 'react-infinite-scroller'; +import { v4 as uuidv4 } from 'uuid'; +import ToursItemSkeleton from '@components/Tours/ToursItemSkeleton'; +import MyTripItem from './MyTripItem'; + +interface MyTripListProps { + myTripsData: { pages: Array<{ data: { content: MyTripType[] } }> }; + fetchNextPage: () => void; + hasNextPage: boolean; + isLoading: boolean; +} + +const MyTripList: React.FC = ({ + myTripsData, + fetchNextPage, + hasNextPage, + isLoading, +}) => { + if (!myTripsData || myTripsData.pages.length === 0) { + return
데이터를 불러오는 중 오류가 발생했습니다.
; + } + + return ( + fetchNextPage()} + hasMore={hasNextPage} + loader={ +
+
+
Loading...
+
+
+ }> +
+ {isLoading + ? Array.from({ length: 10 }, (_, index) => ( + + )) + : myTripsData.pages.map((group) => ( + + {group?.data.content.map((myTripList: MyTripType) => ( + + ))} + + ))} +
+
+ ); +}; + +export default MyTripList; diff --git a/src/components/Wish/WishItem.tsx b/src/components/Wish/WishItem.tsx index e0cfb679..6b30c0b5 100644 --- a/src/components/Wish/WishItem.tsx +++ b/src/components/Wish/WishItem.tsx @@ -57,7 +57,7 @@ const WishItem: React.FC = ({ wishList }) => { {(Math.ceil(ratingAverage * 100) / 100).toFixed(1)} - ({reviewCount.toLocaleString()}) + ({reviewCount ? reviewCount.toLocaleString() : reviewCount}) @@ -66,7 +66,7 @@ const WishItem: React.FC = ({ wishList }) => { - {likedCount.toLocaleString()} + {likedCount ? likedCount.toLocaleString() : likedCount} diff --git a/src/components/common/icons/Icons.tsx b/src/components/common/icons/Icons.tsx index 18f154b1..46163553 100644 --- a/src/components/common/icons/Icons.tsx +++ b/src/components/common/icons/Icons.tsx @@ -119,6 +119,80 @@ export const CalendarIcon: React.FC = ({ ); }; +export const CalendarIcon2: React.FC = ({ size = 25 }) => { + return ( + + + + + + + + + + + + ); +}; + export const HeartIcon: React.FC = ({ size = 25, color = 'black', @@ -997,8 +1071,8 @@ export const TopIcon: React.FC = () => { xmlns="http://www.w3.org/2000/svg"> diff --git a/src/components/common/nav/Nav.tsx b/src/components/common/nav/Nav.tsx index a4f56085..cf3bf6aa 100644 --- a/src/components/common/nav/Nav.tsx +++ b/src/components/common/nav/Nav.tsx @@ -1,5 +1,11 @@ import { useLocation, useNavigate } from 'react-router-dom'; -import { CalendarIcon, HeartIcon, HomeIcon, UserIcon } from '../icons/Icons'; +import { + CalendarIcon, + CalendarIcon2, + HeartIcon, + HomeIcon, + UserIcon, +} from '../icons/Icons'; import { useEffect, useState } from 'react'; import Alert from '../alert/Alert'; @@ -35,12 +41,37 @@ const Nav = () => {

-
navigate('/')} - className="cursor-pointer flex-col items-center justify-center px-2"> - -

일정

-
+ + {isLoggedIn ? ( +
navigate('/mytrip')} + className="cursor-pointer flex-col items-center justify-center px-2"> +
+ {isActive('/mytrip') ? : } +
+

여정

+
+ ) : ( + + 나의 여정 조회를 위해 로그인이 필요합니다. +
+ 로그인 하시겠습니까? + + } + onConfirm={handleConfirm}> +
+
+ +
+

+ 여정 +

+
+
+ )} {isLoggedIn ? (
{ + const parentRef = useRef(null); + return ( - <> +
- - {/* */} - +
+ + +
+
); }; diff --git a/src/pages/myTrip/myTrip.page.tsx b/src/pages/myTrip/myTrip.page.tsx new file mode 100644 index 00000000..a530143c --- /dev/null +++ b/src/pages/myTrip/myTrip.page.tsx @@ -0,0 +1,7 @@ +import MyTrip from '@components/MyTrip/MyTrip'; + +const MyTripPage = () => { + return ; +}; + +export default MyTripPage; diff --git a/src/router/mainRouter.tsx b/src/router/mainRouter.tsx index 4495552a..11048efc 100644 --- a/src/router/mainRouter.tsx +++ b/src/router/mainRouter.tsx @@ -6,6 +6,7 @@ import Detail from '@pages/detail/detail.page'; import ReviewComment from '@pages/reviewComment/reviewComment.page'; import ReviewPosting from '@pages/reviewPosting/reviewPosting.page'; import WishList from '@pages/wishList/wishList.page'; +import MyTripPage from '@pages/myTrip/myTrip.page'; import MyPageReview from '@pages/myPageReview/myPageReview.page'; import { Signup, SignupSuccess, SignupSurvey, SignupInfo } from '@pages/signup'; import { Login, LoginKakao } from '@pages/login'; @@ -29,6 +30,7 @@ const MainRouter = () => { } /> } /> } /> + } /> } /> } /> } />