d3ro-voice/apps/web/src/components/layout/sidebar.tsx
윤찬 667c09242b feat(web): 관리자 CRM 웹페이지 — admin role + 4개 페이지
- migration: profiles.role 컬럼 + admin RLS 정책
- admin-guard.ts: RSC용 admin 권한 체크
- /admin: CRM 대시보드 (총 유저, 유료 구독자, API 호출, 만료 예정)
- /admin/users: 유저 목록 (검색/필터/페이지네이션)
- /admin/users/[id]: 유저 상세 (프로필+구독+30일 사용량)
- /admin/subscriptions: 구독 목록 (상태 필터)
- /admin/usage: 사용량 집계 (7/14/30일)
- Sidebar에 Admin 네비게이션 추가
2026-04-12 20:24:26 +09:00

205 lines
5.8 KiB
TypeScript

'use client'
// apps/web/src/components/layout/sidebar.tsx
// 대시보드 좌측 사이드바
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 MeetingRoomIcon from '@mui/icons-material/MeetingRoom'
import MicIcon from '@mui/icons-material/Mic'
import ChatIcon from '@mui/icons-material/Chat'
import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'
import GroupsIcon from '@mui/icons-material/Groups'
import PaymentIcon from '@mui/icons-material/Payment'
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'
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') ?? 'Dashboard',
icon: <DashboardIcon />
},
{
key: 'meetings',
path: '/meetings',
label: t('nav.meetings') ?? 'Meetings',
icon: <MeetingRoomIcon />
},
{
key: 'record',
path: '/record',
label: t('nav.record') ?? 'Record',
icon: <MicIcon />
},
{
key: 'chat',
path: '/chat',
label: t('nav.chat') ?? 'Chat',
icon: <ChatIcon />
},
{
key: 'knowledge',
path: '/knowledge',
label: t('nav.knowledge') ?? 'Knowledge',
icon: <LibraryBooksIcon />
},
{
key: 'actions',
path: '/actions',
label: t('nav.actions') ?? 'Actions',
icon: <AutoAwesomeIcon />
},
{
key: 'teams',
path: '/teams',
label: t('nav.teams') ?? 'Teams',
icon: <GroupsIcon />
},
{
key: 'billing',
path: '/billing',
label: t('nav.billing') ?? 'Billing',
icon: <PaymentIcon />
},
{
key: 'admin',
path: '/admin',
label: 'Admin',
icon: <AdminPanelSettingsIcon />
}
]
async function handleLogout(): Promise<void> {
const supabase = getSupabaseBrowserClient()
await supabase.auth.signOut()
router.replace('/login')
}
return (
<Box
sx={{
width: 240,
minHeight: '100vh',
bgcolor: d3roPalette.bg.sidebar,
borderRight: `1px solid ${d3roPalette.border.default}`,
display: 'flex',
flexDirection: 'column'
}}
>
<Box sx={{ p: 3, borderBottom: `1px solid ${d3roPalette.border.default}` }}>
<PhosphorText variant="heading">
D3RO VOICE
</PhosphorText>
</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={{
'&.Mui-selected': {
bgcolor: d3roPalette.bg.inset,
borderLeft: `3px solid ${d3roPalette.accent.amber}`
}
}}
>
<ListItemIcon sx={{ color: active ? d3roPalette.accent.amber : d3roPalette.text.label }}>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.label}
primaryTypographyProps={{
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={{ color: d3roPalette.text.label }}>
<LogoutIcon />
</ListItemIcon>
<ListItemText
primary={t('nav.logout') ?? 'Logout'}
primaryTypographyProps={{ color: d3roPalette.text.secondary }}
/>
</ListItemButton>
</ListItem>
</List>
</Box>
)
}