Version 1.1.0 was published on 2026-09-15, so its tag is closed. Every commit since then, the update-feed rework, dictionary import and export, the extra push transports, and entitlement gating, needs its own immutable version. Product version, Android version code, iOS build number, and all package and store surfaces move to 1.2.0 / 1020001, with Korean and English store notes for the new version code. The download centers read that version and its release date from one place instead of repeating them inline.
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.2.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>
|
|
)
|
|
}
|