Repository: swyp-web9th-team8/frontend
Pull request #36: Feat: Nav 컴포넌트 제작 및 Storybook 설정에 @svgr/webpack 추가
Author: login: hazzuuu
Labels: name: enhancement
description: New feature or request
Created: 2025-05-01T08:56:53Z | Merged: 2025-05-02T08:34:38Z

## 작업의 목적
- 네비게이션 바 컴포넌트 생성 및 스토리북 문서화
<img width="551" alt="{CCD17675-5CD6-4B9C-B83F-AAFA41276DD3}" src="https://github.com/user-attachments/assets/ba4ff1d0-7393-457b-9f38-fb837423a529" />


## 기대효과
- 사용자에게 일관된 네비게이션 바 UI 제공
- 동료 개발자가 프로젝트를 실행하지 않아도 Storybook을 통해 쉽게 UI를 확인할 수 있어 협업 효율 향상

## 주요 구현 사항
- 네비게이션 바 컴포넌트 UI 구현(홈, 메뉴 아이콘 포함) 및 스토리 작성
- 스토리북 `main.ts`에서 @svgr/webpack 설정을 추가하여 svg 아이콘을 사용할 수 있도록 함
- svg 이미지 파일내 색상을 `currentColor`로 변경 및 width, height 삭제로 tailwind 를 통해 동적으로 속성 변경할 수 있도록 함

## 개선 점
- Storybook 내에서 `position: fixed`로 인해 실제 위치보다 잘못 표시되는 현상 존재
- Storybook 내에서 `Link` 컴포넌트를 누를 경우 잘못된 경로로 페이지가 이동됨
  
closed #35 


Linked issue #35: Feat: GNB 컴포넌트 생성
## ✨ 요약

> GNB 컴포넌트를 생성합니다.


## #️⃣ 생성할 브랜치

- feat/35--nav

## 🔨 작업 내용

- [x] GNB 바
- [x] naxt/link

## 📝 자세한 설명

> 작업할 내용에 대해 자세한 내용을 작성해 주세요


