From 0043fe76f4a8a7f6c784b13fc9f27cdf842d7931 Mon Sep 17 00:00:00 2001 From: harry kim <73218463+hanyugeon@users.noreply.github.com> Date: Wed, 13 Mar 2024 23:24:43 +0900 Subject: [PATCH] =?UTF-8?q?[#489]=20[=EB=8F=84=EC=84=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89]=20=EB=8F=84=EC=84=9C=EA=B2=80=EC=83=89=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B0=9C=EC=84=A0=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?(#490)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 타입 수정 --- .storybook/preview.tsx | 20 ++++++ src/app/book/search/page.tsx | 69 +++++++++++-------- src/queries/book/key.ts | 7 +- src/queries/book/useBestSellersQuery.ts | 11 +-- src/queries/book/useBookSearchQuery.ts | 6 +- src/queries/book/useRecentSearchesQuery.ts | 15 ++-- .../bookSearch/BestSellers.stories.tsx | 40 +---------- .../bookSearch/BookSearchInput.stories.tsx | 18 ----- .../bookSearch/RecentSearch.stories.tsx | 4 +- src/v1/bookSearch/BestSellers.tsx | 49 ++++++++++++- src/v1/bookSearch/BookSearchInput.tsx | 23 ------- src/v1/bookSearch/RecentSearch.tsx | 51 +++++++++----- 12 files changed, 169 insertions(+), 144 deletions(-) delete mode 100644 src/stories/bookSearch/BookSearchInput.stories.tsx delete mode 100644 src/v1/bookSearch/BookSearchInput.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 919cde76..7e72a23c 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -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({ diff --git a/src/app/book/search/page.tsx b/src/app/book/search/page.tsx index 91eb837c..8cc087a9 100644 --- a/src/app/book/search/page.tsx +++ b/src/app/book/search/page.tsx @@ -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(''); - const queryKeyword = useDebounceValue(inputSearchValue, 1000); + const { register, watch, setValue } = useForm({ + mode: 'all', + defaultValues: { + searchValue: '', + }, + }); + + const queryKeyword = useDebounceValue(watch('searchValue'), 1000); const { ref: inViewRef, inView } = useInView(); @@ -44,16 +54,6 @@ const BookSearch = () => { ? recentSearchesInfo.data.bookRecentSearchResponses : undefined; - const handleInputValueChange = ( - event: React.ChangeEvent - ) => { - if (!event.target) return; - - const inputValue = event.target.value; - - setInputSearchValue(inputValue.trim()); - }; - useEffect(() => { if (inView && bookSearchInfo.hasNextPage) { bookSearchInfo.fetchNextPage(); @@ -70,27 +70,40 @@ const BookSearch = () => { <>
- - {inputSearchValue ? ( +
+ + +
+ {watch('searchValue') ? ( <>
) : ( - <> + }> setValue('searchValue', keyword)} /> - + )}
); }; +const ContentsSkelton = () => { + return ( + <> + + + + ); +}; + export default BookSearch; diff --git a/src/queries/book/key.ts b/src/queries/book/key.ts index b6d4e61e..02b990f8 100644 --- a/src/queries/book/key.ts +++ b/src/queries/book/key.ts @@ -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; diff --git a/src/queries/book/useBestSellersQuery.ts b/src/queries/book/useBestSellersQuery.ts index 4a370f48..c66e56c2 100644 --- a/src/queries/book/useBestSellersQuery.ts +++ b/src/queries/book/useBestSellersQuery.ts @@ -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) => - useQuery( +const useBestSellersQuery = ( + options?: UseQueryOptionWithoutSuspense +) => + useQueryWithSuspense( bookKeys.bestSeller(), () => bookAPI.getBestSellers().then(({ data }) => data), options diff --git a/src/queries/book/useBookSearchQuery.ts b/src/queries/book/useBookSearchQuery.ts index 6ef21799..e1f0e1c8 100644 --- a/src/queries/book/useBookSearchQuery.ts +++ b/src/queries/book/useBookSearchQuery.ts @@ -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, @@ -11,7 +13,7 @@ const useBookSearchQuery = ({ pageSize: number; }) => useInfiniteQuery( - ['booksearch', query], + bookKeys.bookSearch(query), ({ pageParam = page }) => bookAPI .searchBooks({ query, page: pageParam, pageSize }) diff --git a/src/queries/book/useRecentSearchesQuery.ts b/src/queries/book/useRecentSearchesQuery.ts index 518e3fc1..4838b88a 100644 --- a/src/queries/book/useRecentSearchesQuery.ts +++ b/src/queries/book/useRecentSearchesQuery.ts @@ -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) => - useQuery( - ['recentSearches'], +import bookKeys from './key'; + +const useRecentSearchesQuery = ( + options?: UseQueryOptionWithoutSuspense +) => + useQueryWithSuspense( + bookKeys.recentSearch(), () => bookAPI.getRecentSearches().then(({ data }) => data), options ); diff --git a/src/stories/bookSearch/BestSellers.stories.tsx b/src/stories/bookSearch/BestSellers.stories.tsx index e6faf9a6..6cd49747 100644 --- a/src/stories/bookSearch/BestSellers.stories.tsx +++ b/src/stories/bookSearch/BestSellers.stories.tsx @@ -11,42 +11,4 @@ export default meta; type Story = StoryObj; -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 = {}; diff --git a/src/stories/bookSearch/BookSearchInput.stories.tsx b/src/stories/bookSearch/BookSearchInput.stories.tsx deleted file mode 100644 index a86fec85..00000000 --- a/src/stories/bookSearch/BookSearchInput.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import BookSearchInput from '@/v1/bookSearch/BookSearchInput'; - -const meta: Meta = { - title: 'bookSearch/BookSearchInput', - component: BookSearchInput, - tags: ['autodocs'], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - value: '', - }, -}; diff --git a/src/stories/bookSearch/RecentSearch.stories.tsx b/src/stories/bookSearch/RecentSearch.stories.tsx index 38ce6a5c..c4f2ffc3 100644 --- a/src/stories/bookSearch/RecentSearch.stories.tsx +++ b/src/stories/bookSearch/RecentSearch.stories.tsx @@ -14,7 +14,7 @@ type Story = StoryObj; export const Default: Story = { args: { recentSearches: undefined, - setInputSearchValue: () => alert('선택한 검색어 검색!'), + onClick: () => alert('선택한 검색어 검색!'), }, }; @@ -29,6 +29,6 @@ export const RecentSearches: Story = { { keyword: '풀어', modifiedAt: 'now' }, { keyword: '어때', modifiedAt: 'now' }, ], - setInputSearchValue: () => alert('선택한 검색어 검색!'), + onClick: () => alert('선택한 검색어 검색!'), }, }; diff --git a/src/v1/bookSearch/BestSellers.tsx b/src/v1/bookSearch/BestSellers.tsx index 36751c84..522a7f45 100644 --- a/src/v1/bookSearch/BestSellers.tsx +++ b/src/v1/bookSearch/BestSellers.tsx @@ -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', @@ -49,9 +51,9 @@ const BestSellers = () => {

