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}`)}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(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={
+
+ }>
+
+ {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 = () => {
} />
} />
} />
+ } />
>