diff --git a/src/App.jsx b/src/App.jsx
index 9ab39ae..24d742d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,40 +1,190 @@
import { Routes, Route } from 'react-router-dom'
-import { Box, Typography, Container, Button, Stack } from '@mui/material'
+import { Box, Typography, Container, Card, CardContent, Grid, Button } from '@mui/material'
+import {
+ RecordVoiceOver as InterviewIcon,
+ Headphones as OpicIcon,
+ Chat as FreetalkIcon,
+ Edit as WritingIcon,
+} from '@mui/icons-material'
+import MainLayout from './layouts/MainLayout'
+
+// 임시 대시보드 페이지
+function Dashboard() {
+ const learningModes = [
+ {
+ id: 'interview',
+ title: '면접 시뮬레이션',
+ description: 'AI 면접관과 실전처럼 연습하세요',
+ icon: InterviewIcon,
+ color: '#0124ac',
+ path: '/interview',
+ },
+ {
+ id: 'opic',
+ title: 'OPIC 연습',
+ description: '레벨별 맞춤 문제로 실력 향상',
+ icon: OpicIcon,
+ color: '#2196f3',
+ path: '/opic',
+ },
+ {
+ id: 'freetalk',
+ title: '프리토킹',
+ description: 'AI와 자유롭게 영어로 대화',
+ icon: FreetalkIcon,
+ color: '#4caf50',
+ path: '/freetalk',
+ },
+ {
+ id: 'writing',
+ title: '작문 연습',
+ description: '문법 교정과 표현 피드백',
+ icon: WritingIcon,
+ color: '#ff9800',
+ path: '/writing',
+ },
+ ]
-function Home() {
return (
-
-
- Welcome to FE Repository
+
+
+ 안녕하세요! 👋
-
- React + Vite + MUI 프로젝트가 준비되었습니다.
+
+ 오늘은 어떤 학습을 해볼까요?
+
+
+
+ {learningModes.map((mode) => {
+ const Icon = mode.icon
+ return (
+
+
+
+
+
+
+
+ {mode.title}
+
+
+ {mode.description}
+
+
+
+
+ )
+ })}
+
-
-
-
-
-
-
-
-
- Primary Color: #0124ac
-
-
+ {/* 최근 학습 */}
+
+
+ 최근 학습
+
+
+
+
+ 아직 학습 기록이 없습니다. 학습을 시작해보세요!
+
+
+
+
+
+ )
+}
+
+// 임시 페이지들
+function InterviewPage() {
+ return (
+
+ 면접 시뮬레이션
+ AI 면접관과 실전 연습
+
+ )
+}
+
+function OpicPage() {
+ return (
+
+ OPIC 연습
+ 레벨별 맞춤 연습
+
+ )
+}
+
+function FreetalkPage() {
+ return (
+
+ 프리토킹
+ AI와 자유로운 대화
+
+ )
+}
+
+function WritingPage() {
+ return (
+
+ 작문 연습
+ 문법 교정 & 피드백
+
+ )
+}
+
+function ReportsPage() {
+ return (
+
+ 내 리포트
+ 학습 결과 분석
+
+ )
+}
+
+function SettingsPage() {
+ return (
+
+ 설정
+ 계정 및 앱 설정
+
+ )
+}
+
+function NotFound() {
+ return (
+
+
+
+ 404
+
+
+ 페이지를 찾을 수 없습니다
+
+
)
@@ -43,7 +193,20 @@ function Home() {
function App() {
return (
- } />
+ {/* MainLayout 적용 라우트 */}
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ {/* 404 */}
+ } />
)
}
diff --git a/src/contexts/ThemeContext.jsx b/src/contexts/ThemeContext.jsx
new file mode 100644
index 0000000..cad8b7b
--- /dev/null
+++ b/src/contexts/ThemeContext.jsx
@@ -0,0 +1,79 @@
+import { createContext, useContext, useState, useEffect, useMemo } from 'react'
+import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'
+import { CssBaseline } from '@mui/material'
+import { lightTheme, darkTheme } from '../theme/theme'
+
+const ThemeContext = createContext({
+ mode: 'light',
+ toggleTheme: () => {},
+ setMode: () => {},
+})
+
+export const useThemeMode = () => {
+ const context = useContext(ThemeContext)
+ if (!context) {
+ throw new Error('useThemeMode must be used within ThemeProvider')
+ }
+ return context
+}
+
+export const ThemeProvider = ({ children }) => {
+ // localStorage에서 테마 모드 불러오기 (시스템 설정 기본값)
+ const [mode, setMode] = useState(() => {
+ const savedMode = localStorage.getItem('themeMode')
+ if (savedMode) return savedMode
+
+ // 시스템 다크모드 감지
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ return 'dark'
+ }
+ return 'light'
+ })
+
+ // 시스템 테마 변경 감지
+ useEffect(() => {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+ const handleChange = (e) => {
+ const savedMode = localStorage.getItem('themeMode')
+ if (!savedMode) {
+ setMode(e.matches ? 'dark' : 'light')
+ }
+ }
+
+ mediaQuery.addEventListener('change', handleChange)
+ return () => mediaQuery.removeEventListener('change', handleChange)
+ }, [])
+
+ // 테마 변경 시 localStorage 저장
+ useEffect(() => {
+ localStorage.setItem('themeMode', mode)
+ // body 클래스 추가 (CSS 변수 등에서 활용)
+ document.body.classList.remove('light-mode', 'dark-mode')
+ document.body.classList.add(`${mode}-mode`)
+ }, [mode])
+
+ const toggleTheme = () => {
+ setMode((prev) => (prev === 'light' ? 'dark' : 'light'))
+ }
+
+ const theme = useMemo(() => {
+ return mode === 'dark' ? darkTheme : lightTheme
+ }, [mode])
+
+ const contextValue = useMemo(() => ({
+ mode,
+ toggleTheme,
+ setMode,
+ }), [mode])
+
+ return (
+
+
+
+ {children}
+
+
+ )
+}
+
+export default ThemeContext
diff --git a/src/layouts/MainLayout/Footer/index.jsx b/src/layouts/MainLayout/Footer/index.jsx
new file mode 100644
index 0000000..5a54f86
--- /dev/null
+++ b/src/layouts/MainLayout/Footer/index.jsx
@@ -0,0 +1,71 @@
+import { Box, Typography, Link, Container, Divider } from '@mui/material'
+
+const Footer = () => {
+ return (
+
+
+
+ {/* 저작권 */}
+
+ © 2026 AI Language Learning. All rights reserved.
+
+
+ {/* 링크 */}
+
+
+ 이용약관
+
+
+ 개인정보처리방침
+
+
+ 고객센터
+
+
+
+
+
+ )
+}
+
+export default Footer
diff --git a/src/layouts/MainLayout/Header/index.jsx b/src/layouts/MainLayout/Header/index.jsx
new file mode 100644
index 0000000..90f60d6
--- /dev/null
+++ b/src/layouts/MainLayout/Header/index.jsx
@@ -0,0 +1,256 @@
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import {
+ AppBar,
+ Toolbar,
+ Typography,
+ IconButton,
+ Box,
+ Button,
+ Avatar,
+ Menu,
+ MenuItem,
+ Divider,
+ Badge,
+ useMediaQuery,
+ useTheme,
+} from '@mui/material'
+import {
+ Menu as MenuIcon,
+ Notifications as NotificationsIcon,
+ Person as PersonIcon,
+ Settings as SettingsIcon,
+ Logout as LogoutIcon,
+ School as SchoolIcon,
+ DarkMode as DarkModeIcon,
+ LightMode as LightModeIcon,
+} from '@mui/icons-material'
+import { useThemeMode } from '../../../contexts/ThemeContext'
+
+const Header = ({ onMenuClick, sidebarOpen }) => {
+ const theme = useTheme()
+ const navigate = useNavigate()
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'))
+ const { mode, toggleTheme } = useThemeMode()
+
+ const [anchorEl, setAnchorEl] = useState(null)
+ const [notificationAnchor, setNotificationAnchor] = useState(null)
+
+ const handleProfileMenuOpen = (event) => {
+ setAnchorEl(event.currentTarget)
+ }
+
+ const handleProfileMenuClose = () => {
+ setAnchorEl(null)
+ }
+
+ const handleNotificationOpen = (event) => {
+ setNotificationAnchor(event.currentTarget)
+ }
+
+ const handleNotificationClose = () => {
+ setNotificationAnchor(null)
+ }
+
+ const handleLogout = () => {
+ handleProfileMenuClose()
+ // TODO: 로그아웃 로직
+ navigate('/login')
+ }
+
+ return (
+
+
+ {/* 햄버거 메뉴 (모바일/태블릿) */}
+ {isMobile && (
+
+
+
+ )}
+
+ {/* 로고 & 서비스명 */}
+ navigate('/')}
+ >
+
+
+ AI 언어 학습
+
+
+
+ {/* 중앙 네비게이션 (데스크톱) */}
+ {!isMobile && (
+
+
+
+
+
+ )}
+
+
+
+ {/* 우측 아이콘들 */}
+
+ {/* 다크모드 토글 */}
+
+ {mode === 'dark' ? : }
+
+
+ {/* 알림 */}
+
+
+
+
+
+
+ {/* 프로필 */}
+
+
+ U
+
+
+
+
+ {/* 알림 메뉴 */}
+
+
+ {/* 프로필 메뉴 */}
+
+
+
+ )
+}
+
+export default Header
diff --git a/src/layouts/MainLayout/Sidebar/index.jsx b/src/layouts/MainLayout/Sidebar/index.jsx
new file mode 100644
index 0000000..4900e66
--- /dev/null
+++ b/src/layouts/MainLayout/Sidebar/index.jsx
@@ -0,0 +1,262 @@
+import { useLocation, useNavigate } from 'react-router-dom'
+import {
+ Drawer,
+ Box,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ IconButton,
+ Divider,
+ Typography,
+ useTheme,
+ useMediaQuery,
+} from '@mui/material'
+import {
+ ChevronLeft as ChevronLeftIcon,
+ ChevronRight as ChevronRightIcon,
+ Dashboard as DashboardIcon,
+ RecordVoiceOver as InterviewIcon,
+ Headphones as OpicIcon,
+ Chat as FreetalkIcon,
+ Edit as WritingIcon,
+ Assessment as ReportIcon,
+ Settings as SettingsIcon,
+} from '@mui/icons-material'
+
+const DRAWER_WIDTH = 260
+const DRAWER_WIDTH_COLLAPSED = 72
+
+const menuItems = [
+ {
+ category: '학습 모드',
+ items: [
+ {
+ id: 'interview',
+ label: '면접 시뮬레이션',
+ icon: InterviewIcon,
+ path: '/interview',
+ description: 'AI 면접관과 실전 연습'
+ },
+ {
+ id: 'opic',
+ label: 'OPIC 연습',
+ icon: OpicIcon,
+ path: '/opic',
+ description: '레벨별 맞춤 연습'
+ },
+ {
+ id: 'freetalk',
+ label: '프리토킹',
+ icon: FreetalkIcon,
+ path: '/freetalk',
+ description: 'AI와 자유로운 대화'
+ },
+ {
+ id: 'writing',
+ label: '작문 연습',
+ icon: WritingIcon,
+ path: '/writing',
+ description: '문법 교정 & 피드백'
+ },
+ ],
+ },
+ {
+ category: '기타',
+ items: [
+ {
+ id: 'dashboard',
+ label: '대시보드',
+ icon: DashboardIcon,
+ path: '/dashboard',
+ description: '학습 현황 요약'
+ },
+ {
+ id: 'reports',
+ label: '내 리포트',
+ icon: ReportIcon,
+ path: '/reports',
+ description: '학습 결과 분석'
+ },
+ {
+ id: 'settings',
+ label: '설정',
+ icon: SettingsIcon,
+ path: '/settings',
+ description: '계정 및 앱 설정'
+ },
+ ],
+ },
+]
+
+const Sidebar = ({ open, collapsed, onToggleCollapse, onClose }) => {
+ const theme = useTheme()
+ const location = useLocation()
+ const navigate = useNavigate()
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'))
+
+ const drawerWidth = collapsed ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH
+
+ const handleNavigation = (path) => {
+ navigate(path)
+ if (isMobile) {
+ onClose()
+ }
+ }
+
+ const isActive = (path) => location.pathname === path
+
+ const drawerContent = (
+
+ {/* 헤더 영역 - Toolbar 높이만큼 여백 */}
+
+
+ {/* 접기/펼치기 버튼 */}
+ {!isMobile && (
+
+
+ {collapsed ? : }
+
+
+ )}
+
+ {/* 메뉴 리스트 */}
+
+ {menuItems.map((category, categoryIndex) => (
+
+ {!collapsed && (
+
+ {category.category}
+
+ )}
+
+
+ {category.items.map((item) => {
+ const Icon = item.icon
+ const active = isActive(item.path)
+
+ return (
+
+ handleNavigation(item.path)}
+ sx={{
+ borderRadius: 2,
+ minHeight: 48,
+ justifyContent: collapsed ? 'center' : 'flex-start',
+ px: collapsed ? 1 : 2,
+ backgroundColor: active ? 'primary.main' : 'transparent',
+ color: active ? 'white' : 'text.primary',
+ '&:hover': {
+ backgroundColor: active
+ ? 'primary.dark'
+ : 'action.hover',
+ },
+ }}
+ >
+
+
+
+
+ {!collapsed && (
+
+ )}
+
+
+ )
+ })}
+
+
+ {categoryIndex < menuItems.length - 1 && !collapsed && (
+
+ )}
+
+ ))}
+
+
+ {/* 하단 정보 */}
+ {!collapsed && (
+
+
+ 오늘의 학습 시간
+
+
+ 1시간 23분
+
+
+ )}
+
+ )
+
+ // 모바일: 임시 Drawer
+ if (isMobile) {
+ return (
+
+ {drawerContent}
+
+ )
+ }
+
+ // 데스크톱: 고정 Drawer
+ return (
+
+ {drawerContent}
+
+ )
+}
+
+export default Sidebar
diff --git a/src/layouts/MainLayout/index.jsx b/src/layouts/MainLayout/index.jsx
new file mode 100644
index 0000000..bc3d376
--- /dev/null
+++ b/src/layouts/MainLayout/index.jsx
@@ -0,0 +1,95 @@
+import { useState, useEffect } from 'react'
+import { Outlet } from 'react-router-dom'
+import { Box, useTheme, useMediaQuery } from '@mui/material'
+import Header from './Header'
+import Sidebar from './Sidebar'
+import Footer from './Footer'
+
+const DRAWER_WIDTH = 260
+const DRAWER_WIDTH_COLLAPSED = 72
+
+const MainLayout = () => {
+ const theme = useTheme()
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'))
+
+ // 모바일 사이드바 열림 상태
+ const [mobileOpen, setMobileOpen] = useState(false)
+
+ // 데스크톱 사이드바 접힘 상태 (localStorage 저장)
+ const [collapsed, setCollapsed] = useState(() => {
+ const saved = localStorage.getItem('sidebarCollapsed')
+ return saved ? JSON.parse(saved) : false
+ })
+
+ // collapsed 상태 localStorage 저장
+ useEffect(() => {
+ localStorage.setItem('sidebarCollapsed', JSON.stringify(collapsed))
+ }, [collapsed])
+
+ const handleMobileToggle = () => {
+ setMobileOpen(!mobileOpen)
+ }
+
+ const handleMobileClose = () => {
+ setMobileOpen(false)
+ }
+
+ const handleCollapseToggle = () => {
+ setCollapsed(!collapsed)
+ }
+
+ const drawerWidth = isMobile ? 0 : (collapsed ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH)
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Sidebar */}
+
+
+ {/* Main Content */}
+
+ {/* Toolbar 높이만큼 여백 */}
+
+
+ {/* 콘텐츠 영역 */}
+
+
+
+
+ {/* Footer */}
+
+
+
+ )
+}
+
+export default MainLayout
diff --git a/src/main.jsx b/src/main.jsx
index b103b9f..847cc86 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -2,18 +2,16 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
-import { ThemeProvider, CssBaseline } from '@mui/material'
import App from './App.jsx'
import { store } from './store'
-import theme from './theme/theme'
+import { ThemeProvider } from './contexts/ThemeContext'
import './index.css'
createRoot(document.getElementById('root')).render(
-
-
+
diff --git a/src/theme/theme.js b/src/theme/theme.js
index 7622490..2658c28 100644
--- a/src/theme/theme.js
+++ b/src/theme/theme.js
@@ -1,7 +1,50 @@
import { createTheme } from '@mui/material/styles'
-const theme = createTheme({
+const baseTheme = {
+ typography: {
+ fontFamily: '"Roboto", "Noto Sans KR", "Helvetica", "Arial", sans-serif',
+ h1: { fontWeight: 700 },
+ h2: { fontWeight: 600 },
+ h3: { fontWeight: 600 },
+ h4: { fontWeight: 600 },
+ h5: { fontWeight: 500 },
+ h6: { fontWeight: 500 },
+ },
+ shape: {
+ borderRadius: 8,
+ },
+ components: {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ textTransform: 'none',
+ borderRadius: 8,
+ fontWeight: 500,
+ },
+ },
+ },
+ MuiCard: {
+ styleOverrides: {
+ root: {
+ borderRadius: 12,
+ },
+ },
+ },
+ MuiPaper: {
+ styleOverrides: {
+ root: {
+ borderRadius: 12,
+ },
+ },
+ },
+ },
+}
+
+// 라이트 모드
+export const lightTheme = createTheme({
+ ...baseTheme,
palette: {
+ mode: 'light',
primary: {
main: '#0124ac',
light: '#4a52d4',
@@ -18,36 +61,42 @@ const theme = createTheme({
default: '#f5f7fa',
paper: '#ffffff',
},
- },
- typography: {
- fontFamily: '"Roboto", "Noto Sans KR", "Helvetica", "Arial", sans-serif',
- h1: {
- fontWeight: 700,
+ text: {
+ primary: '#1a1a2e',
+ secondary: '#6b7280',
},
- h2: {
- fontWeight: 600,
+ divider: '#e5e7eb',
+ },
+})
+
+// 다크 모드
+export const darkTheme = createTheme({
+ ...baseTheme,
+ palette: {
+ mode: 'dark',
+ primary: {
+ main: '#4a6cf7',
+ light: '#7b93f9',
+ dark: '#2148c4',
+ contrastText: '#ffffff',
},
- h3: {
- fontWeight: 600,
+ secondary: {
+ main: '#64b5f6',
+ light: '#90caf9',
+ dark: '#42a5f5',
+ contrastText: '#000000',
},
- },
- components: {
- MuiButton: {
- styleOverrides: {
- root: {
- textTransform: 'none',
- borderRadius: 8,
- },
- },
+ background: {
+ default: '#0f172a',
+ paper: '#1e293b',
},
- MuiCard: {
- styleOverrides: {
- root: {
- borderRadius: 12,
- },
- },
+ text: {
+ primary: '#f1f5f9',
+ secondary: '#94a3b8',
},
+ divider: '#334155',
},
})
-export default theme
+// 기본 export (하위 호환성)
+export default lightTheme