219 lines
6.6 KiB
TypeScript
219 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
// apps/web/src/components/layout/sidebar.tsx
|
|
// D3RO VOICE 대시보드 좌측 사이드바 — 정본 네비게이션
|
|
|
|
import { usePathname, useRouter } from 'next/navigation'
|
|
import {
|
|
Box,
|
|
FormControl,
|
|
InputLabel,
|
|
List,
|
|
ListItem,
|
|
ListItemButton,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
MenuItem,
|
|
Select,
|
|
type SelectChangeEvent
|
|
} from '@mui/material'
|
|
import DashboardIcon from '@mui/icons-material/Dashboard'
|
|
import MicIcon from '@mui/icons-material/Mic'
|
|
import HistoryIcon from '@mui/icons-material/History'
|
|
import ChatIcon from '@mui/icons-material/Chat'
|
|
import BookIcon from '@mui/icons-material/Book'
|
|
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'
|
|
import PaymentIcon from '@mui/icons-material/Payment'
|
|
import LogoutIcon from '@mui/icons-material/Logout'
|
|
import PaletteIcon from '@mui/icons-material/Palette'
|
|
import type { ThemeMode } from '@d3ro/core/types'
|
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
|
import { d3roPalette } from '@d3ro/ui/theme'
|
|
import { useI18n } from '@d3ro/i18n'
|
|
import { getSupabaseBrowserClient } from '@/lib/supabase-browser'
|
|
import { AVAILABLE_MODES, useThemeMode } from '@/components/providers/theme-mode-context'
|
|
|
|
interface NavItem {
|
|
key: string
|
|
path: string
|
|
label: string
|
|
icon: React.ReactElement
|
|
}
|
|
|
|
export function Sidebar(): React.ReactElement {
|
|
const router = useRouter()
|
|
const pathname = usePathname()
|
|
const { t } = useI18n()
|
|
const { mode, setMode } = useThemeMode()
|
|
|
|
const handleThemeChange = (event: SelectChangeEvent<ThemeMode>): void => {
|
|
setMode(event.target.value as ThemeMode)
|
|
}
|
|
|
|
const items: NavItem[] = [
|
|
{
|
|
key: 'dashboard',
|
|
path: '/dashboard',
|
|
label: t('nav.dashboard') ?? '대시보드',
|
|
icon: <DashboardIcon />
|
|
},
|
|
{
|
|
key: 'record',
|
|
path: '/record',
|
|
label: t('nav.record') ?? '실시간 녹음',
|
|
icon: <MicIcon />
|
|
},
|
|
{
|
|
key: 'history',
|
|
path: '/history',
|
|
label: t('nav.history') ?? '전사 히스토리',
|
|
icon: <HistoryIcon />
|
|
},
|
|
{
|
|
key: 'chat',
|
|
path: '/chat',
|
|
label: t('nav.chat') ?? '음성 대화',
|
|
icon: <ChatIcon />
|
|
},
|
|
{
|
|
key: 'dictionary',
|
|
path: '/dictionary',
|
|
label: t('nav.dictionary') ?? '커스텀 사전',
|
|
icon: <BookIcon />
|
|
},
|
|
{
|
|
key: 'commands',
|
|
path: '/commands',
|
|
label: t('nav.commands') ?? '명령어',
|
|
icon: <AutoAwesomeIcon />
|
|
},
|
|
{
|
|
key: 'billing',
|
|
path: '/billing',
|
|
label: t('nav.billing') ?? '구독 & 업그레이드',
|
|
icon: <PaymentIcon />
|
|
}
|
|
]
|
|
|
|
async function handleLogout(): Promise<void> {
|
|
const supabase = getSupabaseBrowserClient()
|
|
await supabase.auth.signOut()
|
|
router.replace('/login')
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
width: 240,
|
|
minHeight: '100dvh',
|
|
bgcolor: d3roPalette.bg.sidebar,
|
|
borderRight: `1px solid ${d3roPalette.border.default}`,
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
<Box sx={{ p: 3, borderBottom: `1px solid ${d3roPalette.border.default}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
<Box sx={{ display: 'flex', gap: 0.75 }}>
|
|
<Box
|
|
sx={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: '50%',
|
|
bgcolor: 'var(--d3-accent-main)',
|
|
boxShadow: '0 0 8px var(--d3-accent-main)'
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: '50%',
|
|
bgcolor: 'var(--d3-tag-green)',
|
|
boxShadow: '0 0 6px var(--d3-tag-green)'
|
|
}}
|
|
/>
|
|
</Box>
|
|
<PhosphorText variant="heading" sx={{ letterSpacing: '0.1em' }}>
|
|
D3RO VOICE
|
|
</PhosphorText>
|
|
</Box>
|
|
<Box sx={{ fontSize: '10px', fontFamily: 'ui-monospace, monospace', color: d3roPalette.text.muted }}>
|
|
v1.1.0
|
|
</Box>
|
|
</Box>
|
|
|
|
<List sx={{ flex: 1, py: 2 }}>
|
|
{items.map((item) => {
|
|
const active = pathname === item.path || pathname.startsWith(`${item.path}/`)
|
|
return (
|
|
<ListItem key={item.key} disablePadding>
|
|
<ListItemButton
|
|
selected={active}
|
|
onClick={() => router.push(item.path)}
|
|
sx={{
|
|
py: 1.25,
|
|
'&.Mui-selected': {
|
|
bgcolor: d3roPalette.bg.inset,
|
|
borderLeft: `3px solid ${d3roPalette.accent.main}`
|
|
}
|
|
}}
|
|
>
|
|
<ListItemIcon sx={{ minWidth: 36, color: active ? d3roPalette.accent.main : d3roPalette.text.label }}>
|
|
{item.icon}
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={item.label}
|
|
primaryTypographyProps={{
|
|
fontSize: 13,
|
|
fontWeight: active ? 600 : 400,
|
|
color: active ? d3roPalette.text.primary : d3roPalette.text.secondary
|
|
}}
|
|
/>
|
|
</ListItemButton>
|
|
</ListItem>
|
|
)
|
|
})}
|
|
</List>
|
|
|
|
<Box sx={{ px: 2, py: 1.5, borderTop: `1px solid ${d3roPalette.border.default}` }}>
|
|
<FormControl fullWidth size="small">
|
|
<InputLabel
|
|
id="theme-mode-label"
|
|
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
|
>
|
|
<PaletteIcon fontSize="inherit" />
|
|
{t('theme.label') ?? 'Theme'}
|
|
</InputLabel>
|
|
<Select<ThemeMode>
|
|
labelId="theme-mode-label"
|
|
id="theme-mode-select"
|
|
value={mode}
|
|
onChange={handleThemeChange}
|
|
label={t('theme.label') ?? 'Theme'}
|
|
>
|
|
{AVAILABLE_MODES.map((m) => (
|
|
<MenuItem key={m} value={m}>
|
|
{t(`theme.${m}`) ?? m}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
|
|
<List sx={{ borderTop: `1px solid ${d3roPalette.border.default}` }}>
|
|
<ListItem disablePadding>
|
|
<ListItemButton onClick={() => void handleLogout()}>
|
|
<ListItemIcon sx={{ minWidth: 36, color: d3roPalette.text.label }}>
|
|
<LogoutIcon />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={t('nav.logout') ?? '로그아웃'}
|
|
primaryTypographyProps={{ fontSize: 13, color: d3roPalette.text.secondary }}
|
|
/>
|
|
</ListItemButton>
|
|
</ListItem>
|
|
</List>
|
|
</Box>
|
|
)
|
|
}
|