diff --git a/src/app/(routes)/(quiz)/page.tsx b/src/app/(routes)/(quiz)/page.tsx
new file mode 100644
index 00000000..1e82d122
--- /dev/null
+++ b/src/app/(routes)/(quiz)/page.tsx
@@ -0,0 +1,5 @@
+import { Button } from '@/components/ui/button'
+
+export default function Quiz() {
+ return
+}
diff --git a/src/app/(routes)/layout.tsx b/src/app/(routes)/layout.tsx
new file mode 100644
index 00000000..7cfc6599
--- /dev/null
+++ b/src/app/(routes)/layout.tsx
@@ -0,0 +1,18 @@
+import { LeftNavLayout } from '@/components/left-nav-layout'
+import { Viewport } from 'next'
+import { PropsWithChildren } from 'react'
+
+interface LayoutProps extends PropsWithChildren {}
+
+export const viewport: Viewport = {
+ initialScale: 1.0,
+ userScalable: false,
+ maximumScale: 1.0,
+ minimumScale: 1.0,
+}
+
+const Layout = ({ children }: LayoutProps) => {
+ return {children}
+}
+
+export default Layout
diff --git a/src/app/(routes)/repository/page.tsx b/src/app/(routes)/repository/page.tsx
new file mode 100644
index 00000000..afa24e7a
--- /dev/null
+++ b/src/app/(routes)/repository/page.tsx
@@ -0,0 +1,3 @@
+export default function Repository() {
+ return
Repository
+}
diff --git a/src/app/(routes)/review/page.tsx b/src/app/(routes)/review/page.tsx
new file mode 100644
index 00000000..31ee268d
--- /dev/null
+++ b/src/app/(routes)/review/page.tsx
@@ -0,0 +1,3 @@
+export default function Review() {
+ return Review
+}
diff --git a/src/app/signin/page.tsx b/src/app/(routes)/signin/page.tsx
similarity index 100%
rename from src/app/signin/page.tsx
rename to src/app/(routes)/signin/page.tsx
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f6caad8b..c70c6527 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -17,7 +17,9 @@ export default function RootLayout({
}>) {
return (
- {children}
+
+ {children}
+
)
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
deleted file mode 100644
index c32119cd..00000000
--- a/src/app/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Button } from '@/components/ui/button'
-
-export default function Home() {
- return (
-
-
-
- )
-}
diff --git a/src/components/left-nav-layout.tsx b/src/components/left-nav-layout.tsx
new file mode 100644
index 00000000..5ed5ec94
--- /dev/null
+++ b/src/components/left-nav-layout.tsx
@@ -0,0 +1,257 @@
+'use client'
+
+import { cn } from '@/lib/utils'
+import { useSelectedLayoutSegments } from 'next/navigation'
+import { PropsWithChildren, ReactNode, useMemo } from 'react'
+import { Button } from './ui/button'
+import Link from 'next/link'
+
+interface IconProps {
+ isActive: boolean
+}
+
+interface NavItem {
+ href: string
+ Icon: (props: IconProps) => ReactNode
+ title: string
+ segments: string[][]
+}
+
+export const LeftNavLayout = ({ children }: PropsWithChildren) => {
+ const segments = useSelectedLayoutSegments()
+ const navItems: NavItem[] = useMemo(
+ () => [
+ {
+ href: '/',
+ title: '파워업 퀴즈',
+ Icon: PowerUpIcon,
+ segments: [['(quiz)']],
+ },
+ {
+ href: '/review',
+ title: '복습 체크',
+ Icon: ReviewCheckIcon,
+ segments: [['review']],
+ },
+ {
+ href: '/repository',
+ title: '공부 창고',
+ Icon: StudyRepositoryIcon,
+ segments: [['repository']],
+ },
+ ],
+ [],
+ )
+
+ const activeItem = useMemo(() => findActiveNav(navItems, segments), [navItems, segments])
+
+ return (
+
+ {activeItem && (
+
+
+
+
+ {navItems.map((item) => {
+ const { href, Icon, title } = item
+ const isActive = activeItem == item
+ return (
+
+
+ {title}
+
+ )
+ })}
+
+
+ )}
+ {children}
+
+ )
+}
+
+interface SegmentsRecord {
+ segments: string[]
+ item: NavItem
+}
+
+const descendingOrderOfSegments = (recordA: SegmentsRecord, recordB: SegmentsRecord): number =>
+ recordB.segments.length - recordA.segments.length
+
+const getIsActiveNav = (currentSegments: string[]) => (record: SegmentsRecord) =>
+ record.segments.every((seg, index) => currentSegments[index] === seg)
+
+const findActiveNav = (items: NavItem[], currentSegments: string[]): NavItem | undefined => {
+ const segments = items.reduce((result, item) => {
+ item.segments.forEach((segments) => {
+ result.push({
+ segments,
+ item,
+ })
+ })
+ return result
+ }, [])
+
+ const isActiveSegment = getIsActiveNav(currentSegments)
+ return segments.sort(descendingOrderOfSegments).find(isActiveSegment)?.item
+}
+
+// TODO: Icon 컴포넌트 구현
+function PowerUpIcon({ isActive }: IconProps) {
+ return (
+
+ )
+}
+
+function ReviewCheckIcon({ isActive }: IconProps) {
+ return (
+
+ )
+}
+
+function StudyRepositoryIcon({ isActive }: IconProps) {
+ return (
+
+ )
+}
+
+function LogoIcon() {
+ return (
+
+ )
+}
+
+function PlusIcon() {
+ return (
+
+ )
+}