diff --git a/src/Router.tsx b/src/Router.tsx
index 4fb79e6..a65001f 100644
--- a/src/Router.tsx
+++ b/src/Router.tsx
@@ -5,6 +5,8 @@ import DetailPage from './views/Detail/pages/DetailPage';
import LoginCallBack from './views/Login/components/LoginCallBack';
import MainPage from './views/Main/pages/MainPage';
import Mypage from './views/Mypage/pages/Mypage';
+import SearchPage from './views/Search/pages/SearchPage';
+import SearchResultPage from './views/Search/pages/SearchResultPage';
const router = createBrowserRouter([
{
@@ -16,6 +18,15 @@ const router = createBrowserRouter([
],
},
{ path: '/detail', element: },
+ {
+ path: '/search',
+ element: ,
+ children: [{}],
+ },
+ {
+ path: '/search/:word',
+ element: ,
+ },
{
path: '/mypage',
element: ,
diff --git a/src/assets/icon/Chevron_Left.svg b/src/assets/icon/Chevron_Left.svg
new file mode 100644
index 0000000..cad1537
--- /dev/null
+++ b/src/assets/icon/Chevron_Left.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icon/icon-check-fill.svg b/src/assets/icon/icon-check-fill.svg
new file mode 100644
index 0000000..ef7c6ff
--- /dev/null
+++ b/src/assets/icon/icon-check-fill.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/icon/icon-search-set.svg b/src/assets/icon/icon-search-set.svg
new file mode 100644
index 0000000..cfc488b
--- /dev/null
+++ b/src/assets/icon/icon-search-set.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/assets/icon/icon-x-mono.svg b/src/assets/icon/icon-x-mono.svg
index 8a9cc4f..608d35b 100644
--- a/src/assets/icon/icon-x-mono.svg
+++ b/src/assets/icon/icon-x-mono.svg
@@ -1,3 +1,3 @@
diff --git a/src/assets/icon/index.ts b/src/assets/icon/index.ts
index 69d826f..01d5162 100644
--- a/src/assets/icon/index.ts
+++ b/src/assets/icon/index.ts
@@ -19,7 +19,6 @@ export { default as MapPinIcon } from './icon_map_pin.svg?react';
export { default as StarIcon } from './icon_star_filled.svg?react';
export { default as Star1Icon } from './Star 1.svg?react';
export { default as UnitripIcon } from './UNITRIP.svg?react';
-
export { default as ArrowToggleClosed } from './icon_arrow_toggle_closed.svg?react';
export { default as ArrowToggleOpen } from './icon_arrow_toggle_open.svg?react';
export { default as AudioGuideDefaultIcon } from './icon_audio_guide_default.svg?react';
@@ -70,6 +69,11 @@ export { default as VideoGuideSubtitleDefaultIcon } from './icon_vedio_guide_sub
export { default as VideoGuideSubtitleNoneIcon } from './icon_video_guide_subtitle_none.svg?react';
export { default as WheelchairTicketOfficeDefaultIcon } from './icon_wheelchair_ticket_office_default.svg?react';
export { default as WheelchairTicketOfficeNoneIcon } from './icon_wheelchair_ticket_office_none.svg?react';
+export { default as ChevronLeftIcon } from './Chevron_Left.svg?react';
+export { default as ToggleXIcon } from './toggle-x.svg?react';
+export { default as ResetXIcon } from './reset-x.svg?react';
+export { default as SearchSetIcon } from './icon-search-set.svg?react';
+export { default as CheckFillIcon } from './icon-check-fill.svg?react';
export { default as ArrowRightIcon } from './icon-arrow-right.svg?react';
export { default as HeaderBackIcon } from './icon_header_back.svg?react';
diff --git a/src/assets/icon/reset-x.svg b/src/assets/icon/reset-x.svg
new file mode 100644
index 0000000..a3aef32
--- /dev/null
+++ b/src/assets/icon/reset-x.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/icon/toggle-x.svg b/src/assets/icon/toggle-x.svg
new file mode 100644
index 0000000..f8a2b7f
--- /dev/null
+++ b/src/assets/icon/toggle-x.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx
index 1ece1ac..b164fcd 100644
--- a/src/components/LoginModal.tsx
+++ b/src/components/LoginModal.tsx
@@ -97,4 +97,6 @@ const closeButtonCss = css`
position: absolute;
top: 2rem;
right: 2rem;
+
+ color: ${COLORS.gray5};
`;
diff --git a/src/components/MenuBar.tsx b/src/components/MenuBar.tsx
index 55dd616..6bff744 100644
--- a/src/components/MenuBar.tsx
+++ b/src/components/MenuBar.tsx
@@ -19,13 +19,14 @@ const PATH_MATCH = [
const MenuBar = () => {
const { pathname } = useLocation();
+ const firstPathname = `/${pathname.split('/')[1]}`;
const menuList = PATH_MATCH.map(({ url, name, icon }) => {
return (
{icon}
{name}
diff --git a/src/components/PlaceCard.tsx b/src/components/PlaceCard.tsx
new file mode 100644
index 0000000..85e0160
--- /dev/null
+++ b/src/components/PlaceCard.tsx
@@ -0,0 +1,73 @@
+import { css } from '@emotion/react';
+import { Link } from 'react-router-dom';
+
+import { HeartMonoIcon, PinLocationMonoIcon } from '@/assets/icon';
+import { COLORS, FONTS } from '@/styles/constants';
+
+interface PlaceCardProps {
+ placeName: string;
+ address: string;
+}
+
+/**
+ * @param placeName 장소 이름
+ * @param address 주소
+ */
+
+const PlaceCard = (props: PlaceCardProps) => {
+ const { placeName, address } = props;
+ return (
+
+
+
{placeName}
+
+ {address}
+
+
+ );
+};
+
+export default PlaceCard;
+
+const cardContainerCss = css`
+ display: flex;
+ flex-direction: column;
+ position: relative;
+
+ width: 100%;
+ height: 16.8rem;
+ border-radius: 1.2rem;
+
+ background-color: gray;
+
+ color: ${COLORS.white};
+`;
+
+const titleCss = css`
+ margin: 9.4rem 0 0 1.6rem;
+ ${FONTS.H3};
+
+ text-align: left;
+`;
+
+const addressCss = css`
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+
+ margin-left: 1.6rem;
+
+ ${FONTS.Small1};
+
+ & > span {
+ padding-top: 0.1rem;
+ }
+`;
+
+const iconCss = css`
+ position: absolute;
+ top: 1.6rem;
+ right: 1.6rem;
+`;
diff --git a/src/utils/storageHideGuide.ts b/src/utils/storageHideGuide.ts
new file mode 100644
index 0000000..0f7a68b
--- /dev/null
+++ b/src/utils/storageHideGuide.ts
@@ -0,0 +1,39 @@
+import { STORAGE_KEY } from '@/views/Search/constants/localStorageKey';
+
+const key = STORAGE_KEY.hideSearchGuide;
+
+const getStorageHideGuide = () => {
+ const hideGuideTime = localStorage.getItem(key);
+ return hideGuideTime ? Number(hideGuideTime) : null;
+};
+
+// 24시간을 ms로
+const EXPIRATION_PERIOD = 24 * 60 * 60 * 1000;
+export const setStorageHideGuide = () => {
+ const date = new Date();
+ const expirationTime = date.getTime() + EXPIRATION_PERIOD;
+ localStorage.setItem(key, String(expirationTime));
+};
+
+const removeStorageHideGuide = () => {
+ localStorage.removeItem(key);
+};
+
+export const isGuideShown = () => {
+ const hideGuideTime = getStorageHideGuide();
+ const nowDate = new Date();
+
+ if (hideGuideTime) {
+ // 만료 X
+ if (nowDate.getTime() <= hideGuideTime) {
+ return false;
+ }
+ // 만료 O
+ else {
+ removeStorageHideGuide();
+ return true;
+ }
+ }
+ // 만료 O
+ else return true;
+};
diff --git a/src/utils/storageSearchWord.ts b/src/utils/storageSearchWord.ts
new file mode 100644
index 0000000..61e01c2
--- /dev/null
+++ b/src/utils/storageSearchWord.ts
@@ -0,0 +1,36 @@
+import { STORAGE_KEY } from '@/views/Search/constants/localStorageKey';
+
+const key = STORAGE_KEY.recentSearch;
+const LIST_MAX_LENGTH = 10;
+
+export const getStorageSearchWord = (): string[] => {
+ return JSON.parse(localStorage.getItem(key) || '[]');
+};
+
+export const setStorageSearchWord = (newValue: string) => {
+ const previousList: string[] = JSON.parse(localStorage.getItem(key) || '[]');
+
+ // 이미 존재하는지 확인
+ const previousIndex = previousList.findIndex((item) => item === newValue);
+
+ // 존재하면 삭제
+ if (previousIndex !== -1) previousList.splice(previousIndex, 1);
+ // 존재하지 않으면 max length 확인
+ else {
+ if (previousList.length === LIST_MAX_LENGTH) previousList.pop();
+ }
+
+ // 새로운 값 추가
+ localStorage.setItem(key, JSON.stringify([newValue, ...previousList]));
+};
+
+export const removeStorageSearchWord = (newValue: string): string[] => {
+ const list: string[] = JSON.parse(localStorage.getItem(key) || '[]');
+
+ // 인덱스 찾아서 제거
+ const previousIndex = list.findIndex((item) => item === newValue);
+ list.splice(previousIndex, 1);
+ localStorage.setItem(key, JSON.stringify([...list]));
+
+ return list;
+};
diff --git a/src/views/Search/components/RelatedWordList.tsx b/src/views/Search/components/RelatedWordList.tsx
new file mode 100644
index 0000000..08f46a7
--- /dev/null
+++ b/src/views/Search/components/RelatedWordList.tsx
@@ -0,0 +1,56 @@
+import { css } from '@emotion/react';
+
+import { SearchMonoIcon } from '@/assets/icon';
+import { COLORS, FONTS } from '@/styles/constants';
+
+interface RelatedWordListProps {
+ searchWord: string;
+}
+
+const RelatedWordList = (props: RelatedWordListProps) => {
+ const { searchWord } = props;
+ console.log(searchWord);
+
+ return (
+
+ -
+
+
+ -
+
+
+
+ );
+};
+
+export default RelatedWordList;
+
+const containerCss = css`
+ display: flex;
+ flex-direction: column;
+
+ margin-top: 2.4rem;
+`;
+
+const wordCss = css`
+ display: flex;
+ gap: 2.2rem;
+ align-items: center;
+
+ height: 5.9rem;
+ margin-left: 2.4rem;
+`;
+
+const wordTextCss = css`
+ padding-top: 0.2rem;
+
+ color: ${COLORS.brand1};
+
+ ${FONTS.Body3};
+`;
diff --git a/src/views/Search/components/Result/Guide.tsx b/src/views/Search/components/Result/Guide.tsx
new file mode 100644
index 0000000..f62ecf4
--- /dev/null
+++ b/src/views/Search/components/Result/Guide.tsx
@@ -0,0 +1,96 @@
+import { css } from '@emotion/react';
+
+import { CheckFillIcon, XMonoIcon } from '@/assets/icon';
+import { COLORS, FONTS } from '@/styles/constants';
+import { setStorageHideGuide } from '@/utils/storageHideGuide';
+
+interface GuideProps {
+ handleSetShowGuide: (value: boolean) => void;
+}
+
+const Guide = (props: GuideProps) => {
+ const { handleSetShowGuide } = props;
+
+ const hideGuideForADay = () => {
+ setStorageHideGuide();
+ handleSetShowGuide(false);
+ };
+
+ return (
+
+
+
+
+
+
+ 내가 설정한 여행자 유형이
+
+ 필터에 반영되었어요!
+
+
+
+
+ );
+};
+
+export default Guide;
+
+const containerCss = css`
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 999;
+
+ width: 100vw;
+ height: 100vh;
+`;
+
+const section1Css = css`
+ position: relative;
+
+ height: 6.4rem;
+
+ background-color: rgb(82 82 82 / 72%);
+`;
+
+const section2Css = css`
+ height: 100%;
+ margin-top: 4.6rem;
+
+ background-color: rgb(82 82 82 / 72%);
+`;
+
+const buttonCss = css`
+ position: absolute;
+ top: 2.5rem;
+ right: 2.4rem;
+
+ color: ${COLORS.white};
+`;
+
+const textCss = css`
+ padding-top: 2rem;
+ margin-left: 2rem;
+
+ color: ${COLORS.white};
+
+ ${FONTS.H5};
+`;
+
+const buttonTextCss = css`
+ display: flex;
+ gap: 0.6rem;
+
+ margin: 2.8rem 0 0 2rem;
+
+ color: ${COLORS.white};
+
+ ${FONTS.Body4};
+`;
diff --git a/src/views/Search/components/Result/SearchResult.tsx b/src/views/Search/components/Result/SearchResult.tsx
new file mode 100644
index 0000000..51fcc93
--- /dev/null
+++ b/src/views/Search/components/Result/SearchResult.tsx
@@ -0,0 +1,47 @@
+import { css } from '@emotion/react';
+
+import PlaceCard from '@/components/PlaceCard';
+
+const SearchResult = () => {
+ return (
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+ );
+};
+
+export default SearchResult;
+
+const containerCss = css`
+ display: flex;
+ gap: 1.2rem;
+ flex-direction: column;
+
+ height: calc(100vh - 11rem);
+ overflow-y: scroll;
+
+ padding: 1.6rem 2rem 0;
+`;
diff --git a/src/views/Search/components/Search/PopularSearch.tsx b/src/views/Search/components/Search/PopularSearch.tsx
new file mode 100644
index 0000000..f2b7416
--- /dev/null
+++ b/src/views/Search/components/Search/PopularSearch.tsx
@@ -0,0 +1,69 @@
+import { css } from '@emotion/react';
+import { useNavigate } from 'react-router-dom';
+
+import { COLORS, FONTS } from '@/styles/constants';
+import { setStorageSearchWord } from '@/utils/storageSearchWord';
+
+const PopularSearch = () => {
+ const navigate = useNavigate();
+
+ const handleOnClick = (searchWord: string) => {
+ setStorageSearchWord(searchWord);
+ navigate(searchWord);
+ };
+
+ const wordList = [
+ '비대면 관광',
+ '대전시립미술관',
+ '대전 휴양림',
+ '장미꽃 명소',
+ '가족과 함께',
+ ].map((item, idx) => {
+ return (
+
+
+
+ );
+ });
+
+ return (
+
+
지금 가장 인기있는 검색어
+
{wordList}
+
+ );
+};
+
+export default PopularSearch;
+
+const container = css`
+ margin-top: 3.6rem;
+`;
+
+const title = css`
+ margin: 0 0 1rem 2rem;
+
+ color: ${COLORS.brand1};
+
+ ${FONTS.Body2};
+`;
+
+const word = css`
+ display: flex;
+ gap: 2.6rem;
+ align-items: center;
+
+ height: 4.4rem;
+ margin-left: 2.5rem;
+
+ color: ${COLORS.gray6};
+
+ ${FONTS.Body4};
+`;
+
+const idxCss = css`
+ width: 0.9rem;
+`;
diff --git a/src/views/Search/components/Search/RecentSearch.tsx b/src/views/Search/components/Search/RecentSearch.tsx
new file mode 100644
index 0000000..8b0f24a
--- /dev/null
+++ b/src/views/Search/components/Search/RecentSearch.tsx
@@ -0,0 +1,103 @@
+import { css } from '@emotion/react';
+import { MouseEvent, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { ToggleXIcon } from '@/assets/icon';
+import { COLORS, FONTS } from '@/styles/constants';
+import {
+ getStorageSearchWord,
+ removeStorageSearchWord,
+ setStorageSearchWord,
+} from '@/utils/storageSearchWord';
+
+const RecentSearch = () => {
+ const navigate = useNavigate();
+ const savedWordList = getStorageSearchWord();
+ const [wordList, setWordList] = useState(savedWordList);
+
+ const handleOnClickWordButton = (word: string) => {
+ setStorageSearchWord(word);
+ navigate(word);
+ };
+
+ const handleOnClickDeleteButton = (
+ e: MouseEvent,
+ word: string,
+ ) => {
+ e.stopPropagation();
+ const newList = removeStorageSearchWord(word);
+ setWordList(newList);
+ };
+
+ const wordButtonList = wordList.map((item) => {
+ return (
+
+
+
+ );
+ });
+
+ return (
+ <>
+ 최근 검색어
+
+ >
+ );
+};
+
+export default RecentSearch;
+
+const title = css`
+ margin: 2.7rem 0 2rem 2rem;
+
+ color: ${COLORS.brand1};
+
+ ${FONTS.Body2};
+`;
+
+const word = css`
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+
+ width: max-content;
+ height: 3.7rem;
+ padding: 0 0.9rem 0 1.5rem;
+ border-radius: 4rem;
+
+ background-color: ${COLORS.gray1};
+`;
+
+const wordText = css`
+ padding-top: 0.2rem;
+
+ color: ${COLORS.gray6};
+
+ ${FONTS.Body3}
+`;
+
+const wordContainer = css`
+ display: flex;
+ gap: 1rem;
+
+ width: 100vw;
+ overflow-x: auto;
+
+ & > li:first-child {
+ margin-left: 2rem;
+ }
+
+ & > li:last-child {
+ margin-right: 1rem;
+ }
+`;
diff --git a/src/views/Search/components/SearchBar.tsx b/src/views/Search/components/SearchBar.tsx
new file mode 100644
index 0000000..3bd7803
--- /dev/null
+++ b/src/views/Search/components/SearchBar.tsx
@@ -0,0 +1,86 @@
+import { css } from '@emotion/react';
+import { ChangeEvent, KeyboardEvent } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { ChevronLeftIcon, ResetXIcon } from '@/assets/icon';
+import { COLORS, FONTS } from '@/styles/constants';
+import { setStorageSearchWord } from '@/utils/storageSearchWord';
+
+interface SearchBarProps {
+ searchWord: string;
+ handleSearchWord: (word: string) => void;
+}
+
+const SearchBar = (props: SearchBarProps) => {
+ const { searchWord, handleSearchWord } = props;
+
+ const navigate = useNavigate();
+
+ const handleOnChange = (e: ChangeEvent) => {
+ handleSearchWord(e.currentTarget.value);
+ };
+
+ const handleOnClick = () => {
+ handleSearchWord('');
+ };
+
+ const handleOnKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ setStorageSearchWord(searchWord);
+ navigate(`/search/${searchWord}`);
+ }
+ };
+
+ return (
+
+ navigate(-1)}>
+
+
+
+ {searchWord && (
+
+
+
+ )}
+
+ );
+};
+
+export default SearchBar;
+
+const containerCss = css`
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+
+ width: 100%;
+ padding: 0.8rem 2rem 0;
+`;
+
+const inputCss = css`
+ width: 100%;
+ padding: 1.2rem 1.6rem;
+ margin-left: 1.2rem;
+ border: 1px solid ${COLORS.brand1};
+ border-radius: 99.9rem;
+
+ color: ${COLORS.gray9};
+ ${FONTS.Body2};
+
+ &::placeholder {
+ color: ${COLORS.gray4};
+ ${FONTS.Body2};
+ }
+`;
+
+const deleteButtonCss = css`
+ position: absolute;
+ top: 2.1rem;
+ right: 3.6rem;
+`;
diff --git a/src/views/Search/constants/localStorageKey.ts b/src/views/Search/constants/localStorageKey.ts
new file mode 100644
index 0000000..3947e58
--- /dev/null
+++ b/src/views/Search/constants/localStorageKey.ts
@@ -0,0 +1,4 @@
+export const STORAGE_KEY = {
+ recentSearch: 'recent-search',
+ hideSearchGuide: 'hide-search-guide',
+};
diff --git a/src/views/Search/pages/SearchPage.tsx b/src/views/Search/pages/SearchPage.tsx
new file mode 100644
index 0000000..353b834
--- /dev/null
+++ b/src/views/Search/pages/SearchPage.tsx
@@ -0,0 +1,33 @@
+import { useState } from 'react';
+
+import MenuBar from '@/components/MenuBar';
+
+import RelatedWordList from '../components/RelatedWordList';
+import PopularSearch from '../components/Search/PopularSearch';
+import RecentSearch from '../components/Search/RecentSearch';
+import SearchBar from '../components/SearchBar';
+
+const SearchPage = () => {
+ const [searchWord, setSearchWord] = useState('');
+
+ const handleSearchWord = (word: string) => {
+ setSearchWord(word);
+ };
+
+ return (
+ <>
+
+ {searchWord ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ >
+ );
+};
+
+export default SearchPage;
diff --git a/src/views/Search/pages/SearchResultPage.tsx b/src/views/Search/pages/SearchResultPage.tsx
new file mode 100644
index 0000000..d6ce5cb
--- /dev/null
+++ b/src/views/Search/pages/SearchResultPage.tsx
@@ -0,0 +1,69 @@
+import { css } from '@emotion/react';
+import { useState } from 'react';
+import { useParams } from 'react-router-dom';
+
+import { SearchSetIcon } from '@/assets/icon';
+import MenuBar from '@/components/MenuBar';
+import { COLORS, FONTS } from '@/styles/constants';
+import { isGuideShown } from '@/utils/storageHideGuide';
+
+import RelatedWordList from '../components/RelatedWordList';
+import Guide from '../components/Result/Guide';
+import SearchResult from '../components/Result/SearchResult';
+import SearchBar from '../components/SearchBar';
+
+const SearchResultPage = () => {
+ const { word: initialWord } = useParams();
+ const [searchWord, setSearchWord] = useState(initialWord || '');
+
+ const [showGuide, setShowGuide] = useState(() => isGuideShown());
+
+ const handleSearchWord = (word: string) => {
+ setSearchWord(word);
+ };
+
+ const handleSetShowGuide = (value: boolean) => {
+ setShowGuide(value);
+ };
+
+ return (
+
+
+ {searchWord !== '' && searchWord !== initialWord ? (
+
+ ) : (
+ <>
+
+ 기본 편의시설, 지체장애
+
+
+ >
+ )}
+
+ {showGuide && }
+
+
+ );
+};
+
+export default SearchResultPage;
+
+const buttonCss = css`
+ display: flex;
+ gap: 1.2rem;
+ align-items: center;
+
+ width: 100%;
+ height: 4.6rem;
+ padding-left: 2.3rem;
+ margin-top: 0.8rem;
+
+ background-color: ${COLORS.gray1};
+
+ color: ${COLORS.brand1};
+
+ ${FONTS.Body3};
+`;