인기 도서

    -
    +
  • 종합

    -
  • +
    @@ -152,3 +154,44 @@ const BestSeller = ({ ); }; + +const BestSellerSkeleton = () => { + return ( +
    + +
    + +
    + + +
    +
    +
    + ); +}; + +export const BestSellersSkeleton = () => { + return ( + +
    + +
      + +
    +
      + + + +
    +
      + + + + +
    +
    +
    + ); +}; + +// 'w-[11.0rem] h-[15.4rem]' diff --git a/src/v1/bookSearch/BookSearchInput.tsx b/src/v1/bookSearch/BookSearchInput.tsx deleted file mode 100644 index dd26ee29..00000000 --- a/src/v1/bookSearch/BookSearchInput.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ChangeEventHandler, ComponentPropsWithoutRef } from 'react'; - -import { IconSearch } from '@public/icons'; - -type BookSearchInputProps = { - onChange: ChangeEventHandler; -} & ComponentPropsWithoutRef<'input'>; - -const BookSearchInput = ({ onChange, ...props }: BookSearchInputProps) => { - return ( -
    - - -
    - ); -}; - -export default BookSearchInput; diff --git a/src/v1/bookSearch/RecentSearch.tsx b/src/v1/bookSearch/RecentSearch.tsx index 7e1f8050..be8c89c2 100644 --- a/src/v1/bookSearch/RecentSearch.tsx +++ b/src/v1/bookSearch/RecentSearch.tsx @@ -1,34 +1,33 @@ import type { APIBookRecentSearchResponse } from '@/types/book'; import Button from '@/v1/base/Button'; +import Skeleton from '@/v1/base/Skeleton'; type RecentSearchProps = { recentSearches?: APIBookRecentSearchResponse[]; - setInputSearchValue: (searchValue: string) => void; + onClick: (keyword: string) => void; }; -const RecentSearch = ({ - recentSearches, - setInputSearchValue, -}: RecentSearchProps) => { +const RecentSearch = ({ recentSearches, onClick }: RecentSearchProps) => { return (

    최근 검색어

    {recentSearches ? ( -
    - {recentSearches.map(value => ( - +
      + {recentSearches.map(item => ( +
    • + +
    • ))} -
    +
) : (

검색 기록이 없어요! @@ -39,3 +38,19 @@ const RecentSearch = ({ }; export default RecentSearch; + +export const RecentSearchSkeleton = () => { + return ( + +

+ +
    + + + + +
+
+ + ); +};