-
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 5 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 { IconHeart, IconArrowLeft, IconShare, IconKakao } from '@public/icons'; | ||
import { useToast } from '@/hooks/toast'; | ||
import useBookshelfBooksQuery from '@/queries/bookshelf/useBookshelfBookListQuery'; | ||
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'; | ||
|
||
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,55 +28,16 @@ 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> | ||
); | ||
} | ||
|
||
if (!(infoIsSuccess && booksIsSuccess)) return null; | ||
|
||
const filtered = () => { | ||
const data = booksData.pages[0].books; | ||
|
||
if (isAuthed()) return data; | ||
|
||
return data.slice(0, 4); | ||
}; | ||
const router = useRouter(); | ||
|
||
const filteredData = filtered(); | ||
if (!isSuccess) return null; | ||
|
||
const handleClickShareButton = () => { | ||
const url = 'https://dev.dadok.site' + pathname; | ||
const url = window.location.href; | ||
Comment on lines
-91
to
+40
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. comment; 매직 스트링을 잘 바꿔주셨네요 👍 |
||
|
||
navigator.clipboard | ||
.writeText(url) | ||
|
@@ -100,69 +49,126 @@ export default function UserBookShelfPage({ | |
}); | ||
}; | ||
|
||
const handleBookshelfLikeButton = () => { | ||
!infoData.isLiked ? likeBookshelf() : unlikeBookshelf(); | ||
const handleClickLikeButton = () => { | ||
!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 | ||
onClick={handleClickLikeButton} | ||
> | ||
<div className="bold flex items-center gap-[0.4rem] text-xs"> | ||
<IconHeart fill="white" height="1.3rem" w="1.3rem" /> | ||
{data.likeCount} | ||
</div> | ||
</Button> | ||
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. |
||
</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,9 @@ | ||
import { APIBookshelf } from '@/types/bookshelf'; | ||
|
||
const bookShelfKeys = { | ||
all: ['bookShelf'] 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; |
This file was deleted.
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.
p1;
새로운 Toast 컴포넌트가 사용될 수 있도록
@/ui/Base/Toast/useToast
에서 import 해주세요! 아래 토스트 훅을 사용할 때 적절한type
도 함께 작성되면 좋을 것 같아요~! 😀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.
@gxxrxn 반영해두었어요! b55f27d