d3ro-voice/apps/web/src/components/layout/sidebar.tsx
Yun Chan 796916ba92
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 53s
ci / 모바일 린트·타입·Jest (push) Successful in 45s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 24s
ci / .NET API 서버 테스트 (push) Successful in 17s
deploy-site / deploy (push) Successful in 53s
ci / 워크스페이스 빌드 검증 (push) Successful in 35s
release / release-windows (push) Failing after 9m17s
portable-unsigned / portable-windows (push) Failing after 28m49s
release: ship v1.9.0 with transcript segments and preset prompts on the phone
Desktop transcript edits, auto-polish and speaker labels now reach the phone,
which draws meetings from transcript segments, and prompt edits of the four
shared preset commands are used by the phone's commands.

Bumps the product version to 1.9.0 (Android/iOS build 1090000).
2026-09-27 16:24:48 +09:00

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.9.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>
)
}