From 7ea46c0399cb50c1c84cfb1509dc5e6cc69fa18b Mon Sep 17 00:00:00 2001
From: Jungu Lee <100949102+jobkaeHenry@users.noreply.github.com>
Date: Tue, 28 Nov 2023 22:28:51 +0900
Subject: [PATCH] =?UTF-8?q?=EC=9C=84=ED=82=A4-=EC=88=A0-=EA=B2=80=EC=83=89?=
=?UTF-8?q?=EA=B8=B0=EB=8A=A5-=EC=B6=94=EA=B0=80=20(#58)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Minor : 스타일링 체인지
* Minor : 사용하지 않는 모듈제거, 포매팅
* Refactor : 술 리스트 컴포넌트, 페이지네이션 분리
* Refactor : 술 리스트 컴포넌트, 페이지네이션 분리
* Refactor : 키워드가 입력되지 않았을 경우 처리
* New : 로컬스토리지 키를 상수로 관리
* New : UX개선을 위한 스켈레톤 표시 딜레이 타이머 추가
* New : 술 검색기능 구현
---
client/src/app/wiki/layout.tsx | 42 ++++++-----
client/src/app/wiki/page.tsx | 4 +-
client/src/components/SearchHistory.tsx | 72 +++++++++++++++++++
.../components/newpost/SearchAlcoholInput.tsx | 8 ++-
client/src/components/search/SearchArea.tsx | 4 +-
client/src/components/wiki/AlcoholList.tsx | 53 ++++++--------
.../components/wiki/AlcoholListSkeleton.tsx | 31 ++++++++
.../src/components/wiki/AlcoholPagination.tsx | 24 +++++++
.../components/wiki/WikiAlcoholSelector.tsx | 28 ++++----
client/src/components/wiki/WikiAppbar.tsx | 9 ++-
.../wiki/searchDrawer/WikiSearchDrawer.tsx | 47 ++++++++++++
.../wiki/searchDrawer/WikiSerachArea.tsx | 59 +++++++++++++++
client/src/const/localstorageKey.ts | 1 +
client/src/hooks/useSkeletonTimer.ts | 21 ++++++
.../alcohol/useGetAlcoholListQuery.tsx | 3 +
client/src/store/wiki/WikiPageContext.ts | 13 ++++
16 files changed, 347 insertions(+), 72 deletions(-)
create mode 100644 client/src/components/SearchHistory.tsx
create mode 100644 client/src/components/wiki/AlcoholListSkeleton.tsx
create mode 100644 client/src/components/wiki/AlcoholPagination.tsx
create mode 100644 client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx
create mode 100644 client/src/components/wiki/searchDrawer/WikiSerachArea.tsx
create mode 100644 client/src/const/localstorageKey.ts
create mode 100644 client/src/hooks/useSkeletonTimer.ts
create mode 100644 client/src/store/wiki/WikiPageContext.ts
diff --git a/client/src/app/wiki/layout.tsx b/client/src/app/wiki/layout.tsx
index 3183fee..3f89e4d 100644
--- a/client/src/app/wiki/layout.tsx
+++ b/client/src/app/wiki/layout.tsx
@@ -1,25 +1,33 @@
+"use client";
import { Paper, Container } from "@mui/material";
import WikiAppbar from "@/components/wiki/WikiAppbar";
-import { ReactNode } from "react";
+import { ReactNode, useState } from "react";
+import WikiPageContext from "@/store/wiki/WikiPageContext";
+import WikiSearchDrawer from "@/components/wiki/searchDrawer/WikiSearchDrawer";
const layout = ({ children }: { children: ReactNode }) => {
+ const [isSearching, setIsSearching] = useState(false);
+
return (
-
-
-
-
- {children}
-
-
-
+
+
+
+
+
+
+ {children}
+
+
+
+
);
};
diff --git a/client/src/app/wiki/page.tsx b/client/src/app/wiki/page.tsx
index c0d19e2..a74f28a 100644
--- a/client/src/app/wiki/page.tsx
+++ b/client/src/app/wiki/page.tsx
@@ -1,4 +1,4 @@
-import AlcoholList from "@/components/wiki/AlcoholList";
+import AlcoholPagination from "@/components/wiki/AlcoholPagination";
import WikiAlcoholSelector from "@/components/wiki/WikiAlcoholSelector";
import { Stack } from "@mui/material";
import SectionHeading from "@/components/SectionHeading";
@@ -14,7 +14,7 @@ const WikiPage = async () => {
subTitle={"투파이아들이 쓴 리뷰를 확인할 수 있어요!"}
/>
-
+
diff --git a/client/src/components/SearchHistory.tsx b/client/src/components/SearchHistory.tsx
new file mode 100644
index 0000000..873ce5b
--- /dev/null
+++ b/client/src/components/SearchHistory.tsx
@@ -0,0 +1,72 @@
+import { Button, Stack, StackProps, Typography } from "@mui/material";
+import { useCallback, useState } from "react";
+import XIcon from "@/assets/icons/XIcon.svg";
+
+interface SearchHistoryProps extends Omit {
+ storageKey: string;
+ onClick: () => 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]
+ );
+
+ return searchHistory.length > 0 ? (
+ <>
+
+
+ 최근 검색어
+
+
+
+
+ {searchHistory.map((keyword) => (
+
+ {keyword}
+
+
+ ))}
+
+ >
+ ) : (
+ <>>
+ );
+};
+
+export default SearchHistory;
diff --git a/client/src/components/newpost/SearchAlcoholInput.tsx b/client/src/components/newpost/SearchAlcoholInput.tsx
index 1aba2bf..5884c5c 100644
--- a/client/src/components/newpost/SearchAlcoholInput.tsx
+++ b/client/src/components/newpost/SearchAlcoholInput.tsx
@@ -35,7 +35,6 @@ const SearchAlcoholInput = ({ setAlcoholNo }: SearchAlcoholInputInterface) => {
const [selectedAlcohol, setSelectedAlcohol] =
useState();
-
useEffect(() => {
setSearchKeyword(selectedAlcohol?.alcoholName ?? "");
setAlcoholNo(selectedAlcohol?.alcoholNo);
@@ -48,6 +47,9 @@ const SearchAlcoholInput = ({ setAlcoholNo }: SearchAlcoholInputInterface) => {
name="positionInfo"
size="small"
InputProps={{
+ sx: {
+ borderRadius: 12,
+ },
startAdornment: (
@@ -109,8 +111,8 @@ const SearchAlcoholInput = ({ setAlcoholNo }: SearchAlcoholInputInterface) => {
const WrapperStyle = {
width: "calc(100% - 32px)",
minHeight: "50px",
- maxHeight:'142px',
- overflowY:'auto',
+ maxHeight: "142px",
+ overflowY: "auto",
backgroundColor: "#F5F5F5",
border: "1px solid #E6E6E6",
borderRadius: 1.5,
diff --git a/client/src/components/search/SearchArea.tsx b/client/src/components/search/SearchArea.tsx
index 1b7f445..7f6f7fe 100644
--- a/client/src/components/search/SearchArea.tsx
+++ b/client/src/components/search/SearchArea.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Box, CircularProgress, Paper, TextField } from "@mui/material";
-import React, { useState, useMemo, Suspense } from "react";
+import { Paper, TextField } from "@mui/material";
+import React, { useState, useMemo } from "react";
import PostCardList from "@/components/post/PostCardList";
import { AugmentedGetPostListResponse } from "@/queries/post/useGetPostListInfiniteQuery";
import useDebounce from "@/hooks/useDebounce";
diff --git a/client/src/components/wiki/AlcoholList.tsx b/client/src/components/wiki/AlcoholList.tsx
index 56c66b3..298a1b9 100644
--- a/client/src/components/wiki/AlcoholList.tsx
+++ b/client/src/components/wiki/AlcoholList.tsx
@@ -1,40 +1,29 @@
"use client";
import AlcoholNameTag from "@/components/wiki/AlcoholNameTag";
-import useGetAlcoholListQuery from "@/queries/alcohol/useGetAlcoholListQuery";
-import { Box, Pagination, Skeleton, Stack } from "@mui/material";
+import { AlcoholDetailInterface } from "@/types/alcohol/AlcoholInterface";
+import { Typography } from "@mui/material";
import { memo } from "react";
-const AlcoholList = () => {
- const { data: alcohols } = useGetAlcoholListQuery();
+const AlcoholList = ({
+ data: alcohols,
+}: {
+ data: AlcoholDetailInterface[];
+}) => {
return (
-
-
- {alcohols ? (
- alcohols.list.map((alcohol) => (
-
- ))
- ) : (
-
- )}
-
-
-
+ <>
+ {alcohols?.length > 0 ? (
+ alcohols.map((alcohol) => (
+
+ ))
+ ) : (
+ 검색 결과가 없어요
+ )}
+ >
);
};
+export default memo(AlcoholList);
-const AlcoholListSkeleton = memo(() => {
- return Array.from(new Array(5)).map(() => (
-
- ));
-});
-
-export default AlcoholList;
diff --git a/client/src/components/wiki/AlcoholListSkeleton.tsx b/client/src/components/wiki/AlcoholListSkeleton.tsx
new file mode 100644
index 0000000..3cbf74b
--- /dev/null
+++ b/client/src/components/wiki/AlcoholListSkeleton.tsx
@@ -0,0 +1,31 @@
+import { memo } from "react";
+import { Skeleton } from "@mui/material";
+
+import useSkeletonTimer from "@/hooks/useSkeletonTimer";
+
+interface AlcoholListSkeletonInterface {
+ size?: number;
+ disableTimer?: boolean;
+}
+
+const AlcoholListSkeleton = memo(
+ ({ size = 5, disableTimer }: AlcoholListSkeletonInterface) => {
+ const isOver200ms = !!disableTimer ? true : useSkeletonTimer();
+
+ return isOver200ms ? (
+ Array.from(new Array(size)).map((_e, i) => (
+
+ ))
+ ) : (
+ <>>
+ );
+ }
+);
+
+export default AlcoholListSkeleton;
diff --git a/client/src/components/wiki/AlcoholPagination.tsx b/client/src/components/wiki/AlcoholPagination.tsx
new file mode 100644
index 0000000..dbeae6f
--- /dev/null
+++ b/client/src/components/wiki/AlcoholPagination.tsx
@@ -0,0 +1,24 @@
+"use client";
+import useGetAlcoholListQuery from "@/queries/alcohol/useGetAlcoholListQuery";
+import AlcoholList from "@/components/wiki/AlcoholList";
+import { Pagination, Stack } from "@mui/material";
+import AlcoholListSkeleton from "@/components/wiki/AlcoholListSkeleton";
+
+const AlcoholPagenation = () => {
+ const { data: alcohols, isSuccess } = useGetAlcoholListQuery();
+
+ return (
+
+
+ {isSuccess ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+export default AlcoholPagenation;
diff --git a/client/src/components/wiki/WikiAlcoholSelector.tsx b/client/src/components/wiki/WikiAlcoholSelector.tsx
index 2af1e9a..79aa30a 100644
--- a/client/src/components/wiki/WikiAlcoholSelector.tsx
+++ b/client/src/components/wiki/WikiAlcoholSelector.tsx
@@ -9,28 +9,30 @@ import TraditionalAlcoholIcon from "@/assets/icons/Alcohol/TraditionalAlcoholIco
import SakeIcon from "@/assets/icons/Alcohol/SakeIcon.svg";
const WikiAlcoholSelector = () => {
-
- const btnList =useMemo(()=>[
- { title: "포도주", iconComponent: },
- { title: "위스키", iconComponent: },
- { title: "증류주", iconComponent: },
- { title: "우리술", iconComponent: },
- { title: "사케", iconComponent: },
- ],[])
+ const btnList = useMemo(
+ () => [
+ { title: "포도주", iconComponent: },
+ { title: "위스키", iconComponent: },
+ { title: "증류주", iconComponent: },
+ { title: "우리술", iconComponent: },
+ { title: "사케", iconComponent: },
+ ],
+ []
+ );
const [selectedAlcohol, setSelectedAlcohol] = useState(btnList[0].title);
- const clickHandler = useCallback((title:string)=>{
- setSelectedAlcohol(title)
- },[])
+ const clickHandler = useCallback((title: string) => {
+ setSelectedAlcohol(title);
+ }, []);
return (
-
+
{btnList.map((btnInfo) => (
clickHandler(btnInfo.title)}
+ onClick={() => clickHandler(btnInfo.title)}
{...btnInfo}
/>
))}
diff --git a/client/src/components/wiki/WikiAppbar.tsx b/client/src/components/wiki/WikiAppbar.tsx
index 94c823e..2e7b6f2 100644
--- a/client/src/components/wiki/WikiAppbar.tsx
+++ b/client/src/components/wiki/WikiAppbar.tsx
@@ -1,15 +1,18 @@
-'use client'
+"use client";
import CustomAppbar from "@/components/CustomAppbar";
import SearchIcon from "@/assets/icons/SearchIcon.svg";
+import { memo, useContext } from "react";
+import WikiPageContext from "@/store/wiki/WikiPageContext";
const WikiAppbar = () => {
+ const { setIsSearching } = useContext(WikiPageContext);
return (
}
- onClickButton={() => console.log("눌림")}
+ onClickButton={() => setIsSearching(true)}
/>
);
};
-export default WikiAppbar;
+export default memo(WikiAppbar);
diff --git a/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx b/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx
new file mode 100644
index 0000000..2538faf
--- /dev/null
+++ b/client/src/components/wiki/searchDrawer/WikiSearchDrawer.tsx
@@ -0,0 +1,47 @@
+import { SwipeableDrawer, Stack, styled, Box } from "@mui/material";
+import { useContext } from "react";
+import WikiPageContext from "@/store/wiki/WikiPageContext";
+import WikiSerachArea from "@/components/wiki/searchDrawer/WikiSerachArea";
+
+const WikiSearchDrawer = () => {
+ const { isSearching, setIsSearching } = useContext(WikiPageContext);
+
+ const pullerBleed = 24;
+ return (
+ setIsSearching(true)}
+ onClose={() => setIsSearching(false)}
+ anchor="bottom"
+ disableSwipeToOpen
+ PaperProps={{
+ sx: {
+ p: 2,
+ borderTopLeftRadius: pullerBleed,
+ borderTopRightRadius: pullerBleed,
+ overFlow: "hidden",
+ },
+ }}
+ ModalProps={{
+ keepMounted: false,
+ }}
+ >
+
+
+
+
+
+ );
+};
+
+export default WikiSearchDrawer;
+
+const Puller = styled(Box)(() => ({
+ width: 56,
+ height: 4,
+ backgroundColor: "#F6EAFB",
+ borderRadius: 3,
+ position: "absolute",
+ top: 8,
+ left: "calc(50% - 28px)",
+}));
diff --git a/client/src/components/wiki/searchDrawer/WikiSerachArea.tsx b/client/src/components/wiki/searchDrawer/WikiSerachArea.tsx
new file mode 100644
index 0000000..7c517fc
--- /dev/null
+++ b/client/src/components/wiki/searchDrawer/WikiSerachArea.tsx
@@ -0,0 +1,59 @@
+import { 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";
+
+const WikiSerachArea = () => {
+ const [searchKeyword, setSearchKeyword] = useState("");
+ const debouncedValue = useDebounce(searchKeyword, 300);
+ const { data: alcohols, isSuccess } = useGetAlcoholListQuery(debouncedValue);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+
+ return (
+ <>
+ setSearchKeyword(target.value)}
+ InputProps={{
+ endAdornment: ,
+ sx: {
+ borderRadius: "12px",
+ },
+ }}
+ />
+
+ {searchKeyword ? (
+ // 입력중인 경우
+ <>
+ {isSuccess ? (
+
+ ) : (
+
+ )}
+ >
+ ) : (
+ // 입력이 없는경우 검색기록 표출
+ console.log("눌림")}
+ storageKey={ALCOHOL_SEARCH_HISTORY}
+ />
+ )}
+
+ >
+ );
+};
+
+export default WikiSerachArea;
diff --git a/client/src/const/localstorageKey.ts b/client/src/const/localstorageKey.ts
new file mode 100644
index 0000000..26b6387
--- /dev/null
+++ b/client/src/const/localstorageKey.ts
@@ -0,0 +1 @@
+export const ALCOHOL_SEARCH_HISTORY ='alcohol-search-history' as const
\ No newline at end of file
diff --git a/client/src/hooks/useSkeletonTimer.ts b/client/src/hooks/useSkeletonTimer.ts
new file mode 100644
index 0000000..d444958
--- /dev/null
+++ b/client/src/hooks/useSkeletonTimer.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from "react";
+/**
+ * 시간을 인자로 받아 해당 시간이 지난 후 true를 리턴, 시간이 지나지 않았을 경우 false를 리턴
+ * @param time ms 단위, 기본값은 200ms
+ * @returns 입력받은 시간이 지났는지 여부
+ */
+const useSkeletonTimer = (time: number = 200) => {
+ const [isTimePassed, setTimer] = useState(false);
+
+ useEffect(() => {
+ const timerId = setTimeout(() => {
+ setTimer(true);
+ }, time);
+ return () => {
+ clearTimeout(timerId);
+ };
+ }, []);
+ return isTimePassed;
+};
+
+export default useSkeletonTimer;
diff --git a/client/src/queries/alcohol/useGetAlcoholListQuery.tsx b/client/src/queries/alcohol/useGetAlcoholListQuery.tsx
index b1f5e3d..b2556e5 100644
--- a/client/src/queries/alcohol/useGetAlcoholListQuery.tsx
+++ b/client/src/queries/alcohol/useGetAlcoholListQuery.tsx
@@ -11,6 +11,9 @@ const useGetAlcoholListQuery = (keyword?: string) => {
};
export const getAlcoholListByKeyword = async (keyword?: string) => {
+ if (keyword === "") {
+ return { list: [], totalCount: 0 };
+ }
const { data } = await axios.get<{
list: AlcoholDetailInterface[];
totalCount: number;
diff --git a/client/src/store/wiki/WikiPageContext.ts b/client/src/store/wiki/WikiPageContext.ts
new file mode 100644
index 0000000..b6e5c83
--- /dev/null
+++ b/client/src/store/wiki/WikiPageContext.ts
@@ -0,0 +1,13 @@
+import { Dispatch, SetStateAction, createContext } from "react";
+
+interface WikiPageContextInterface {
+ isSearching: boolean;
+ setIsSearching: Dispatch>;
+}
+
+const WikiPageContext = createContext({
+ isSearching: true,
+ setIsSearching: () => {},
+});
+
+export default WikiPageContext;