Diff:
diff --git a/src/assets/icons/group02.svg b/src/assets/icons/group02.svg
new file mode 100644
index 0000000..987ea07
--- /dev/null
+++ b/src/assets/icons/group02.svg
@@ -0,0 +1,5 @@
+<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="IconGroup02">
+<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M14.7461 6.49609C14.7461 8.01609 15.9761 9.24609 17.4961 9.24609C19.0161 9.24609 20.2461 8.01609 20.2461 6.49609C20.2461 4.97609 19.0161 3.74609 17.4961 3.74609C15.9761 3.74609 14.7461 4.97609 14.7461 6.49609ZM16.2461 6.49609C16.2461 6.33188 16.2785 6.16928 16.3414 6.01759C16.4044 5.86589 16.4965 5.72808 16.6127 5.612C16.7289 5.49593 16.8667 5.40387 17.0185 5.34109C17.1702 5.27831 17.3329 5.24603 17.4971 5.24609C17.6613 5.24616 17.8239 5.27857 17.9756 5.34147C18.1273 5.40438 18.2651 5.49654 18.3812 5.61271C18.4972 5.72888 18.5893 5.86677 18.6521 6.01851C18.7149 6.17025 18.7471 6.33288 18.7471 6.49709C18.7471 6.82861 18.6154 7.14656 18.381 7.38098C18.1465 7.6154 17.8286 7.74709 17.4971 7.74709C17.1655 7.74709 16.8476 7.6154 16.6132 7.38098C16.3788 7.14656 16.2471 6.82861 16.2471 6.49709M8.74707 10.9971C8.74707 12.7871 10.2071 14.2471 11.9971 14.2471C13.7871 14.2471 15.2471 12.7871 15.2471 10.9971C15.2471 9.20709 13.7871 7.74709 11.9971 7.74709C10.2071 7.74709 8.74707 9.20709 8.74707 10.9971ZM10.2471 10.9971C10.2471 10.0371 11.0371 9.24709 11.9971 9.24709C12.9571 9.24709 13.7471 10.0371 13.7471 10.9971C13.7471 11.9571 12.9571 12.7471 11.9971 12.7471C11.0371 12.7471 10.2471 11.9571 10.2471 10.9971ZM15.7471 19.4971C15.7471 19.9071 16.0871 20.2471 16.4971 20.2471V20.2571C16.9071 20.2571 17.2471 19.9171 17.2471 19.5071C17.2471 18.1571 16.7071 17.0271 15.7771 16.3971C13.5271 14.8971 10.4671 14.8971 8.20707 16.3971C7.27707 17.0171 6.74707 18.1571 6.74707 19.4971C6.74707 19.9071 7.08707 20.2471 7.49707 20.2471C7.90707 20.2471 8.24707 19.8971 8.24707 19.4971C8.24707 19.0971 8.32707 18.1171 9.04707 17.6371C10.8071 16.4671 13.1971 16.4771 14.9471 17.6371C15.6671 18.1271 15.7471 19.0871 15.7471 19.4971ZM21.4971 15.1971C21.0871 15.1971 20.7471 14.9071 20.7471 14.4971C20.7471 13.9271 20.6571 13.0771 20.0371 12.6171C19.2771 12.0471 18.3971 11.7471 17.4971 11.7471C17.0871 11.7471 16.7471 11.4071 16.7471 10.9971C16.7471 10.5871 17.0871 10.2471 17.4971 10.2471C18.7271 10.2471 19.9171 10.6471 20.9471 11.4171C21.7871 12.0571 22.2471 13.1171 22.2471 14.3971C22.2471 14.8071 21.9071 15.1971 21.4971 15.1971ZM2.49707 15.1971C2.08707 15.1971 1.74707 14.9071 1.74707 14.4971C1.74707 13.1171 2.20707 12.0571 3.04707 11.4171C4.07707 10.6471 5.26707 10.2471 6.49707 10.2471C6.90707 10.2471 7.24707 10.5871 7.24707 10.9971C7.24707 11.4071 6.90707 11.7471 6.49707 11.7471C5.58707 11.7471 4.70707 12.0471 3.94707 12.6171C3.33707 13.0771 3.24707 13.9271 3.24707 14.3971C3.24707 14.8071 2.90707 15.1971 2.49707 15.1971ZM3.74707 6.49709C3.74707 8.01709 4.97707 9.24709 6.49707 9.24709C8.01707 9.24709 9.24707 8.01709 9.24707 6.49709C9.24707 4.97709 8.01707 3.74709 6.49707 3.74709C4.97707 3.74709 3.74707 4.97709 3.74707 6.49709ZM5.24707 6.49709C5.2472 6.16544 5.37908 5.84742 5.61369 5.613C5.72985 5.49693 5.86774 5.40487 6.01949 5.34209C6.17123 5.27931 6.33385 5.24703 6.49807 5.24709C6.66229 5.24716 6.82489 5.27957 6.97658 5.34247C7.12827 5.40538 7.26609 5.49754 7.38216 5.61371C7.49823 5.72988 7.59029 5.86777 7.65307 6.01951C7.71586 6.17125 7.74814 6.33388 7.74807 6.49809C7.74807 6.82961 7.61637 7.14756 7.38195 7.38198C7.14753 7.6164 6.82959 7.74809 6.49807 7.74809C6.16655 7.74809 5.84861 7.6164 5.61419 7.38198C5.37977 7.14756 5.24807 6.82961 5.24807 6.49809" fill="currentColor"/>
+</g>
+</svg>
diff --git a/src/assets/icons/home.svg b/src/assets/icons/home.svg
new file mode 100644
index 0000000..1926eb5
--- /dev/null
+++ b/src/assets/icons/home.svg
@@ -0,0 +1,6 @@
+<svg viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Group">
+<path id="Vector" d="M7 21L7.002 16.998C7.002 16.067 7.002 15.602 7.155 15.235C7.25551 14.9922 7.4029 14.7716 7.58872 14.5857C7.77455 14.3999 7.99518 14.2525 8.238 14.152C8.605 14 9.07 14 10 14C10.93 14 11.396 14 11.764 14.152C12.0068 14.2525 12.2274 14.3999 12.4133 14.5857C12.5991 14.7716 12.7465 14.9922 12.847 15.235C13 15.603 13 16.068 13 17V21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M5.088 3.762L4.088 4.543C2.572 5.727 1.813 6.319 1.407 7.153C1 7.988 1 8.952 1 10.88V12.972C1 16.756 1 18.648 2.172 19.824C3.344 21 5.229 21 9 21H11C14.771 21 16.657 21 17.828 19.824C18.999 18.648 19 16.756 19 12.971V10.881C19 8.952 19 7.988 18.593 7.153C18.186 6.319 17.428 5.727 15.912 4.543L14.912 3.763C12.552 1.92 11.372 1 10 1C8.628 1 7.448 1.92 5.088 3.762Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>
diff --git a/src/assets/icons/medal02.svg b/src/assets/icons/medal02.svg
new file mode 100644
index 0000000..43337e4
--- /dev/null
+++ b/src/assets/icons/medal02.svg
@@ -0,0 +1,6 @@
+<svg viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Group">
+<path id="Vector" d="M1.5 13.5C1.5 11.5109 2.29018 9.60322 3.6967 8.1967C5.10322 6.79018 7.01088 6 9 6C10.9891 6 12.8968 6.79018 14.3033 8.1967C15.7098 9.60322 16.5 11.5109 16.5 13.5C16.5 15.4891 15.7098 17.3968 14.3033 18.8033C12.8968 20.2098 10.9891 21 9 21C7.01088 21 5.10322 20.2098 3.6967 18.8033C2.29018 17.3968 1.5 15.4891 1.5 13.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M10.56 1L7.999 6M14.999 1L12.178 6.602M7.44 1L9 3.594M3 1L5.821 6.5M6 13.5C6 11.843 7.295 10.5 8.893 10.5H9.107C10.705 10.5 12 11.843 12 13.5C12 15.157 10.705 16.5 9.107 16.5H8.893C7.295 16.5 6 15.157 6 13.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>
diff --git a/src/assets/icons/profile.svg b/src/assets/icons/profile.svg
new file mode 100644
index 0000000..a5d7bc5
--- /dev/null
+++ b/src/assets/icons/profile.svg
@@ -0,0 +1,6 @@
+<svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Group">
+<path id="Vector" d="M11 21C16.5228 21 21 16.5228 21 11C21 5.47715 16.5228 1 11 1C5.47715 1 1 5.47715 1 11C1 16.5228 5.47715 21 11 21Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="Vector_2" d="M6.5 16C8.832 13.558 13.143 13.443 15.5 16M13.495 8.5C13.495 9.88 12.375 11 10.992 11C10.6634 11.0005 10.3378 10.9363 10.0341 10.8109C9.73029 10.6854 9.45422 10.5014 9.22165 10.2692C8.98908 10.037 8.80457 9.76121 8.67868 9.45763C8.5528 9.15406 8.488 8.82864 8.488 8.5C8.488 7.12 9.608 6 10.992 6C11.3206 5.99961 11.646 6.06398 11.9496 6.18944C12.2533 6.3149 12.5292 6.49899 12.7617 6.73117C12.9942 6.96336 13.1786 7.23909 13.3044 7.5426C13.4302 7.84611 13.495 8.17144 13.495 8.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>
diff --git a/src/components/organisms/Nav.tsx b/src/components/organisms/Nav.tsx
new file mode 100644
index 0000000..0b1c61c
--- /dev/null
+++ b/src/components/organisms/Nav.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import GatheringIcon from "@/assets/icons/group02.svg";
+import HomeIcon from "@/assets/icons/home.svg";
+import RankingIcon from "@/assets/icons/medal02.svg";
+import ProfileIcon from "@/assets/icons/profile.svg";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+const iconStyle = "w-7 h-7";
+
+const NAV_ITEMS = [
+  {
+    label: "홈",
+    icon: <HomeIcon className={iconStyle} />,
+    href: `/home`,
+  },
+  {
+    label: "모임",
+    icon: <GatheringIcon className={iconStyle} />,
+    href: `/gatherings`,
+  },
+  {
+    label: "랭킹",
+    icon: <RankingIcon className={iconStyle} />,
+    href: `/ranking`,
+  },
+  {
+    label: "프로필",
+    icon: <ProfileIcon className={iconStyle} />,
+    href: `/profile`,
+  },
+];
+
+const NAV_ITEM_STYLE = {
+  default: {
+    text: "text-grey-400",
+    icon: "text-green-950",
+  },
+  active: {
+    text: "text-green",
+    icon: "text-green",
+  },
+};
+
+export default function Nav() {
+  const pathname = usePathname();
+  const pathnameFirst = pathname.split("/")[1];
+  console.log(pathnameFirst);
+  return (
+    <div className="shadow-[0px_4px_24px_0px_rgba(170,170,170,0.30)]justify-center fixed bottom-0 flex w-96 w-full max-w-[500px] items-end bg-white px-10 pt-2.5 pb-5">
+      {NAV_ITEMS.map((item, index) => (
+        <Link
+          key={index}
+          href={item.href}
+          className={`flex flex-1 flex-col items-center justify-center gap-0.5 ${`/${pathnameFirst}` === item.href ? NAV_ITEM_STYLE.active.icon : NAV_ITEM_STYLE.default.icon}`}
+        >
+          {item.icon}
+          <div
+            className={`text-body3-medium ${`/${pathnameFirst}` === item.href ? NAV_ITEM_STYLE.active.text : NAV_ITEM_STYLE.default.text}`}
+          >
+            {item.label}
+          </div>
+        </Link>
+      ))}
+    </div>
+  );
+}
diff --git a/src/stories/Nav.stories.tsx b/src/stories/Nav.stories.tsx
new file mode 100644
index 0000000..67a2d20
--- /dev/null
+++ b/src/stories/Nav.stories.tsx
@@ -0,0 +1,89 @@
+import Nav from "@/components/organisms/Nav";
+import type { Meta, StoryObj } from "@storybook/react";
+
+const meta = {
+  title: "Organisms/Nav",
+  component: Nav,
+
+  parameters: {
+    layout: "centered",
+    nextjs: {
+      appDirectory: true,
+    },
+    docs: {
+      story: {
+        inline: true,
+        iframeHeight: 100,
+      },
+      description: {
+        component: `앱 하단에 고정된 메인 네비게이션 바 컴포넌트입니다. 현재 경로에 따라 활성화된 메뉴가 강조됩니다.
+          (주의) storybook 환경에서 네비게이션 메뉴가 적절히 동작하지 않을 수 있습니다.`,
+      },
+    },
+  },
+
+  tags: ["autodocs"],
+} satisfies Meta<typeof Nav>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Home: Story = {
+  parameters: {
+    nextjs: {
+      navigation: {
+        pathname: "/home",
+      },
+    },
+    docs: {
+      description: {
+        story: "'홈' 메뉴가 활성화된 상태입니다.",
+      },
+    },
+  },
+};
+
+export const Gatherings: Story = {
+  parameters: {
+    nextjs: {
+      navigation: {
+        pathname: "/gatherings",
+      },
+    },
+    docs: {
+      description: {
+        story: "'모임' 메뉴가 활성화된 상태입니다.",
+      },
+    },
+  },
+};
+
+export const Ranking: Story = {
+  parameters: {
+    nextjs: {
+      navigation: {
+        pathname: "/ranking",
+      },
+    },
+    docs: {
+      description: {
+        story: "'랭킹' 메뉴가 활성화된 상태입니다.",
+      },
+    },
+  },
+};
+
+export const Profile: Story = {
+  parameters: {
+    nextjs: {
+      navigation: {
+        pathname: "/profile",
+      },
+    },
+    docs: {
+      description: {
+        story: "'프로필' 메뉴가 활성화된 상태입니다.",
+      },
+    },
+  },
+};


