From 4c55e8741696cda52f09f6300e40c9d2ecd755a9 Mon Sep 17 00:00:00 2001
From: afds4567 <33995840+afds4567@users.noreply.github.com>
Date: Fri, 6 Oct 2023 11:09:24 +0900
Subject: [PATCH] =?UTF-8?q?[FE]=20Feat/#554=20search=20=EC=A7=80=EB=8F=84?=
=?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
=?UTF-8?q?=20(#555)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: SearchBar 컴포넌트 구현
* feat: Search 페이지 구현
* feat: 홈페이지에 searchBar 적용
* refactor: 불필요한 memo 제거
---
frontend/src/assets/search.svg | 2 +
.../src/components/SearchBar/SearchBar.tsx | 71 +++++++++
frontend/src/hooks/useNavigator.ts | 1 +
frontend/src/pages/Home.tsx | 7 +-
frontend/src/pages/Search.tsx | 138 ++++++++++++++++++
frontend/src/router.tsx | 6 +
6 files changed, 223 insertions(+), 2 deletions(-)
create mode 100644 frontend/src/assets/search.svg
create mode 100644 frontend/src/components/SearchBar/SearchBar.tsx
create mode 100644 frontend/src/pages/Search.tsx
diff --git a/frontend/src/assets/search.svg b/frontend/src/assets/search.svg
new file mode 100644
index 00000000..0c9648b3
--- /dev/null
+++ b/frontend/src/assets/search.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/frontend/src/components/SearchBar/SearchBar.tsx b/frontend/src/components/SearchBar/SearchBar.tsx
new file mode 100644
index 00000000..5df898bd
--- /dev/null
+++ b/frontend/src/components/SearchBar/SearchBar.tsx
@@ -0,0 +1,71 @@
+import { useState } from 'react';
+import styled from 'styled-components';
+import useNavigator from '../../hooks/useNavigator';
+import SearchIcon from '../../assets/search.svg';
+
+const SearchBar = () => {
+ const { routingHandlers } = useNavigator();
+
+ const [searchTerm, setSearchTerm] = useState('');
+
+ const onInputChange = (e: React.ChangeEvent) => {
+ setSearchTerm(e.target.value);
+ };
+
+ const onSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ routingHandlers.search(searchTerm);
+ };
+
+ return (
+
+
+
+
+ );
+};
+export default SearchBar;
+
+const SearchBarWrapper = styled.form`
+ display: flex;
+ padding-left: 20px;
+ position: relative;
+ border-radius: 5px;
+ border: 1px solid #ccc;
+ box-shadow: 0px 1px 5px 3px rgba(0, 0, 0, 0.12);
+`;
+
+const SearchInput = styled.input`
+ height: 45px;
+ width: 100%;
+ outline: none;
+ border: none;
+ border-radius: 5px;
+ padding: 0 60px 0 20px;
+ font-size: 18px;
+ font-weight: 500;
+ &:focus {
+ outline: none !important;
+ box-shadow:
+ inset -1px -1px rgba(0, 0, 0, 0.075),
+ inset -1px -1px rgba(0, 0, 0, 0.075),
+ inset -3px -3px rgba(255, 255, 255, 0.6),
+ inset -4px -4px rgba(255, 255, 255, 0.6),
+ inset rgba(255, 255, 255, 0.6),
+ inset rgba(64, 64, 64, 0.15);
+ }
+`;
+
+const StyledSearchIcon = styled(SearchIcon)`
+ position: absolute;
+ top: 50%;
+ left: 10px;
+ transform: translateY(-50%);
+ width: 20px;
+ height: 20px;
+ fill: #ccc;
+`;
diff --git a/frontend/src/hooks/useNavigator.ts b/frontend/src/hooks/useNavigator.ts
index 77f76cde..b6021d52 100644
--- a/frontend/src/hooks/useNavigator.ts
+++ b/frontend/src/hooks/useNavigator.ts
@@ -27,6 +27,7 @@ const useNavigator = () => {
routePage('/new-pin', topicId);
closeModal('addMapOrPin');
},
+ search: (searchTerm: string) => routePage(`/search?${searchTerm}`),
goToPopularTopics: () => routePage('see-all/popularity'),
goToNearByMeTopics: () => routePage('see-all/near'),
goToLatestTopics: () => routePage('see-all/latest'),
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 5ca139b9..f8c32e76 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -1,6 +1,6 @@
import Space from '../components/common/Space';
import useNavigator from '../hooks/useNavigator';
-import { css, styled } from 'styled-components';
+import { styled } from 'styled-components';
import useSetLayoutWidth from '../hooks/useSetLayoutWidth';
import { FULLSCREEN } from '../constants';
import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight';
@@ -8,6 +8,7 @@ import { Suspense, lazy, useContext, useEffect } from 'react';
import { MarkerContext } from '../context/MarkerContext';
import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton';
import { setFullScreenResponsive } from '../constants/responsive';
+import SearchBar from '../components/SearchBar/SearchBar';
const TopicListContainer = lazy(
() => import('../components/TopicCardContainer'),
@@ -33,7 +34,9 @@ const Home = () => {
return (
-
+
+
+
}>
{
+ const { fetchGet } = useGet();
+
+ const [originalTopics, setOriginalTopics] = useState(
+ null,
+ );
+ const [displayedTopics, setDisplayedTopics] = useState<
+ TopicCardProps[] | null
+ >(null);
+ const searchQuery = decodeURIComponent(useLocation().search.substring(1));
+
+ const getTopicsFromServer = async () => {
+ fetchGet(
+ '/topics',
+ '지도를 가져오는데 실패했습니다.',
+ (response) => {
+ setOriginalTopics(response);
+ const searchResult = response.filter((topic) =>
+ topic.name.includes(searchQuery),
+ );
+ setDisplayedTopics(searchResult);
+ },
+ );
+ };
+
+ useEffect(() => {
+ getTopicsFromServer();
+ }, []);
+
+ useEffect(() => {
+ if (originalTopics) {
+ const searchResult = originalTopics.filter((topic) =>
+ topic.name.includes(searchQuery),
+ );
+ setDisplayedTopics(searchResult);
+ }
+ }, [searchQuery]);
+
+ return (
+
+
+
+
+
+
+
+ 찾았을 지도?
+
+
+
+ 검색한 지도를 확인해보세요.
+
+
+
+
+
+ {displayedTopics?.length === 0 ? (
+ // 검색 결과가 없을 때의 UI
+
+
+
+
+ '{searchQuery}'에 대한
+ {'검색 결과가 없습니다.'}
+
+
+
+
+
+ ) : (
+
+ {displayedTopics?.map((topic) => (
+
+
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default Search;
+
+const Wrapper = styled.article`
+ width: 1036px;
+ margin: 0 auto;
+ position: relative;
+
+ ${setFullScreenResponsive()}
+`;
+
+const CardListWrapper = styled.ul`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+`;
+
+const EmptyWrapper = styled.section`
+ height: 240px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx
index 7f5833bd..55204a63 100644
--- a/frontend/src/router.tsx
+++ b/frontend/src/router.tsx
@@ -5,6 +5,7 @@ import RootPage from './pages/RootPage';
import { ReactNode } from 'react';
import AuthLayout from './components/Layout/AuthLayout';
import NotFound from './pages/NotFound';
+import Search from './pages/Search';
const SelectedTopic = lazy(() => import('./pages/SelectedTopic'));
const NewPin = lazy(() => import('./pages/NewPin'));
@@ -147,6 +148,11 @@ const routes: routeElement[] = [
),
withAuth: false,
},
+ {
+ path: '/search',
+ element: ,
+ withAuth: false,
+ },
],
},
];