d3ro-voice/apps/admin/src/lib/console-theme.ts
Yun Chan 5a34f66981 feat(admin): 예전/최신 어드민 통합 — 실데이터 복원 + 인증 아키텍처 정리
예전 배포본(HEAD)의 상세 기능을 새 아키텍처(인증=.NET 백엔드, 데이터=Supabase)
위에 실데이터로 복원. 예전 HEAD는 서명 쿠키를 거부하고 위조 쿠키는 통과시키는
인증 결함이 있었고, 삭제된 페이지 다수는 백엔드 호출 0인 하드코딩 목업이었음.

인증/세션
- 로그인 이메일 전용화(username 폐지), 에러 키별 안내 메시지
- ADMIN_COOKIE_SECURE 옵션: TLS 없는 LAN HTTP 배포에서 Secure 쿠키 유실로
  로그인이 유지되지 않던 문제 해결 (login/logout route, admin-session, compose, .env.example)
- Supabase 미설정 시 우아한 저하: isSupabaseAdminConfigured + UnavailableAdminPanel

기능 복원 (실데이터)
- Release Hub: Forgejo API 실데이터(다운로드 수/SHA-256 체크섬/릴리스 이력)
- Ad Monetization: 데스크톱 미디에이션 10개 어댑터 로스터(fail-closed) + ad_reward_claims 통계
- License Issuer: 서버사이드 Ed25519 서명(/api/admin/license, super_admin 전용),
  개인키는 ADMIN_LICENSE_PRIVATE_KEY env로만, 발급 감사를 .NET AdminAuditEntries에 기록
- Service Models: STT 7종/LLM 5종 프리셋 드롭다운 + 자동채움
- 대시보드 ARR/MRR KPI: Supabase 구독 실집계(티어 월단가 기반)
- 사용자 상세 티어별 기능 배지(pro_plus 조건부)

.NET
- SuperAdminOnly 정책 추가, /api/admin/license-audit 엔드포인트, LicenseAuditDto
2026-08-23 23:38:08 +09:00

204 lines
6.1 KiB
TypeScript

