- 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
88 lines
2.5 KiB
TypeScript
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>
|
|
)
|
|
}
|