Skip to content

Commit

Permalink
[#591] 3D 책 컴포넌트, 책장 페이지 개선 (#594)
Browse files Browse the repository at this point in the history
* feat: 3d 책 컴포넌트 placeholder 적용

- global css에 bg-blur 클래스 추가

* feat: 책장 상세정보 query suspense 적용

- 책장페이지 ssr 되도록 컴포넌트 분리

* style: 책장 padding 수정

* style: BookShelf.Book 컴포넌트 패딩 1 -> 1.5rem으로 수정

* chore: 불필요한 prop, 태그 제거
  • Loading branch information
gxxrxn committed Jun 17, 2024
1 parent 0ff4ca0 commit 2beb0f2
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 99 deletions.
165 changes: 97 additions & 68 deletions src/app/bookshelf/[bookshelfId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
'use client';

import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import Link from 'next/link';
import { useInView } from 'react-intersection-observer';

import type { APIBookshelf } from '@/types/bookshelf';

import useBookShelfBooksQuery from '@/queries/bookshelf/useBookShelfBookListQuery';
import useBookShelfInfoQuery from '@/queries/bookshelf/useBookShelfInfoQuery';
import useMutateBookshelfLikeQuery from '@/queries/bookshelf/useMutateBookshelfLikeQuery';
import useToast from '@/v1/base/Toast/useToast';
import { useMyProfileId } from '@/queries/user/useMyProfileQuery';
import { checkAuthentication } from '@/utils/helpers';
import { IconKakao } from '@public/icons';
import { KAKAO_LOGIN_URL } from '@/constants/url';

import useToast from '@/v1/base/Toast/useToast';
import TopNavigation from '@/v1/base/TopNavigation';
import BookShelfRow from '@/v1/bookShelf/BookShelfRow';
import Button from '@/v1/base/Button';
import LikeButton from '@/v1/base/LikeButton';
import BackButton from '@/v1/base/BackButton';
import ShareButton from '@/v1/base/ShareButton';
import type { APIBookshelf, APIBookshelfInfo } from '@/types/bookshelf';

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 },
Expand All @@ -26,64 +29,76 @@ export default function UserBookShelfPage({
bookshelfId: APIBookshelf['bookshelfId'];
};
}) {
return (
<div className="flex w-full flex-col">
<TopNavigation>
<TopNavigation.LeftItem>
<BackButton />
</TopNavigation.LeftItem>
<TopNavigation.RightItem>
<ShareButton />
</TopNavigation.RightItem>
</TopNavigation>

<BookShelfInfo bookshelfId={bookshelfId} />
<BookShelfContent bookshelfId={bookshelfId} />
</div>
);
}

const BookShelfInfo = ({ bookshelfId }: { bookshelfId: number }) => {
const isAuthenticated = checkAuthentication();
const { data, isSuccess } = useBookShelfInfoQuery({ bookshelfId });
const { show: showToast } = useToast();

const { data } = useBookShelfInfoQuery(bookshelfId);
const { isLiked, likeCount, userId, userNickname, job } = data;

const { mutate: mutateBookshelfLike } =
useMutateBookshelfLikeQuery(bookshelfId);
const { show: showToast } = useToast();

if (!isSuccess) return null;
const { data: myId } = useMyProfileId({ enabled: isAuthenticated });

const handleClickLikeButton = () => {
if (!isAuthenticated) {
showToast({ message: '로그인 후 이용해주세요.', type: 'normal' });
return;
}

mutateBookshelfLike(data.isLiked);
if (userId === myId) {
showToast({
message: '내 책장에는 좋아요를 누를 수 없어요.',
type: 'normal',
});
return;
}

mutateBookshelfLike(isLiked);
};

return (
<div className="flex w-full flex-col">
<TopNavigation>
<TopNavigation.LeftItem>
<BackButton />
</TopNavigation.LeftItem>
<TopNavigation.RightItem>
<ShareButton />
</TopNavigation.RightItem>
</TopNavigation>
<div className="mt-[0.8rem] flex flex-col gap-[0.8rem] pb-[2rem] pt-[1rem] font-bold">
<h1 className="font-subheading-bold">
<span className="text-main-900">{data.userNickname}</span>
님의 책장
</h1>
<div className="flex items-center justify-between">
<span className="text-black-600 font-body2-regular">
{`${data.job.jobGroupKoreanName}${data.job.jobNameKoreanName}`}
</span>
<LikeButton
isLiked={data.isLiked}
likeCount={data.likeCount}
onClick={handleClickLikeButton}
/>
</div>
<div className="mt-[0.8rem] flex flex-col gap-[0.8rem] pb-[2rem] pt-[1rem] font-bold">
<h1 className="font-subheading-bold">
<span className="text-main-900">{userNickname}</span>
님의 책장
</h1>
<div className="flex items-center justify-between">
<span className="text-black-600 font-body2-regular">
{`${job.jobGroupKoreanName}${job.jobNameKoreanName}`}
</span>
<LikeButton
isLiked={isLiked}
likeCount={likeCount}
onClick={handleClickLikeButton}
/>
</div>

<BookShelfContent
bookshelfId={bookshelfId}
userNickname={data.userNickname}
/>
</div>
);
}
};

