Skip to content
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

[#591] 3D 책 컴포넌트, 책장 페이지 개선 #594

Merged
merged 5 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}

Comment on lines +67 to +74
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment;

👍

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
Loading