Repository: InnerCircle-ICD3/o2o-FE
Pull request #61: feat: layout 구현
Author: login: SeoJaeWan
Labels: name: Needs Review
description: 리뷰해주세요 🙏🏻
Created: 2025-05-15T01:54:26Z | Merged: 2025-05-17T15:08:12Z

## #️⃣ 연관된 이슈

> ex) #이슈번호, #이슈번호

#60 

## 📝 작업 내용

> 이번 PR에서 작업한 내용을 간략히 설명해주세요

정말 최종의 최종 공통으로 필요하다고 생각해서 작업했습니다... ㅠ
화면에 기본적으로 모바일 UI로 나와야 하는 부분이 있어서 이부분 반영했고 
bottom nav를 전역으로 사용할 수 있게 작업했습니다. 

Bottom Nav가 필요한 페이지는 (bottomNav) 폴더 안에 넣어주시면 자동으로 반영됩니다! 
추후 " < 제목 " 형식인 페이지도 이런 방식으로 작업하면 좋지 않을까? 생각이 되네용!  

기본 width 값을 임의로 max 480 으로 했습니다. 

## 🧾 작업 상세 내용

-   [x] 전역 Layout 구현
-   [x] Bottom Nav 구현

## 📸 스크린샷 (선택)

| 작업  | 
| --- | 
| ![image](https://github.com/user-attachments/assets/5c72fb7c-e4fb-43a1-b9d8-e240b124be4c) |     

## 💬 리뷰 요구사항

> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요
>
> ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

svg를 사용해서 아이콘을 관리했습니다. 
png 또는 svg를 이미지처럼 사용해서 상태에 따라 src를 바꾸는 것도 괜찮은 방법 같은데 굳이 src를 바꾸기보단 색상만 변경되니 
svg 태그를 사용하면 css로 처리가 가능한 것 같아서 이런 방법을 사용했습니다! 

layout을 구성하면서 작업한 부분에 더 좋은 방법이 있다면 피드백 부탁드립니다! 

다른 분들 작업에 제약이 생길 것 같아 공통으로 사용될 부분만 작업하고 실제 페이지는 만들지 않아 Home 외 요소는 누를 경우 
404가 나올 것 같습니당 

## 🧪 테스트 방법

배포된 [경로](https://o2o-fe-user-git-feat-layout-o2o-fe.vercel.app/)에서 확인하실 수 있습니다. 

## 📚 참고할만한 자료 (선택)



Diff:
diff --git a/README.md b/README.md
index 3d73463c..a87a9a2a 100644
--- a/README.md
+++ b/README.md
@@ -107,3 +107,4 @@ VERCEL_PROJECT_ID_OWNER: Owner 앱의 Vercel 프로젝트 ID
 - [pnpm Workspaces](https://pnpm.io/workspaces) - 모노레포 관리
 - [GitHub Actions](https://docs.github.com/en/actions) - CI/CD 파이프라인
 - [Vercel](https://vercel.com/docs) - 배포 플랫폼
+
diff --git a/apps/user/public/icons/home.svg b/apps/user/public/icons/home.svg
new file mode 100644
index 00000000..cc9200a8
--- /dev/null
+++ b/apps/user/public/icons/home.svg
@@ -0,0 +1,10 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_543_2787)">
+<path d="M12 3L4 9V21H9V14H15V21H20V9L12 3Z" fill="#999999"/>
+</g>
+<defs>
+<clipPath id="clip0_543_2787">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/apps/user/public/icons/location.svg b/apps/user/public/icons/location.svg
new file mode 100644
index 00000000..14e3bff7
--- /dev/null
+++ b/apps/user/public/icons/location.svg
@@ -0,0 +1,10 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_543_2790)">
+<path d="M12 2C8.13 2 5 5.13 5 9C5 14.25 12 22 12 22C12 22 19 14.25 19 9C19 5.13 15.87 2 12 2ZM12 11.5C10.62 11.5 9.5 10.38 9.5 9C9.5 7.62 10.62 6.5 12 6.5C13.38 6.5 14.5 7.62 14.5 9C14.5 10.38 13.38 11.5 12 11.5Z" fill="#999999"/>
+</g>
+<defs>
+<clipPath id="clip0_543_2790">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/apps/user/public/icons/mypage.svg b/apps/user/public/icons/mypage.svg
new file mode 100644
index 00000000..c5a68b42
--- /dev/null
+++ b/apps/user/public/icons/mypage.svg
@@ -0,0 +1,11 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_543_2802)">
+<path d="M12 13C14.4853 13 16.5 10.9853 16.5 8.5C16.5 6.01472 14.4853 4 12 4C9.51472 4 7.5 6.01472 7.5 8.5C7.5 10.9853 9.51472 13 12 13Z" fill="#999999"/>
+<path d="M12 15.25C8.99625 15.25 3 16.7575 3 19.75V22H21V19.75C21 16.7575 15.0037 15.25 12 15.25Z" fill="#999999"/>
+</g>
+<defs>
+<clipPath id="clip0_543_2802">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/apps/user/public/icons/order.svg b/apps/user/public/icons/order.svg
new file mode 100644
index 00000000..b927a888
--- /dev/null
+++ b/apps/user/public/icons/order.svg
@@ -0,0 +1,10 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_543_2796)">
+<path d="M17.21 8.99953L12.83 2.43953C12.64 2.15953 12.32 2.01953 12 2.01953C11.68 2.01953 11.36 2.15953 11.17 2.44953L6.79 8.99953H2C1.45 8.99953 1 9.44953 1 9.99953C1 10.0895 1.01 10.1795 1.04 10.2695L3.58 19.5395C3.81 20.3795 4.58 20.9995 5.5 20.9995H18.5C19.42 20.9995 20.19 20.3795 20.43 19.5395L22.97 10.2695L23 9.99953C23 9.44953 22.55 8.99953 22 8.99953H17.21ZM9 8.99953L12 4.59953L15 8.99953H9ZM12 16.9995C10.9 16.9995 10 16.0995 10 14.9995C10 13.8995 10.9 12.9995 12 12.9995C13.1 12.9995 14 13.8995 14 14.9995C14 16.0995 13.1 16.9995 12 16.9995Z" fill="#999999"/>
+</g>
+<defs>
+<clipPath id="clip0_543_2796">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/apps/user/public/icons/subscribe.svg b/apps/user/public/icons/subscribe.svg
new file mode 100644
index 00000000..b9333a49
--- /dev/null
+++ b/apps/user/public/icons/subscribe.svg
@@ -0,0 +1,10 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_543_2781)">
+<path d="M12.5001 21L11.1568 19.7771C6.3857 15.4507 3.23584 12.5973 3.23584 9.09537C3.23584 6.24196 5.4778 4 8.33121 4C9.9432 4 11.4903 4.75041 12.5001 5.93624C13.51 4.75041 15.0571 4 16.6691 4C19.5225 4 21.7645 6.24196 21.7645 9.09537C21.7645 12.5973 18.6146 15.4507 13.8435 19.7864L12.5001 21Z" fill="#999999"/>
+</g>
+<defs>
+<clipPath id="clip0_543_2781">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/apps/user/src/__test__/page.test.tsx b/apps/user/src/__test__/page.test.tsx
index 2016aefe..40de0886 100644
--- a/apps/user/src/__test__/page.test.tsx
+++ b/apps/user/src/__test__/page.test.tsx
@@ -1,4 +1,4 @@
-import Page from "@/app/page";
+import Page from "@/app/(bottomNav)/page";
 import { render, screen } from "@testing-library/react";
 import { expect, test } from "vitest";
 
