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:
parent
daed9f90d5
commit
46673ee941
23 changed files with 536 additions and 343 deletions
64
apps/admin/src/app/(admin)/page.tsx
Normal file
64
apps/admin/src/app/(admin)/page.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// apps/admin/src/app/(admin)/page.tsx
|
||||
// CRM 대시보드 — 요약 카드 4개
|
||||
|
||||
import { Box, Grid } from '@mui/material'
|
||||
import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
|
||||
import { d3roPalette } from '@d3ro/ui/theme'
|
||||
import { getSupabaseServerClient } from '@/lib/supabase-server'
|
||||
|
||||
interface StatCard {
|
||||
label: string
|
||||
value: string | number
|
||||
color?: string
|
||||
}
|
||||
|
||||
async function loadStats(): Promise<StatCard[]> {
|
||||
const supabase = await getSupabaseServerClient()
|
||||
|
||||
const [profilesRes, paidRes, usageRes, expiringRes] = await Promise.all([
|
||||
supabase.from('profiles').select('id', { count: 'exact', head: true }),
|
||||
supabase.from('subscriptions').select('id', { count: 'exact', head: true })
|
||||
.neq('tier', 'free').eq('status', 'active'),
|
||||
supabase.from('daily_usage').select('count')
|
||||
.eq('date', new Date().toISOString().split('T')[0]),
|
||||
supabase.from('subscriptions').select('id', { count: 'exact', head: true })
|
||||
.eq('status', 'active').eq('payment_provider', 'payple')
|
||||
.lte('current_period_end', new Date(Date.now() + 7 * 86400000).toISOString()),
|
||||
])
|
||||
|
||||
const todayUsage = (usageRes.data as Array<{ count: number }> | null)
|
||||
?.reduce((sum, r) => sum + (r.count ?? 0), 0) ?? 0
|
||||
|
||||
return [
|
||||
{ label: 'TOTAL USERS', value: profilesRes.count ?? 0 },
|
||||
{ label: 'PAID SUBSCRIBERS', value: paidRes.count ?? 0, color: d3roPalette.tag.green },
|
||||
{ label: 'TODAY API CALLS', value: todayUsage, color: d3roPalette.accent.amber },
|
||||
{ label: 'EXPIRING (7D)', value: expiringRes.count ?? 0, color: d3roPalette.tag.red },
|
||||
]
|
||||
}
|
||||
|
||||
export default async function AdminOverviewPage(): Promise<React.ReactElement> {
|
||||
const stats = await loadStats()
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<PhosphorText variant="title" sx={{ mb: 3 }}>OVERVIEW</PhosphorText>
|
||||
<Grid container spacing={2}>
|
||||
{stats.map((stat) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }} key={stat.label}>
|
||||
<MetalCard>
|
||||
<Box sx={{ textAlign: 'center', py: 2 }}>
|
||||
<PhosphorText variant="label" sx={{ mb: 1, display: 'block', color: d3roPalette.text.label }}>
|
||||
{stat.label}
|
||||
</PhosphorText>
|
||||
<PhosphorText variant="hero" sx={{ color: stat.color ?? d3roPalette.text.primary }}>
|
||||
{stat.value}
|
||||
</PhosphorText>
|
||||
</Box>
|
||||
</MetalCard>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue