diff --git a/src/components/navbar/Navbar.tsx b/src/components/navbar/Navbar.tsx
index ebf03ff..351629e 100644
--- a/src/components/navbar/Navbar.tsx
+++ b/src/components/navbar/Navbar.tsx
@@ -1,11 +1,41 @@
import styled from "@emotion/styled";
+import NavbarMenuItem from "components/navbar/components/NavbarMenuItem";
+import NavbarSliderIndicator from "components/navbar/components/NavbarSliderIndicator";
+import { NAVBAR_MENU_LIST } from "components/navbar/constants/navbarMenuList";
+import { TNavbarMenu } from "components/navbar/types/TNavbarMenu";
import { LAYOUT_MARGIN } from "constant/layoutMargin";
import { NAVBAR_HEIGHT } from "constant/navbarHeight";
+import { useState } from "react";
-interface Props {}
+const Navbar = () => {
+ const [activeMenuIndex, setActiveMenuIndex] = useState(-1);
+ const hasNavbarMenuInitialized = activeMenuIndex > -1; // 직접 메인 페이지 이외 접근 시 슬라이더가 홈에 왔다가 가는 것을 방지
-const Navbar = ({}: Props) => {
- return Navbar;
+ const handleClickNavbarMenuItem = (menuIndex: TNavbarMenu["menuIndex"]) => {
+ setActiveMenuIndex(menuIndex);
+ };
+
+ return (
+
+
+
+ );
};
export default Navbar;
@@ -14,13 +44,16 @@ const EmotionWrapper = styled.div`
position: fixed;
bottom: 0;
left: 0;
-
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: ${({ theme }) => theme.color.gray100};
width: 100%;
+ background-color: ${({ theme }) => theme.color.gray100};
height: ${NAVBAR_HEIGHT}px;
padding: ${LAYOUT_MARGIN};
- padding-bottom: 20px; // iOS 하단바 대응
+ padding-bottom: 0px; // iOS 하단바 대응
+ display: flex;
+ justify-content: center;
+
+ nav {
+ position: relative;
+ display: flex;
+ }
`;
diff --git a/src/components/navbar/components/NavbarMenuItem.tsx b/src/components/navbar/components/NavbarMenuItem.tsx
new file mode 100644
index 0000000..9ad8866
--- /dev/null
+++ b/src/components/navbar/components/NavbarMenuItem.tsx
@@ -0,0 +1,72 @@
+import Link from "next/link";
+import styled from "@emotion/styled";
+import { useRouter } from "next/router";
+import { TNavbarMenu } from "components/navbar/types/TNavbarMenu";
+import { isNavbarMenuActive } from "components/navbar/functions/isNavbarMenuActive";
+import { useEffect } from "react";
+
+type NavbarMenuItemComponentProps = {
+ onClick: (menuIndex: TNavbarMenu["menuIndex"]) => void;
+};
+
+type Props = NavbarMenuItemComponentProps & TNavbarMenu;
+
+const NavbarMenuItem = ({ path, label, icon, menuIndex, onClick }: Props) => {
+ const { pathname } = useRouter();
+
+ const active = isNavbarMenuActive({
+ currentPathname: pathname,
+ navbarPathname: path,
+ });
+
+ useEffect(() => {
+ if (active) onClick(menuIndex);
+ }, [active, onClick, menuIndex, pathname]);
+
+ return (
+ {
+ onClick(menuIndex);
+ }}
+ >
+ {icon}
+ {label}
+
+ );
+};
+
+export default NavbarMenuItem;
+
+const EmotionWrapper = styled(Link)<{ active: boolean }>`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 80px;
+ height: 60px;
+ padding: 10px auto;
+ text-decoration: none; // TODO: globalStyles 에서 resetAnchorStyle 머지 후 제거
+ color: ${({ theme, active }) => (active ? theme.color.gray700 : theme.color.gray200)};
+ stroke: ${({ theme, active }) => (active ? theme.color.gray700 : theme.color.gray200)};
+
+ ${({ theme }) => theme.device.fold} {
+ width: 60px;
+ }
+
+ &:hover {
+ color: ${({ theme }) => theme.color.gray500};
+ stroke: ${({ theme }) => theme.color.gray500};
+ }
+
+ transition:
+ color 0.2s ease-in-out,
+ stroke 0.2s ease-in-out;
+
+ svg {
+ path {
+ stroke: inherit;
+ }
+ }
+`;
diff --git a/src/components/navbar/components/NavbarSliderIndicator.tsx b/src/components/navbar/components/NavbarSliderIndicator.tsx
new file mode 100644
index 0000000..187f9dc
--- /dev/null
+++ b/src/components/navbar/components/NavbarSliderIndicator.tsx
@@ -0,0 +1,29 @@
+import styled from "@emotion/styled";
+
+interface Props {
+ activeMenuIndex: number;
+}
+
+const NavbarSliderIndicator = ({ activeMenuIndex }: Props) => {
+ return ;
+};
+
+export default NavbarSliderIndicator;
+
+const EmotionWrapper = styled.div`
+ position: absolute;
+
+ transition: left 0.2s ease-in-out;
+
+ top: 0;
+ width: 80px;
+ left: ${({ activeMenuIndex }) => activeMenuIndex * 80}px;
+
+ ${({ theme }) => theme.device.fold} {
+ width: 60px;
+ left: ${({ activeMenuIndex }) => activeMenuIndex * 60}px;
+ }
+ height: 4px;
+ background-color: ${({ theme }) => theme.color.primary500};
+ border-radius: 0 0 6px 6px;
+`;
diff --git a/src/components/navbar/components/icons/IconHome.tsx b/src/components/navbar/components/icons/IconHome.tsx
new file mode 100644
index 0000000..467916f
--- /dev/null
+++ b/src/components/navbar/components/icons/IconHome.tsx
@@ -0,0 +1,27 @@
+import styled from "@emotion/styled";
+
+const IconHome = () => {
+ return (
+
+
+
+ );
+};
+
+export default IconHome;
+
+const EmotionWrapper = styled.div``;
diff --git a/src/components/navbar/components/icons/IconMypage.tsx b/src/components/navbar/components/icons/IconMypage.tsx
new file mode 100644
index 0000000..955250d
--- /dev/null
+++ b/src/components/navbar/components/icons/IconMypage.tsx
@@ -0,0 +1,27 @@
+import styled from "@emotion/styled";
+
+const IconMypage = () => {
+ return (
+
+
+
+ );
+};
+
+export default IconMypage;
+
+const EmotionWrapper = styled.div``;
diff --git a/src/components/navbar/components/icons/IconOrganization.tsx b/src/components/navbar/components/icons/IconOrganization.tsx
new file mode 100644
index 0000000..80c2aef
--- /dev/null
+++ b/src/components/navbar/components/icons/IconOrganization.tsx
@@ -0,0 +1,27 @@
+import styled from "@emotion/styled";
+
+const IconOrganization = () => {
+ return (
+
+
+
+ );
+};
+
+export default IconOrganization;
+
+const EmotionWrapper = styled.div``;
diff --git a/src/components/navbar/components/icons/IconRestaurant.tsx b/src/components/navbar/components/icons/IconRestaurant.tsx
new file mode 100644
index 0000000..a85f410
--- /dev/null
+++ b/src/components/navbar/components/icons/IconRestaurant.tsx
@@ -0,0 +1,27 @@
+import styled from "@emotion/styled";
+
+const IconRestaurant = () => {
+ return (
+
+
+
+ );
+};
+
+export default IconRestaurant;
+
+const EmotionWrapper = styled.div``;
diff --git a/src/components/navbar/constants/navbarMenuList.tsx b/src/components/navbar/constants/navbarMenuList.tsx
new file mode 100644
index 0000000..9072d10
--- /dev/null
+++ b/src/components/navbar/constants/navbarMenuList.tsx
@@ -0,0 +1,32 @@
+import IconHome from "components/navbar/components/icons/IconHome";
+import IconMypage from "components/navbar/components/icons/IconMypage";
+import IconOrganization from "components/navbar/components/icons/IconOrganization";
+import IconRestaurant from "components/navbar/components/icons/IconRestaurant";
+import { TNavbarMenu } from "components/navbar/types/TNavbarMenu";
+
+export const NAVBAR_MENU_LIST: TNavbarMenu[] = [
+ {
+ menuIndex: 0,
+ label: "홈",
+ icon: ,
+ path: "/",
+ },
+ {
+ menuIndex: 1,
+ label: "맛집",
+ icon: ,
+ path: "/restaurants",
+ },
+ {
+ menuIndex: 2,
+ label: "단체",
+ icon: ,
+ path: "/organizations",
+ },
+ {
+ menuIndex: 3,
+ label: "마이",
+ icon: ,
+ path: "/mypage",
+ },
+];
diff --git a/src/components/navbar/functions/isNavbarMenuActive.ts b/src/components/navbar/functions/isNavbarMenuActive.ts
new file mode 100644
index 0000000..6ba0d48
--- /dev/null
+++ b/src/components/navbar/functions/isNavbarMenuActive.ts
@@ -0,0 +1,21 @@
+type TIsNavbarMenuActive = {
+ currentPathname: string; // 현재 보여지고 있는 페이지의 pathname
+ navbarPathname: string; // navbar 의 각 메뉴의 pathname
+};
+
+export const isNavbarMenuActive = ({
+ currentPathname,
+ navbarPathname,
+}: TIsNavbarMenuActive): boolean => {
+ // 현재 보여지고 있는 페이지의 pathname 과 navbar 의 각 메뉴의 pathname 이 일치하면 true 를 반환합니다.
+
+ // 메인페이지는 항상 "/" 로 시작하기에 필요한 예외처리
+ // 더 좋은 로직이 있다면 교체 바람.
+ const isMainPage = currentPathname === "/";
+ const isMainPageActive = isMainPage && navbarPathname === "/";
+ const isOtherPagesActive = navbarPathname !== "/" && currentPathname.startsWith(navbarPathname);
+
+ const active = isMainPage ? isMainPageActive : isOtherPagesActive;
+
+ return active;
+};
diff --git a/src/components/navbar/types/TNavbarMenu.ts b/src/components/navbar/types/TNavbarMenu.ts
new file mode 100644
index 0000000..28f5121
--- /dev/null
+++ b/src/components/navbar/types/TNavbarMenu.ts
@@ -0,0 +1,8 @@
+import { ReactNode } from "react";
+
+export type TNavbarMenu = {
+ menuIndex: number;
+ path: string;
+ label: string;
+ icon: ReactNode;
+};
diff --git a/src/constant/layoutMargin.ts b/src/constant/layoutMargin.ts
index cccd1ae..b665535 100644
--- a/src/constant/layoutMargin.ts
+++ b/src/constant/layoutMargin.ts
@@ -1 +1 @@
-export const LAYOUT_MARGIN = "0 40px";
+export const LAYOUT_MARGIN = "0 20px";
diff --git a/src/constant/navbarHeight.ts b/src/constant/navbarHeight.ts
index 83bc07a..005162b 100644
--- a/src/constant/navbarHeight.ts
+++ b/src/constant/navbarHeight.ts
@@ -1 +1 @@
-export const NAVBAR_HEIGHT = 60;
+export const NAVBAR_HEIGHT = 70;
diff --git a/src/styles/theme.ts b/src/styles/theme.ts
index 1f50a29..c92c323 100644
--- a/src/styles/theme.ts
+++ b/src/styles/theme.ts
@@ -1,11 +1,13 @@
import { DeviceMediaTheme, DeviceTheme, Theme } from "@emotion/react";
const size: DeviceTheme = {
+ fold: 350, // 갤럭시 폴드 최하 280px ~ 350px 소형 스마트폰 대응
mobile: 768 + 80,
};
// 미디어 쿼리의 중복 코드를 줄이기위해 정의된 변수입니다
const device: DeviceMediaTheme = {
+ fold: `@media only screen and (max-width: ${size.fold}px)`,
mobile: `@media only screen and (max-width: ${size.mobile}px)`,
pc: `@media only screen and (min-width: ${size.mobile}px)`,
};
diff --git a/src/types/emotion.d.ts b/src/types/emotion.d.ts
index 0137c69..27e26bc 100644
--- a/src/types/emotion.d.ts
+++ b/src/types/emotion.d.ts
@@ -2,9 +2,11 @@ import "@emotion/react";
declare module "@emotion/react" {
export interface DeviceTheme {
+ fold: number;
mobile: number;
}
export interface DeviceMediaTheme {
+ fold: string;
mobile: string;
pc: string;
}