Skip to content

Commit

Permalink
[#481] [책 상세] 책 정보 컴포넌트 (#482)
Browse files Browse the repository at this point in the history
* style: BookCover 책 상세 페이지 2xlarge 사이즈 수정

* feat: AvatarGroup 컴포넌트 작성

* feat: 책 상세 페이지 BookInfo 컴포넌트 작성

* refactor: 책 상세 query hook 이름 수정, 공백 제거

* chore: 불필요한 useBookTitle hook 제거
  • Loading branch information
gxxrxn authored Feb 7, 2024
1 parent 9a11193 commit b4d3c0a
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 23 deletions.
4 changes: 2 additions & 2 deletions public/icons/bookmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/queries/book/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const bookKeys = {
details: () => [...bookKeys.all, 'detail'] as const,
detail: (bookId: APIBook['bookId']) =>
[...bookKeys.details(), bookId] as const,
bookmark: (bookId: APIBook['bookId']) =>
[...bookKeys.detail(bookId), 'bookmark'] as const,
};

export default bookKeys;
30 changes: 24 additions & 6 deletions src/queries/book/useBookInfoQuery.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { APIBook, APIBookDetail } from '@/types/book';
import useQueryWithSuspense, {
UseQueryOptionWithoutSuspense,
} from '@/hooks/useQueryWithSuspense';
import { UseQueryOptions } from '@tanstack/react-query';

import type { APIBook, APIBookDetail, BookDetail } from '@/types/book';

import bookAPI from '@/apis/book';
import useQueryWithSuspense from '@/hooks/useQueryWithSuspense';
import bookKeys from './key';

const useBookInfoQuery = (
const useBookInfoQuery = <TData = APIBookDetail>(
bookId: APIBook['bookId'],
options?: UseQueryOptionWithoutSuspense<APIBookDetail>
options?: UseQueryOptions<APIBookDetail, unknown, TData>
) =>
useQueryWithSuspense(
bookKeys.detail(bookId),
Expand All @@ -16,3 +17,20 @@ const useBookInfoQuery = (
);

export default useBookInfoQuery;

const transformBookData = (data: APIBookDetail) =>
({
bookId: data.bookId,
title: data.title,
author: data.author,
isbn: data.isbn,
summary: data.contents,
bookUrl: data.url,
imageUrl: data.imageUrl.replace('R120x174.q85', 'R300x0.q100'),
publisher: data.publisher,
} as BookDetail);

export const useBookInfo = (bookId: APIBook['bookId']) =>
useBookInfoQuery(bookId, {
select: transformBookData,
});
18 changes: 8 additions & 10 deletions src/queries/book/useBookUserInfoQuery.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { UseQueryOptions } from '@tanstack/react-query';
import { APIBookmarkedUserList } from '@/types/book';
import bookAPI from '@/apis/book';
import useQueryWithSuspense from '@/hooks/useQueryWithSuspense';
import bookKeys from './key';

const useBookUserInfoQuery = (
const useBookUserInfoQuery = <TData = APIBookmarkedUserList>(
bookId: number,
options?: Pick<
UseQueryOptions<
Awaited<ReturnType<typeof bookAPI.getBookUserInfo>>['data']
>,
'enabled'
>
options?: UseQueryOptions<APIBookmarkedUserList, unknown, TData>
) =>
useQuery(
['bookUserInfo', bookId],
useQueryWithSuspense(
bookKeys.bookmark(bookId),
() => bookAPI.getBookUserInfo(bookId).then(({ data }) => data),
options
);
Expand Down
15 changes: 15 additions & 0 deletions src/stories/book/detail/BookInfo.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Meta, StoryObj } from '@storybook/react';
import BookInfo from '@/v1/book/detail/BookInfo';

const meta: Meta<typeof BookInfo> = {
title: 'book/detail/BookInfo',
component: BookInfo,
};

export default meta;

type Story = StoryObj<typeof BookInfo>;

export const Default: Story = {
args: { bookId: 22 },
};
11 changes: 11 additions & 0 deletions src/types/book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ export interface APIBookDetail extends APIBook {
imageKey: string;
}

export interface BookDetail {
bookId: APIBookDetail['bookId'];
title: APIBookDetail['title'];
author: APIBookDetail['author'];
isbn: APIBookDetail['isbn'];
summary: APIBookDetail['contents'];
bookUrl: APIBookDetail['url'];
imageUrl: APIBookDetail['imageUrl'];
publisher: APIBookDetail['publisher'];
}

export interface APIBookmarkedUserList {
bookId: APIBook['bookId'];
totalCount: number;
Expand Down
25 changes: 22 additions & 3 deletions src/v1/base/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useState } from 'react';
'use client';

import { Children, ReactNode, useState } from 'react';
import Image from 'next/image';

type AvatarSize = 'small' | 'medium' | 'large';
interface AvatarProps {
name?: string;
src?: string;
size?: AvatarSize;
border?: boolean;
}

const FALLBACK_IMAGE_SRC = '/icons/avatar.svg';
Expand Down Expand Up @@ -33,15 +36,17 @@ const getAvatarSize = (size: AvatarSize) => {
}
};

const Avatar = ({ name, src, size = 'medium' }: AvatarProps) => {
const Avatar = ({ name, src, size = 'medium', border }: AvatarProps) => {
const [image, setImage] = useState(src ?? FALLBACK_IMAGE_SRC);

const { sizeClasses, sizeProps } = getAvatarSize(size);
const borderClass = border ? 'border-[0.15rem]' : 'border-none';

const setFallbackImage = () => setImage(FALLBACK_IMAGE_SRC);

return (
<span
className={`relative inline-block rounded-full bg-white ${sizeClasses}`}
className={`relative inline-block rounded-full border-white bg-white ${sizeClasses} ${borderClass}`}
>
<Image
alt={name || 'avatar'}
Expand All @@ -55,3 +60,17 @@ const Avatar = ({ name, src, size = 'medium' }: AvatarProps) => {
};

export default Avatar;

const AvatarGroup = ({ children }: { children?: ReactNode }) => {
return (
<div className="mr-[0.75rem] flex flex-row-reverse items-center justify-end">
{Children.toArray(children).map((avatar, idx) => (
<span key={idx} className={`-me-[0.75rem] leading-none`}>
{avatar}
</span>
))}
</div>
);
};

export { AvatarGroup };
4 changes: 2 additions & 2 deletions src/v1/book/BookCover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ const getCoverSize = (size: BookCoverSize) => {
}
case '2xlarge': {
return {
sizeClasses: 'w-[18.0rem] h-[25.2rem]',
sizeProps: { width: 180, height: 252 },
sizeClasses: 'w-[14.0rem] h-[19.6rem]',
sizeProps: { width: 140, height: 196 },
} as const;
}
}
Expand Down
103 changes: 103 additions & 0 deletions src/v1/book/detail/BookInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { APIBook } from '@/types/book';
import { IconBookmark } from '@public/icons';
import { useBookInfo } from '@/queries/book/useBookInfoQuery';

import Avatar, { AvatarGroup } from '@/v1/base/Avatar';
import BookCover from '@/v1/book/BookCover';
import useBookUserInfoQuery from '@/queries/book/useBookUserInfoQuery';

const BookInfo = ({ bookId }: { bookId: APIBook['bookId'] }) => {
const { data } = useBookInfo(bookId);
const { title, author, imageUrl, summary, bookUrl } = data;

return (
<div className="flex flex-col gap-[2rem] rounded-l-[1.5rem] bg-white p-[2rem] shadow-bookcard">
<div className="flex items-end gap-[2rem]">
<BookCover size="2xlarge" src={imageUrl} />

<div className="flex flex-col gap-[0.5rem]">
<BookmarkButton />
<BookTitle title={title} />
<BookAuthor author={author} />
</div>
</div>

<BookSummary summary={summary} bookUrl={bookUrl} />
<BookmarkUserInfo bookId={bookId} />
</div>
);
};

export default BookInfo;

const BookTitle = ({ title }: { title: string }) => (
<p className="text-lg font-bold">{title}</p>
);

const BookAuthor = ({ author }: { author: string }) => (
<p className="text-sm">{author}</p>
);

const BookmarkButton = () => {
return (
<IconBookmark
className="mb-[0.5rem] h-[2.4rem] w-[2.4rem] cursor-pointer stroke-main-900 stroke-[0.15rem]"
fill="white"
/>
);
};

const BookSummary = ({
summary,
bookUrl,
}: {
summary: string;
bookUrl: string;
}) => (
<p className="text-md">
{summary}&nbsp;...&nbsp;
{bookUrl && (
<a target="_blank" href={bookUrl}>
<span className="cursor-pointer text-main-900">더보기</span>
</a>
)}
</p>
);

const BookmarkUserInfo = ({ bookId }: { bookId: APIBook['bookId'] }) => {
const { data } = useBookUserInfoQuery(bookId);
const { totalCount, users } = data;
const avatarCount = users.length;

return (
<div className="flex items-center gap-[0.5rem]">
{avatarCount !== 0 && (
<AvatarGroup>
{users.map(({ userId, profileImage }) => (
<a key={userId} href={`/profile/${userId}`}>
<Avatar src={profileImage} border />
</a>
))}
</AvatarGroup>
)}
<p className="text-sm">
{getBookmarkedUserCountText(totalCount, avatarCount)}
</p>
</div>
);
};

const getBookmarkedUserCountText = (
totalCount: number,
avatarCount: number
) => {
const otherCount = totalCount - avatarCount;

if (otherCount === 0 && totalCount === 0) {
return '아직 이 책을 책장에 꽂은 사람이 없어요.';
} else if (otherCount === 0) {
return '님이 이 책을 책장에 꽂았어요.';
}

return `외 ${otherCount}명이 이 책을 책장에 꽂았어요.`;
};

0 comments on commit b4d3c0a

Please sign in to comment.