-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#449] [책장] 책장 상세 페이지 #451
Changes from all commits
b9e099c
b4ba64e
d756fb0
8d01777
adf08e9
b55f27d
8571a5e
bd028f2
94e965f
281a826
f9bcaa8
658fa06
3ae3e07
f4e6f32
78a80c6
ba304bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,25 @@ | ||
'use client'; | ||
|
||
import { useToast } from '@/hooks/toast'; | ||
import useBookshelfBooksQuery from '@/queries/bookshelf/useBookshelfBookListQuery'; | ||
import useBookshelfInfoQuery from '@/queries/bookshelf/useBookshelfInfoQuery'; | ||
import { IconHeart, IconArrowLeft, IconShare, IconKakao } from '@public/icons'; | ||
import useToast from '@/ui/Base/Toast/useToast'; | ||
import useBookShelfBooksQuery from '@/queries/bookshelf/useBookShelfBookListQuery'; | ||
import useBookShelfInfoQuery from '@/queries/bookshelf/useBookShelfInfoQuery'; | ||
import { | ||
useBookshelfLike, | ||
useBookshelfUnlike, | ||
} from '@/queries/bookshelf/useBookshelfLikeMutation'; | ||
import { APIBookshelf } from '@/types/bookshelf'; | ||
import Button from '@/ui/common/Button'; | ||
import IconButton from '@/ui/common/IconButton'; | ||
import { LikeButton } from '@/ui/common/BookshelfLike/'; | ||
import TopNavigation from '@/ui/common/TopNavigation'; | ||
import InteractiveBookShelf from '@/ui/InteractiveBookShelf'; | ||
import InitialBookShelfData from '@/ui/InteractiveBookShelf/InitialBookShelfData'; | ||
import UserJobInfoTag from '@/ui/UserJobInfoTag'; | ||
import { isAuthed } from '@/utils/helpers'; | ||
import { | ||
Box, | ||
Flex, | ||
Highlight, | ||
HStack, | ||
Image, | ||
Link, | ||
Skeleton, | ||
Text, | ||
VStack, | ||
} from '@chakra-ui/react'; | ||
import { usePathname } from 'next/navigation'; | ||
} from '@/queries/bookshelf/useBookShelfLikeMutation'; | ||
|
||
import { useEffect } from 'react'; | ||
import { useInView } from 'react-intersection-observer'; | ||
import Button from '@/ui/Base/Button'; | ||
import TopNavigation from '@/ui/Base/TopNavigation'; | ||
import BookShelfRow from '@/v1/bookShelf/BookShelfRow'; | ||
import { useRouter } from 'next/navigation'; | ||
import { isAuthed } from '@/utils/helpers'; | ||
import Link from 'next/link'; | ||
import type { APIBookshelf, APIBookshelfInfo } from '@/types/bookshelf'; | ||
|
||
const kakaoUrl = `${process.env.NEXT_PUBLIC_API_URL}/oauth2/authorize/kakao?redirect_uri=${process.env.NEXT_PUBLIC_CLIENT_REDIRECT_URI}`; | ||
const KAKAO_OAUTH_LOGIN_URL = `${process.env.NEXT_PUBLIC_API_URL}/oauth2/authorize/kakao?redirect_uri=${process.env.NEXT_PUBLIC_CLIENT_REDIRECT_URI}`; | ||
|
||
export default function UserBookShelfPage({ | ||
params: { bookshelfId }, | ||
|
@@ -40,129 +28,159 @@ export default function UserBookShelfPage({ | |
bookshelfId: APIBookshelf['bookshelfId']; | ||
}; | ||
}) { | ||
const { ref, inView } = useInView(); | ||
const { data: infoData, isSuccess: infoIsSuccess } = useBookshelfInfoQuery({ | ||
bookshelfId, | ||
}); | ||
const { data, isSuccess } = useBookShelfInfoQuery({ bookshelfId }); | ||
const { mutate: likeBookshelf } = useBookshelfLike(bookshelfId); | ||
const { mutate: unlikeBookshelf } = useBookshelfUnlike(bookshelfId); | ||
const pathname = usePathname(); | ||
const { showToast } = useToast(); | ||
const { | ||
data: booksData, | ||
fetchNextPage, | ||
hasNextPage, | ||
isSuccess: booksIsSuccess, | ||
isLoading, | ||
isFetching, | ||
isFetchingNextPage, | ||
} = useBookshelfBooksQuery({ bookshelfId }); | ||
|
||
useEffect(() => { | ||
if (inView && hasNextPage) { | ||
fetchNextPage(); | ||
} | ||
}, [fetchNextPage, inView, hasNextPage]); | ||
|
||
if (isLoading) { | ||
return ( | ||
<VStack gap="2rem" mt="7.8rem"> | ||
<Skeleton width="100%" height="15.2rem" /> | ||
<Skeleton width="100%" height="15.2rem" /> | ||
<Skeleton width="100%" height="15.2rem" /> | ||
<Skeleton width="100%" height="15.2rem" /> | ||
</VStack> | ||
); | ||
} | ||
const { show: showToast } = useToast(); | ||
const router = useRouter(); | ||
|
||
if (!(infoIsSuccess && booksIsSuccess)) return null; | ||
|
||
const filtered = () => { | ||
const data = booksData.pages[0].books; | ||
|
||
if (isAuthed()) return data; | ||
|
||
return data.slice(0, 4); | ||
}; | ||
|
||
const filteredData = filtered(); | ||
if (!isSuccess) return null; | ||
|
||
const handleClickShareButton = () => { | ||
const url = 'https://dev.dadok.site' + pathname; | ||
const url = window.location.href; | ||
|
||
navigator.clipboard | ||
.writeText(url) | ||
.then(() => { | ||
showToast({ message: '복사 성공!' }); | ||
showToast({ message: '링크를 복사했어요.', type: 'success' }); | ||
}) | ||
.catch(() => { | ||
showToast({ message: '잠시 후 다시 시도해주세요' }); | ||
showToast({ message: '잠시 후 다시 시도해주세요', type: 'error' }); | ||
}); | ||
}; | ||
|
||
const handleBookshelfLikeButton = () => { | ||
!infoData.isLiked ? likeBookshelf() : unlikeBookshelf(); | ||
const handleClickLikeButton = () => { | ||
if (!isAuthed()) { | ||
showToast({ message: '로그인 후 이용해주세요.', type: 'normal' }); | ||
return; | ||
} | ||
|
||
!data.isLiked ? likeBookshelf() : unlikeBookshelf(); | ||
}; | ||
|
||
return ( | ||
<VStack width="100%" height="100%"> | ||
<Flex width="100%" align="center"> | ||
<TopNavigation pageTitle={infoData.bookshelfName} /> | ||
<IconButton | ||
name="share" | ||
size="2.2rem" | ||
onClick={handleClickShareButton} | ||
cursor="pointer" | ||
marginBottom="1rem" | ||
/> | ||
</Flex> | ||
<Flex width="100%" height="3rem" align="center" justify="space-between"> | ||
<HStack gap="0.08rem" py="1.6rem"> | ||
<UserJobInfoTag tag={infoData.job.jobGroupKoreanName} /> | ||
{infoData.job.jobNameKoreanName && ( | ||
<UserJobInfoTag tag={infoData.job.jobNameKoreanName} /> | ||
)} | ||
</HStack> | ||
<LikeButton | ||
handleBookshelfLikeButton={handleBookshelfLikeButton} | ||
isLiked={infoData.isLiked} | ||
likeCount={infoData.likeCount} | ||
/> | ||
</Flex> | ||
<VStack width="100%" spacing="2rem"> | ||
{isAuthed() ? ( | ||
booksData.pages.map((page, idx) => ( | ||
<InteractiveBookShelf key={idx} books={page.books} /> | ||
)) | ||
) : ( | ||
<> | ||
<InteractiveBookShelf books={filteredData} /> | ||
<InitialBookShelfData /> | ||
<Text textAlign="center" fontSize="lg" pt="5rem"> | ||
로그인 후에 | ||
<br /> | ||
<Highlight | ||
query={infoData.bookshelfName} | ||
styles={{ color: 'main', fontWeight: 'bold' }} | ||
> | ||
{`${infoData.bookshelfName}을 확인해 주세요!`} | ||
</Highlight> | ||
</Text> | ||
<Link href={kakaoUrl} style={{ width: '100%' }}> | ||
<Button scheme="kakao" fullWidth> | ||
<Image | ||
src="/icons/kakao-legacy.svg" | ||
alt="카카오 로고" | ||
width={21} | ||
height={19} | ||
/> | ||
카카오 로그인 | ||
</Button> | ||
</Link> | ||
</> | ||
)} | ||
{isFetching && !isFetchingNextPage ? null : <Box ref={ref} />} | ||
</VStack> | ||
</VStack> | ||
<div className="flex w-full flex-col"> | ||
<TopNavigation> | ||
<TopNavigation.LeftItem> | ||
<button onClick={() => router.back()}> | ||
<IconArrowLeft /> | ||
</button> | ||
</TopNavigation.LeftItem> | ||
<TopNavigation.RightItem> | ||
<button onClick={handleClickShareButton}> | ||
<IconShare /> | ||
</button> | ||
</TopNavigation.RightItem> | ||
</TopNavigation> | ||
<div className="mt-[0.8rem] flex flex-col gap-[0.8rem] pb-[2rem] pt-[1rem] font-bold"> | ||
<h1 className="text-[1.8rem]"> | ||
<span className="text-main-900">{data.userNickname}</span> | ||
님의 책장 | ||
</h1> | ||
<div className="flex items-center justify-between"> | ||
<span className="text-[1.4rem] text-[#939393]"> | ||
{`${data.job.jobGroupKoreanName} • ${data.job.jobNameKoreanName}`} | ||
</span> | ||
<Button | ||
size="small" | ||
colorScheme="warning" | ||
fullRadius | ||
fill={data.isLiked ? true : false} | ||
onClick={handleClickLikeButton} | ||
> | ||
<div className="bold flex items-center gap-[0.3rem] text-xs"> | ||
<IconHeart | ||
fill={data.isLiked ? '#F56565' : 'white'} | ||
stroke={!data.isLiked ? '#F56565' : 'white'} | ||
stroke-width={1.5} | ||
height="1.3rem" | ||
w="1.3rem" | ||
/> | ||
{data.likeCount} | ||
</div> | ||
</Button> | ||
</div> | ||
</div> | ||
|
||
<BookShelfContent | ||
bookshelfId={bookshelfId} | ||
userNickname={data.userNickname} | ||
/> | ||
</div> | ||
); | ||
} | ||
|
||
const BookShelfContent = ({ | ||
bookshelfId, | ||
userNickname, | ||
}: { | ||
bookshelfId: APIBookshelf['bookshelfId']; | ||
userNickname: APIBookshelfInfo['userNickname']; | ||
}) => { | ||
const { ref, inView } = useInView(); | ||
|
||
const { | ||
data: booksData, | ||
fetchNextPage, | ||
hasNextPage, | ||
isSuccess, | ||
isFetching, | ||
isFetchingNextPage, | ||
} = useBookShelfBooksQuery({ bookshelfId }); | ||
|
||
useEffect(() => { | ||
if (inView && hasNextPage) { | ||
fetchNextPage(); | ||
} | ||
}, [fetchNextPage, inView, hasNextPage]); | ||
|
||
// TODO: Suspense 적용 | ||
if (!isSuccess) return null; | ||
|
||
return isAuthed() ? ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
<> | ||
{booksData.pages.map(page => | ||
page.books.map((rowBooks, idx) => ( | ||
<BookShelfRow key={idx} books={rowBooks} /> | ||
)) | ||
)} | ||
|
||
{isFetching && !isFetchingNextPage ? null : <div ref={ref} />} | ||
</> | ||
) : ( | ||
<> | ||
<BookShelfRow books={booksData.pages[0].books[0]} /> | ||
<div className="pointer-events-none blur-sm"> | ||
<BookShelfRow books={initialBookImageUrl} /> | ||
</div> | ||
<div className="mt-[3.8rem] flex flex-col gap-[2rem] rounded-[4px] border border-[#CFCFCF] px-[1.7rem] py-[4rem]"> | ||
<p className="text-center text-md font-bold"> | ||
지금 로그인하면 | ||
<br /> | ||
책장에 담긴 모든 책을 볼 수 있어요! | ||
</p> | ||
<p className="text-center text-xs text-placeholder"> | ||
<span className="text-main-900">{userNickname}</span>님의 책장에서 | ||
다양한 | ||
<br /> | ||
인사이트를 얻을 수 있어요. | ||
</p> | ||
<Link href={KAKAO_OAUTH_LOGIN_URL}> | ||
<Button colorScheme="kakao" size="full"> | ||
<div className="flex justify-center gap-[1rem]"> | ||
<IconKakao width={16} height={'auto'} /> | ||
<span className="text-md font-normal">카카오 로그인</span> | ||
</div> | ||
</Button> | ||
</Link> | ||
</div> | ||
</> | ||
); | ||
}; | ||
|
||
const initialBookImageUrl = [ | ||
{ bookId: 1, title: 'book1', imageUrl: '/images/book-cover/book1.jpeg' }, | ||
{ bookId: 2, title: 'book2', imageUrl: '/images/book-cover/book2.jpeg' }, | ||
{ bookId: 3, title: 'book3', imageUrl: '/images/book-cover/book3.jpeg' }, | ||
{ bookId: 4, title: 'book4', imageUrl: '/images/book-cover/book4.jpeg' }, | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { APIBookshelf } from '@/types/bookshelf'; | ||
|
||
const bookShelfKeys = { | ||
all: ['bookShelf'] as const, | ||
info: (bookshelfId: APIBookshelf['bookshelfId']) => | ||
[...bookShelfKeys.all, bookshelfId] as const, | ||
books: (bookshelfId: APIBookshelf['bookshelfId']) => | ||
[...bookShelfKeys.all, bookshelfId, 'books'] as const, | ||
}; | ||
|
||
export default bookShelfKeys; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3;
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import bookshelfAPI from '@/apis/bookshelf'; | ||
import { APIBookshelf } from '@/types/bookshelf'; | ||
import { useInfiniteQuery } from '@tanstack/react-query'; | ||
import bookShelfKeys from './key'; | ||
|
||
const useBookShelfBooksQuery = ({ | ||
bookshelfId, | ||
}: { | ||
bookshelfId: APIBookshelf['bookshelfId']; | ||
}) => | ||
useInfiniteQuery({ | ||
queryKey: bookShelfKeys.books(bookshelfId), | ||
queryFn: ({ pageParam = '' }) => | ||
bookshelfAPI | ||
.getBookshelfBooks(bookshelfId, pageParam) | ||
.then(response => response.data), | ||
getNextPageParam: lastPage => | ||
!lastPage.isLast ? lastPage.books[15].bookshelfItemId : undefined, | ||
staleTime: 3000, | ||
|
||
select: data => { | ||
const pages = data.pages.map(({ books, ...page }) => { | ||
const newBooks = []; | ||
for (let i = 0; i < books.length; i += 4) { | ||
newBooks.push(books.slice(i, i + 4)); | ||
} | ||
return { ...page, books: newBooks }; | ||
}); | ||
|
||
return { | ||
pages, | ||
pageParams: [...data.pageParams], | ||
}; | ||
}, | ||
}); | ||
|
||
export default useBookShelfBooksQuery; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,15 @@ | ||
import bookshelfAPI from '@/apis/bookshelf'; | ||
import { APIBookshelfInfo } from '@/types/bookshelf'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import bookShelfKeys from './key'; | ||
|
||
const useBookshelfInfoQuery = ({ | ||
const useBookShelfInfoQuery = ({ | ||
bookshelfId, | ||
}: { | ||
bookshelfId: APIBookshelfInfo['bookshelfId']; | ||
}) => | ||
useQuery(['bookshelfInfo', bookshelfId], () => | ||
useQuery(bookShelfKeys.info(bookshelfId), () => | ||
bookshelfAPI.getBookshelfInfo(bookshelfId).then(response => response.data) | ||
); | ||
|
||
export default useBookshelfInfoQuery; | ||
export default useBookShelfInfoQuery; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment;
매직 스트링을 잘 바꿔주셨네요 👍