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; }