diff --git a/apps/user/src/app/(bottomNav)/layout.tsx b/apps/user/src/app/(bottomNav)/layout.tsx
new file mode 100644
index 00000000..1553e6ef
--- /dev/null
+++ b/apps/user/src/app/(bottomNav)/layout.tsx
@@ -0,0 +1,10 @@
+import BottomNav from "@/components/ui/layout/bottomNav";
+import type { PropsWithChildren } from "react";
+
+const Layout = (props: PropsWithChildren) => {
+  const { children } = props;
+
+  return <BottomNav>{children}</BottomNav>;
+};
+
+export default Layout;
diff --git a/apps/user/src/app/page.css.ts b/apps/user/src/app/(bottomNav)/page.css.ts
similarity index 100%
rename from apps/user/src/app/page.css.ts
rename to apps/user/src/app/(bottomNav)/page.css.ts
diff --git a/apps/user/src/app/page.tsx b/apps/user/src/app/(bottomNav)/page.tsx
similarity index 100%
rename from apps/user/src/app/page.tsx
rename to apps/user/src/app/(bottomNav)/page.tsx
diff --git a/apps/user/src/app/layout.css.ts b/apps/user/src/app/layout.css.ts
new file mode 100644
index 00000000..eae601c6
--- /dev/null
+++ b/apps/user/src/app/layout.css.ts
@@ -0,0 +1,15 @@
+import { globalTheme } from "@/styles/theme.css";
+import { style } from "@vanilla-extract/css";
+
+export const container = style({
+  width: "100%",
+  maxWidth: "480px",
+
+  margin: "0 auto",
+  borderLeft: `1px solid ${globalTheme.color.line}`,
+  borderRight: `1px solid ${globalTheme.color.line}`,
+});
+
+export const main = style({
+  backgroundColor: globalTheme.color.white,
+});
diff --git a/apps/user/src/app/layout.tsx b/apps/user/src/app/layout.tsx
index b2540ba1..96dee04f 100644
--- a/apps/user/src/app/layout.tsx
+++ b/apps/user/src/app/layout.tsx
@@ -2,6 +2,7 @@ import Providers from "@/providers";
 import type { Metadata } from "next";
 import { Geist, Geist_Mono } from "next/font/google";
 import "@/styles/reset.css";