Reviews:
user: login: bhoh032019
body: 고생하셨습니다!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:57:17Z
user: login: hazzuuu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:27:26Z

Review comments:
diff_hunk: @@ -0,0 +1,68 @@
+"use client";
+
+import GatheringIcon from "@/assets/icons/group02.svg";
+import HomeIcon from "@/assets/icons/home.svg";
+import RankingIcon from "@/assets/icons/medal02.svg";
+import ProfileIcon from "@/assets/icons/profile.svg";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+const iconStyle = "w-7 h-7";
+
+const NAV_ITEMS = [
+  {
+    label: "홈",
+    icon: <HomeIcon className={iconStyle} />,
+    href: `/home`,
+  },
+  {
+    label: "모임",
+    icon: <GatheringIcon className={iconStyle} />,
+    href: `/gatherings`,
+  },
+  {
+    label: "랭킹",
+    icon: <RankingIcon className={iconStyle} />,
+    href: `/ranking`,
+  },
+  {
+    label: "프로필",
+    icon: <ProfileIcon className={iconStyle} />,
+    href: `/profile`,
+  },
+];
+
+const NAV_ITEM_STYLE = {
+  default: {
+    text: "text-grey-400",
+    icon: "text-green-950",
+  },
+  active: {
+    text: "text-green",
+    icon: "text-green",
+  },
+};
+
+export default function Nav() {
+  const pathname = usePathname();
+  const pathnameFirst = pathname.split("/")[1];
+  console.log(pathnameFirst);
+  return (
+    <div className="shadow-[0px_4px_24px_0px_rgba(170,170,170,0.30)]justify-center fixed bottom-0 flex w-96 w-full max-w-[500px] items-end bg-white px-10 pt-2.5 pb-5">
path: src/components/organisms/Nav.tsx
user: login: bhoh032019
body: 이 부분에서 w-96과 w-full이 동시에 선언되어 있어 우선순위가 혼선될 수 있어 보입니다.
created_at: 2025-05-02T06:45:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,5 +13,30 @@ const config: StorybookConfig = {
     options: {},
   },
   staticDirs: ["../public"],
+  // webpackFinal을 통해 Webpack 설정을 변경
+  webpackFinal: async (config) => {
+    if (!config.module || !config.module.rules) {
+      return config;
+    }
+
+    config.module.rules = [
+      ...config.module.rules.map((rule) => {
+        if (!rule || rule === "...") {
+          return rule;
+        }
+
+        if (rule.test && /svg/.test(String(rule.test))) {
+          return { ...rule, exclude: /\.svg$/i };
+        }
+        return rule;
+      }),
+      {
+        test: /\.svg$/,
+        use: ["@svgr/webpack"],
+      },
+    ];
+
+    return config;
+  },
path: .storybook/main.ts
user: login: bhoh032019
body: webpackFinal에서 SVG를 @svgr/webpack으로 처리하도록 분리한 덕분에, 아이콘을 React 컴포넌트로 쉽게 다룰 수 있을 것 같네요!
created_at: 2025-05-02T06:56:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+"use client";
+
+import GatheringIcon from "@/assets/icons/group02.svg";
+import HomeIcon from "@/assets/icons/home.svg";
+import RankingIcon from "@/assets/icons/medal02.svg";
+import ProfileIcon from "@/assets/icons/profile.svg";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+const iconStyle = "w-7 h-7";
+
+const NAV_ITEMS = [
+  {
+    label: "홈",
+    icon: <HomeIcon className={iconStyle} />,
+    href: `/home`,
+  },
+  {
+    label: "모임",
+    icon: <GatheringIcon className={iconStyle} />,
+    href: `/gatherings`,
+  },
+  {
+    label: "랭킹",
+    icon: <RankingIcon className={iconStyle} />,
+    href: `/ranking`,
+  },
+  {
+    label: "프로필",
+    icon: <ProfileIcon className={iconStyle} />,
+    href: `/profile`,
+  },
+];
+
+const NAV_ITEM_STYLE = {
+  default: {
+    text: "text-grey-400",
+    icon: "text-green-950",
+  },
+  active: {
+    text: "text-green",
+    icon: "text-green",
+  },
+};
+
+export default function Nav() {
+  const pathname = usePathname();
+  const pathnameFirst = pathname.split("/")[1];
+  console.log(pathnameFirst);
+  return (
+    <div className="shadow-[0px_4px_24px_0px_rgba(170,170,170,0.30)]justify-center fixed bottom-0 flex w-96 w-full max-w-[500px] items-end bg-white px-10 pt-2.5 pb-5">
path: src/components/organisms/Nav.tsx
user: login: hazzuuu
body: 현재 작업하는 브랜치에서 수정해놓았습니다!
created_at: 2025-05-02T08:27:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-05-01T08:57:17Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *swyp-8th* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 40125157535484d622011572ac3f04bc38cdc56e |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/swyp-8th/deploys/6814829f99a7810008a43165 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-36--swyp-8th.netlify.app](https://deploy-preview-36--swyp-8th.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTM2LS1zd3lwLTh0aC5uZXRsaWZ5LmFwcCJ9.J2O2Oor0SxMSCuOfd-FJ94RztwKhkgcgWw9MH7nLes4)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [swyp-8th Preview](https://deploy-preview-36--swyp-8th.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/swyp-8th/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-01T08:59:34Z
body: 🚀storybook: https://68005b461a20132e6eeebd84-gwikwugyup.chromatic.com/
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-02T08:32:06Z
body: 🚀storybook: https://68005b461a20132e6eeebd84-wslcaajvzj.chromatic.com/
author_association: NONE