d3ro-voice/apps/admin/src/components/memo-dialog.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

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>
)
}