diff --git a/client/src/components/SearchHistory.tsx b/client/src/components/SearchHistory.tsx index 5b1f382..84dce40 100644 --- a/client/src/components/SearchHistory.tsx +++ b/client/src/components/SearchHistory.tsx @@ -1,34 +1,19 @@ import { Button, Stack, StackProps, Typography } from "@mui/material"; -import { useCallback, useState } from "react"; import XIcon from "@/assets/icons/XIcon.svg"; +import { SearchHistoryKeyType } from "@/types/LocalStorageKey"; +import useSearchHistory from "@/hooks/searchHistory/useSearchHistory"; interface SearchHistoryProps extends Omit { - storageKey: string; - onClick: () => void; + storageKey: SearchHistoryKeyType; + onClick: (keyword: string) => void; } const SearchHistory = ({ storageKey, onClick }: SearchHistoryProps) => { - const getItems = useCallback(() => { - return JSON.parse(localStorage.getItem(storageKey) ?? "[]") as string[]; - }, [storageKey]); - - const [searchHistory, setSearchHistory] = useState(getItems()); - - const removeAll = useCallback(() => { - localStorage.setItem(storageKey, "[]"); - setSearchHistory(getItems()); - }, [storageKey]); - - const removeByKeyword = useCallback( - (keyword: string) => { - const filteredHistory = searchHistory.filter( - (prevKeyword) => prevKeyword !== keyword - ); - localStorage.setItem(storageKey, JSON.stringify(filteredHistory)); - setSearchHistory(getItems()); - }, - [storageKey] - ); + const { + state: searchHistory, + removeAll, + removeByKeyword, + } = useSearchHistory(storageKey); return searchHistory.length > 0 ? ( <> @@ -45,7 +30,7 @@ const SearchHistory = ({ storageKey, onClick }: SearchHistoryProps) => { onClick(keyword)} direction="row" justifyContent="space-between" alignItems="center" diff --git a/client/src/components/newpost/SearchAlcoholInput.tsx b/client/src/components/newpost/SearchAlcoholInput.tsx index f3b9a27..e7fecdd 100644 --- a/client/src/components/newpost/SearchAlcoholInput.tsx +++ b/client/src/components/newpost/SearchAlcoholInput.tsx @@ -67,6 +67,7 @@ const SearchAlcoholInput = ({ setAlcoholNo }: SearchAlcoholInputInterface) => { onBlur={() => setIsSearchingAlCohol(false)} autoComplete="off" /> + {/* FIXME List 컴포넌트로 분리 */} {isSearchingAlcohol && ( diff --git a/client/src/components/wiki/AlcoholList.tsx b/client/src/components/wiki/AlcoholList.tsx index dc6bb3f..4a54333 100644 --- a/client/src/components/wiki/AlcoholList.tsx +++ b/client/src/components/wiki/AlcoholList.tsx @@ -3,27 +3,41 @@ import AlcoholNameTag from "@/components/wiki/AlcoholNameTag"; import { AlcoholDetailInterface } from "@/types/alcohol/AlcoholInterface"; import { Typography } from "@mui/material"; import { memo } from "react"; +import AlcoholListSkeleton from "./AlcoholListSkeleton"; -const AlcoholList = ({ - data: alcohols, -}: { - data: AlcoholDetailInterface[]; -}) => { - return ( +interface AlcoholList { + data?: AlcoholDetailInterface[]; + onClickElement?: (data: AlcoholDetailInterface) => void; +} +/** + * 술 정보 Array 를 입력받아 List로 맵핑해주는 컴포넌트 + * onClickElement 속성으로 각 엘리먼트 클릭 시 속성을 지정가능 + * @returns + */ +const AlcoholList = ({ data: alcohols, onClickElement }: AlcoholList) => { + return alcohols ? ( <> - {alcohols?.length > 0 ? ( - alcohols.map(({ alcoholName, alcoholNo, alcoholType }) => ( - - )) + {alcohols.length > 0 ? ( + alcohols.map((alcohol) => { + const { alcoholName, alcoholNo, alcoholType } = alcohol; + return ( + { + onClickElement && onClickElement(alcohol); + }} + /> + ); + }) ) : ( 검색 결과가 없어요 )} + ) : ( + ); }; export default memo(AlcoholList); diff --git a/client/src/components/wiki/AlcoholNameTag.tsx b/client/src/components/wiki/AlcoholNameTag.tsx index 58bac47..54a6815 100644 --- a/client/src/components/wiki/AlcoholNameTag.tsx +++ b/client/src/components/wiki/AlcoholNameTag.tsx @@ -2,10 +2,8 @@ import { Box, BoxProps, Chip, IconButton, Typography } from "@mui/material"; import PostSeeMoreIcon from "@/assets/icons/PostSeeMoreIcon.svg"; import { AlcoholDetailInterface } from "@/types/alcohol/AlcoholInterface"; import XIcon from "@/assets/icons/XIcon.svg"; -import { useRouter } from "next/navigation"; -import { WIKI_DETAIL } from "@/const/clientPath"; -interface AlcoholNameTagInterface extends BoxProps { +export interface AlcoholNameTagInterface extends BoxProps { alcoholName: AlcoholDetailInterface["alcoholName"]; alcoholType: AlcoholDetailInterface["alcoholType"]; alcoholNo: AlcoholDetailInterface["alcoholNo"]; @@ -21,7 +19,6 @@ const AlcoholNameTag = ({ onClickRemove, ...others }: AlcoholNameTagInterface) => { - const router = useRouter(); return ( ) : ( - router.push(WIKI_DETAIL(String(alcoholNo)))} - > + )} diff --git a/client/src/components/wiki/AlcoholPagination.tsx b/client/src/components/wiki/AlcoholPagination.tsx index dbeae6f..967e9f3 100644 --- a/client/src/components/wiki/AlcoholPagination.tsx +++ b/client/src/components/wiki/AlcoholPagination.tsx @@ -2,19 +2,18 @@ import useGetAlcoholListQuery from "@/queries/alcohol/useGetAlcoholListQuery"; import AlcoholList from "@/components/wiki/AlcoholList"; import { Pagination, Stack } from "@mui/material"; -import AlcoholListSkeleton from "@/components/wiki/AlcoholListSkeleton"; +import usePushToWikiDetail from "@/hooks/wiki/usePushToWikiDetail"; const AlcoholPagenation = () => { - const { data: alcohols, isSuccess } = useGetAlcoholListQuery(); - + const { data: alcohols } = useGetAlcoholListQuery(); + const onClickElementHandler = usePushToWikiDetail(); return ( - {isSuccess ? ( - - ) : ( - - )} + diff --git a/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx b/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx index a07c2bd..c57b1c5 100644 --- a/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx +++ b/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx @@ -1,4 +1,4 @@ -import { SwipeableDrawer, Stack, styled, Box } from "@mui/material"; +import { Stack } from "@mui/material"; import { useContext } from "react"; import WikiPageContext from "@/store/wiki/WikiPageContext"; import WikiSerachArea from "@/components/wiki/searchDrawer/WikiSerachArea"; @@ -7,7 +7,6 @@ import CustomSwipeableDrawer from "@/components/CustomSwipeableDrawer"; const WikiSearchDrawer = () => { const { isSearching, setIsSearching } = useContext(WikiPageContext); - return ( { }; export default WikiSearchDrawer; - diff --git a/client/src/components/wiki/searchDrawer/WikiSerachArea.tsx b/client/src/components/wiki/searchDrawer/WikiSerachArea.tsx index 7c517fc..a34e85a 100644 --- a/client/src/components/wiki/searchDrawer/WikiSerachArea.tsx +++ b/client/src/components/wiki/searchDrawer/WikiSerachArea.tsx @@ -1,19 +1,24 @@ -import { useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import useDebounce from "@/hooks/useDebounce"; import InputSearchIcon from "@/assets/icons/InputSearchIcon.svg"; import { Stack, TextField } from "@mui/material"; import useGetAlcoholListQuery from "@/queries/alcohol/useGetAlcoholListQuery"; import AlcoholList from "@/components/wiki/AlcoholList"; -import AlcoholListSkeleton from "../AlcoholListSkeleton"; import SearchHistory from "@/components/SearchHistory"; import { ALCOHOL_SEARCH_HISTORY } from "@/const/localstorageKey"; +import WikiPageContext from "@/store/wiki/WikiPageContext"; +import usePushToWikiDetail from "@/hooks/wiki/usePushToWikiDetail"; const WikiSerachArea = () => { + const { setIsSearching } = useContext(WikiPageContext); + const [searchKeyword, setSearchKeyword] = useState(""); const debouncedValue = useDebounce(searchKeyword, 300); - const { data: alcohols, isSuccess } = useGetAlcoholListQuery(debouncedValue); - const inputRef = useRef(null); + const { data: alcohols } = useGetAlcoholListQuery(debouncedValue); + + const onClickElementHandler = usePushToWikiDetail(); + const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, []); @@ -37,17 +42,17 @@ const WikiSerachArea = () => { {searchKeyword ? ( // 입력중인 경우 - <> - {isSuccess ? ( - - ) : ( - - )} - + { + onClickElementHandler(alcoholData); + setIsSearching(false); + }} + /> ) : ( // 입력이 없는경우 검색기록 표출 console.log("눌림")} + onClick={(keyword) => setSearchKeyword(keyword)} storageKey={ALCOHOL_SEARCH_HISTORY} /> )} diff --git a/client/src/hooks/localStorage/useLocalStorage.ts b/client/src/hooks/localStorage/useLocalStorage.ts new file mode 100644 index 0000000..d042640 --- /dev/null +++ b/client/src/hooks/localStorage/useLocalStorage.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useState } from "react"; + +const useLocalStorage = (storageKey: string) => { + /** + * 로컬스토리지에 아이템을 stringify해 저장하는 함수 + */ + const setItem = useCallback( + (keyword: T) => { + localStorage.setItem(storageKey, JSON.stringify(keyword)); + }, + [storageKey] + ); + /** + * 로컬 스토리지 아이템을 파싱해서 리턴하는 함수 + */ + const getItems = useCallback((): T | null => { + return JSON.parse(localStorage.getItem(storageKey) || "null"); + }, [storageKey]); + + const [storageValue, setStorageValue] = useState(getItems()); + + // 새로운 값이 저장될 경우, 로컬스토리지에도 같이 저장 + useEffect(() => { + if (!storageValue) { + return; + } + setItem(storageValue); + }, [storageValue]); + + return [storageValue, setStorageValue] as const; +}; + +export default useLocalStorage; diff --git a/client/src/hooks/searchHistory/useSearchHistory.tsx b/client/src/hooks/searchHistory/useSearchHistory.tsx new file mode 100644 index 0000000..6e97d22 --- /dev/null +++ b/client/src/hooks/searchHistory/useSearchHistory.tsx @@ -0,0 +1,54 @@ +import { SearchHistoryKeyType } from "@/types/LocalStorageKey"; +import { useCallback, useEffect } from "react"; +import useLocalStorage from "../localStorage/useLocalStorage"; + +/** + * 로컬스토리지 키를 입력받아 + * 해당 스토리지를 바라보는 state를 리턴 (State 업데이트시 자동으로 반영) + * + * @param storageKey 로컬스토리지 키 + * @returns + */ +const useSearchHistory = (storageKey: SearchHistoryKeyType) => { + const [searchHistory, setSearchHistory] = + useLocalStorage(storageKey); + + useEffect(() => { + if (searchHistory === null) { + setSearchHistory([]); + } + }, []); + + const removeAll = useCallback(() => { + setSearchHistory([]); + }, [storageKey]); + + const removeByKeyword = useCallback( + (keyword: string) => { + const filteredHistory = (searchHistory ?? []).filter( + (prevKeyword) => prevKeyword !== keyword + ); + setSearchHistory(filteredHistory); + }, + [storageKey] + ); + const addSearchHistory = useCallback( + (keyword: string) => { + setSearchHistory((prev) => { + return [ + keyword, + ...(prev ?? []).filter((prevKeyword) => prevKeyword !== keyword), + ]; + }); + }, + [storageKey] + ); + return { + state: searchHistory ?? [], + add: addSearchHistory, + removeAll, + removeByKeyword, + }; +}; + +export default useSearchHistory; diff --git a/client/src/hooks/wiki/usePushToWikiDetail.ts b/client/src/hooks/wiki/usePushToWikiDetail.ts new file mode 100644 index 0000000..9880f16 --- /dev/null +++ b/client/src/hooks/wiki/usePushToWikiDetail.ts @@ -0,0 +1,27 @@ +import { AlcoholDetailInterface } from "@/types/alcohol/AlcoholInterface"; +import { useRouter } from "next/navigation"; +import { useCallback } from "react"; +import useSearchHistory from "../searchHistory/useSearchHistory"; +import { ALCOHOL_SEARCH_HISTORY } from "@/const/localstorageKey"; +import { WIKI_DETAIL } from "@/const/clientPath"; +/** + * 검색히스토리에 해당 술을 남기고, 디테일페이지로 이동시키는 함수를 리턴하는 훅 + * @returns 해당 callback함수 + */ +const usePushToWikiDetail = () => { + const { add: addToSearchHistory } = useSearchHistory(ALCOHOL_SEARCH_HISTORY); + const router = useRouter(); + /** + * 검색히스토리에 해당 술을 남기고, 디테일페이지로 이동시키는 함수를 리턴하는 함수 + */ + const onClickElementHandler = useCallback( + ({ alcoholName, alcoholNo }: AlcoholDetailInterface) => { + addToSearchHistory(alcoholName); + router.push(WIKI_DETAIL(String(alcoholNo))); + }, + [addToSearchHistory] + ); + return onClickElementHandler; +}; + +export default usePushToWikiDetail; diff --git a/client/src/types/LocalStorageKey.ts b/client/src/types/LocalStorageKey.ts new file mode 100644 index 0000000..aa2936d --- /dev/null +++ b/client/src/types/LocalStorageKey.ts @@ -0,0 +1,4 @@ +/** + * 검색 기록 관련 키 타입 + */ +export type SearchHistoryKeyType = "alcohol-search-history";