const BookShelfContent = ({
bookshelfId,
userNickname,
}: {
bookshelfId: APIBookshelf['bookshelfId'];
userNickname: APIBookshelfInfo['userNickname'];
}) => {
const isAuthenticated = checkAuthentication();
const { ref, inView } = useInView();
Expand All @@ -93,7 +108,6 @@ const BookShelfContent = ({
fetchNextPage,
hasNextPage,
isSuccess,
isFetching,
isFetchingNextPage,
} = useBookShelfBooksQuery({ bookshelfId });

Expand All @@ -113,39 +127,54 @@ const BookShelfContent = ({
<BookShelfRow key={idx} books={rowBooks} />
))
)}

{isFetching && !isFetchingNextPage ? null : <div ref={ref} />}
{!isFetchingNextPage && <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 font-body1-bold">
지금 로그인하면
<br />
책장에 담긴 모든 책을 볼 수 있어요!
</p>
<p className="text-center text-placeholder font-body2-regular">
<span className="text-main-900">{userNickname}</span>님의 책장에서
다양한
<br />
인사이트를 얻을 수 있어요.
</p>
<Link href={KAKAO_OAUTH_LOGIN_URL}>
<Button colorScheme="kakao" size="full">
<div className="flex items-center justify-center gap-[1rem]">
<IconKakao width={16} height={'auto'} />
<span className="font-body1-regular">카카오 로그인</span>
</div>
</Button>
</Link>
</div>
<DummyBookShelfRow />
<BookShelfLoginBox bookshelfId={bookshelfId} />
</>
);
};
const DummyBookShelfRow = () => (
<div className="pointer-events-none blur-sm">
<BookShelfRow books={initialBookImageUrl} />
</div>
);

const BookShelfLoginBox = ({
bookshelfId,
}: {
bookshelfId: APIBookshelf['bookshelfId'];
}) => {
const { data } = useBookShelfInfoQuery(bookshelfId);
const { userNickname } = data;

return (
<div className="mt-[3.8rem] flex flex-col gap-[2rem] rounded-[4px] border border-[#CFCFCF] px-[1.7rem] py-[4rem]">
<p className="text-center font-body1-bold">
지금 로그인하면
<br />
책장에 담긴 모든 책을 볼 수 있어요!
</p>
<p className="text-center text-placeholder font-body2-regular">
<span className="text-main-900">{userNickname}</span>님의 책장에서
다양한
<br />
인사이트를 얻을 수 있어요.
</p>
<Link href={KAKAO_LOGIN_URL}>
<Button colorScheme="kakao" size="full">
<div className="flex items-center justify-center gap-[1rem]">
<IconKakao width={16} height={'auto'} />
<span className="font-body1-regular">카카오 로그인</span>
</div>
</Button>
</Link>
</div>
);
};

