Skip to content

Commit

Permalink
[#489] [도서 검색] 도서검색 페이지 개선 작업 (#490)
Browse files Browse the repository at this point in the history
* refactor: BookSearchInput을 Input 공통 컴포넌트로 대체

* refactor: 검색 기능에 setState 제거 및 react-hook-form 적용

* chore: BookSearchInput 스토리 제거

* refactor: 검색페이지 useQuery를 useQueryWithSuspense로 교체

* feat: 도서 검색 페이지 스켈레톤 추가

* fix: storybook build 에러 수정

* fix: BestSellers 스토리북 오류 수정

* chore: 최근 검색어 시맨틱 태그 적용

* fix: 도서 검색 페이지 Skeleton 작성 부 수정

* refactor: 도서 검색 관련 useQuery에 쿼리키 팩토리 패턴 적용

* refactor: book 쿼리 키에 누락된 리터럴 타입 적용

* refactor: RecentSearch 컴포넌트의 react-hook-form 결합도 개선

* fix: storybook 빌드에러 수정 (args 수정)

* refactor: BestSeller, RecentSearch 쿼리 함수의 options 타입 수정
  • Loading branch information
hanyugeon authored Mar 13, 2024
1 parent 1e67705 commit 9131fdf
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 144 deletions.
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('선택한 검색어 검색!'),
},
};
49 changes: 46 additions & 3 deletions src/v1/bookSearch/BestSellers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import type { APIBestSellerSearchRange, APISearchedBook } from '@/types/book';
import useBestSellersQuery from '@/queries/book/useBestSellersQuery';
import bookAPI from '@/apis/book';

import BookCover from '@/v1/book/BookCover';
import useToast from '@/v1/base/Toast/useToast';

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

const SEARCH_RANGES = {
주간: 'WEEKLY',
월간: 'MONTHLY',
Expand Down Expand Up @@ -49,9 +51,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]">
<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 @@ -152,3 +154,44 @@ const BestSeller = ({
</div>
);
};

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]'
Loading

0 comments on commit 9131fdf

Please sign in to comment.