diff --git a/src/@types/tours.types.ts b/src/@types/tours.types.ts index c819462e..2b722c6d 100644 --- a/src/@types/tours.types.ts +++ b/src/@types/tours.types.ts @@ -25,6 +25,7 @@ export interface ToursCategoryProps extends ToursListProps { } export interface TourType { + contentTypeId?: number; id: number; title: string; liked: boolean; diff --git a/src/api/client.ts b/src/api/client.ts index 4e7b5bb2..9a34e7ef 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,18 +1,30 @@ import axios from 'axios'; -let accessToken; -if (window.localStorage.getItem('accessToken')) { - accessToken = window.localStorage.getItem('accessToken'); -} - -// axios 인스턴스를 생성합니다. const client = axios.create({ baseURL: import.meta.env.VITE_SERVER_URL, headers: { 'Content-Type': 'application/json', - ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, withCredentials: true, }); export default client; + +// 아래부터는 지수님 구현 사항에 따라 삭제하시면 될 것 같습니다. (좋아요 기능 때문에 잠깐 만들어둠) +client.interceptors.request.use((config) => { + const accessToken = window.localStorage.getItem('accessToken'); + + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}`; + } + + return config; +}); + +client.interceptors.response.use((res) => { + if (200 <= res.status && res.status < 300) { + return res; + } + + return Promise.reject(res.data); +}); diff --git a/src/api/member.ts b/src/api/member.ts index 5bcd784c..8f5d4528 100644 --- a/src/api/member.ts +++ b/src/api/member.ts @@ -27,9 +27,30 @@ export const getMemberTrips = async () => { }; // 나의 관심 여행지 조회 -export const getMemberTours = async () => { - const res = await client.get(`member/tours`); - return res; +export const getMemberTours = async (page?: number, size?: number) => { + try { + const res = await client.get(`member/tours?&page=${page}&size=${size}`); + return res.data; + } catch (e) { + console.error(e); + } +}; + +export const getTours = async ( + region?: string, + page?: number, + size?: number, +) => { + try { + const res = await client.get( + `tours?${ + region !== '전체' && `region=${region}` + }&page=${page}&size=${size}`, + ); + return res; + } catch (e) { + console.error(e); + } }; // 나의 리뷰 조회 diff --git a/src/components/DetailSectionTop/DetailToursMap.tsx b/src/components/DetailSectionTop/DetailToursMap.tsx index 7105f7d6..6f3807d4 100644 --- a/src/components/DetailSectionTop/DetailToursMap.tsx +++ b/src/components/DetailSectionTop/DetailToursMap.tsx @@ -63,19 +63,6 @@ export default function DetailToursMap({ mapData }: DetailToursMapProps) { position={{ lat: Number(latitude), lng: Number(longitude), - }} - image={{ - src: 'https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/marker_red.png', - size: { - width: 45, - height: 50, - }, - options: { - offset: { - x: 18, - y: 50, - }, - }, }}> diff --git a/src/components/Wish/Wish.tsx b/src/components/Wish/Wish.tsx new file mode 100644 index 00000000..b0367cca --- /dev/null +++ b/src/components/Wish/Wish.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { getMemberTours } from '@api/member'; +import WishCategory from './WishCategory'; +import WishList from './WishList'; + +const Wish = () => { + const [selectedContentTypeId, setSelectedContentTypeId] = useState< + null | number + >(null); + + const { fetchNextPage, hasNextPage, data, isLoading, error } = + useInfiniteQuery({ + queryKey: ['wishList'], + queryFn: ({ pageParam = 0 }) => getMemberTours(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; + }, + }); + + const handleCategoryClick = (contentTypeId: number | null) => { + setSelectedContentTypeId(contentTypeId); + }; + + if (error) { + return
데이터를 불러오는 중 오류가 발생했습니다.
; + } + + return ( +
+
+

나의 관심 여행지

+ +
+ + +
+ ); +}; + +export default Wish; diff --git a/src/components/Wish/WishCategory.tsx b/src/components/Wish/WishCategory.tsx new file mode 100644 index 00000000..c288671b --- /dev/null +++ b/src/components/Wish/WishCategory.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import WishCategoryItem from './WishCategoryItem'; + +interface WishCategoryProps { + onCategoryClick: (contentTypeId: number | null) => void; +} + +const WishCategory: React.FC = ({ onCategoryClick }) => { + const [selectedCategory, setSelectedCategory] = useState('전체'); + + const categories = [ + { code: null, name: '전체' }, + { code: 12, name: '관광지' }, + { code: 32, name: '숙소' }, + { code: 39, name: '식당' }, + ]; + + const handleSelectCategory = (name: string) => { + setSelectedCategory(name); + window.scrollTo({ + top: 0, + left: 0, + behavior: 'smooth', + }); + }; + + return ( +
+ {categories.map((category) => { + return ( + + ); + })} +
+ ); +}; + +export default WishCategory; diff --git a/src/components/Wish/WishCategoryItem.tsx b/src/components/Wish/WishCategoryItem.tsx new file mode 100644 index 00000000..43ada79e --- /dev/null +++ b/src/components/Wish/WishCategoryItem.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +interface WishCategoryItemProps { + category: { code: number | null; name: string }; + onCategoryClick: (contentTypeId: number | null) => void; + onSelect: (name: string) => void; + isSelected: boolean; +} + +const WishCategoryItem: React.FC = ({ + category, + onCategoryClick, + onSelect, + isSelected, +}) => { + const handleCategoryClick = () => { + if (category.code !== undefined) { + onCategoryClick(category.code); + onSelect(category.name); + } + }; + + const buttonStyle = isSelected + ? 'bg-[#28D8FF] text-white font-bold' + : 'bg-[#fff] text-[#888] border-[#ededed]'; + + return ( + + ); +}; + +export default WishCategoryItem; diff --git a/src/components/Wish/WishItem.tsx b/src/components/Wish/WishItem.tsx new file mode 100644 index 00000000..a5cff99a --- /dev/null +++ b/src/components/Wish/WishItem.tsx @@ -0,0 +1,79 @@ +import { TourType } from '@/@types/tours.types'; +import { HeartIcon, StarIcon } from '@components/common/icons/Icons'; +import Like from '@components/common/like/Like'; +import { useNavigate } from 'react-router-dom'; + +interface WishItemProps { + wishList: TourType; +} + +const WishItem: React.FC = ({ wishList }) => { + const { + id, + title, + liked, + likedCount, + ratingAverage, + reviewCount, + smallThumbnailUrl, + tourAddress, + } = wishList; + + const navigate = useNavigate(); + + return ( +
navigate(`/detail/${id}`)}> +
+
+ 여행지 이미지 +
+ +
+
+ +
+
+

+ {title} +

+
+

{tourAddress}

+
+
+ +
+
+
+ +
+
+ + {(Math.ceil(ratingAverage * 100) / 100).toFixed(1)} + + + ({reviewCount.toLocaleString()}) + +
+
+
+
+ +
+ + {likedCount.toLocaleString()} + +
+
+
+
+
+ ); +}; + +export default WishItem; diff --git a/src/components/Wish/WishList.tsx b/src/components/Wish/WishList.tsx new file mode 100644 index 00000000..43fabae2 --- /dev/null +++ b/src/components/Wish/WishList.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import InfiniteScroll from 'react-infinite-scroller'; +import { v4 as uuidv4 } from 'uuid'; +import WishItem from './WishItem'; +import ToursItemSkeleton from '@components/Tours/ToursItemSkeleton'; +import { TourType } from '@/@types/tours.types'; + +interface WishListProps { + toursData: { pages: Array<{ data: { content: TourType[] } }> }; + fetchNextPage: () => void; + hasNextPage: boolean; + isLoading: boolean; + selectedContentTypeId: number | null; +} + +const WishList: React.FC = ({ + toursData, + fetchNextPage, + hasNextPage, + isLoading, + selectedContentTypeId, +}) => { + if (!toursData || toursData.pages.length === 0) { + return
데이터를 불러오는 중 오류가 발생했습니다.
; + } + + const filteredData = + selectedContentTypeId !== null + ? toursData.pages.map((group) => ({ + data: { + content: group.data.content.filter( + (item) => item.contentTypeId === selectedContentTypeId, + ), + }, + })) + : toursData.pages; + + return ( + fetchNextPage()} + hasMore={hasNextPage} + loader={ +
+
+
Loading...
+
+
+ }> +
+ {isLoading + ? Array.from({ length: 10 }, (_, index) => ( + + )) + : filteredData.map((group) => ( + + {group?.data.content.map((wishList: TourType) => ( + + ))} + + ))} +
+
+ ); +}; + +export default WishList; diff --git a/src/components/common/like/Like.tsx b/src/components/common/like/Like.tsx index e47b1730..89135cf4 100644 --- a/src/components/common/like/Like.tsx +++ b/src/components/common/like/Like.tsx @@ -12,6 +12,7 @@ const Like = ({ liked, id }: LikeProps) => { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['details'] }); queryClient.invalidateQueries({ queryKey: ['tours'] }); + queryClient.invalidateQueries({ queryKey: ['wishList'] }); }, onError: () => console.log('error'), }); @@ -21,6 +22,7 @@ const Like = ({ liked, id }: LikeProps) => { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['details'] }); queryClient.invalidateQueries({ queryKey: ['tours'] }); + queryClient.invalidateQueries({ queryKey: ['wishList'] }); }, onError: () => console.log('error'), }); diff --git a/src/components/common/nav/Nav.tsx b/src/components/common/nav/Nav.tsx index 1295f159..1247d98f 100644 --- a/src/components/common/nav/Nav.tsx +++ b/src/components/common/nav/Nav.tsx @@ -25,10 +25,10 @@ const Nav = () => {

일정

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

diff --git a/src/pages/wishList/wishList.page.tsx b/src/pages/wishList/wishList.page.tsx new file mode 100644 index 00000000..2f7e3f56 --- /dev/null +++ b/src/pages/wishList/wishList.page.tsx @@ -0,0 +1,11 @@ +import Wish from '@components/Wish/Wish'; + +const WishList = () => { + return ( + <> + + + ); +}; + +export default WishList; diff --git a/src/router/mainRouter.tsx b/src/router/mainRouter.tsx index 549ca215..96debe91 100644 --- a/src/router/mainRouter.tsx +++ b/src/router/mainRouter.tsx @@ -4,6 +4,7 @@ import { Search } from '@pages/search/search.page'; 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 MainLayout from './routerLayout'; const MainRouter = () => { @@ -16,6 +17,7 @@ const MainRouter = () => { } /> } /> } /> + } />