d3ro-voice/apps/web/src/app/(app)/admin/page.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

61 lines
2.3 KiB
TypeScript

// apps/web/src/app/(app)/admin/page.tsx
// Admin 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 AdminPage(): Promise<React.ReactElement> {
const stats = await loadStats()
return (
<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>
)
}