Skip to content

Commit

Permalink
Merge pull request #152 from FinalDoubleTen/FE-84--feat/myTrip/MarkupAPI
Browse files Browse the repository at this point in the history
Feat: 나의 여정 마크업 및 API 연동 완료
  • Loading branch information
NohWookJin authored Jan 12, 2024
2 parents 156cf1f + 561212b commit 17dae06
Show file tree
Hide file tree
Showing 12 changed files with 368 additions and 46 deletions.
12 changes: 11 additions & 1 deletion src/@types/trips.types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
interface TripRequest {
export interface TripRequest {
tripName: string;
numberOfPeople: number;
startDate: string;
endDate: string;
area: string;
subarea: string;
}

export interface MyTripType {
tripId: number;
tripName: string;
startDate: string;
endDate: string;
numberOfTripMembers: number;
tripStatus: string;
tripThumbnailUrl: string;
}
10 changes: 7 additions & 3 deletions src/api/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};

// 나의 관심 여행지 조회
Expand Down
62 changes: 36 additions & 26 deletions src/components/DetailSectionTop/DetailTopButton.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);
const [scrollPosition, setScrollPosition] = useState<number>(0);
const [viewportHeight, setViewportHeight] = useState<number>(0);

const scrollToTop = () => {
window.scroll({
top: 0,
behavior: 'smooth',
});
};
const scrollButtonRef = useRef<HTMLDivElement>(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 && (
<div
onClick={scrollToTop}
className="scroll__container sticky bottom-3 z-20 flex cursor-pointer items-center justify-end">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-white shadow-md">
<TopIcon />
</div>
</div>
)
<div
ref={scrollButtonRef}
className={`absolute right-2 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-white shadow-md transition-opacity duration-500 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
onClick={scrollToTop}
style={{ top: `${scrollPosition}px` }}>
<TopIcon />
</div>
);
}
58 changes: 58 additions & 0 deletions src/components/MyTrip/MyTrip.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>데이터를 불러오는 중 오류가 발생했습니다.</div>;
}

return (
<div className="mt-3 min-h-[100vh]">
<div className=" mb-6 bg-white py-0.5">
<h1 className="title2 pt-3">나의 여정</h1>
</div>
{data?.pages[0].data.content.length > 0 ? (
<MyTripList
myTripsData={data || { pages: [] }}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isLoading={isLoading}
/>
) : (
<NoDataMessage
message1="저장된 관심 여행지가 없습니다."
message2="가고 싶은 장소를 저장해보세요!"
icon={<PenIcon size={44} fill="#EDEDED" color="#EDEDED" />}
/>
)}
</div>
);
};

export default MyTrip;
64 changes: 64 additions & 0 deletions src/components/MyTrip/MyTripItem.tsx
Original file line number Diff line number Diff line change
@@ -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<MyTripItemProps> = ({ myTripList }) => {
const {
tripName,
startDate,
endDate,
numberOfTripMembers,
tripStatus,
tripThumbnailUrl,
} = myTripList;

const ACTIVE_TRIP = '여행 중';

return (
<div className="relative cursor-pointer pb-4">
<div className="flex min-h-[96px]">
<div>
<img
className="rounded-1 h-[96px] min-h-[96px] w-[96px] rounded-[16px] object-cover"
src={tripThumbnailUrl}
alt="여행지 이미지"
/>
<div className="absolute right-[0px] top-[0px] z-10 w-[24px]">
<MoreIcon size={20} fill="gray3" color="gray3" />
</div>
</div>
<div className="ml-[10px] flex flex-col items-start justify-between gap-[15px]">
<div className="max-w-[300px]">
<div
className={`inline-flex ${
tripStatus === ACTIVE_TRIP ? 'bg-cyan-400' : 'border-cyan-400'
} h-[22px] items-center justify-center gap-[8px] rounded-2xl border border-solid border-cyan-400 px-[8px] py-[10px] pt-[11px]`}>
<span
className={`text-xs font-semibold ${
tripStatus === ACTIVE_TRIP ? 'text-white' : 'text-cyan-400'
}`}>
{tripStatus}
</span>
</div>
<div className="mt-[7.5px] w-56 truncate text-base font-bold text-black">
{tripName}
</div>
<div className="h-4 w-56 font-['Pretendard'] text-sm font-medium tracking-tight text-zinc-500">
{startDate} ~ {endDate}
</div>
<div className="mt-[7.5px] inline-flex h-4 w-56 items-start justify-start gap-2">
<div className=" text-xs font-normal text-zinc-500">
{numberOfTripMembers}명과 공유중
</div>
</div>
</div>
</div>
</div>
</div>
);
};

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

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} />
))
: myTripsData.pages.map((group) => (
<React.Fragment key={uuidv4()}>
{group?.data.content.map((myTripList: MyTripType) => (
<MyTripItem key={uuidv4()} myTripList={myTripList} />
))}
</React.Fragment>
))}
</div>
</InfiniteScroll>
);
};

export default MyTripList;
4 changes: 2 additions & 2 deletions src/components/Wish/WishItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const WishItem: React.FC<WishItemProps> = ({ wishList }) => {
{(Math.ceil(ratingAverage * 100) / 100).toFixed(1)}
</span>
<span className="text-xs">
({reviewCount.toLocaleString()})
({reviewCount ? reviewCount.toLocaleString() : reviewCount})
</span>
</div>
</div>
Expand All @@ -66,7 +66,7 @@ const WishItem: React.FC<WishItemProps> = ({ wishList }) => {
<HeartIcon size={14} color="#FF2167" fill="#FF2167" />
</div>
<span className="ml-1 mt-[2.8px] text-xs">
{likedCount.toLocaleString()}
{likedCount ? likedCount.toLocaleString() : likedCount}
</span>
</div>
</div>
Expand Down
Loading

0 comments on commit 17dae06

Please sign in to comment.