From 8ccf30a3a137931b9dc14706680611e2decfaed9 Mon Sep 17 00:00:00 2001 From: Bori <85009583+Bori-github@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:50:15 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EC=97=90=20=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=20(#255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ #245 - 정렬 관련 상수 선언 및 타입 정의 * ✨ #254 - useDiary에 sortBy 매개변수 추가 * ✨ #254 - 검색 결과 정렬 기능 구현 * ♻️ #254 - 정렬 기준 변경 시 정렬 목록을 순회하며 연산하는 코드 개선 * ♻️ #254 - 정렬 목록 초기값을 복사하여 useState에 적용 --- src/api/diaries.ts | 2 + src/components/search/SearchResultHeader.tsx | 59 ++++++++------------ src/constants/search/index.ts | 1 + src/constants/search/sortBy.ts | 23 ++++++++ src/hooks/services/queries/useDiaries.ts | 6 +- src/pages/search/[keyword].tsx | 24 ++++++-- src/types/diary.ts | 2 + src/types/search.ts | 10 ++++ 8 files changed, 86 insertions(+), 41 deletions(-) create mode 100644 src/constants/search/index.ts create mode 100644 src/constants/search/sortBy.ts diff --git a/src/api/diaries.ts b/src/api/diaries.ts index fb57b924..7320401b 100644 --- a/src/api/diaries.ts +++ b/src/api/diaries.ts @@ -16,6 +16,7 @@ import axios from 'lib/axios'; export const getDiaries = async ({ currentPage, searchKeyword, + sortBy, }: GetDiariesRequest) => { const currentPageIndex = currentPage - 1; const { @@ -25,6 +26,7 @@ export const getDiaries = async ({ skip: PAGE_SIZE * currentPageIndex, take: PAGE_SIZE, searchKeyword, + sortBy, }, }); diff --git a/src/components/search/SearchResultHeader.tsx b/src/components/search/SearchResultHeader.tsx index a268333c..cd50e5e6 100644 --- a/src/components/search/SearchResultHeader.tsx +++ b/src/components/search/SearchResultHeader.tsx @@ -1,51 +1,40 @@ import styled from '@emotion/styled'; -import { useState } from 'react'; +import { useCallback, type Dispatch, type SetStateAction } from 'react'; +import type { SortByOption } from 'types/search'; import { ArrowDownIcon, CheckIcon } from 'assets/icons'; import { Popover } from 'components/common'; import { useClickOutside } from 'hooks/common'; interface SearchResultHeaderProps { totalCount: number; + selectedSortOption: SortByOption; + sortOptions: SortByOption[]; + setSortOptions: Dispatch>; } -interface SortOptions { - id: string; - title: string; - selected: boolean; -} - -const initialSortOptions = [ - { - id: 'latest', - title: '최신순', - selected: true, - }, - { - id: 'comments', - title: '댓글순', - selected: false, - }, -]; - -export const SearchResultHeader = ({ totalCount }: SearchResultHeaderProps) => { - const [sortOptions, setSortOptions] = - useState(initialSortOptions); - +export const SearchResultHeader = ({ + totalCount, + selectedSortOption, + sortOptions, + setSortOptions, +}: SearchResultHeaderProps) => { const { ref, isVisible, setIsVisible } = useClickOutside(); const handleSortSearchResult = () => { setIsVisible((state) => !state); }; - // TODO: 정렬 기능 추가 - const handleSelectSortOption = (selectedIndex: number) => () => { - setSortOptions((prevState) => { - return prevState.map((state, stateIndex) => ({ - ...state, - selected: stateIndex === selectedIndex, - })); - }); - }; + const handleSelectSortOption = useCallback( + (selectedIndex: number) => () => { + setSortOptions((prevState) => { + return prevState.map((state, stateIndex) => ({ + ...state, + selected: stateIndex === selectedIndex, + })); + }); + }, + [], + ); return ( @@ -55,11 +44,11 @@ export const SearchResultHeader = ({ totalCount }: SearchResultHeaderProps) => { type="button" onClick={handleSortSearchResult} > - {sortOptions.find((option) => option.selected)?.title} + {selectedSortOption.title} {isVisible && ( - +
    {sortOptions.map((option, index) => { const { id, selected, title } = option; diff --git a/src/constants/search/index.ts b/src/constants/search/index.ts new file mode 100644 index 00000000..1308707f --- /dev/null +++ b/src/constants/search/index.ts @@ -0,0 +1 @@ +export * from './sortBy'; diff --git a/src/constants/search/sortBy.ts b/src/constants/search/sortBy.ts new file mode 100644 index 00000000..716c52c1 --- /dev/null +++ b/src/constants/search/sortBy.ts @@ -0,0 +1,23 @@ +export const SORT_BY_ID = { + latest: 'latest', + popularity: 'popularity', + comments: 'comments', +} as const; + +export const INITIAL_SORT_BY_LIST = [ + { + id: SORT_BY_ID.latest, + title: '최신순', + selected: true, + }, + { + id: SORT_BY_ID.popularity, + title: '인기순', + selected: false, + }, + { + id: SORT_BY_ID.comments, + title: '댓글순', + selected: false, + }, +] as const; diff --git a/src/hooks/services/queries/useDiaries.ts b/src/hooks/services/queries/useDiaries.ts index 165dfd25..84fcc707 100644 --- a/src/hooks/services/queries/useDiaries.ts +++ b/src/hooks/services/queries/useDiaries.ts @@ -1,15 +1,17 @@ import { useInfiniteQuery } from '@tanstack/react-query'; +import type { SortByType } from 'types/search'; import * as api from 'api'; import { queryKeys } from 'constants/services'; -export const useDiaries = (searchKeyword?: string) => { +export const useDiaries = (searchKeyword?: string, sortBy?: SortByType) => { const { data, isFetching, isFetchingNextPage, isError, fetchNextPage } = useInfiniteQuery({ - queryKey: [queryKeys.diaries, searchKeyword], + queryKey: [queryKeys.diaries, searchKeyword, sortBy], queryFn: async ({ pageParam = 1 }) => await api.getDiaries({ currentPage: pageParam as number, searchKeyword, + sortBy, }), getNextPageParam: (lastPage) => lastPage.nextPage, }); diff --git a/src/pages/search/[keyword].tsx b/src/pages/search/[keyword].tsx index fd2879c4..5618241b 100644 --- a/src/pages/search/[keyword].tsx +++ b/src/pages/search/[keyword].tsx @@ -1,12 +1,13 @@ import styled from '@emotion/styled'; import { getServerSession } from 'next-auth'; +import { useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import type { GetServerSideProps, InferGetServerSidePropsType, NextPage, } from 'next'; -import type { SearchForm } from 'types/search'; +import type { SearchForm, SortByOption } from 'types/search'; import { ObserverTarget, Seo } from 'components/common'; import { DiariesContainer } from 'components/diary'; import { @@ -15,6 +16,7 @@ import { SearchResultHeader, } from 'components/search'; import { PAGE_PATH } from 'constants/common'; +import { INITIAL_SORT_BY_LIST } from 'constants/search'; import { useIntersectionObserver } from 'hooks/common'; import { useDiaries } from 'hooks/services'; import { authOptions } from 'pages/api/auth/[...nextauth]'; @@ -22,10 +24,21 @@ import { authOptions } from 'pages/api/auth/[...nextauth]'; const SearchResultPage: NextPage< InferGetServerSidePropsType > = ({ keyword }) => { + const [sortOptions, setSortOptions] = useState([ + ...INITIAL_SORT_BY_LIST, + ]); + + const selectedSortOption = useMemo( + () => sortOptions.find((option) => option.selected) ?? sortOptions[0], + [sortOptions], + ); + const methods = useForm({ mode: 'onChange' }); - const { diariesData, isLoading, isError, fetchNextPage } = - useDiaries(keyword); + const { diariesData, isLoading, isError, fetchNextPage } = useDiaries( + keyword, + selectedSortOption.id, + ); const { setTargetRef } = useIntersectionObserver({ onIntersect: fetchNextPage, }); @@ -49,7 +62,10 @@ const SearchResultPage: NextPage< } header={ } highlightKeyword={keyword} diff --git a/src/types/diary.ts b/src/types/diary.ts index f7bd09c5..ae6c6d7a 100644 --- a/src/types/diary.ts +++ b/src/types/diary.ts @@ -1,3 +1,4 @@ +import type { SortByType } from './search'; import type { AxiosRequestConfig } from 'axios'; import type { User } from 'next-auth'; @@ -33,6 +34,7 @@ export type DiaryForm = Pick< export interface GetDiariesRequest { currentPage: number; searchKeyword?: string; + sortBy?: SortByType; } export interface GetDiariesByUsernameRequest extends GetDiariesRequest { diff --git a/src/types/search.ts b/src/types/search.ts index 7f07fd63..b923be0f 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -1,3 +1,13 @@ +import type { SORT_BY_ID } from 'constants/search'; + export interface SearchForm { searchKeyword: string; } + +export type SortByType = keyof typeof SORT_BY_ID; + +export interface SortByOption { + id: SortByType; + title: string; + selected: boolean; +}