+import * as style from "./layout.css";
 
 if (process.env.NEXT_RUNTIME === "nodejs" && process.env.NODE_ENV === "development") {
   const { worker } = await import("../mocks/server");
@@ -31,9 +32,11 @@ export default async function RootLayout({
 }>) {
   return (
     <html lang="en">
-      <body className={`${geistSans.variable} ${geistMono.variable}`}>
-        <Providers>{children}</Providers>
-        <div id="bottom-sheet" />
+      <body className={`${geistSans.variable} ${geistMono.variable} ${style.container}`}>
+        <Providers>
+          <div className={style.main}>{children}</div>
+          <div id="bottom-sheet" />
+        </Providers>
       </body>
     </html>
   );
diff --git a/apps/user/src/components/ui/layout/bottomNav/bottomNav.css.ts b/apps/user/src/components/ui/layout/bottomNav/bottomNav.css.ts
new file mode 100644
index 00000000..ad803c19
--- /dev/null
+++ b/apps/user/src/components/ui/layout/bottomNav/bottomNav.css.ts
@@ -0,0 +1,56 @@
+import { globalTheme } from "@/styles/theme.css";
+import { style, styleVariants } from "@vanilla-extract/css";
+
+export const container = style({
+  display: "flex",
+  flexDirection: "column",
+
+  width: "100%",
+  height: "100vh",
+});
+
+export const main = style({
+  height: "calc(100% - 70px)",
+
+  padding: "0 20px",
+});
+
+export const bottomNav = style({
+  display: "flex",
+  justifyContent: "space-between",
+  alignItems: "center",
+
+  width: "100%",
+  height: "70px",
+
+  padding: "14px 20px",
+
+  borderTop: `1px solid ${globalTheme.color.line}`,
+});
+
+const navItemBase = style({
+  display: "flex",
+  flexDirection: "column",
+  alignItems: "center",
+  gap: "6px",
+
+  width: "60px",
+
+  fontSize: "12px",
+  fontWeight: 500,
+});
+
+export const navItem = styleVariants({
+  default: [navItemBase],
+  active: [
+    navItemBase,
+    {
+      color: `${globalTheme.color.primary} !important`,
+
+      path: {
+        fill: globalTheme.color.primary,
+        stroke: globalTheme.color.primary,
+      },
+    },
+  ],
+});
diff --git a/apps/user/src/components/ui/layout/bottomNav/bottomNav.test.tsx b/apps/user/src/components/ui/layout/bottomNav/bottomNav.test.tsx
new file mode 100644
index 00000000..910d86d5
--- /dev/null
+++ b/apps/user/src/components/ui/layout/bottomNav/bottomNav.test.tsx
@@ -0,0 +1,54 @@
+import { cleanup, render, screen } from "@testing-library/react";
+import * as navigation from "next/navigation";
+import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import BottomNav from ".";
+
+vi.mock("next/navigation", async () => ({
+  useSelectedLayoutSegment: vi.fn(),
+}));
+
+describe("BottomNav", () => {
+  beforeEach(() => {
+    vi.resetAllMocks();
+  });
+
+  afterEach(() => {
+    cleanup();
+  });
+
+  it("children이 main 영역에 렌더링되어야 한다", () => {
+    render(
+      <BottomNav>
+        <p>테스트 children</p>
+      </BottomNav>,
+    );
+
+    expect(screen.getByText("테스트 children")).not.toBeNull();
+  });
+
+  it("ROUTE에 정의된 항목들이 모두 렌더링되어야 한다", () => {
+    render(
+      <BottomNav>
+        <div />
+      </BottomNav>,
+    );
+
+    expect(screen.getByText("홈")).not.toBeNull();
+    expect(screen.getByText("찜")).not.toBeNull();
+    expect(screen.getByText("지도")).not.toBeNull();
+    expect(screen.getByText("주문내역")).not.toBeNull();
+    expect(screen.getByText("마이")).not.toBeNull();
+  });
+
+  it("현재 segment에 따라 active 스타일이 적용되어야 한다", () => {
+    (navigation.useSelectedLayoutSegment as Mock).mockReturnValue("my-orders");
+
+    render(
+      <BottomNav>
+        <div />
+      </BottomNav>,
+    );
+    const activeLink = screen.getByText("주문내역").closest("a") as HTMLAnchorElement;
+    expect(activeLink.className).toContain("active");
+  });
+});
diff --git a/apps/user/src/components/ui/layout/bottomNav/index.tsx b/apps/user/src/components/ui/layout/bottomNav/index.tsx
new file mode 100644
index 00000000..a87e0f9f
--- /dev/null
+++ b/apps/user/src/components/ui/layout/bottomNav/index.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import Link from "next/link";
+import { useSelectedLayoutSegment } from "next/navigation";
+import type { PropsWithChildren } from "react";
+import * as style from "./bottomNav.css";
+
+const ROUTE = [
+  {
+    name: "홈",
+    path: "/",
+    icon: () => (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+        fill="none"
+        aria-hidden="true"
+      >
+        <path d="M12 3L4 9V21H9V14H15V21H20V9L12 3Z" fill="#999999" />
+
+        <defs>
+          <clipPath id="clip0_543_2787">
+            <rect width="24" height="24" fill="white" />
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+  },
+  {
+    name: "찜",
+    path: "/subscribe",
+    icon: () => (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+        fill="none"
+        aria-hidden="true"
+      >
+        <path
+          d="M12.5001 21L11.1568 19.7771C6.3857 15.4507 3.23584 12.5973 3.23584 9.09537C3.23584 6.24196 5.4778 4 8.33121 4C9.9432 4 11.4903 4.75041 12.5001 5.93624C13.51 4.75041 15.0571 4 16.6691 4C19.5225 4 21.7645 6.24196 21.7645 9.09537C21.7645 12.5973 18.6146 15.4507 13.8435 19.7864L12.5001 21Z"
+          fill="#999999"
+        />
+        <defs>
+          <clipPath id="clip0_543_2781">
+            <rect width="24" height="24" fill="white" />
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+  },
+  {
+    name: "지도",
+    path: "/location",
+    icon: () => (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+        fill="none"
+        aria-hidden="true"
+      >
+        <path
+          d="M12 2C8.13 2 5 5.13 5 9C5 14.25 12 22 12 22C12 22 19 14.25 19 9C19 5.13 15.87 2 12 2ZM12 11.5C10.62 11.5 9.5 10.38 9.5 9C9.5 7.62 10.62 6.5 12 6.5C13.38 6.5 14.5 7.62 14.5 9C14.5 10.38 13.38 11.5 12 11.5Z"
+          fill="#999999"
+        />
+        <defs>
+          <clipPath id="clip0_543_2790">
+            <rect width="24" height="24" fill="white" />
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+  },
+  {
+    name: "주문내역",
+    path: "/my-orders",
+    icon: () => (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+        fill="none"
+        aria-hidden="true"
+      >
+        <path
+          d="M17.21 8.99953L12.83 2.43953C12.64 2.15953 12.32 2.01953 12 2.01953C11.68 2.01953 11.36 2.15953 11.17 2.44953L6.79 8.99953H2C1.45 8.99953 1 9.44953 1 9.99953C1 10.0895 1.01 10.1795 1.04 10.2695L3.58 19.5395C3.81 20.3795 4.58 20.9995 5.5 20.9995H18.5C19.42 20.9995 20.19 20.3795 20.43 19.5395L22.97 10.2695L23 9.99953C23 9.44953 22.55 8.99953 22 8.99953H17.21ZM9 8.99953L12 4.59953L15 8.99953H9ZM12 16.9995C10.9 16.9995 10 16.0995 10 14.9995C10 13.8995 10.9 12.9995 12 12.9995C13.1 12.9995 14 13.8995 14 14.9995C14 16.0995 13.1 16.9995 12 16.9995Z"
+          fill="#999999"
+        />
+        <defs>
+          <clipPath id="clip0_543_2796">
+            <rect width="24" height="24" fill="white" />
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+  },
+  {
+    name: "마이",
+    path: "/my-page",
+    icon: () => (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+        fill="none"
+        aria-hidden="true"
+      >
+        <path
+          d="M12 13C14.4853 13 16.5 10.9853 16.5 8.5C16.5 6.01472 14.4853 4 12 4C9.51472 4 7.5 6.01472 7.5 8.5C7.5 10.9853 9.51472 13 12 13Z"
+          fill="#999999"
+        />
+        <path
+          d="M12 15.25C8.99625 15.25 3 16.7575 3 19.75V22H21V19.75C21 16.7575 15.0037 15.25 12 15.25Z"
+          fill="#999999"
+        />
+        <defs>
+          <clipPath id="clip0_543_2802">
+            <rect width="24" height="24" fill="white" />
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+  },
+];
+
+const BottomNav = (props: PropsWithChildren) => {
+  const { children } = props;
+  const segment = useSelectedLayoutSegment();
+
+  const getRouteStyle = (path: string) => {
+    let isActive = false;
+    if (segment) {
+      isActive = path.includes(segment);
+    } else {
+      isActive = path === "/";
+    }
+
+    return isActive ? style.navItem.active : style.navItem.default;
+  };
+
+  return (
+    <div className={style.container}>
+      <main className={style.main}>{children}</main>
+      <footer>
+        <nav className={style.bottomNav}>
+          {ROUTE.map(({ path, name, icon: Icon }) => (
+            <Link href={path} key={name} className={getRouteStyle(path)}>
+              <Icon />
+              <span>{name}</span>
+            </Link>
+          ))}
+        </nav>
+      </footer>
+    </div>
+  );
+};
+
+export default BottomNav;
diff --git a/apps/user/src/styles/reset.css.ts b/apps/user/src/styles/reset.css.ts
index af9495f7..e6366e5a 100644
--- a/apps/user/src/styles/reset.css.ts
+++ b/apps/user/src/styles/reset.css.ts
@@ -26,4 +26,5 @@ globalStyle("ul, ol", {
 // 링크 초기화
 globalStyle("a", {
   textDecoration: "none",
+  color: globalTheme.color.black,
 });
diff --git a/apps/user/src/styles/theme.css.ts b/apps/user/src/styles/theme.css.ts
index 648b7d9e..91892c6c 100644
--- a/apps/user/src/styles/theme.css.ts
+++ b/apps/user/src/styles/theme.css.ts
@@ -9,6 +9,8 @@ export const globalTheme = createThemeContract({
     black: null,
     white: null,
 
+    line: null,
+
     gray: {
       dark: null,
       base: null,
@@ -53,12 +55,14 @@ export const theme = createGlobalTheme(":root", globalTheme, {
     black: "#333333",
     white: "#ffffff",
 
+    line: "#d9d9d9",
+
     gray: {
       dark: "#666666",
       base: "#999999",
       light: "#CCCCCC",
     },
-    
+
     label: {
       sales: {
         background: "#D8F4E4",


Reviews:
user: login: brparkk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T09:43:42Z
user: login: brparkk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T09:47:57Z
user: login: brparkk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T09:48:34Z
user: login: SeoJaeWan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T09:58:20Z
user: login: SeoJaeWan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T09:59:46Z
user: login: brparkk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T10:23:03Z
user: login: brparkk
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-17T10:23:14Z
user: login: brparkk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T13:11:55Z
user: login: brparkk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T13:19:48Z
user: login: SeoJaeWan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T13:28:40Z
user: login: SeoJaeWan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T13:39:31Z
user: login: brparkk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T01:14:44Z
user: login: brparkk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T01:20:37Z

Review comments:
diff_hunk: @@ -0,0 +1,56 @@
+import { globalTheme } from "@/styles/theme.css";
+import { style, styleVariants } from "@vanilla-extract/css";
+
+export const container = style({
+  display: "flex",
+  flexDirection: "column",
+
+  width: "100%",
+  height: "100vh",
+});
+
+export const main = style({
+  height: "calc(100% - 70px)",
+
+  padding: "0 20px",
+});
+
+export const bottomNav = style({
+  display: "flex",
+  justifyContent: "space-between",
+  alignItems: "center",
+
+  width: "100%",
+  height: "70px",
+
+  padding: "14px 20px",
+
+  borderTop: `1px solid ${globalTheme.color.line}`,
+});
+
+const navItemBase = style({
+  display: "flex",
+  flexDirection: "column",
+  alignItems: "center",
+  gap: "6px",
+
+  width: "60px",
+
path: apps/user/src/components/ui/layout/bottomNav/bottomNav.css.ts
user: login: brparkk
body: 중간에 공백 들어가있는건 biome 설정인가요?🧐
created_at: 2025-05-17T09:43:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,56 @@
+import { globalTheme } from "@/styles/theme.css";
+import { style, styleVariants } from "@vanilla-extract/css";
+
+export const container = style({
+  display: "flex",
+  flexDirection: "column",
+
+  width: "100%",
+  height: "100vh",
+});
+
+export const main = style({
+  height: "calc(100% - 70px)",
+
+  padding: "0 20px",
+});
+
+export const bottomNav = style({
+  display: "flex",
+  justifyContent: "space-between",
+  alignItems: "center",
+
+  width: "100%",
+  height: "70px",
+
+  padding: "14px 20px",
+
+  borderTop: `1px solid ${globalTheme.color.line}`,
+});
+
+const navItemBase = style({
+  display: "flex",
+  flexDirection: "column",
+  alignItems: "center",
+  gap: "6px",
+
+  width: "60px",
+
+  fontSize: "12px",
+  fontWeight: 500,
+});
+
+export const navItem = styleVariants({
path: apps/user/src/components/ui/layout/bottomNav/bottomNav.css.ts
user: login: brparkk
body: 👍
created_at: 2025-05-17T09:47:57Z
author_association: COLLABORATOR
diff_hunk: @@ -31,9 +32,11 @@ export default async function RootLayout({
 }>) {
   return (
     <html lang="en">
-      <body className={`${geistSans.variable} ${geistMono.variable}`}>
-        <Providers>{children}</Providers>
-        <div id="bottom-sheet" />
+      <body className={`${geistSans.variable} ${geistMono.variable} ${style.container}`}>
+        <Providers>
+          <div className={style.main}>{children}</div>
+          <div id="bottom-sheet" />
path: apps/user/src/app/layout.tsx
user: login: brparkk
body: 여기는 왜 id가 필요한가요?
created_at: 2025-05-17T09:48:34Z
author_association: COLLABORATOR
diff_hunk: @@ -31,9 +32,11 @@ export default async function RootLayout({
 }>) {
   return (
     <html lang="en">
-      <body className={`${geistSans.variable} ${geistMono.variable}`}>
-        <Providers>{children}</Providers>
-        <div id="bottom-sheet" />
+      <body className={`${geistSans.variable} ${geistMono.variable} ${style.container}`}>
+        <Providers>
+          <div className={style.main}>{children}</div>
+          <div id="bottom-sheet" />
path: apps/user/src/app/layout.tsx
user: login: SeoJaeWan
body: 모달 요소는 react portal을 통해서 main 밖에서 렌더링하면 
z index 등 관리에 좀 더 자유로워지는 것 같아서 그렇게 적용했습니다! 
created_at: 2025-05-17T09:58:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,56 @@
+import { globalTheme } from "@/styles/theme.css";
+import { style, styleVariants } from "@vanilla-extract/css";
+
+export const container = style({
+  display: "flex",
+  flexDirection: "column",
+
+  width: "100%",
+  height: "100vh",
+});
+
+export const main = style({
+  height: "calc(100% - 70px)",
+
+  padding: "0 20px",
+});
+
+export const bottomNav = style({
+  display: "flex",
+  justifyContent: "space-between",
+  alignItems: "center",
+
+  width: "100%",
+  height: "70px",
+
+  padding: "14px 20px",
+
+  borderTop: `1px solid ${globalTheme.color.line}`,
+});
+
+const navItemBase = style({
+  display: "flex",
+  flexDirection: "column",
+  alignItems: "center",
+  gap: "6px",
+
+  width: "60px",
+
path: apps/user/src/components/ui/layout/bottomNav/bottomNav.css.ts
user: login: SeoJaeWan
body: gap과 width 사이 공백 라인을 이야기하시는 거라면 
제 개발 습관입니다! 

비슷한 요소끼리 묶어서 작성하면 저는 가독성이 더 좋은 것 같더라구요! 
```
display: "flex"
gap: "6px"   <-- flex관련

width: "50px",
height: "60px" <-- size 관련

padding: "6px",
margin: "7px" << ...~

```
created_at: 2025-05-17T09:59:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,165 @@
+"use client";
+
+import Link from "next/link";
+import { useSelectedLayoutSegment } from "next/navigation";
+import type { PropsWithChildren } from "react";
+import * as style from "./bottomNav.css";
+
+const ROUTE = [
+  {
+    name: "홈",
+    path: "/",
+    icon: () => (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+        fill="none"
+        aria-hidden="true"
+      >
+        <path d="M12 3L4 9V21H9V14H15V21H20V9L12 3Z" fill="#999999" />
+
+        <defs>
+          <clipPath id="clip0_543_2787">
+            <rect width="24" height="24" fill="white" />
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+  },
+  {
+    name: "찜",
+    path: "/subscribe",
+    icon: () => (
+      <svg
path: apps/user/src/components/ui/layout/bottomNav/index.tsx
user: login: brparkk
body: 아이콘 에셋으로 관리하는건 어떠신지요!
created_at: 2025-05-17T10:23:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,56 @@
+import { globalTheme } from "@/styles/theme.css";
+import { style, styleVariants } from "@vanilla-extract/css";
+
+export const container = style({
+  display: "flex",
+  flexDirection: "column",
+
+  width: "100%",
+  height: "100vh",
+});
+
+export const main = style({
+  height: "calc(100% - 70px)",
+
+  padding: "0 20px",
+});
+
+export const bottomNav = style({
+  display: "flex",
+  justifyContent: "space-between",
+  alignItems: "center",
+
+  width: "100%",
+  height: "70px",
+
+  padding: "14px 20px",
+
+  borderTop: `1px solid ${globalTheme.color.line}`,
+});
+
+const navItemBase = style({
+  display: "flex",
+  flexDirection: "column",
+  alignItems: "center",
+  gap: "6px",
+
+  width: "60px",
+
path: apps/user/src/components/ui/layout/bottomNav/bottomNav.css.ts
user: login: brparkk
body: 아하 그러쿤요! 취향차이 인거 같네요 ㅋㅋ
created_at: 2025-05-17T13:11:55Z
author_association: COLLABORATOR
diff_hunk: @@ -31,9 +32,11 @@ export default async function RootLayout({
 }>) {
   return (
     <html lang="en">
-      <body className={`${geistSans.variable} ${geistMono.variable}`}>
-        <Providers>{children}</Providers>
-        <div id="bottom-sheet" />
+      <body className={`${geistSans.variable} ${geistMono.variable} ${style.container}`}>
+        <Providers>
+          <div className={style.main}>{children}</div>
+          <div id="bottom-sheet" />
path: apps/user/src/app/layout.tsx
user: login: brparkk
body: 저는 주로 저렇게 특정 id 요소를 사용하지 않고, `document.body`에 위치시켰어서 여쭤봤습니다!
id로 관리하면 작업자가 한명이 아닐떄 중복될 여지가 있다고 생각했었는데, 
GPT한테 물어보니 아래와 같은 답변을 받았습니다!😁

```mdx
document.body를 사용하는 것과 특정 id를 가진 요소를 사용하는 것 모두 유효한 방법입니다. 두 접근법의 차이점은:

**document.body 사용할 때**
- 장점: 별도 요소 생성 없이 바로 사용 가능
- 단점: 여러 포탈이 모두 body에 붙어 관리가 어려울 수 있음

**특정 id 요소 사용할 때**
- 장점: 
  - 포탈을 논리적으로 그룹화 가능
  - 레이아웃 구조를 더 명확하게 관리 가능
  - 여러 종류의 오버레이(모달, 바텀시트, 툴팁 등)를 구분해서 관리 가능
- 단점: 추가 마크업 필요

대규모 애플리케이션에서는 특정 id를 사용하는 것이 유지보수 측면에서 더 좋을 수 있지만, 작은 프로젝트에서는 document.body로도 충분합니다. 개인 선호와 프로젝트 요구사항에 따라 선택하면 됩니다.
```

created_at: 2025-05-17T13:19:48Z
author_association: COLLABORATOR
diff_hunk: @@ -31,9 +32,11 @@ export default async function RootLayout({
 }>) {
   return (
     <html lang="en">
-      <body className={`${geistSans.variable} ${geistMono.variable}`}>
-        <Providers>{children}</Providers>
-        <div id="bottom-sheet" />
+      <body className={`${geistSans.variable} ${geistMono.variable} ${style.container}`}>
+        <Providers>
+          <div className={style.main}>{children}</div>
+          <div id="bottom-sheet" />
path: apps/user/src/app/layout.tsx
user: login: SeoJaeWan
body: 아~  
GPT의 이유 중 하나가 큰 것 같긴해여 
portal이 특정 요소를 밖으로 빼는 이유로 사용하는데 모든 요소가 document.body에 있으면 
결국 포탈을 사용한 요소끼리의 z index를 관리할 때 불편함이 있더라구요! 

예를들어, modal과 tooltip이 있다고 했을 때, tooptip보다 modal이 상위에 있어야 하는 경우가 있는데, 모든 modal에 z index를 관리하기 보단 상단의 modal id를 가진 값에 z index를 줘서 작업하는 영역에선 애초에 신경을 쓰지 않는 방법을 저는 자주 사용했거든요! 

z index에 스트레스를 받다보니 최대한 신경 안쓰는 방법을 고려하다보니 나온 방식 같아요! 
~(그래도 놓치는 부분이 있지만요....)~
created_at: 2025-05-17T13:28:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,165 @@
+"use client";
+
+import Link from "next/link";
+import { useSelectedLayoutSegment } from "next/navigation";
+import type { PropsWithChildren } from "react";
+import * as style from "./bottomNav.css";
+
+const ROUTE = [
+  {
+    name: "홈",
+    path: "/",
+    icon: () => (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+        fill="none"
+        aria-hidden="true"
+      >
+        <path d="M12 3L4 9V21H9V14H15V21H20V9L12 3Z" fill="#999999" />
+
+        <defs>
+          <clipPath id="clip0_543_2787">
+            <rect width="24" height="24" fill="white" />
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+  },
+  {
+    name: "찜",
+    path: "/subscribe",
+    icon: () => (
+      <svg
path: apps/user/src/components/ui/layout/bottomNav/index.tsx
user: login: SeoJaeWan
body: 저는 svg 파일을 커스텀으로 수정하는 경우가 있을 때만 svg 태그를 사용해서 관리하구 
아니면 img 태그의 src에 넣어서 사용하는데 혹시 차이가 있을까요?? 

아이콘을 에셋으로 관리한다는게 어떤 느낌인지 제가 이해를 제대로 못한 것 같아요! 
created_at: 2025-05-17T13:39:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,165 @@
+"use client";
+
+import Link from "next/link";
+import { useSelectedLayoutSegment } from "next/navigation";
+import type { PropsWithChildren } from "react";
+import * as style from "./bottomNav.css";
+
+const ROUTE = [
+  {
+    name: "홈",
+    path: "/",
+    icon: () => (
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+        fill="none"
+        aria-hidden="true"
+      >
+        <path d="M12 3L4 9V21H9V14H15V21H20V9L12 3Z" fill="#999999" />
+
+        <defs>
+          <clipPath id="clip0_543_2787">
+            <rect width="24" height="24" fill="white" />
+          </clipPath>
+        </defs>
+      </svg>
+    ),
+  },
+  {
+    name: "찜",
+    path: "/subscribe",
+    icon: () => (
+      <svg
path: apps/user/src/components/ui/layout/bottomNav/index.tsx
user: login: brparkk
body: 아 src/assets에 svg icon 들 넣어두고 `<LikeIcon />` 이런식으로 가져오는 걸 말씀드린거였어요! svg 때문에 라인수가 길어지는 거 같아 제안드려봤습니다!
created_at: 2025-05-18T01:14:44Z
author_association: COLLABORATOR
diff_hunk: @@ -31,9 +32,11 @@ export default async function RootLayout({
 }>) {
   return (
     <html lang="en">
-      <body className={`${geistSans.variable} ${geistMono.variable}`}>
-        <Providers>{children}</Providers>
-        <div id="bottom-sheet" />
+      <body className={`${geistSans.variable} ${geistMono.variable} ${style.container}`}>
+        <Providers>
+          <div className={style.main}>{children}</div>
+          <div id="bottom-sheet" />
path: apps/user/src/app/layout.tsx
user: login: brparkk
body: 오 그렇네요 컨테이너 요소에서만 관리하면 관리이슈가 줄긴 하겠네요! (id가 잘 관리된다는 전제하에..)
created_at: 2025-05-18T01:20:37Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-15T01:54:31Z
body: [vc]: #RK5pdJgd9LJZWt6vMFu8vIWPjM0aOoiuVtiWIY8hyfI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJvMm8tZmUtb3duZXIiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9vd25lciIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9vMm8tZmUvbzJvLWZlLW93bmVyLzlBSmliaXozcXByUDVaQkxpOHYzRE43UExxVDEiLCJwcmV2aWV3VXJsIjoibzJvLWZlLW93bmVyLWdpdC1mZWF0LWxheW91dC1vMm8tZmUudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJvMm8tZmUtb3duZXItZ2l0LWZlYXQtbGF5b3V0LW8yby1mZS52ZXJjZWwuYXBwIn19LHsibmFtZSI6Im8yby1mZS11c2VyIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvdXNlciIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9vMm8tZmUvbzJvLWZlLXVzZXIvRjU1alExRFV0TFRrY0dpOFZ0TGJDV3pGbmdOWCIsInByZXZpZXdVcmwiOiJvMm8tZmUtdXNlci1naXQtZmVhdC1sYXlvdXQtbzJvLWZlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoibzJvLWZlLXVzZXItZ2l0LWZlYXQtbGF5b3V0LW8yby1mZS52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **o2o-fe-owner** | ✅ Ready ([Inspect](https://vercel.com/o2o-fe/o2o-fe-owner/9AJibiz3qprP5ZBLi8v3DN7PLqT1)) | [Visit Preview](https://o2o-fe-owner-git-feat-layout-o2o-fe.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/o2o-fe-owner-git-feat-layout-o2o-fe.vercel.app?via=pr-comment-feedback-link) | May 16, 2025 10:21am |
| **o2o-fe-user** | ✅ Ready ([Inspect](https://vercel.com/o2o-fe/o2o-fe-user/F55jQ1DUtLTkcGi8VtLbCWzFngNX)) | [Visit Preview](https://o2o-fe-user-git-feat-layout-o2o-fe.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/o2o-fe-user-git-feat-layout-o2o-fe.vercel.app?via=pr-comment-feedback-link) | May 16, 2025 10:21am |



author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T11:30:46Z
body: ✅ User 앱 개발 환경 배포 완료: 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T11:37:41Z
body: ✅ User 앱 개발 환경 배포 완료: 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T11:42:10Z
body: ✅ User 앱 개발 환경 배포 완료: 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T11:57:44Z
body: ✅ User 앱 개발 환경 배포 완료: 
author_association: NONE
user: login: brparkk
created_at: 2025-05-15T14:11:00Z
body: 우와 이걸 어느새...빠름빠름...
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-05-16T10:23:07Z
body: ✅ User 앱 개발 환경 배포 완료: 
author_association: NONE