diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js
index 36a992745..d8bc3ace3 100644
--- a/frontend/public/mockServiceWorker.js
+++ b/frontend/public/mockServiceWorker.js
@@ -2,7 +2,7 @@
/* tslint:disable */
/**
- * Mock Service Worker (1.2.3).
+ * Mock Service Worker (1.3.0).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
diff --git a/frontend/src/components/Common/Header/Header.tsx b/frontend/src/components/Common/Header/Header.tsx
index a52adaf21..ae2431ed8 100644
--- a/frontend/src/components/Common/Header/Header.tsx
+++ b/frontend/src/components/Common/Header/Header.tsx
@@ -2,14 +2,33 @@ import { Link } from '@fun-eat/design-system';
import { Link as RouterLink } from 'react-router-dom';
import styled from 'styled-components';
+import SvgIcon from '../Svg/SvgIcon';
+
import Logo from '@/assets/logo.svg';
import { PATH } from '@/constants/path';
-const Header = () => {
+interface HeaderProps {
+ hasSearch?: boolean;
+}
+
+const Header = ({ hasSearch = true }: HeaderProps) => {
+ if (hasSearch) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
return (
-
+
);
@@ -17,6 +36,15 @@ const Header = () => {
export default Header;
+const HeaderWithSearchContainer = styled.header`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: calc(100% - 40px);
+ height: 60px;
+ margin: 0 auto;
+`;
+
const HeaderContainer = styled.header`
display: flex;
justify-content: center;
diff --git a/frontend/src/components/Common/Title/Title.stories.tsx b/frontend/src/components/Common/Title/Title.stories.tsx
deleted file mode 100644
index 9f455c1a5..000000000
--- a/frontend/src/components/Common/Title/Title.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import Title from './Title';
-
-const meta: Meta = {
- title: 'common/Title',
- component: Title,
- args: {
- headingTitle: '상품 목록',
- },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/frontend/src/components/Common/Title/Title.tsx b/frontend/src/components/Common/Title/Title.tsx
deleted file mode 100644
index 96aa3e7ef..000000000
--- a/frontend/src/components/Common/Title/Title.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Link, Text, theme } from '@fun-eat/design-system';
-import { Link as RouterLink } from 'react-router-dom';
-import styled from 'styled-components';
-
-import SvgIcon from '../Svg/SvgIcon';
-
-import { PATH } from '@/constants/path';
-
-interface TitleProps {
- headingTitle: string;
- routeDestination: string;
-}
-
-const Title = ({ headingTitle, routeDestination }: TitleProps) => {
- return (
-
-
-
-
-
-
- {headingTitle}
-
-
-
-
- );
-};
-
-export default Title;
-
-const TitleContainer = styled.div`
- position: relative;
- display: flex;
- flex-direction: row;
- justify-content: center;
-`;
-
-const HomeLink = styled(Link)`
- position: absolute;
- top: 8px;
- left: 0;
-`;
-
-const TitleLink = styled(Link)`
- display: flex;
- gap: 20px;
- align-items: center;
-`;
-
-const DropDownIcon = styled(SvgIcon)`
- rotate: 270deg;
-`;
diff --git a/frontend/src/components/Common/index.ts b/frontend/src/components/Common/index.ts
index 395277010..e48a05d20 100644
--- a/frontend/src/components/Common/index.ts
+++ b/frontend/src/components/Common/index.ts
@@ -7,7 +7,6 @@ export { default as SvgSprite } from './Svg/SvgSprite';
export { default as SvgIcon } from './Svg/SvgIcon';
export { default as TabMenu } from './TabMenu/TabMenu';
export { default as TagList } from './TagList/TagList';
-export { default as Title } from './Title/Title';
export { default as SectionTitle } from './SectionTitle/SectionTitle';
export { default as ScrollButton } from './ScrollButton/ScrollButton';
export { default as Input } from './Input/Input';
diff --git a/frontend/src/components/Layout/AuthLayout.tsx b/frontend/src/components/Layout/AuthLayout.tsx
index e65109b6b..0cc0570c2 100644
--- a/frontend/src/components/Layout/AuthLayout.tsx
+++ b/frontend/src/components/Layout/AuthLayout.tsx
@@ -1,10 +1,13 @@
-import type { PropsWithChildren } from 'react';
import { Navigate } from 'react-router-dom';
import { PATH } from '@/constants/path';
import { useMemberQuery } from '@/hooks/queries/members';
-const AuthLayout = ({ children }: PropsWithChildren) => {
+interface AuthLayoutProps {
+ children: JSX.Element;
+}
+
+const AuthLayout = ({ children }: AuthLayoutProps) => {
const { data: member } = useMemberQuery();
if (!member) {
diff --git a/frontend/src/components/Layout/SimpleHeaderLayout.tsx b/frontend/src/components/Layout/SimpleHeaderLayout.tsx
new file mode 100644
index 000000000..c6c469169
--- /dev/null
+++ b/frontend/src/components/Layout/SimpleHeaderLayout.tsx
@@ -0,0 +1,31 @@
+import type { PropsWithChildren } from 'react';
+import styled from 'styled-components';
+
+import Header from '../Common/Header/Header';
+import NavigationBar from '../Common/NavigationBar/NavigationBar';
+
+const SimpleHeaderLayout = ({ children }: PropsWithChildren) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export default SimpleHeaderLayout;
+
+const SimpleHeaderLayoutContainer = styled.div`
+ height: 100%;
+ max-width: 600px;
+ margin: 0 auto;
+`;
+
+const MainWrapper = styled.main`
+ position: relative;
+ height: calc(100% - 120px);
+ padding: 20px;
+ overflow-x: hidden;
+ overflow-y: auto;
+`;
diff --git a/frontend/src/components/Layout/index.ts b/frontend/src/components/Layout/index.ts
index 32a1e00e0..69ec1a40c 100644
--- a/frontend/src/components/Layout/index.ts
+++ b/frontend/src/components/Layout/index.ts
@@ -2,3 +2,4 @@ export { default as DefaultLayout } from './DefaultLayout';
export { default as MinimalLayout } from './MinimalLayout';
export { default as HeaderOnlyLayout } from './HeaderOnlyLayout';
export { default as AuthLayout } from './AuthLayout';
+export { default as SimpleHeaderLayout } from './SimpleHeaderLayout';
diff --git a/frontend/src/components/Product/ProductTitle/ProductTitle.stories.tsx b/frontend/src/components/Product/ProductTitle/ProductTitle.stories.tsx
new file mode 100644
index 000000000..f4e210607
--- /dev/null
+++ b/frontend/src/components/Product/ProductTitle/ProductTitle.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import ProductTitle from './ProductTitle';
+
+const meta: Meta = {
+ title: 'common/ProductTitle',
+ component: ProductTitle,
+ args: {
+ headingTitle: '상품 목록',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/frontend/src/components/Product/ProductTitle/ProductTitle.tsx b/frontend/src/components/Product/ProductTitle/ProductTitle.tsx
new file mode 100644
index 000000000..be0e499f1
--- /dev/null
+++ b/frontend/src/components/Product/ProductTitle/ProductTitle.tsx
@@ -0,0 +1,51 @@
+import { Heading, Link, theme } from '@fun-eat/design-system';
+import { Link as RouterLink } from 'react-router-dom';
+import styled from 'styled-components';
+
+import SvgIcon from '../../Common/Svg/SvgIcon';
+
+import { PATH } from '@/constants/path';
+
+interface ProductTitleProps {
+ content: string;
+ routeDestination: string;
+}
+
+const ProductTitle = ({ content, routeDestination }: ProductTitleProps) => {
+ return (
+
+
+ {content}
+
+
+
+
+
+
+ );
+};
+
+export default ProductTitle;
+
+const ProductTitleContainer = styled.div`
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+const ProductTitleLink = styled(Link)`
+ display: flex;
+ gap: 20px;
+ align-items: center;
+ margin-left: 36%;
+`;
+
+const HeadingTitle = styled(Heading)`
+ font-size: 2.4rem;
+`;
+
+const DropDownIcon = styled(SvgIcon)`
+ rotate: 270deg;
+`;
diff --git a/frontend/src/components/Product/index.ts b/frontend/src/components/Product/index.ts
index 4038ee740..47ce2703d 100644
--- a/frontend/src/components/Product/index.ts
+++ b/frontend/src/components/Product/index.ts
@@ -4,3 +4,4 @@ export { default as ProductList } from './ProductList/ProductList';
export { default as ProductOverviewItem } from './ProductOverviewItem/ProductOverviewItem';
export { default as PBProductList } from './PBProductList/PBProductList';
export { default as ProductRecipeList } from './ProductRecipeList/ProductRecipeList';
+export { default as ProductTitle } from './ProductTitle/ProductTitle';
diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts
index a45e4103f..66e733697 100644
--- a/frontend/src/constants/index.ts
+++ b/frontend/src/constants/index.ts
@@ -3,11 +3,6 @@ import { PATH } from './path';
import type { NavigationMenu } from '@/types/common';
export const NAVIGATION_MENU: NavigationMenu[] = [
- {
- variant: 'search',
- name: '검색',
- path: PATH.SEARCH,
- },
{
variant: 'list',
name: '목록',
@@ -58,7 +53,8 @@ export const TAG_TITLE = {
export const MIN_DISPLAYED_TAGS_LENGTH = 3;
-export const SEARCH_PAGE_TABS = ['상품', '꿀조합'] as const;
+export const SEARCH_TAB_VARIANTS = ['상품', '꿀조합'];
+export const SEARCH_PAGE_VARIANTS = { products: '상품', recipes: '꿀조합', integrated: '통합' } as const;
export const CATEGORY_TYPE = {
FOOD: 'food',
diff --git a/frontend/src/pages/IntegratedSearchPage.tsx b/frontend/src/pages/IntegratedSearchPage.tsx
new file mode 100644
index 000000000..64d66d155
--- /dev/null
+++ b/frontend/src/pages/IntegratedSearchPage.tsx
@@ -0,0 +1,123 @@
+import { Button, Heading, Spacing, Text } from '@fun-eat/design-system';
+import { useQueryErrorResetBoundary } from '@tanstack/react-query';
+import type { MouseEventHandler } from 'react';
+import { Suspense, useEffect, useState } from 'react';
+import styled from 'styled-components';
+
+import { ErrorBoundary, ErrorComponent, Input, Loading, SvgIcon, TabMenu } from '@/components/Common';
+import { RecommendList, ProductSearchResultList, RecipeSearchResultList } from '@/components/Search';
+import { SEARCH_TAB_VARIANTS } from '@/constants';
+import { useDebounce } from '@/hooks/common';
+import { useSearch } from '@/hooks/search';
+
+const isProductSearchTab = (tabMenu: string) => tabMenu === SEARCH_TAB_VARIANTS[0];
+const getInputPlaceholder = (tabMenu: string) =>
+ isProductSearchTab(tabMenu) ? '상품 이름을 검색해보세요.' : '꿀조합에 포함된 상품을 입력해보세요.';
+
+const IntegratedSearchPage = () => {
+ const {
+ inputRef,
+ searchQuery,
+ isSubmitted,
+ isAutocompleteOpen,
+ handleSearchQuery,
+ handleSearch,
+ handleSearchClick,
+ handleAutocompleteClose,
+ } = useSearch();
+ const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery || '');
+ const [selectedTabMenu, setSelectedTabMenu] = useState(SEARCH_TAB_VARIANTS[0]);
+ const { reset } = useQueryErrorResetBoundary();
+
+ const handleTabMenuSelect: MouseEventHandler = (event) => {
+ setSelectedTabMenu(event.currentTarget.value);
+ };
+
+ useDebounce(
+ () => {
+ setDebouncedSearchQuery(searchQuery);
+ },
+ 200,
+ [searchQuery]
+ );
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+ {!isSubmitted && debouncedSearchQuery && isAutocompleteOpen && (
+
+ }>
+
+
+
+ )}
+
+
+
+
+ {isSubmitted && debouncedSearchQuery ? (
+ <>
+
+ '{searchQuery}'에 대한 검색결과입니다.
+
+
+ }>
+
+ {isProductSearchTab(selectedTabMenu) ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ ) : (
+ {selectedTabMenu}을 검색해보세요.
+ )}
+
+ >
+ );
+};
+
+export default IntegratedSearchPage;
+
+const SearchSection = styled.section`
+ position: relative;
+`;
+
+const SearchResultSection = styled.section`
+ margin-top: 30px;
+`;
+
+const Mark = styled.mark`
+ font-weight: ${({ theme }) => theme.fontWeights.bold};
+ background-color: ${({ theme }) => theme.backgroundColors.default};
+`;
diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx
index 8e7da4b50..32ecc5475 100644
--- a/frontend/src/pages/ProductListPage.tsx
+++ b/frontend/src/pages/ProductListPage.tsx
@@ -8,13 +8,12 @@ import {
CategoryMenu,
SortButton,
SortOptionList,
- Title,
ScrollButton,
Loading,
ErrorBoundary,
ErrorComponent,
} from '@/components/Common';
-import { ProductList } from '@/components/Product';
+import { ProductTitle, ProductList } from '@/components/Product';
import { PRODUCT_SORT_OPTIONS } from '@/constants';
import { PATH } from '@/constants/path';
import { useSortOption } from '@/hooks/common';
@@ -40,8 +39,8 @@ const ProductListPage = () => {
return (
<>
-
diff --git a/frontend/src/pages/RecipePage.tsx b/frontend/src/pages/RecipePage.tsx
index 43bf5a82f..706e6b941 100644
--- a/frontend/src/pages/RecipePage.tsx
+++ b/frontend/src/pages/RecipePage.tsx
@@ -16,6 +16,7 @@ import {
} from '@/components/Common';
import { RecipeList, RecipeRegisterForm } from '@/components/Recipe';
import { RECIPE_SORT_OPTIONS } from '@/constants';
+import { PATH } from '@/constants/path';
import RecipeFormProvider from '@/contexts/RecipeFormContext';
import { useSortOption } from '@/hooks/common';
@@ -41,12 +42,12 @@ const RecipePage = () => {
return (
<>
-
- {RECIPE_PAGE_TITLE}
-
-
-
-
+
+ {RECIPE_PAGE_TITLE}
+
+
+
+
}>
@@ -84,14 +85,14 @@ const RecipePage = () => {
export default RecipePage;
-const Title = styled(Heading)`
- font-size: 24px;
+const TitleWrapper = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
`;
-const SearchPageLink = styled(Link)`
- position: absolute;
- top: 24px;
- right: 20px;
+const Title = styled(Heading)`
+ font-size: 24px;
`;
const SortButtonWrapper = styled.div`
diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx
index 7089b0dfc..6a1c9f892 100644
--- a/frontend/src/pages/SearchPage.tsx
+++ b/frontend/src/pages/SearchPage.tsx
@@ -1,18 +1,20 @@
-import { Button, Heading, Spacing, Text } from '@fun-eat/design-system';
+import { Button, Heading, Spacing, Text, useTheme } from '@fun-eat/design-system';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
-import type { MouseEventHandler } from 'react';
import { Suspense, useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
import styled from 'styled-components';
-import { ErrorBoundary, ErrorComponent, Input, Loading, SvgIcon, TabMenu } from '@/components/Common';
+import { ErrorBoundary, ErrorComponent, Input, Loading, SvgIcon } from '@/components/Common';
import { RecommendList, ProductSearchResultList, RecipeSearchResultList } from '@/components/Search';
-import { SEARCH_PAGE_TABS } from '@/constants';
-import { useDebounce } from '@/hooks/common';
+import { SEARCH_PAGE_VARIANTS } from '@/constants';
+import { useDebounce, useRoutePage } from '@/hooks/common';
import { useSearch } from '@/hooks/search';
-const isProductSearchTab = (tabMenu: string) => tabMenu === SEARCH_PAGE_TABS[0];
-const getInputPlaceholder = (tabMenu: string) =>
- isProductSearchTab(tabMenu) ? '상품 이름을 검색해보세요.' : '꿀조합에 포함된 상품을 입력해보세요.';
+const isProductSearchPage = (path: string) => path === 'products';
+const getInputPlaceholder = (path: string) =>
+ isProductSearchPage(path) ? '상품 이름을 검색해보세요.' : '꿀조합에 포함된 상품을 입력해보세요.';
+
+type SearchPageType = keyof typeof SEARCH_PAGE_VARIANTS;
const SearchPage = () => {
const {
@@ -26,12 +28,12 @@ const SearchPage = () => {
handleAutocompleteClose,
} = useSearch();
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery || '');
- const [selectedTabMenu, setSelectedTabMenu] = useState(SEARCH_PAGE_TABS[0]);
const { reset } = useQueryErrorResetBoundary();
+ const { routeBack } = useRoutePage();
- const handleTabMenuSelect: MouseEventHandler = (event) => {
- setSelectedTabMenu(event.currentTarget.value);
- };
+ const theme = useTheme();
+
+ const { searchVariant } = useParams();
useDebounce(
() => {
@@ -47,13 +49,28 @@ const SearchPage = () => {
}
}, []);
+ const isSearchVariant = (value: string): value is SearchPageType => {
+ return value === 'products' || value === 'recipes';
+ };
+
+ if (!searchVariant || !isSearchVariant(searchVariant)) {
+ return null;
+ }
+
return (
<>
+
+
+ {SEARCH_PAGE_VARIANTS[searchVariant]} 검색
+
+
)}
-
-
{isSubmitted && debouncedSearchQuery ? (
<>
@@ -91,7 +102,7 @@ const SearchPage = () => {
}>
- {isProductSearchTab(selectedTabMenu) ? (
+ {isProductSearchPage(searchVariant) ? (
) : (
@@ -100,7 +111,7 @@ const SearchPage = () => {
>
) : (
- {selectedTabMenu}을 검색해보세요.
+ {SEARCH_PAGE_VARIANTS[searchVariant]}을 검색해보세요.
)}
>
@@ -117,6 +128,16 @@ const SearchResultSection = styled.section`
margin-top: 30px;
`;
+const TitleWrapper = styled.div`
+ display: flex;
+ gap: 12px;
+ align-items: center;
+`;
+
+const HeadingTitle = styled(Heading)`
+ font-size: 2.4rem;
+`;
+
const Mark = styled.mark`
font-weight: ${({ theme }) => theme.fontWeights.bold};
background-color: ${({ theme }) => theme.backgroundColors.default};
diff --git a/frontend/src/router/App.tsx b/frontend/src/router/App.tsx
index b4428b177..a8c4bfd10 100644
--- a/frontend/src/router/App.tsx
+++ b/frontend/src/router/App.tsx
@@ -3,11 +3,11 @@ import { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { ErrorBoundary, ErrorComponent, Loading } from '@/components/Common';
-import { MinimalLayout, DefaultLayout, HeaderOnlyLayout } from '@/components/Layout';
+import { MinimalLayout, DefaultLayout, HeaderOnlyLayout, SimpleHeaderLayout } from '@/components/Layout';
import { useRouteChangeTracker } from '@/hooks/common';
interface AppProps {
- layout?: 'default' | 'headerOnly' | 'minimal';
+ layout?: 'default' | 'headerOnly' | 'minimal' | 'simpleHeader';
}
const App = ({ layout = 'default' }: AppProps) => {
@@ -39,6 +39,18 @@ const App = ({ layout = 'default' }: AppProps) => {
);
}
+ if (layout === 'simpleHeader') {
+ return (
+
+ }>
+
+
+
+
+
+ );
+ }
+
return (
}>
diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx
index 8391765a3..0eaa96fb6 100644
--- a/frontend/src/router/index.tsx
+++ b/frontend/src/router/index.tsx
@@ -7,6 +7,7 @@ import { PATH } from '@/constants/path';
import CategoryProvider from '@/contexts/CategoryContext';
import AuthPage from '@/pages/AuthPage';
import HomePage from '@/pages/HomePage';
+import IntegratedSearchPage from '@/pages/IntegratedSearchPage';
import LoginPage from '@/pages/LoginPage';
import MemberModifyPage from '@/pages/MemberModifyPage';
import MemberPage from '@/pages/MemberPage';
@@ -33,18 +34,6 @@ const router = createBrowserRouter([
),
},
- {
- path: `${PATH.PRODUCT_LIST}/:category`,
- element: (
-
-
-
- ),
- },
- {
- path: PATH.RECIPE,
- element: ,
- },
{
path: `${PATH.RECIPE}/:recipeId`,
element: (
@@ -53,10 +42,6 @@ const router = createBrowserRouter([
),
},
- {
- path: PATH.SEARCH,
- element: ,
- },
{
path: PATH.MEMBER,
element: (
@@ -117,6 +102,33 @@ const router = createBrowserRouter([
},
],
},
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ path: `${PATH.PRODUCT_LIST}/:category`,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: PATH.RECIPE,
+ element: ,
+ },
+ {
+ path: `${PATH.SEARCH}/integrated`,
+ element: ,
+ },
+ {
+ path: `${PATH.SEARCH}/:searchVariant`,
+ element: ,
+ },
+ ],
+ },
]);
export default router;