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

[#489] [도서 검색] 도서검색 페이지 개선 작업 #490

Merged
merged 15 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
20 changes: 20 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ initialize({}, [

return res(ctx.json({ ...originResponseData }));
}),
rest.get(
nextApi('/aladin-api?QueryType=Bestseller&Cover=Big'),
async (req, res, ctx) => {
return res(
ctx.json({
item: [
{
isbn: '9791162242742',
title: '리팩터링',
author: '마틴 파울러',
cover:
'https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F5326912%3Ftimestamp%3D20231207165435',
bestRank: 1,
link: 'https://search.daum.net/search?w=bookpage&bookId=5326912&q=%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81',
},
],
})
);
}
),
]);

const queryClient = new QueryClient({
Expand Down
69 changes: 41 additions & 28 deletions src/app/book/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
'use client';

import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import { useForm } from 'react-hook-form';

import useBookSearchQuery from '@/queries/book/useBookSearchQuery';
import useRecentSearchesQuery from '@/queries/book/useRecentSearchesQuery';

import SSRSafeSuspense from '@/components/SSRSafeSuspense';
import useDebounceValue from '@/hooks/useDebounce';
import { checkAuthentication } from '@/utils/helpers';

import { IconSearch } from '@public/icons';
import TopHeader from '@/v1/base/TopHeader';
import BookSearchInput from '@/v1/bookSearch/BookSearchInput';
import BestSellers from '@/v1/bookSearch/BestSellers';
import RecentSearch from '@/v1/bookSearch/RecentSearch';
import Input from '@/v1/base/Input';
import RecentSearch, {
RecentSearchSkeleton,
} from '@/v1/bookSearch/RecentSearch';
import BestSellers, { BestSellersSkeleton } from '@/v1/bookSearch/BestSellers';
import BookSearchResults from '@/v1/bookSearch/SearchResult';

/**
* @todo
* recentSearchedInfo 계속해서 refetch되는 현상 고치기
*/
type FormValues = {
searchValue: string;
};

const BookSearch = () => {
const isAuthenticated = checkAuthentication();
const [inputSearchValue, setInputSearchValue] = useState<string>('');

const queryKeyword = useDebounceValue(inputSearchValue, 1000);
const { register, watch, setValue } = useForm<FormValues>({
mode: 'all',
defaultValues: {
searchValue: '',
},
});

const queryKeyword = useDebounceValue(watch('searchValue'), 1000);

const { ref: inViewRef, inView } = useInView();

Expand All @@ -44,16 +54,6 @@ const BookSearch = () => {
? recentSearchesInfo.data.bookRecentSearchResponses
: undefined;

const handleInputValueChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (!event.target) return;

const inputValue = event.target.value;

setInputSearchValue(inputValue.trim());
};

useEffect(() => {
if (inView && bookSearchInfo.hasNextPage) {
bookSearchInfo.fetchNextPage();
Expand All @@ -70,27 +70,40 @@ const BookSearch = () => {
<>
<TopHeader text={'Discover'} />
<article className="flex h-full w-full flex-col gap-[3.8rem]">
<BookSearchInput
value={inputSearchValue}
onChange={handleInputValueChange}
/>
{inputSearchValue ? (
<div className="flex w-full items-center gap-[2rem] border-b-[0.05rem] border-black-900 p-[1rem] focus-within:border-main-900 [&>div]:w-full">
<IconSearch className="fill-black h-[2.1rem] w-[2.1rem]" />
<Input
className="w-full appearance-none text-sm font-normal focus:outline-none"
placeholder="책 제목, 작가를 검색해보세요"
{...register('searchValue')}
/>
</div>
{watch('searchValue') ? (
<>
<BookSearchResults searchedBooks={searchedBooks} />
<div ref={inViewRef} />
</>
) : (
<>
<SSRSafeSuspense fallback={<ContentsSkelton />}>
<RecentSearch
recentSearches={recentSearches}
setInputSearchValue={setInputSearchValue}
onClick={(keyword: string) => setValue('searchValue', keyword)}
/>
<BestSellers />
</>
</SSRSafeSuspense>
)}
</article>
</>
);
};

const ContentsSkelton = () => {
return (
<>
<RecentSearchSkeleton />
<BestSellersSkeleton />
</>
);
};

export default BookSearch;
7 changes: 5 additions & 2 deletions src/queries/book/key.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { APIBook } from '@/types/book';

const bookKeys = {
all: ['book'],
all: ['book'] as const,
details: () => [...bookKeys.all, 'detail'] as const,
detail: (bookId: APIBook['bookId']) =>
[...bookKeys.details(), bookId] as const,
bestSeller: () => [...bookKeys.all, 'bestSeller'],
bookmark: (bookId: APIBook['bookId']) =>
[...bookKeys.detail(bookId), 'bookmark'] as const,
comments: (bookId: APIBook['bookId']) =>
[...bookKeys.detail(bookId), 'comments'] as const,
bookSearch: (query: string) =>
[...bookKeys.all, 'bookSearch', query] as const,
recentSearch: () => [...bookKeys.all, 'recentSearch'] as const,
bestSeller: () => [...bookKeys.all, 'bestSeller'] as const,
};

export default bookKeys;
11 changes: 7 additions & 4 deletions src/queries/book/useBestSellersQuery.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useQuery } from '@tanstack/react-query';
import type { QueryOptions } from '@/types/query';
import useQueryWithSuspense, {
type UseQueryOptionWithoutSuspense,
} from '@/hooks/useQueryWithSuspense';

import bookAPI from '@/apis/book';
import type { APIBestSellerRes } from '@/types/book';

import bookKeys from './key';

const useBestSellersQuery = (options?: QueryOptions<APIBestSellerRes>) =>
useQuery(
const useBestSellersQuery = <TData = APIBestSellerRes>(
options?: UseQueryOptionWithoutSuspense<APIBestSellerRes, unknown, TData>
) =>
useQueryWithSuspense(
bookKeys.bestSeller(),
() => bookAPI.getBestSellers().then(({ data }) => data),
options
Expand Down
6 changes: 4 additions & 2 deletions src/queries/book/useBookSearchQuery.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import bookAPI from '@/apis/book';
import { useInfiniteQuery } from '@tanstack/react-query';

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

const useBookSearchQuery = ({
query,
page,
Expand All @@ -11,7 +13,7 @@ const useBookSearchQuery = ({
pageSize: number;
}) =>
useInfiniteQuery(
['booksearch', query],
bookKeys.bookSearch(query),
({ pageParam = page }) =>
bookAPI
.searchBooks({ query, page: pageParam, pageSize })
Expand Down
15 changes: 10 additions & 5 deletions src/queries/book/useRecentSearchesQuery.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import useQueryWithSuspense, {
type UseQueryOptionWithoutSuspense,
} from '@/hooks/useQueryWithSuspense';

import bookAPI from '@/apis/book';
import type { APIRecentSearches } from '@/types/book';
import type { QueryOptions } from '@/types/query';

const useRecentSearchesQuery = (options?: QueryOptions<APIRecentSearches>) =>
useQuery(
['recentSearches'],
import bookKeys from './key';

const useRecentSearchesQuery = <TData = APIRecentSearches>(
options?: UseQueryOptionWithoutSuspense<APIRecentSearches, unknown, TData>
) =>
useQueryWithSuspense(
bookKeys.recentSearch(),
() => bookAPI.getRecentSearches().then(({ data }) => data),
options
);
Expand Down
40 changes: 1 addition & 39 deletions src/stories/bookSearch/BestSellers.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,4 @@ export default meta;

type Story = StoryObj<typeof BestSellers>;

type BestSellerProps = {
isbn: string;
title: string;
author: string;
cover: string;
bestRank: number;
link: string;
};

const BESTSELLER: BestSellerProps = {
isbn: '9791162242742',
title: '리팩터링',
author: '마틴 파울러',
cover:
'https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F5326912%3Ftimestamp%3D20231207165435',
bestRank: 1,
link: 'https://search.daum.net/search?w=bookpage&bookId=5326912&q=%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81',
};

export const Default: Story = {
args: {
bestSellers: [BESTSELLER, BESTSELLER, BESTSELLER],
searchRange: 'WEEKLY',
},
};

export const MonthlyBestSeller: Story = {
args: {
bestSellers: [],
searchRange: 'MONTHLY',
},
};

export const YearlyBestSeller: Story = {
args: {
bestSellers: [],
searchRange: 'YEARLY',
},
};
export const Default: Story = {};
18 changes: 0 additions & 18 deletions src/stories/bookSearch/BookSearchInput.stories.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions src/stories/bookSearch/RecentSearch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type Story = StoryObj<typeof RecentSearch>;
export const Default: Story = {
args: {
recentSearches: undefined,
setInputSearchValue: () => alert('선택한 검색어 검색!'),
onClick: () => alert('선택한 검색어 검색!'),
},
};

Expand All @@ -29,6 +29,6 @@ export const RecentSearches: Story = {
{ keyword: '풀어', modifiedAt: 'now' },
{ keyword: '어때', modifiedAt: 'now' },
],
setInputSearchValue: () => alert('선택한 검색어 검색!'),
onClick: () => alert('선택한 검색어 검색!'),
},
};
46 changes: 44 additions & 2 deletions src/v1/bookSearch/BestSellers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { APIBestSellerSearchRange } from '@/types/book';
import useBestSellersQuery from '@/queries/book/useBestSellersQuery';

import BookCover from '@/v1/book/BookCover';
import Skeleton from '@/v1/base/Skeleton';

const SEARCH_RANGES = {
주간: 'WEEKLY',
Expand All @@ -30,9 +31,9 @@ const BestSellers = () => {
<section className="flex flex-col gap-[1.7rem]">
<h2 className="h-[2.4rem] text-lg">인기 도서</h2>
<ul className="flex w-[calc(100%+2rem)] gap-[0.8rem] overflow-x-scroll whitespace-nowrap">
<div className="rounded-[1.5rem] bg-[#5C5C5C] px-[1.5rem] py-[0.3rem]">
<li className="rounded-[1.5rem] bg-[#5C5C5C] px-[1.5rem] py-[0.3rem]">
hanyugeon marked this conversation as resolved.
Show resolved Hide resolved
<p className="text-sm font-normal text-white">종합</p>
</div>
</li>
</ul>

<ul className="flex w-full flex-row items-center divide-x divide-black-900 text-sm">
Expand Down Expand Up @@ -111,3 +112,44 @@ const BestSeller = ({
</Link>
);
};

const BestSellerSkeleton = () => {
return (
<div className="flex w-[12.7rem] flex-col gap-[1.3rem] px-[0.7rem]">
<Skeleton.Rect width="11rem" height="15.4rem" rounded="medium" />
<div className="flex flex-row gap-[1rem]">
<Skeleton.Text width="1.25rem" fontSize="2xlarge" />
<div className="flex min-w-0 flex-col gap-[0.3rem]">
<Skeleton.Text width="8.75rem" fontSize="2xlarge" />
<Skeleton.Text width="7.25rem" fontSize="xlarge" />
</div>
</div>
</div>
);
};

export const BestSellersSkeleton = () => {
return (
<Skeleton>
<section className="flex flex-col gap-[1.7rem]">
<Skeleton.Text width="7rem" fontSize="2xlarge" />
<ul className="flex w-full gap-[1rem] pb-[1rem]">
<Skeleton.Rect width="5.5rem" height="2.7rem" rounded="large" />
</ul>
<ul className="flex w-[12.8rem] flex-row justify-around">
<Skeleton.Text width="2.5rem" fontSize="xsmall" />
<Skeleton.Text width="2.5rem" fontSize="xsmall" />
<Skeleton.Text width="2.5rem" fontSize="xsmall" />
</ul>
<ul className="flex w-[calc(100%+2rem)] overflow-x-scroll">
<BestSellerSkeleton />
<BestSellerSkeleton />
<BestSellerSkeleton />
<BestSellerSkeleton />
</ul>
</section>
</Skeleton>
);
};

// 'w-[11.0rem] h-[15.4rem]'
23 changes: 0 additions & 23 deletions src/v1/bookSearch/BookSearchInput.tsx

This file was deleted.

Loading
Loading