Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

하단 내비게이션 바를 개발합니다. #21

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions src/components/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -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 <EmotionWrapper>Navbar</EmotionWrapper>;
const handleClickNavbarMenuItem = (menuIndex: TNavbarMenu["menuIndex"]) => {
setActiveMenuIndex(menuIndex);
};

return (
<EmotionWrapper>
<nav>
{hasNavbarMenuInitialized && <NavbarSliderIndicator activeMenuIndex={activeMenuIndex} />}
{NAVBAR_MENU_LIST.map((navbarMenu) => {
const { label, icon, path, menuIndex } = navbarMenu;

return (
<NavbarMenuItem
key={label}
label={label}
icon={icon}
path={path}
menuIndex={menuIndex}
onClick={handleClickNavbarMenuItem}
/>
);
})}
</nav>
</EmotionWrapper>
);
};

export default Navbar;
Expand All @@ -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;
}
`;
72 changes: 72 additions & 0 deletions src/components/navbar/components/NavbarMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<EmotionWrapper
active={active}
href={path}
onClick={() => {
onClick(menuIndex);
}}
>
{icon}
<p className="menu-item-name">{label}</p>
</EmotionWrapper>
);
};

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;
}
}
`;
29 changes: 29 additions & 0 deletions src/components/navbar/components/NavbarSliderIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import styled from "@emotion/styled";

interface Props {
activeMenuIndex: number;
}

const NavbarSliderIndicator = ({ activeMenuIndex }: Props) => {
return <EmotionWrapper activeMenuIndex={activeMenuIndex} />;
};

export default NavbarSliderIndicator;

const EmotionWrapper = styled.div<Props>`
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;
`;
27 changes: 27 additions & 0 deletions src/components/navbar/components/icons/IconHome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from "@emotion/styled";

const IconHome = () => {
return (
<EmotionWrapper>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 21V15C9 14.4696 9.21071 13.9609 9.58579 13.5858C9.96086 13.2107 10.4696 13 11 13H13C13.5304 13 14.0391 13.2107 14.4142 13.5858C14.7893 13.9609 15 14.4696 15 15V21M5 12H3L12 3L21 12H19V19C19 19.5304 18.7893 20.0391 18.4142 20.4142C18.0391 20.7893 17.5304 21 17 21H7C6.46957 21 5.96086 20.7893 5.58579 20.4142C5.21071 20.0391 5 19.5304 5 19V12Z"
stroke="#C6C6C6"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</EmotionWrapper>
);
};

export default IconHome;

const EmotionWrapper = styled.div``;
27 changes: 27 additions & 0 deletions src/components/navbar/components/icons/IconMypage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from "@emotion/styled";

const IconMypage = () => {
return (
<EmotionWrapper>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.16797 18.849C6.41548 18.0252 6.92194 17.3032 7.61222 16.79C8.30249 16.2768 9.13982 15.9997 9.99997 16H14C14.8612 15.9997 15.6996 16.2774 16.3904 16.7918C17.0811 17.3062 17.5874 18.0298 17.834 18.855M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM15 10C15 11.6569 13.6569 13 12 13C10.3431 13 9 11.6569 9 10C9 8.34315 10.3431 7 12 7C13.6569 7 15 8.34315 15 10Z"
stroke="#C6C6C6"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</EmotionWrapper>
);
};

export default IconMypage;

const EmotionWrapper = styled.div``;
27 changes: 27 additions & 0 deletions src/components/navbar/components/icons/IconOrganization.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from "@emotion/styled";

const IconOrganization = () => {
return (
<EmotionWrapper>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15M13 7C13 9.20914 11.2091 11 9 11C6.79086 11 5 9.20914 5 7C5 4.79086 6.79086 3 9 3C11.2091 3 13 4.79086 13 7Z"
stroke="#C6C6C6"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</EmotionWrapper>
);
};

export default IconOrganization;

const EmotionWrapper = styled.div``;
27 changes: 27 additions & 0 deletions src/components/navbar/components/icons/IconRestaurant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from "@emotion/styled";

const IconRestaurant = () => {
return (
<EmotionWrapper>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 5L16.5 16.5M3 7L6.05 10.15C6.59809 10.672 7.32853 10.959 8.08536 10.9498C8.84218 10.9406 9.56541 10.6358 10.1006 10.1006C10.6358 9.56541 10.9406 8.84218 10.9498 8.08536C10.959 7.32853 10.672 6.59809 10.15 6.05L7 3M19.347 16.5749L20.427 17.6539C20.7945 18.0217 21.001 18.5204 21.0009 19.0403C21.0008 19.5602 20.7942 20.0589 20.4265 20.4264C20.0587 20.794 19.56 21.0005 19.0401 21.0004C18.5202 21.0003 18.0215 20.7937 17.654 20.4259L16.574 19.3469C16.2064 18.9792 15.9999 18.4805 16 17.9606C16.0001 17.4406 16.2067 16.942 16.5745 16.5744C16.9422 16.2068 17.4409 16.0004 17.9608 16.0005C18.4808 16.0006 18.9794 16.2072 19.347 16.5749Z"
stroke="#C6C6C6"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</EmotionWrapper>
);
};

export default IconRestaurant;

const EmotionWrapper = styled.div``;
32 changes: 32 additions & 0 deletions src/components/navbar/constants/navbarMenuList.tsx
Original file line number Diff line number Diff line change
@@ -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: <IconHome />,
path: "/",
},
{
menuIndex: 1,
label: "맛집",
icon: <IconRestaurant />,
path: "/restaurants",
},
{
menuIndex: 2,
label: "단체",
icon: <IconOrganization />,
path: "/organizations",
},
{
menuIndex: 3,
label: "마이",
icon: <IconMypage />,
path: "/mypage",
},
];
21 changes: 21 additions & 0 deletions src/components/navbar/functions/isNavbarMenuActive.ts
Original file line number Diff line number Diff line change
@@ -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;
};
8 changes: 8 additions & 0 deletions src/components/navbar/types/TNavbarMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from "react";

export type TNavbarMenu = {
menuIndex: number;
path: string;
label: string;
icon: ReactNode;
};
2 changes: 1 addition & 1 deletion src/constant/layoutMargin.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const LAYOUT_MARGIN = "0 40px";
export const LAYOUT_MARGIN = "0 20px";
2 changes: 1 addition & 1 deletion src/constant/navbarHeight.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const NAVBAR_HEIGHT = 60;
export const NAVBAR_HEIGHT = 70;
2 changes: 2 additions & 0 deletions src/styles/theme.ts
Original file line number Diff line number Diff line change
@@ -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)`,
};
Expand Down
2 changes: 2 additions & 0 deletions src/types/emotion.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down