Skip to content

Commit

Permalink
Merge pull request #99 from FinalDoubleTen/FE-67--Mypage/View
Browse files Browse the repository at this point in the history
Feat : 마이페이지/나의관심여행지 기능 구현
  • Loading branch information
suehub authored Jan 9, 2024
2 parents f919dba + b44b629 commit b463cd6
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 25 deletions.
1 change: 1 addition & 0 deletions src/@types/tours.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ToursCategoryProps extends ToursListProps {
}

export interface TourType {
contentTypeId?: number;
id: number;
title: string;
liked: boolean;
Expand Down
26 changes: 19 additions & 7 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -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);
});
27 changes: 24 additions & 3 deletions src/api/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};

// 나의 리뷰 조회
Expand Down
13 changes: 0 additions & 13 deletions src/components/DetailSectionTop/DetailToursMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
}}></MapMarker>
</Map>
</div>
Expand Down
61 changes: 61 additions & 0 deletions src/components/Wish/Wish.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>데이터를 불러오는 중 오류가 발생했습니다.</div>;
}

return (
<div className="mt-3">
<div className="sticky top-0 z-[105] bg-white py-0.5">
<h1 className="title2 pt-3">나의 관심 여행지</h1>
<WishCategory onCategoryClick={handleCategoryClick} />
</div>

<WishList
toursData={data || { pages: [] }}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isLoading={isLoading}
selectedContentTypeId={selectedContentTypeId}
/>
</div>
);
};

export default Wish;
44 changes: 44 additions & 0 deletions src/components/Wish/WishCategory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useState } from 'react';
import WishCategoryItem from './WishCategoryItem';

interface WishCategoryProps {
onCategoryClick: (contentTypeId: number | null) => void;
}

const WishCategory: React.FC<WishCategoryProps> = ({ onCategoryClick }) => {
const [selectedCategory, setSelectedCategory] = useState<string>('전체');

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 (
<div className="no-scrollbar mb-6 mt-3 flex w-[100%] overflow-scroll overflow-y-hidden bg-white">
{categories.map((category) => {
return (
<WishCategoryItem
key={category.code}
category={category}
onCategoryClick={onCategoryClick}
isSelected={category.name === selectedCategory}
onSelect={handleSelectCategory}
/>
);
})}
</div>
);
};

export default WishCategory;
36 changes: 36 additions & 0 deletions src/components/Wish/WishCategoryItem.tsx
Original file line number Diff line number Diff line change
@@ -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<WishCategoryItemProps> = ({
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 (
<button
onClick={handleCategoryClick}
className={`body4 mr-[4px] flex items-center justify-center whitespace-nowrap rounded-[30px] border border-solid bg-[#28D8FF] px-[16px] py-[7px] leading-normal ${buttonStyle}`}>
{category.name}
</button>
);
};

export default WishCategoryItem;
79 changes: 79 additions & 0 deletions src/components/Wish/WishItem.tsx
Original file line number Diff line number Diff line change
@@ -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<WishItemProps> = ({ wishList }) => {
const {
id,
title,
liked,
likedCount,
ratingAverage,
reviewCount,
smallThumbnailUrl,
tourAddress,
} = wishList;

const navigate = useNavigate();

return (
<div
className={`relative cursor-pointer pb-4 `}
onClick={() => navigate(`/detail/${id}`)}>
<div className="flex">
<div>
<img
className="rounded-1 h-[82px] max-h-[82px] w-[82px] rounded-[16px] object-cover"
src={smallThumbnailUrl}
alt="여행지 이미지"
/>
<div className="absolute right-[0px] top-[0px] z-10 w-[24px]">
<Like liked={liked} id={id} />
</div>
</div>

<div className="ml-[8px] flex flex-col items-start justify-between gap-[15px]">
<div className="max-w-[240px]">
<p className="overflow-hidden truncate text-clip whitespace-nowrap px-[2px] font-['Pretendard'] text-[16px] font-bold leading-normal text-black">
{title}
</p>
<div className="ml-[3px] mt-[5px] max-w-[260px]">
<p className="text-[14px] text-gray6">{tourAddress}</p>
</div>
</div>

<div className="caption1 mb-[2px] ml-[3px] flex justify-center leading-normal text-gray4">
<div className="mr-[5px] flex items-center">
<div className="mb-[2px]">
<StarIcon size={12.5} color="#FFEC3E" fill="#FFEC3E" />
</div>
<div className="flex items-center pt-[1.5px]">
<span className="ml-1 mr-0.5 text-xs font-bold">
{(Math.ceil(ratingAverage * 100) / 100).toFixed(1)}
</span>
<span className="text-xs">
({reviewCount.toLocaleString()})
</span>
</div>
</div>
<div className="flex items-center justify-center ">
<div className="mb-[0.5px]">
<HeartIcon size={14} color="#FF2167" fill="#FF2167" />
</div>
<span className="ml-1 mt-[2.8px] text-xs">
{likedCount.toLocaleString()}
</span>
</div>
</div>
</div>
</div>
</div>
);
};

export default WishItem;
70 changes: 70 additions & 0 deletions src/components/Wish/WishList.tsx
Original file line number Diff line number Diff line change
@@ -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<WishListProps> = ({
toursData,
fetchNextPage,
hasNextPage,
isLoading,
selectedContentTypeId,
}) => {
if (!toursData || toursData.pages.length === 0) {
return <div>데이터를 불러오는 중 오류가 발생했습니다.</div>;
}

const filteredData =
selectedContentTypeId !== null
? toursData.pages.map((group) => ({
data: {
content: group.data.content.filter(
(item) => item.contentTypeId === selectedContentTypeId,
),
},
}))
: toursData.pages;

return (
<InfiniteScroll
pageStart={0}
loadMore={() => fetchNextPage()}
hasMore={hasNextPage}
loader={
<div key={uuidv4()} className="flex justify-center">
<div
className="z-[100] mx-auto h-8 w-8 animate-spin rounded-full border-[3px] border-solid border-current border-t-transparent pt-10 text-[blue-600] dark:text-[#28d8ff]"
role="status"
aria-label="loading">
<div className="sr-only">Loading...</div>
</div>
</div>
}>
<div className="no-scrollbar grid grid-cols-1 gap-[5px] overflow-y-scroll">
{isLoading
? Array.from({ length: 10 }, (_, index) => (
<ToursItemSkeleton key={index} />
))
: filteredData.map((group) => (
<React.Fragment key={uuidv4()}>
{group?.data.content.map((wishList: TourType) => (
<WishItem key={uuidv4()} wishList={wishList} />
))}
</React.Fragment>
))}
</div>
</InfiniteScroll>
);
};

export default WishList;
Loading

0 comments on commit b463cd6

Please sign in to comment.