// apps/admin/src/lib/console-theme.ts
// D3RO Voice Admin CRM — "Midnight Glass v2" Design Tokens & UI Helpers
// Fully aligned with @d3ro/ui/theme SSOT & High-End Awwwards/Linear aesthetic
import { d3roFontSans, d3roFontMono } from '@d3ro/ui/theme'
export const C = {
// backgrounds
base: '#070b16',
app: '#0a0e1c',
card: '#111a30',
cardHover: '#152039',
elevated: '#1a2540',
input: '#0d1526',
sidebar: '#0b101f',
inset: '#0a111f',
chassis: '#111a30',
// borders
border: 'rgba(148, 180, 255, 0.08)',
borderHl: 'rgba(148, 180, 255, 0.16)',
borderStrong: 'rgba(148, 180, 255, 0.24)',
// text
dim: '#67789e',
text: '#93a4c8',
bright: '#eef2fb',
muted: '#3c4763',
// accents & gradients
accent: '#3b82f6',
accentLight: '#60a5fa',
accentDark: '#1d4ed8',
cyan: '#06b6d4',
cyanLight: '#22d3ee',
purple: '#8b5cf6',
purple400: '#a78bfa',
green: '#10b981',
green400: '#34d399',
orange: '#f59e0b',
orange400: '#fbbf24',
red: '#ef4444',
red400: '#f87171',
} as const
export const FONT_SANS = d3roFontSans
export const FONT_MONO = d3roFontMono
/** High-End Double-Bezel Glass Panel Style */
export const panelSx = {
position: 'relative' as const,
bgcolor: 'rgba(17, 26, 48, 0.65)',
backdropFilter: 'blur(24px)',
border: `1px solid ${C.border}`,
borderRadius: '20px',
overflow: 'hidden',
boxShadow: '0 16px 40px rgba(3, 7, 18, 0.5), inset 0 1px 0 rgba(148, 180, 255, 0.08)',
transition: 'border-color 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s ease, transform 0.25s ease',
'&:hover': {
borderColor: C.borderHl,
boxShadow: '0 20px 48px rgba(3, 7, 18, 0.6), inset 0 1px 0 rgba(148, 180, 255, 0.15)',
},
}
/** Inner Glass Core Style (Double-Bezel nested architecture) */
export const innerCoreSx = {
position: 'relative' as const,
borderRadius: '14px',
bgcolor: 'rgba(13, 21, 38, 0.75)',
backdropFilter: 'blur(16px)',
border: `1px solid ${C.border}`,
p: 2.5,
boxShadow: 'inset 0 2px 6px rgba(3, 7, 18, 0.5)',
}
/** Interactive Table Style */
export const tableSx = {
width: '100%',
borderCollapse: 'separate' as const,
borderSpacing: '0 6px',
fontFamily: FONT_SANS,
fontSize: '13px',
'& th': {
px: 2,
pb: 1.5,
fontWeight: 500,
fontSize: '11px',
textTransform: 'uppercase' as const,
letterSpacing: '0.08em',
color: C.dim,
textAlign: 'left' as const,
borderBottom: `1px solid ${C.borderHl}`,
},
'& td': {
px: 2,
py: 1.75,
textAlign: 'left' as const,
color: C.text,
bgcolor: 'rgba(17, 26, 48, 0.45)',
borderTop: `1px solid ${C.border}`,
borderBottom: `1px solid ${C.border}`,
transition: 'all 0.15s cubic-bezier(0.16, 1, 0.3, 1)',
'&:first-of-type': {
borderLeft: `1px solid ${C.border}`,
borderTopLeftRadius: '10px',
borderBottomLeftRadius: '10px',
},
'&:last-of-type': {
borderRight: `1px solid ${C.border}`,
borderTopRightRadius: '10px',
borderBottomRightRadius: '10px',
},
},
'& tr:hover td': {
bgcolor: 'rgba(26, 38, 68, 0.75)',
borderColor: C.borderHl,
color: C.bright,
},
}
/** Filter Pill Button Style */
export const filterBtnSx = (active: boolean) => ({
px: 2,
py: 0.75,
borderRadius: '999px',
fontFamily: FONT_SANS,
fontSize: '11px',
fontWeight: 500,
letterSpacing: '0.04em',
textTransform: 'none' as const,
cursor: 'pointer',
border: `1px solid ${active ? 'rgba(59, 130, 246, 0.4)' : C.border}`,
bgcolor: active ? 'rgba(59, 130, 246, 0.16)' : 'rgba(17, 26, 48, 0.5)',
color: active ? C.bright : C.dim,
boxShadow: active ? '0 0 16px rgba(59, 130, 246, 0.25)' : 'none',
backdropFilter: 'blur(12px)',
'&:hover': {
bgcolor: active ? 'rgba(59, 130, 246, 0.22)' : 'rgba(26, 38, 68, 0.8)',
borderColor: active ? C.accentLight : C.borderHl,
color: C.bright,
transform: 'translateY(-1px)',
},
'&:active': {
transform: 'translateY(0) scale(0.98)',
},
transition: 'all 0.15s cubic-bezier(0.16, 1, 0.3, 1)',
})
/** Semantic Status Badge Style */
export function statusBadgeSx(variant: 'green' | 'red' | 'orange' | 'blue' | 'purple' | 'cyan') {
const colorMap = {
green: { bg: 'rgba(16, 185, 129, 0.12)', fg: C.green400, border: 'rgba(16, 185, 129, 0.3)', glow: '0 0 12px rgba(16, 185, 129, 0.2)' },
red: { bg: 'rgba(239, 68, 68, 0.12)', fg: C.red400, border: 'rgba(239, 68, 68, 0.3)', glow: '0 0 12px rgba(239, 68, 68, 0.2)' },
orange: { bg: 'rgba(245, 158, 11, 0.12)', fg: C.orange400, border: 'rgba(245, 158, 11, 0.3)', glow: '0 0 12px rgba(245, 158, 11, 0.2)' },
blue: { bg: 'rgba(59, 130, 246, 0.12)', fg: C.accentLight, border: 'rgba(59, 130, 246, 0.3)', glow: '0 0 12px rgba(59, 130, 246, 0.2)' },
purple: { bg: 'rgba(139, 92, 246, 0.12)', fg: C.purple400, border: 'rgba(139, 92, 246, 0.3)', glow: '0 0 12px rgba(139, 92, 246, 0.2)' },
cyan: { bg: 'rgba(6, 182, 212, 0.12)', fg: C.cyanLight, border: 'rgba(6, 182, 212, 0.3)', glow: '0 0 12px rgba(6, 182, 212, 0.2)' },
}
const c = colorMap[variant]
return {
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.4,
borderRadius: '999px',
fontSize: '11px',
fontFamily: FONT_SANS,
fontWeight: 500,
letterSpacing: '0.04em',
bgcolor: c.bg,
color: c.fg,
border: `1px solid ${c.border}`,
boxShadow: c.glow,
backdropFilter: 'blur(8px)',
}
}
/** Primary Action Button Style with Gradient & Glow */
export const primaryButtonSx = {
background: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 55%, #60a5fa 100%)',
color: '#ffffff',
fontFamily: FONT_SANS,
fontSize: '13px',
fontWeight: 500,
borderRadius: '10px',
px: 2.5,
py: 1,
boxShadow: '0 0 0 1px rgba(59,130,246,0.35), 0 8px 24px rgba(37,99,235,0.35)',
textTransform: 'none' as const,
transition: 'all 0.2s cubic-bezier(0.16, 1, 0.3, 1)',
'&:hover': {
filter: 'brightness(1.12)',
boxShadow: '0 0 0 1px rgba(96,165,250,0.5), 0 12px 32px rgba(59,130,246,0.5)',
transform: 'translateY(-1px)',
},
'&:active': {
transform: 'translateY(0) scale(0.98)',
},
}
// Backward compatibility alias for FONT
export const FONT = FONT_MONO