diff --git a/src/api/trips.ts b/src/api/trips.ts
index 3d2d2635..be019a7f 100644
--- a/src/api/trips.ts
+++ b/src/api/trips.ts
@@ -47,7 +47,13 @@ export const getTripsLike = async (
// 우리의 관심 목록 등록
export const postTripsLike = async (tripId: number, tourItemIds: number[]) => {
- const res = await client.post(`trips/${tripId}/tripLikedTours`, tourItemIds);
+ const requestBody = {
+ tourItemIds: tourItemIds,
+ };
+ const res = await authClient.post(
+ `trips/${tripId}/tripLikedTours`,
+ requestBody,
+ );
return res;
};
diff --git a/src/components/common/button/ListSelectBtn.tsx b/src/components/common/button/ListSelectBtn.tsx
new file mode 100644
index 00000000..4328e6d2
--- /dev/null
+++ b/src/components/common/button/ListSelectBtn.tsx
@@ -0,0 +1,57 @@
+import { useState, ReactNode } from 'react';
+import { CheckIcon } from '../icons/Icons';
+
+interface ListSelectBtnProps {
+ children: ReactNode;
+ onClick?: () => void;
+}
+
+export const ListSelectBtn = ({ children, onClick }: ListSelectBtnProps) => {
+ const [isActive, setIsActive] = useState(false);
+
+ const handleClick = () => {
+ setIsActive(!isActive);
+ if (onClick) {
+ onClick();
+ }
+ };
+
+ return (
+
+ );
+};
+
+interface ListCheckBtnProps {
+ onClick?: () => void;
+}
+
+export const ListCheckBtn = ({ onClick }: ListCheckBtnProps) => {
+ const [isActive, setIsActive] = useState(false);
+
+ const handleClick = () => {
+ setIsActive(!isActive);
+ if (onClick) {
+ onClick();
+ }
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/src/pages/plan/OurLikedList.tsx b/src/pages/plan/OurLikedList.tsx
new file mode 100644
index 00000000..bb192296
--- /dev/null
+++ b/src/pages/plan/OurLikedList.tsx
@@ -0,0 +1,100 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import ToursCategoryItem from '@components/Tours/ToursCategoryItem';
+import { useEffect, useState } from 'react';
+import { Spinner } from '@components/common/spinner/Spinner';
+import { getMemberTours } from '@api/member';
+
+export const OurLikedList = () => {
+ const categories = ['전체', '숙소', '식당', '관광지'];
+
+ // const [selectedContentTypeId, setSelectedContentTypeId] = useState<
+ // null | number
+ // >(null);
+
+ const [selectedCategory, setSelectedCategory] = useState('전체');
+ useEffect(() => {
+ console.log(selectedCategory);
+ }, [selectedCategory]);
+
+ const handleSelectCategory = (category: string) => {
+ setSelectedCategory(category);
+ };
+
+ // useEffect(() => {
+ // console.log('searchWord: ' + searchWord);
+ // }, [searchWord]);
+ // console.log();
+
+ const {
+ // fetchNextPage, hasNextPage,
+ data,
+ isLoading,
+ isError,
+ } = 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 (isLoading) {
+ return ;
+ }
+ if (isError) {
+ console.log('error fetching search result ');
+ }
+
+ // console.log(data?.pages[0].data.content);
+ const searchResults = data?.pages.flatMap((page) => page.data.content) || [];
+ console.log('searchResults', searchResults);
+ const noResults = searchResults && searchResults.length === 0;
+
+ return (
+ <>
+ 우리의 관심 목록
+
+ {categories.map((category) => (
+
+ ))}
+
+ {noResults ? (
+
+ 나의 관심목록이 없습니다.
+
+ ) : (
+
+ //
+ )}
+ >
+ );
+};
diff --git a/src/pages/plan/addPlace/AddtoListBtn.tsx b/src/pages/plan/addPlace/AddtoListBtn.tsx
new file mode 100644
index 00000000..9e54f742
--- /dev/null
+++ b/src/pages/plan/addPlace/AddtoListBtn.tsx
@@ -0,0 +1,36 @@
+import { useRecoilValue } from 'recoil';
+import { selectedItemsState } from '@recoil/listItem';
+import { ButtonPrimary } from '@components/common/button/Button';
+import { postTripsLike } from '@api/trips';
+// import { useNavigate } from 'react-router-dom';
+
+const AddToListButton = () => {
+ const selectedTourItemIds = useRecoilValue(selectedItemsState);
+ // const navigate = useNavigate();
+
+ const getTripIdFromUrl = () => {
+ const pathSegments = window.location.pathname.split('/');
+ const tripIdIndex =
+ pathSegments.findIndex((segment) => segment === 'trip') + 1;
+ return pathSegments[tripIdIndex]
+ ? parseInt(pathSegments[tripIdIndex], 10)
+ : null;
+ };
+
+ const handleAddClick = async () => {
+ const tripId = getTripIdFromUrl();
+ if (tripId) {
+ try {
+ const response = await postTripsLike(tripId, selectedTourItemIds);
+ console.log('API response:', response);
+ // navigate(`/trip/${tripId}`);
+ } catch (error) {
+ console.error('API error:', error);
+ }
+ }
+ };
+
+ return 추가하기;
+};
+
+export default AddToListButton;
diff --git a/src/pages/plan/addPlace/MyLiked.tsx b/src/pages/plan/addPlace/MyLiked.tsx
new file mode 100644
index 00000000..5e65a8d3
--- /dev/null
+++ b/src/pages/plan/addPlace/MyLiked.tsx
@@ -0,0 +1,73 @@
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { useState } from 'react';
+import { Spinner } from '@components/common/spinner/Spinner';
+import { getMemberTours } from '@api/member';
+import { MyLikedList } from './MyLikedList';
+import WishCategory from '@components/Wish/WishCategory';
+import AddToListButton from './AddtoListBtn';
+
+export const MyLiked = () => {
+ const [selectedContentTypeId, setSelectedContentTypeId] = useState<
+ null | number
+ >(null);
+
+ const handleCategoryClick = (contentTypeId: number | null) => {
+ setSelectedContentTypeId(contentTypeId);
+ };
+
+ const { fetchNextPage, hasNextPage, data, isLoading, isError } =
+ 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;
+ },
+ });
+
+ if (isLoading) {
+ return ;
+ }
+ if (isError) {
+ console.log('error fetching search result ');
+ }
+
+ const searchResults = data?.pages.flatMap((page) => page.data.content) || [];
+ const noResults = searchResults && searchResults.length === 0;
+
+ return (
+ <>
+ 나의 관심 목록
+
+ {noResults ? (
+
+ 나의 관심목록이 없습니다.
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/src/pages/plan/addPlace/MyLikedList.tsx b/src/pages/plan/addPlace/MyLikedList.tsx
new file mode 100644
index 00000000..295328c6
--- /dev/null
+++ b/src/pages/plan/addPlace/MyLikedList.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import InfiniteScroll from 'react-infinite-scroller';
+import { v4 as uuidv4 } from 'uuid';
+import { MyLikedListItem } from './MyLikedListItem';
+import { TourType } from '@/@types/tours.types';
+import { Spinner } from '@components/common/spinner/Spinner';
+
+interface WishListProps {
+ toursData: { pages: Array<{ data: { content: TourType[] } }> };
+ fetchNextPage: () => void;
+ hasNextPage: boolean;
+ isLoading: boolean;
+ selectedContentTypeId: number | null;
+}
+
+export const MyLikedList: 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 &&
+ filteredData.map((group) => (
+
+ {group?.data.content.map((wishList: TourType) => (
+
+ ))}
+
+ ))}
+
+
+
+ );
+};
diff --git a/src/pages/plan/addPlace/MyLikedListItem.tsx b/src/pages/plan/addPlace/MyLikedListItem.tsx
new file mode 100644
index 00000000..c1cdcfa7
--- /dev/null
+++ b/src/pages/plan/addPlace/MyLikedListItem.tsx
@@ -0,0 +1,57 @@
+import { TourType } from '@/@types/tours.types';
+import { ListCheckBtn } from '@components/common/button/ListSelectBtn';
+import { StarIcon } from '@components/common/icons/Icons';
+import { selectedItemsState } from '@recoil/listItem';
+import { useRecoilState } from 'recoil';
+
+interface WishItemProps {
+ wishList: TourType;
+}
+
+export const MyLikedListItem: React.FC = ({ wishList }) => {
+ const {
+ id,
+ title,
+ ratingAverage,
+ reviewCount,
+ smallThumbnailUrl,
+ tourAddress,
+ } = wishList;
+
+ const [selectedItems, setSelectedItems] = useRecoilState(selectedItemsState);
+
+ const handleSelect = () => {
+ if (selectedItems.includes(id)) {
+ setSelectedItems(selectedItems.filter((item) => item !== id));
+ } else {
+ setSelectedItems([...selectedItems, id]);
+ }
+ };
+
+ return (
+
+
+
+
+
+
{title}
+
+
+
+
+
+ {ratingAverage}({reviewCount})
+
+
+ {tourAddress}
+
+
+
+
+
+ );
+};
diff --git a/src/pages/plan/addPlace/PlanAddPlace.page.tsx b/src/pages/plan/addPlace/PlanAddPlace.page.tsx
new file mode 100644
index 00000000..98002a06
--- /dev/null
+++ b/src/pages/plan/addPlace/PlanAddPlace.page.tsx
@@ -0,0 +1,32 @@
+import SearchInput from '@components/search/SearchInput';
+import { useEffect, useState } from 'react';
+import { useLocation } from 'react-router-dom';
+import { SearchResultForPlan } from './SearchResult';
+import { OurLikedList } from '../OurLikedList';
+
+export const PlanAddPlace = () => {
+ const location = useLocation();
+
+ const queryParams = new URLSearchParams(location.search);
+ const searchWordFromQuery = queryParams.get('searchWord');
+
+ const [searchWord, setSearchWord] = useState('');
+
+ useEffect(() => {
+ if (searchWordFromQuery) {
+ setSearchWord(searchWordFromQuery);
+ } else {
+ setSearchWord('');
+ }
+ }, [location, searchWordFromQuery]);
+ return (
+ <>
+
+ {searchWord ? (
+
+ ) : (
+
+ )}
+ >
+ );
+};
diff --git a/src/pages/plan/addPlace/ResultCategoryPlan.tsx b/src/pages/plan/addPlace/ResultCategoryPlan.tsx
new file mode 100644
index 00000000..7e3f8706
--- /dev/null
+++ b/src/pages/plan/addPlace/ResultCategoryPlan.tsx
@@ -0,0 +1,47 @@
+import { ButtonWhite } from '@components/common/button/Button';
+import { TourType } from '@/@types/tours.types';
+import { InfiniteQueryObserverResult } from '@tanstack/react-query';
+import { ResultItemPlan } from './ResultItem';
+import AddToListButton from './AddtoListBtn';
+
+interface ResultCategoryProps {
+ data: TourType[];
+ category: string;
+ fetchNextPage: (() => Promise>) | null;
+ hasNextPage: boolean;
+ isFetchingNextPage: boolean;
+}
+
+export const ResultCategoryPlan = ({
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+}: ResultCategoryProps) => {
+ return (
+ <>
+ ResultCategoryPlan
+ {data.map((item) => (
+
+ ))}
+ {hasNextPage && !isFetchingNextPage ? (
+ fetchNextPage && fetchNextPage()}>
+ 더보기
+
+ ) : isFetchingNextPage ? (
+
+ Loading...
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/src/pages/plan/addPlace/ResultItem.tsx b/src/pages/plan/addPlace/ResultItem.tsx
new file mode 100644
index 00000000..9ad70dde
--- /dev/null
+++ b/src/pages/plan/addPlace/ResultItem.tsx
@@ -0,0 +1,43 @@
+import { TourType } from '@/@types/tours.types';
+import { ListSelectBtn } from '@components/common/button/ListSelectBtn';
+import { StarIcon } from '@components/common/icons/Icons';
+import { selectedItemsState } from '@recoil/listItem';
+import { useRecoilState } from 'recoil';
+
+export const ResultItemPlan = ({ result }: { result: TourType }) => {
+ const [selectedItems, setSelectedItems] = useRecoilState(selectedItemsState);
+ const id = result.id;
+ const handleSelect = () => {
+ if (selectedItems.includes(id)) {
+ setSelectedItems(selectedItems.filter((item) => item !== id));
+ } else {
+ setSelectedItems([...selectedItems, id]);
+ }
+ };
+ return (
+
+
+
+
+
+
{result.title}
+
+
+
+
+
+ {result.ratingAverage}({result.reviewCount})
+
+
+ {result.tourAddress}
+
+
+
+
선택
+
+ );
+};
diff --git a/src/pages/plan/addPlace/SearchResult.tsx b/src/pages/plan/addPlace/SearchResult.tsx
new file mode 100644
index 00000000..485f37e8
--- /dev/null
+++ b/src/pages/plan/addPlace/SearchResult.tsx
@@ -0,0 +1,94 @@
+import { getToursSearch } from '@api/tours';
+import { useInfiniteQuery } from '@tanstack/react-query';
+import ToursCategoryItem from '@components/Tours/ToursCategoryItem';
+import { useEffect, useState } from 'react';
+import { Spinner } from '@components/common/spinner/Spinner';
+import { ResultCategoryPlan } from './ResultCategoryPlan';
+
+interface SearchResultProps {
+ searchWord: string;
+}
+
+export const SearchResultForPlan = ({ searchWord }: SearchResultProps) => {
+ const categories = ['전체', '숙소', '식당', '관광지'];
+ const [selectedCategory, setSelectedCategory] = useState('전체');
+ useEffect(() => {
+ console.log(selectedCategory);
+ }, [selectedCategory]);
+
+ const handleSelectCategory = (category: string) => {
+ setSelectedCategory(category);
+ };
+
+ useEffect(() => {
+ console.log('searchWord: ' + searchWord);
+ }, [searchWord]);
+ console.log();
+
+ const {
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isLoading,
+ isError,
+ isFetchingNextPage,
+ } = useInfiniteQuery({
+ queryKey: ['searchResults', searchWord, selectedCategory],
+ queryFn: ({ pageParam = 0 }) =>
+ getToursSearch({
+ region: '',
+ searchWord: searchWord,
+ category: selectedCategory !== '전체' ? selectedCategory : undefined,
+ page: pageParam,
+ size: 20,
+ }),
+ initialPageParam: 0,
+ getNextPageParam: (lastPage, allPages) => {
+ if (!lastPage.data.data.last) {
+ return allPages.length;
+ }
+ return undefined;
+ },
+ enabled: !!searchWord,
+ retry: 2,
+ });
+
+ if (isLoading) {
+ return ;
+ }
+ if (isError) {
+ console.log('error fetching search result ');
+ }
+
+ const searchResults =
+ data?.pages.flatMap((page) => page.data.data.content) || [];
+ console.log('searchResults', searchResults);
+ const noResults = searchResults && searchResults.length === 0;
+
+ return (
+ <>
+
+ {categories.map((category) => (
+
+ ))}
+
+
+ {noResults ? (
+ 검색결과가 없습니다.
+ ) : (
+
+ )}
+ >
+ );
+};
diff --git a/src/pages/plan/planPlaceTrip.page.tsx b/src/pages/plan/planPlaceTrip.page.tsx
deleted file mode 100644
index 4ff978c4..00000000
--- a/src/pages/plan/planPlaceTrip.page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-const PlanPlaceTrip = () => {
- return 여행계획 - 장소 추가 페이지
;
-};
-
-export default PlanPlaceTrip;
diff --git a/src/pages/trip/AddOurList.tsx b/src/pages/trip/AddOurList.tsx
new file mode 100644
index 00000000..80bce838
--- /dev/null
+++ b/src/pages/trip/AddOurList.tsx
@@ -0,0 +1,32 @@
+import SearchInput from '@components/search/SearchInput';
+import { MyLiked } from '@pages/plan/addPlace/MyLiked';
+import { SearchResultForPlan } from '@pages/plan/addPlace/SearchResult';
+import { useEffect, useState } from 'react';
+import { useLocation } from 'react-router-dom';
+
+export const AddOurList = () => {
+ const location = useLocation();
+
+ const queryParams = new URLSearchParams(location.search);
+ const searchWordFromQuery = queryParams.get('searchWord');
+
+ const [searchWord, setSearchWord] = useState('');
+
+ useEffect(() => {
+ if (searchWordFromQuery) {
+ setSearchWord(searchWordFromQuery);
+ } else {
+ setSearchWord('');
+ }
+ }, [location, searchWordFromQuery]);
+ return (
+
+
+ {searchWord ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/src/recoil/listItem.ts b/src/recoil/listItem.ts
new file mode 100644
index 00000000..72eb7456
--- /dev/null
+++ b/src/recoil/listItem.ts
@@ -0,0 +1,6 @@
+import { atom } from 'recoil';
+
+export const selectedItemsState = atom({
+ key: 'selectedItemsState',
+ default: [],
+});
diff --git a/src/router/socketRouter.tsx b/src/router/socketRouter.tsx
index 4503f830..a48c16dc 100644
--- a/src/router/socketRouter.tsx
+++ b/src/router/socketRouter.tsx
@@ -1,12 +1,13 @@
import { Route, Routes } from 'react-router-dom';
import { useSocket, socketContext } from '@hooks/useSocket';
import PlanTrip from '@pages/plan/planTrip.page';
-import PlanPlaceTrip from '@pages/plan/planPlaceTrip.page';
+import { PlanAddPlace } from '@pages/plan/addPlace/PlanAddPlace.page';
import PlanPlaceSearch from '@pages/plan/planPlaceSearch.page';
import Trip from '@pages/trip/trip.page';
import MainLayout from './routerLayout';
import { useRecoilValue } from 'recoil';
import { tripIdState, visitDateState } from '@recoil/socket';
+import { AddOurList } from '@pages/trip/AddOurList';
const SocketRoutes = () => {
const tripId = useRecoilValue(tripIdState);
@@ -19,7 +20,7 @@ const SocketRoutes = () => {
} />
- } />
+ } />
} />
@@ -31,6 +32,7 @@ const SocketRouter = () => {
}>
} />
+ } />
} />