diff --git a/package.json b/package.json
index 9c2c2f7..a89564b 100644
--- a/package.json
+++ b/package.json
@@ -14,8 +14,8 @@
"dependencies": {
"@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1",
- "@svgr/webpack": "^5.5.0",
"@reduxjs/toolkit": "^1.8.0",
+ "@svgr/webpack": "^5.5.0",
"@types/react-redux": "^7.1.23",
"@types/yup": "^0.29.13",
"emotion-normalize": "^11.0.1",
@@ -23,6 +23,7 @@
"formik": "^2.2.9",
"next": "12.1.0",
"next-redux-wrapper": "^7.0.5",
+ "polished": "^4.1.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-icons": "^4.3.1",
diff --git a/src/assets/icons/logo.svg b/src/assets/icons/logo.svg
new file mode 100644
index 0000000..72c12eb
--- /dev/null
+++ b/src/assets/icons/logo.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/components/Button/Button.types.ts b/src/components/Button/Button.types.ts
index a513ea2..9867044 100644
--- a/src/components/Button/Button.types.ts
+++ b/src/components/Button/Button.types.ts
@@ -10,6 +10,8 @@ export interface StyledButtonProps {
color: Color;
style?: React.CSSProperties;
className?: string;
+ onClick?: () => void;
+ title?: string;
}
export interface ButtonProps extends StyledButtonProps {
@@ -20,9 +22,10 @@ export interface ButtonProps extends StyledButtonProps {
export interface StyledIconButtonProps {
type: Type;
ariaLabel: string;
- circle: boolean;
+ circle?: boolean;
color: Color;
size: Size;
+ onClick?: () => void;
}
export interface IconButtonProps extends StyledIconButtonProps {
diff --git a/src/components/Header/Header.styled.tsx b/src/components/Header/Header.styled.tsx
new file mode 100644
index 0000000..417d864
--- /dev/null
+++ b/src/components/Header/Header.styled.tsx
@@ -0,0 +1,37 @@
+import styled from '@emotion/styled';
+import { IconButton } from 'components';
+
+const headerHeight = 70;
+
+export const StyledHeader = styled.header`
+ background-color: ${({ theme }) => theme.color.white};
+ box-shadow: 0 4px 10px rgba(0 0 0 / 10%);
+ padding: 0 10px 0 20px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ transition: top 0.2s ease-in-out;
+ z-index: 10;
+
+ &.hide {
+ top: ${-1 * headerHeight}px;
+ }
+`;
+
+export const StyledDiv = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ max-width: 1500px;
+ margin: 0 auto;
+ height: ${headerHeight}px;
+`;
+
+export const StyledIconButton = styled(IconButton)`
+ position: fixed;
+ right: 20px;
+ bottom: 20px;
+ cursor: pointer;
+ box-shadow: 1px 4px 9px rgb(0 0 0 / 30%);
+`;
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 97a80da..96f09c3 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -1,3 +1,102 @@
+import { SearchForm, Menu, Button, Logo } from 'components';
+import { useState, useEffect, useRef } from 'react';
+// import { useAuthLoading, useAuthUser } from '../../contexts/AuthContext';
+import lodash from 'lodash';
+import { createPortal } from 'react-dom';
+import { StyledHeader, StyledDiv, StyledIconButton } from './Header.styled';
+
export const Header = (): JSX.Element => {
- return ;
+ // const authLoading = useAuthLoading();
+ // const authUser = useAuthUser();
+ // const [showDialog, setShowDialog] = useState(false);
+
+ const [tempAuth, setTempAuth] = useState(false);
+ const [hideHeader, setHideHeader] = useState(false);
+ const [showScrollToTop, setShowScrollToTop] = useState(false);
+ const oldScrollTop = useRef(0);
+
+ /*
+ const handleOpenDialog = () => {
+ setShowDialog(true);
+ };
+
+ const handleCloseDialog = () => {
+ setShowDialog(false);
+ };
+ */
+
+ const handleFocus = () => {
+ setHideHeader(false);
+ };
+
+ const handleBlur = () => {
+ setHideHeader(window.pageYOffset > 70);
+ };
+
+ const controlHeader = lodash.throttle(() => {
+ const currentScrollTop = window.pageYOffset;
+ setHideHeader(currentScrollTop > 70 && currentScrollTop > oldScrollTop.current);
+ oldScrollTop.current = currentScrollTop;
+ }, 300);
+
+ const controlScrollToTop = lodash.debounce(() => {
+ const currentScrollTop = window.pageYOffset;
+ setShowScrollToTop(currentScrollTop > 500);
+ }, 300);
+
+ useEffect(() => {
+ document.addEventListener('scroll', controlHeader);
+ document.addEventListener('scroll', controlScrollToTop);
+ return () => {
+ document.removeEventListener('scroll', controlHeader);
+ document.removeEventListener('scroll', controlScrollToTop);
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {tempAuth ? (
+
+ ) : (
+ <>
+
+ {/* */}
+ >
+ )}
+ {showScrollToTop &&
+ createPortal(
+ {
+ window.scroll({
+ top: 0,
+ left: 0,
+ behavior: 'smooth',
+ });
+ }}
+ />,
+ document.getElementById('__next')!,
+ )}
+
+
+ );
};
diff --git a/src/components/Logo/Logo.styled.tsx b/src/components/Logo/Logo.styled.tsx
index 46bafb7..550824b 100644
--- a/src/components/Logo/Logo.styled.tsx
+++ b/src/components/Logo/Logo.styled.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { pxToRem, media } from 'utils';
-import { LogoIcon } from './LogoIcon';
+import LogoIcon from 'assets/icons/logo.svg';
export const StyledA = styled.a`
display: flex;
diff --git a/src/components/Logo/LogoIcon.tsx b/src/components/Logo/LogoIcon.tsx
deleted file mode 100644
index 5f61fc6..0000000
--- a/src/components/Logo/LogoIcon.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-export const LogoIcon = () => {
- return (
-
- );
-};
diff --git a/src/components/Menu/Menu.stories.tsx b/src/components/Menu/Menu.stories.tsx
new file mode 100644
index 0000000..fd238f5
--- /dev/null
+++ b/src/components/Menu/Menu.stories.tsx
@@ -0,0 +1,18 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Menu } from './Menu';
+
+export default {
+ title: 'Menu',
+ component: Menu,
+} as ComponentMeta;
+
+const Template: ComponentStory = () => ;
+
+export const Default = Template.bind({});
+Default.decorators = [
+ (Story) => (
+
+
+
+ ),
+];
diff --git a/src/components/Menu/Menu.styled.tsx b/src/components/Menu/Menu.styled.tsx
new file mode 100644
index 0000000..86892ed
--- /dev/null
+++ b/src/components/Menu/Menu.styled.tsx
@@ -0,0 +1,51 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { lighten } from 'polished';
+
+export const StyledNav = styled.nav`
+ position: relative;
+ width: fit-content;
+`;
+
+export const StyledUl = styled.ul`
+ position: absolute;
+ top: 60px;
+ right: 0;
+ background-color: ${({ theme }) => theme.color.menuBg};
+ color: white;
+ padding: 5px 0;
+ white-space: nowrap;
+ z-index: 10;
+
+ & ::before {
+ content: '';
+ position: absolute;
+ right: 20px;
+ top: -9px;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-width: 0 5px 10px 5px;
+ border-color: transparent transparent ${({ theme }) => theme.color.menuBg} transparent;
+ }
+`;
+
+export const StyledLi = styled.li`
+ text-align: center;
+
+ & > button,
+ & > a {
+ width: 100%;
+ display: block;
+ padding: 15px 25px;
+ }
+
+ & :hover {
+ ${({ theme }) => {
+ const menuBg = theme.color.menuBg;
+ return css`
+ background-color: ${lighten(0.2, menuBg)};
+ `;
+ }}
+ }
+`;
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx
new file mode 100644
index 0000000..6aa699d
--- /dev/null
+++ b/src/components/Menu/Menu.tsx
@@ -0,0 +1,65 @@
+import { useState, useEffect } from 'react';
+import { StyledNav, StyledUl, StyledLi } from './Menu.styled';
+import { useRouter } from 'next/router';
+import { IconButton, Button } from 'components';
+import Link from 'next/link';
+import { logOut } from 'api/requestAuth';
+
+export const Menu = () => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const handleBlur = (e: React.FocusEvent) => {
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ setIsOpen(false);
+ }
+ };
+
+ const router = useRouter();
+
+ /*
+ TODO: 스토리북에서 next.js 설정 후 주석 해제
+ useEffect(() => {
+ const handleRouteChange = () => {
+ setIsOpen(false);
+ };
+
+ router.events.on('routeChangeStart', handleRouteChange);
+
+ return () => {
+ router.events.off('routeChangeStart', handleRouteChange);
+ };
+ }, []);
+ */
+
+ return (
+
+
+ {isOpen && (
+
+
+ My Recipes
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/SearchForm/SearchForm.stories.tsx b/src/components/SearchForm/SearchForm.stories.tsx
new file mode 100644
index 0000000..86c20eb
--- /dev/null
+++ b/src/components/SearchForm/SearchForm.stories.tsx
@@ -0,0 +1,11 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { SearchForm } from './SearchForm';
+
+export default {
+ title: 'SearchForm',
+ component: SearchForm,
+} as ComponentMeta;
+
+const Template: ComponentStory = () => ;
+
+export const Default = Template.bind({});
diff --git a/src/components/SearchForm/SearchForm.styled.tsx b/src/components/SearchForm/SearchForm.styled.tsx
new file mode 100644
index 0000000..8dfc134
--- /dev/null
+++ b/src/components/SearchForm/SearchForm.styled.tsx
@@ -0,0 +1,28 @@
+import styled from '@emotion/styled';
+import { media } from 'utils';
+import { IconButton } from 'components';
+
+export const StyledForm = styled.form`
+ position: relative;
+ width: fit-content;
+`;
+
+export const StyledInput = styled.input`
+ width: 40vw;
+ background: ${({ theme }) => theme.color.searchGray};
+ border: none;
+ border-radius: 30px;
+ padding: 10px 38px 10px 18px;
+ line-height: 1;
+
+ ${media.mobile} {
+ width: 50vw;
+ }
+`;
+
+export const StyledIconButton = styled(IconButton)`
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+`;
diff --git a/src/components/SearchForm/SearchForm.tsx b/src/components/SearchForm/SearchForm.tsx
new file mode 100644
index 0000000..e9bce68
--- /dev/null
+++ b/src/components/SearchForm/SearchForm.tsx
@@ -0,0 +1,57 @@
+import { useState, useEffect } from 'react';
+import { StyledForm, StyledInput, StyledIconButton } from './SearchForm.styled';
+import { useRouter } from 'next/router';
+
+export const SearchForm = () => {
+ const [keyword, setKeyword] = useState('');
+
+ const router = useRouter();
+
+ /*
+ TODO: 스토리북에서 next.js 설정 후 주석 해제
+ useEffect(() => {
+ const handleRouteChange = () => {
+ setKeyword('');
+ };
+
+ router.events.on('routeChangeStart', handleRouteChange);
+
+ return () => {
+ router.events.off('routeChangeStart', handleRouteChange);
+ };
+ }, []);
+ */
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setKeyword(e.target.value);
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setKeyword('');
+ if (keyword.trim()) {
+ router.push(`/search/${keyword}`);
+ } else {
+ alert('Please enter the search word.');
+ }
+ };
+
+ return (
+
+
+
+
+ 검색
+
+
+ );
+};
diff --git a/src/components/index.ts b/src/components/index.ts
index d337ac0..7843082 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,7 +1,12 @@
+export * from './Badge/Badge';
+export * from './Button/Button';
+export * from './Button/IconButton';
export * from './CookingInfo/CookingInfo';
export * from './Logo/Logo';
export * from './Loading/Loading';
export * from './Layout/Layout';
+export * from './Menu/Menu';
export * from './Header/Header';
export * from './EmptyPage/EmptyPage';
+export * from './SearchForm/SearchForm';
export * from './SkeletonCard/SkeletonCard';
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index 2d925b4..bb2d440 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,8 +1,16 @@
import { EmptyPage } from 'components';
import Link from 'next/link';
import { NextPage } from 'next';
+import { useEffect } from 'react';
+import { useRouter } from 'next/router';
const NotFound: NextPage = () => {
+ const router = useRouter();
+
+ useEffect(() => {
+ router.push('/404', undefined, { shallow: true });
+ }, []);
+
return (
Page Not Found
diff --git a/yarn.lock b/yarn.lock
index 110512b..dd675ba 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1082,7 +1082,7 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825"
integrity sha512-L6rvG9GDxaLgFjg41K+5Yv9OMrU98sWe+Ykmc6FDJW/+vYZMhdOMKkISgzptMaERHvS2Y2lw9MDRm2gHhlQQoA==
@@ -10976,7 +10976,7 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
-polished@^4.0.5:
+polished@^4.0.5, polished@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/polished/-/polished-4.1.4.tgz#640293ba834109614961a700fdacbb6599fb12d0"
integrity sha512-Nq5Mbza+Auo7N3sQb1QMFaQiDO+4UexWuSGR7Cjb4Sw11SZIJcrrFtiZ+L0jT9MBsUsxDboHVASbCLbE1rnECg==