feat: v3.1 대시보드 추가 (React + FastAPI)

Media Engine Control Panel — 6탭 웹 대시보드

[백엔드] FastAPI (dashboard/backend/)
- server.py: 포트 8080, CORS, React SPA 서빙
- api_overview.py: KPI 카드 + 파이프라인 상태 + 활동 로그
- api_content.py: 칸반 보드 + 승인/거부 + 수동 트리거
- api_analytics.py: 방문자 추이 + 플랫폼/코너별 성과
- api_novels.py: 소설 목록/생성/에피소드 관리
- api_settings.py: engine.json CRUD
- api_connections.py: AI 서비스 연결 관리 + 키 저장
- api_tools.py: 기능별 AI 도구 선택
- api_cost.py: 구독 현황 + API 사용량 추적
- api_logs.py: 시스템 로그 필터/검색

[프론트엔드] React + Vite + Tailwind + Recharts (dashboard/frontend/)
- Overview: KPI 카드 + 파이프라인 + 코너별 바차트 + 활동 로그
- Content: 4열 칸반 보드 + 상세 모달 + 승인/거부
- Analytics: LineChart 방문자 추이 + 플랫폼별 성과
- Novel: 소설 목록 + 에피소드 테이블 + 새 소설 생성 폼
- Settings: 5개 서브탭 (AI연결/도구선택/배포채널/품질/비용관리)
- Logs: 필터/검색 시스템 로그 뷰어

[디자인] CNN 다크+골드 테마
- 배경 #0a0a0d + 액센트 #c8a84e
- 모바일 반응형 (Tailscale 외부 접속 대응)

[실행]
- dashboard/start.bat 더블클릭 → http://localhost:8080

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sinmb79
2026-03-26 13:17:53 +09:00
parent 8a7a122bb3
commit 213f57b52d
35 changed files with 3971 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import React, { useState } from 'react'
import { LayoutDashboard, FileText, BarChart2, BookOpen, Settings, ScrollText } from 'lucide-react'
import Overview from './pages/Overview.jsx'
import Content from './pages/Content.jsx'
import Analytics from './pages/Analytics.jsx'
import Novel from './pages/Novel.jsx'
import SettingsPage from './pages/Settings.jsx'
import Logs from './pages/Logs.jsx'
const TABS = [
{ id: 'overview', label: '개요', icon: LayoutDashboard, component: Overview },
{ id: 'content', label: '콘텐츠', icon: FileText, component: Content },
{ id: 'analytics', label: '분석', icon: BarChart2, component: Analytics },
{ id: 'novel', label: '소설', icon: BookOpen, component: Novel },
{ id: 'settings', label: '설정', icon: Settings, component: SettingsPage },
{ id: 'logs', label: '로그', icon: ScrollText, component: Logs },
]
export default function App() {
const [activeTab, setActiveTab] = useState('overview')
const [systemStatus, setSystemStatus] = useState('ok') // ok | warn | error
const ActiveComponent = TABS.find(t => t.id === activeTab)?.component || Overview
return (
<div className="flex flex-col h-screen bg-bg text-text overflow-hidden">
{/* 헤더 */}
<header className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-border bg-card flex-shrink-0">
<div className="flex items-center gap-3">
<span className="text-accent font-bold text-base md:text-lg tracking-tight">
The 4th Path
</span>
<span className="hidden md:inline text-subtext text-xs">· Control Panel</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${systemStatus === 'ok' ? 'bg-success' : systemStatus === 'warn' ? 'bg-warning' : 'bg-error'}`}></span>
<span className="text-xs text-subtext hidden sm:inline">
{systemStatus === 'ok' ? 'System OK' : systemStatus === 'warn' ? '경고' : '오류'}
</span>
</div>
</header>
{/* 탭 네비게이션 */}
<nav className="flex border-b border-border bg-card flex-shrink-0 overflow-x-auto">
{TABS.map(tab => {
const Icon = tab.icon
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-1.5 px-3 md:px-5 py-3 text-xs md:text-sm font-medium whitespace-nowrap transition-colors border-b-2 ${
isActive
? 'border-accent text-accent'
: 'border-transparent text-subtext hover:text-text'
}`}
>
<Icon size={15} />
<span className="hidden sm:inline">{tab.label}</span>
</button>
)
})}
</nav>
{/* 메인 컨텐츠 */}
<main className="flex-1 overflow-y-auto">
<ActiveComponent />
</main>
</div>
)
}