refactor(admin): CRM을 apps/admin 독립 프로젝트로 분리

- apps/web에서 admin 라우트/가드/sidebar 링크 제거
- apps/admin: 독립 Next.js 앱 (포트 3001)
  - 자체 login/unauthorized/auth callback
  - admin-sidebar: Overview/Users/Subscriptions/Usage
  - requireAdmin() 가드: profile.role='admin' 체크
- monorepo workspace에 apps/admin 등록
This commit is contained in:
윤찬 2026-04-12 20:36:24 +09:00
parent daed9f90d5
commit 46673ee941
23 changed files with 536 additions and 343 deletions

View file

@ -0,0 +1,96 @@
'use client'
// apps/admin/src/components/admin-sidebar.tsx
// Admin CRM 사이드바
import { usePathname, useRouter } from 'next/navigation'
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Button } from '@mui/material'
import DashboardIcon from '@mui/icons-material/Dashboard'
import PeopleIcon from '@mui/icons-material/People'
import SubscriptionsIcon from '@mui/icons-material/Subscriptions'
import BarChartIcon from '@mui/icons-material/BarChart'
import LogoutIcon from '@mui/icons-material/Logout'
import { PhosphorText } from '@d3ro/ui/components/ds'
import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme'
import { getSupabaseBrowserClient } from '@/lib/supabase-browser'
const NAV_ITEMS = [
{ key: 'overview', path: '/', label: 'Overview', icon: <DashboardIcon /> },
{ key: 'users', path: '/users', label: 'Users', icon: <PeopleIcon /> },
{ key: 'subscriptions', path: '/subscriptions', label: 'Subscriptions', icon: <SubscriptionsIcon /> },
{ key: 'usage', path: '/usage', label: 'Usage', icon: <BarChartIcon /> },
]
export function AdminSidebar(): React.ReactElement {
const pathname = usePathname()
const router = useRouter()
const handleLogout = async (): Promise<void> => {
const supabase = getSupabaseBrowserClient()
await supabase.auth.signOut()
router.replace('/login')
}
return (
<Box sx={{
width: 220,
minHeight: '100vh',
bgcolor: d3roPalette.bg.sidebar,
borderRight: `1px solid ${d3roPalette.border.default}`,
display: 'flex',
flexDirection: 'column',
}}>
<Box sx={{ p: 2.5, borderBottom: `1px solid ${d3roPalette.border.default}` }}>
<PhosphorText variant="heading">D3RO ADMIN</PhosphorText>
<PhosphorText variant="dim" sx={{ display: 'block', mt: 0.5, fontSize: 10 }}>CRM Console</PhosphorText>
</Box>
<List sx={{ flex: 1, py: 1 }}>
{NAV_ITEMS.map((item) => {
const active = item.path === '/'
? pathname === '/'
: pathname.startsWith(item.path)
return (
<ListItem key={item.key} disablePadding>
<ListItemButton
selected={active}
onClick={() => router.push(item.path)}
sx={{
fontFamily: d3roFontMono,
'&.Mui-selected': {
bgcolor: d3roPalette.bg.inset,
borderLeft: `3px solid ${d3roPalette.accent.amber}`,
},
}}
>
<ListItemIcon sx={{ minWidth: 36, color: active ? d3roPalette.accent.amber : d3roPalette.text.inactive }}>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.label}
primaryTypographyProps={{
fontFamily: d3roFontMono,
fontSize: 13,
color: active ? d3roPalette.text.primary : d3roPalette.text.secondary,
}}
/>
</ListItemButton>
</ListItem>
)
})}
</List>
<Box sx={{ p: 2, borderTop: `1px solid ${d3roPalette.border.default}` }}>
<Button
fullWidth
size="small"
startIcon={<LogoutIcon />}
onClick={() => void handleLogout()}
sx={{ fontFamily: d3roFontMono, fontSize: 12, color: d3roPalette.text.secondary }}
>
Logout
</Button>
</Box>
</Box>
)
}