Skip to content
Merged
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
223 changes: 155 additions & 68 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,200 @@
import { Routes, Route } from 'react-router-dom'
import { Box, Typography, Container, Card, CardContent, Grid, Button } from '@mui/material'
import { useState } from 'react'
import { Routes, Route, useNavigate } from 'react-router-dom'
import { Box, Typography, Container, Card, CardContent, Grid, Button, Collapse, IconButton } from '@mui/material'
import {
RecordVoiceOver as InterviewIcon,
Headphones as OpicIcon,
School as EnglishIcon,
Chat as FreetalkIcon,
Headphones as OpicIcon,
Edit as WritingIcon,
People as PeopleIcon,
SmartToy as AiIcon,
ChevronRight as ChevronRightIcon,
} from '@mui/icons-material'
import MainLayout from './layouts/MainLayout'
import FreetalkPeoplePage from './domains/freetalk/pages/FreetalkPeoplePage'

// 임시 대시보드 페이지
function Dashboard() {
const navigate = useNavigate()
const [expandedCard, setExpandedCard] = useState(null)

const learningModes = [
{
id: 'interview',
title: '면접 시뮬레이션',
description: 'AI 면접관과 실전처럼 연습하세요',
icon: InterviewIcon,
color: '#0124ac',
path: '/interview',
},
{
id: 'opic',
title: 'OPIC 연습',
description: '레벨별 맞춤 문제로 실력 향상',
icon: OpicIcon,
id: 'english',
title: '영어공부',
description: 'OPIC 연습, 작문 연습으로 영어 실력 향상',
icon: EnglishIcon,
color: '#2196f3',
path: '/opic',
children: [
{ id: 'opic', title: 'OPIC 연습', icon: OpicIcon, path: '/opic', description: '레벨별 맞춤 연습' },
{ id: 'writing', title: '작문 연습', icon: WritingIcon, path: '/writing', description: '문법 교정 & 피드백' },
],
},
{
id: 'freetalk',
title: '프리토킹',
description: 'AI와 자유롭게 영어로 대화',
description: '사람들과 또는 AI와 자유롭게 대화',
icon: FreetalkIcon,
color: '#4caf50',
path: '/freetalk',
},
{
id: 'writing',
title: '작문 연습',
description: '문법 교정과 표현 피드백',
icon: WritingIcon,
color: '#ff9800',
path: '/writing',
children: [
{ id: 'freetalk-people', title: '사람들과', icon: PeopleIcon, path: '/freetalk/people', description: '다른 학습자와 대화' },
{ id: 'freetalk-ai', title: 'AI와', icon: AiIcon, path: '/freetalk/ai', description: 'AI와 자유 대화' },
],
},
]

const handleCardClick = (mode) => {
if (mode.children) {
setExpandedCard(expandedCard === mode.id ? null : mode.id)
} else if (mode.path) {
navigate(mode.path)
}
}

const handleSubItemClick = (path, e) => {
e.stopPropagation()
navigate(path)
}

return (
<Container maxWidth="lg">
<Box sx={{ mb: 4 }}>
<Typography variant="h4" fontWeight={700} gutterBottom>
안녕하세요! 👋
안녕하세요!
</Typography>
<Typography variant="body1" color="text.secondary">
오늘은 어떤 학습을 해볼까요?
</Typography>
</Box>

<Grid container spacing={3}>
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
{learningModes.map((mode) => {
const Icon = mode.icon
const isExpanded = expandedCard === mode.id
const hasChildren = mode.children && mode.children.length > 0

return (
<Grid item xs={12} sm={6} md={3} key={mode.id}>
<Box
key={mode.id}
sx={{
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
width: isExpanded ? { xs: '100%', md: '500px' } : { xs: '100%', sm: 'calc(50% - 12px)', md: 'calc(33.333% - 16px)' },
minWidth: isExpanded ? { xs: '100%', md: '500px' } : 'auto',
}}
>
<Card
onClick={() => handleCardClick(mode)}
sx={{
height: '100%',
cursor: 'pointer',
transition: 'transform 0.2s, box-shadow 0.2s',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
transform: isExpanded ? 'scale(1.02)' : 'scale(1)',
boxShadow: isExpanded ? 8 : 1,
border: isExpanded ? `2px solid ${mode.color}` : '2px solid transparent',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 4,
transform: isExpanded ? 'scale(1.02)' : 'translateY(-4px)',
boxShadow: isExpanded ? 8 : 4,
},
}}
>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Box
sx={{
width: 64,
height: 64,
borderRadius: '50%',
backgroundColor: `${mode.color}15`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 16px',
}}
>
<Icon sx={{ fontSize: 32, color: mode.color }} />
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
{/* 메인 아이콘 */}
<Box
sx={{
width: 64,
height: 64,
borderRadius: '50%',
backgroundColor: `${mode.color}15`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<Icon sx={{ fontSize: 32, color: mode.color }} />
</Box>

{/* 텍스트 */}
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6" fontWeight={600}>
{mode.title}
</Typography>
{hasChildren && (
<ChevronRightIcon
sx={{
transition: 'transform 0.3s',
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
color: mode.color,
}}
/>
)}
</Box>
<Typography variant="body2" color="text.secondary">
{mode.description}
</Typography>
</Box>
</Box>
<Typography variant="h6" fontWeight={600} gutterBottom>
{mode.title}
</Typography>
<Typography variant="body2" color="text.secondary">
{mode.description}
</Typography>

{/* 하위 카테고리 - 애니메이션으로 펼쳐짐 */}
{hasChildren && (
<Collapse in={isExpanded} timeout={400}>
<Box
sx={{
display: 'flex',
gap: 2,
mt: 3,
pt: 3,
borderTop: 1,
borderColor: 'divider',
}}
>
{mode.children.map((child, index) => {
const ChildIcon = child.icon
return (
<Box
key={child.id}
onClick={(e) => handleSubItemClick(child.path, e)}
sx={{
flex: 1,
p: 2,
borderRadius: 2,
backgroundColor: 'action.hover',
cursor: 'pointer',
transition: 'all 0.3s',
transform: isExpanded ? 'translateX(0)' : 'translateX(-20px)',
opacity: isExpanded ? 1 : 0,
transitionDelay: `${index * 100}ms`,
'&:hover': {
backgroundColor: `${mode.color}20`,
transform: 'scale(1.02)',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<ChildIcon sx={{ color: mode.color, fontSize: 24 }} />
<Box>
<Typography variant="subtitle2" fontWeight={600}>
{child.title}
</Typography>
<Typography variant="caption" color="text.secondary">
{child.description}
</Typography>
</Box>
</Box>
</Box>
)
})}
</Box>
</Collapse>
)}
</CardContent>
</Card>
</Grid>
</Box>
)
})}
</Grid>
</Box>

{/* 최근 학습 */}
<Box sx={{ mt: 6 }}>
Expand All @@ -118,15 +214,6 @@ function Dashboard() {
}

// 임시 페이지들
function InterviewPage() {
return (
<Container>
<Typography variant="h4">면접 시뮬레이션</Typography>
<Typography color="text.secondary">AI 면접관과 실전 연습</Typography>
</Container>
)
}

function OpicPage() {
return (
<Container>
Expand All @@ -136,10 +223,10 @@ function OpicPage() {
)
}

function FreetalkPage() {
function FreetalkAiPage() {
return (
<Container>
<Typography variant="h4">프리토킹</Typography>
<Typography variant="h4">프리토킹 - AI와</Typography>
<Typography color="text.secondary">AI와 자유로운 대화</Typography>
</Container>
)
Expand Down Expand Up @@ -197,9 +284,9 @@ function App() {
<Route element={<MainLayout />}>
<Route path="/" element={<Dashboard />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/interview" element={<InterviewPage />} />
<Route path="/opic" element={<OpicPage />} />
<Route path="/freetalk" element={<FreetalkPage />} />
<Route path="/freetalk/people" element={<FreetalkPeoplePage />} />
<Route path="/freetalk/ai" element={<FreetalkAiPage />} />
<Route path="/writing" element={<WritingPage />} />
<Route path="/reports" element={<ReportsPage />} />
<Route path="/settings" element={<SettingsPage />} />
Expand Down
Loading