d3ro-voice/apps/admin/src/components/charts/feature-usage-chart.tsx
윤찬 dca1b90faa feat(admin): Phase V2-6 Admin CRM 고도화 — CRUD + 차트 + 결제 + 감사
- DB: audit_log 테이블(diff 포함) + subscriptions.admin_note + super_admin role
- Edge Functions 4개: admin-users, admin-subscriptions, admin-payments, admin-audit-log
- 공유 유틸: admin-auth.ts(권한 검증), audit.ts(감사로그 기록)
- Swagger UI: 독립 정적 페이지 + OpenAPI 3.0 spec
- CRUD 페이지: 구독 생성/수정/삭제, role 변경, 감사로그 목록/상세
- recharts: feature별 StackedBar + DAU Line + Top Users HorizontalBar
- 결제 이력: DB + Payple API 병행 조회
- 권한: super_admin만 위험 작업, admin은 조회 전용
- RLS: admin/super_admin IN 정책 + super_admin 쓰기 정책
- SQL RPC: admin_usage_by_feature, admin_top_users, admin_dau
2026-04-12 21:32:47 +09:00

88 lines
2.5 KiB
TypeScript

'use client'
// apps/admin/src/components/charts/feature-usage-chart.tsx
// feature별 일간 API 호출 — StackedBar
import {
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from 'recharts'
import { Box } from '@mui/material'
import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme'
interface FeatureUsageRow {
date: string
feature: string
total_count: number
unique_users: number
}
interface FeatureUsageChartProps {
data: FeatureUsageRow[]
}
const FEATURE_COLORS: Record<string, string> = {
llm_haiku: d3roPalette.accent.amber,
llm_sonnet: d3roPalette.tag.green,
llm_opus: d3roPalette.tag.purple,
stt_transcribe: d3roPalette.tag.blue,
translate: d3roPalette.tag.blue,
}
const DEFAULT_COLOR = d3roPalette.text.secondary
export function FeatureUsageChart({ data }: FeatureUsageChartProps): React.ReactElement {
// Pivot: group by date, features as columns
const features = [...new Set(data.map(d => d.feature))]
const dateMap = new Map<string, Record<string, string | number>>()
for (const row of data) {
const existing = dateMap.get(row.date) ?? { date: row.date }
existing[row.feature] = row.total_count
dateMap.set(row.date, existing)
}
const chartData = [...dateMap.values()].sort((a, b) =>
String(a.date).localeCompare(String(b.date))
)
return (
<Box sx={{ width: '100%', height: 300 }}>
<ResponsiveContainer>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke={d3roPalette.border.subtle} />
<XAxis
dataKey="date"
tick={{ fill: d3roPalette.text.muted, fontFamily: d3roFontMono, fontSize: 10 }}
tickFormatter={(val: string) => val.slice(5)}
/>
<YAxis tick={{ fill: d3roPalette.text.muted, fontFamily: d3roFontMono, fontSize: 10 }} />
<Tooltip
contentStyle={{
background: d3roPalette.bg.card,
border: `1px solid ${d3roPalette.border.default}`,
fontFamily: d3roFontMono,
fontSize: 11,
color: d3roPalette.text.primary,
}}
/>
<Legend wrapperStyle={{ fontFamily: d3roFontMono, fontSize: 11 }} />
{features.map((feature) => (
<Bar
key={feature}
dataKey={feature}
stackId="a"
fill={FEATURE_COLORS[feature] ?? DEFAULT_COLOR}
/>
))}
</BarChart>
</ResponsiveContainer>
</Box>
)
}