const initialBookImageUrl = [
{ bookId: 1, title: 'book1', imageUrl: '/images/book-cover/book1.jpeg' },
Expand Down
23 changes: 14 additions & 9 deletions src/queries/bookshelf/useBookShelfInfoQuery.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import bookshelfAPI from '@/apis/bookshelf';
import { UseQueryOptions } from '@tanstack/react-query';
import { APIBookshelfInfo } from '@/types/bookshelf';
import { useQuery } from '@tanstack/react-query';
import useQueryWithSuspense from '@/hooks/useQueryWithSuspense';
import bookshelfAPI from '@/apis/bookshelf';
import bookShelfKeys from './key';

const useBookShelfInfoQuery = ({
bookshelfId,
}: {
bookshelfId: APIBookshelfInfo['bookshelfId'];
}) =>
useQuery(bookShelfKeys.info(bookshelfId), () =>
bookshelfAPI.getBookshelfInfo(bookshelfId).then(response => response.data)
const useBookShelfInfoQuery = <TData = APIBookshelfInfo>(
bookshelfId: APIBookshelfInfo['bookshelfId'],
options?: UseQueryOptions<APIBookshelfInfo, unknown, TData>
) =>
useQueryWithSuspense(
bookShelfKeys.info(bookshelfId),
() =>
bookshelfAPI
.getBookshelfInfo(bookshelfId)
.then(response => response.data),
options
);

export default useBookShelfInfoQuery;
5 changes: 5 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,9 @@
.w-app {
@apply relative -left-[2rem] w-[calc(100%+4rem)];
}

.bg-blur {
box-shadow: inset 0 0 3rem #dddddd;
@apply bg-placeholder;
}
}
58 changes: 38 additions & 20 deletions src/v1/bookShelf/BookShelf.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { APIBookshelf } from '@/types/bookshelf';
import { IconArrowRight, IconHeart } from '@public/icons';
'use client';

import { ReactNode, useState } from 'react';
import Link from 'next/link';
import Badge from '@/v1/base/Badge';
import Image from 'next/image';
import { APIBook } from '@/types/book';
import { ReactNode, useState } from 'react';

import ColorThief from 'colorthief';

import { APIBook } from '@/types/book';
import { APIBookshelf } from '@/types/bookshelf';
import { IconArrowRight, IconHeart } from '@public/icons';

import Badge from '@/v1/base/Badge';

const BookShelf = ({ children }: { children: ReactNode }) => {
return <>{children}</>;
};
Expand All @@ -24,7 +29,7 @@ type InfoProps = Omit<APIBookshelf, 'books'>;

const Info = ({ bookshelfName, bookshelfId, likeCount }: InfoProps) => {
return (
<div className="flex flex-col gap-[1rem]">
<div className="flex flex-col gap-[1rem] px-[2rem]">
<div className="flex items-center justify-between">
<div className="font-body2-bold">{bookshelfName}</div>
<Link href={`/bookshelf/${bookshelfId}`}>
Expand All @@ -45,7 +50,7 @@ type BooksProps = Pick<APIBookshelf, 'books'>;

const Books = ({ books }: BooksProps) => {
return (
<ul className="grid grid-cols-4 px-[0.5rem]">
<ul className="grid grid-cols-4 px-[1.5rem]">
{books.map(book => (
<li key={book.bookId} className="flex justify-center">
<Book {...book} />
Expand All @@ -58,8 +63,10 @@ const Books = ({ books }: BooksProps) => {
const Book = ({
imageUrl,
bookId,
title,
}: Pick<APIBook, 'bookId' | 'title' | 'imageUrl'>) => {
const [bookSpineColor, setBookSpineColor] = useState<string>();
const placeholderClassName = bookSpineColor ? '' : 'bg-blur';

const handleOnLoadImage = (image: HTMLImageElement) => {
const colorThief = new ColorThief();
Expand All @@ -71,44 +78,55 @@ const Book = ({
};

return (
<div
<Link
href={`/book/${bookId}`}
className="relative flex"
style={{
visibility: bookSpineColor ? 'visible' : 'hidden',
transformStyle: 'preserve-3d',
transform: 'perspective(140px)',
}}
>
{/** 책 옆면 (책등) */}
<div
className="h-full w-[1.5rem]"
className={`h-full w-[1.5rem] ${placeholderClassName}`}
style={{
backgroundColor: bookSpineColor,
transform: 'rotateY(320deg) translateX(1rem) translateZ(0.4rem)',
}}
/>
>
{/** 옆면과 표지 사이 여백을 메꾸기 위해 추가 */}
<div
className={`absolute -right-[0.5px] h-full w-[2px] ${placeholderClassName}`}
style={{ backgroundColor: bookSpineColor }}
/>
</div>

{/** 책 하단 그림자 */}
<div
className="absolute bottom-0 h-2 w-[calc(100%-1.5rem)] shadow-[1px_4px_10px_4px_#b1b1b1]"
style={{
transform: 'rotateY(20deg) translateX(1.25rem) translateZ(-0.5rem)',
}}
/>
<Link
className="relative"
href={`/book/${bookId}`}

{/** 책 표지 */}
<div
className="bg-blur relative h-[9.1rem] w-[6.5rem] rounded-[2px] after:absolute after:inset-0 after:border-[1px] after:border-black-900/[.06]"
style={{
transform: 'rotateY(22deg) translateZ(0.3rem)',
}}
>
<Image
src={imageUrl}
width={60}
height={90}
alt="book cover"
alt={title}
onLoadingComplete={handleOnLoadImage}
quality={100}
className=" rounded-[1px] object-cover"
sizes="9.1rem"
fill
style={{ visibility: bookSpineColor ? 'visible' : 'hidden' }}
/>
</Link>
</div>
</div>
</Link>
);
};

Expand Down
Loading

0 comments on commit 2beb0f2

Please sign in to comment.