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

[#449] [책장] 책장 상세 페이지 #451

Merged
merged 16 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b9e099c
refactor: 책장 컴포넌트 추상화
minjongbaek Nov 27, 2023
b4ba64e
feat: useBookshelfBooksQuery 에서 책 데이터를 2차원 배열로 가공하도록 select 추가
minjongbaek Nov 27, 2023
d756fb0
refactor: 책장 페이지에서 쓰일 BookShelfRow 컴포넌트 작성
minjongbaek Nov 28, 2023
8d01777
feat: 책장 페이지 비회원인 경우 카카오 로그인을 유도하도록 구현
minjongbaek Nov 28, 2023
adf08e9
refactor: useBookShelfBookListQuery 에 쿼리키 팩토리 적용
minjongbaek Nov 28, 2023
b55f27d
refactor: 새로운 Toast 컴포넌트로 교체
minjongbaek Dec 2, 2023
8571a5e
feat: 로그인 하지 않은 사용자는 좋아요 버튼을 누를 수 없도록 처리
minjongbaek Dec 2, 2023
bd028f2
chore: 오타 수정
minjongbaek Dec 2, 2023
94e965f
feat: 좋아요 상태에 따라 버튼 UI 가 달라지도록 구현
minjongbaek Dec 2, 2023
281a826
chore: heart.svg 아이콘을 배경이 채워져있는 아이콘으로 교체
minjongbaek Dec 2, 2023
f9bcaa8
feat: 책이 왼쪽부터 차례로 배치되도록 구현
minjongbaek Dec 2, 2023
658fa06
refactor: bookShelf 쿼리 키 리팩터링
minjongbaek Dec 2, 2023
3ae3e07
chore: 파일명 대소문자 변경을 위한 더미 커밋
minjongbaek Dec 2, 2023
f4e6f32
chore: 파일명 대소문자 변경
minjongbaek Dec 2, 2023
78a80c6
feat: 책장이 레이아웃에 잡혀진 패딩을 넘어서 렌더링 되도록 구현
minjongbaek Dec 2, 2023
ba304bc
feat: 좋아요 버튼의 색상 반전 처리
minjongbaek Dec 4, 2023
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
4 changes: 2 additions & 2 deletions public/icons/heart.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
284 changes: 151 additions & 133 deletions src/app/bookshelf/[bookshelfId]/page.tsx
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 },
Expand All @@ -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;
Comment on lines -91 to +40
Copy link
Member

Choose a reason for hiding this comment

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

comment;

매직 스트링을 잘 바꿔주셨네요 👍


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() ? (
Copy link
Member

Choose a reason for hiding this comment

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

p2;

로그인 하지 않은 사용자가 책장 좋아요 버튼을 누를 수 없도록 인터랙션을 막으면 좋을 것 같아요!

책장좋아요이슈

Copy link
Member Author

Choose a reason for hiding this comment

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

@gxxrxn 반영해두었어요! 8571a5e

<>
{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' },
];
11 changes: 11 additions & 0 deletions src/queries/bookshelf/key.ts
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;
Copy link
Member

Choose a reason for hiding this comment

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

p3;

useBookshelfLikeMutation에서 사용하는 bookShelf(id)와 관련된 query key도 해당 파일에서 관리하면 어떨까요? 책장 좋아요 버튼을 클릭할 때 해당 훅들이 사용되는데, 나중에 적용하려고 하면 놓칠 것 같다는 생각이 들어요 😲

Copy link
Member Author

Choose a reason for hiding this comment

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

@gxxrxn 반영했어요 ! f4e6f32

37 changes: 37 additions & 0 deletions src/queries/bookshelf/useBookShelfBookListQuery.ts
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;
Loading