- 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
93 lines
2.4 KiB
TypeScript
93 lines
2.4 KiB
TypeScript
'use client'
|
|
|
|
// apps/admin/src/components/memo-dialog.tsx
|
|
// 메모 입력 다이얼로그 — 감사로그 기록 시 필수
|
|
|
|
import { useState } from 'react'
|
|
import {
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
TextField,
|
|
Button,
|
|
} from '@mui/material'
|
|
import { PhosphorText } from '@d3ro/ui/components/ds'
|
|
import { d3roPalette, d3roFontMono } from '@d3ro/ui/theme'
|
|
|
|
interface MemoDialogProps {
|
|
open: boolean
|
|
title: string
|
|
description?: string
|
|
onConfirm: (memo: string) => void
|
|
onCancel: () => void
|
|
loading?: boolean
|
|
}
|
|
|
|
export function MemoDialog({ open, title, description, onConfirm, onCancel, loading }: MemoDialogProps): React.ReactElement {
|
|
const [memo, setMemo] = useState('')
|
|
|
|
const handleConfirm = (): void => {
|
|
if (memo.trim()) {
|
|
onConfirm(memo.trim())
|
|
setMemo('')
|
|
}
|
|
}
|
|
|
|
const handleCancel = (): void => {
|
|
setMemo('')
|
|
onCancel()
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onClose={handleCancel}
|
|
maxWidth="sm"
|
|
fullWidth
|
|
PaperProps={{ sx: { bgcolor: d3roPalette.bg.card, color: d3roPalette.text.primary } }}
|
|
>
|
|
<DialogTitle sx={{ fontFamily: d3roFontMono }}>
|
|
<PhosphorText variant="heading">{title}</PhosphorText>
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
{description && (
|
|
<PhosphorText variant="dim" sx={{ display: 'block', mb: 2 }}>
|
|
{description}
|
|
</PhosphorText>
|
|
)}
|
|
<TextField
|
|
autoFocus
|
|
fullWidth
|
|
multiline
|
|
rows={3}
|
|
placeholder="Reason for this action (required)..."
|
|
value={memo}
|
|
onChange={(e) => setMemo(e.target.value)}
|
|
sx={{
|
|
mt: 1,
|
|
'& .MuiInputBase-root': {
|
|
fontFamily: d3roFontMono,
|
|
fontSize: 13,
|
|
color: d3roPalette.text.primary,
|
|
bgcolor: d3roPalette.bg.inset,
|
|
},
|
|
}}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
|
<Button onClick={handleCancel} sx={{ fontFamily: d3roFontMono, color: d3roPalette.text.secondary }}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleConfirm}
|
|
disabled={!memo.trim() || loading}
|
|
variant="contained"
|
|
sx={{ fontFamily: d3roFontMono }}
|
|
>
|
|
{loading ? 'Processing...' : 'Confirm'}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
)
|
|
}
|