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:
207
dashboard/backend/api_connections.py
Normal file
207
dashboard/backend/api_connections.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
dashboard/backend/api_connections.py
|
||||
Settings > Connections 탭 API — AI 서비스 연결 상태 확인/테스트
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent.parent
|
||||
CONFIG_PATH = BASE_DIR / "config" / "engine.json"
|
||||
ENV_PATH = BASE_DIR / ".env"
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
AI_SERVICES = [
|
||||
{
|
||||
"id": "claude",
|
||||
"name": "Claude (Anthropic)",
|
||||
"env_key": "ANTHROPIC_API_KEY",
|
||||
"category": "writing",
|
||||
"description": "글쓰기 엔진 — claude-opus-4-5",
|
||||
},
|
||||
{
|
||||
"id": "gemini",
|
||||
"name": "Google Gemini",
|
||||
"env_key": "GEMINI_API_KEY",
|
||||
"category": "writing",
|
||||
"description": "글쓰기 엔진 — gemini-2.0-flash",
|
||||
},
|
||||
{
|
||||
"id": "openai",
|
||||
"name": "OpenAI (GPT + DALL-E + TTS)",
|
||||
"env_key": "OPENAI_API_KEY",
|
||||
"category": "multi",
|
||||
"description": "이미지(DALL-E 3) + TTS(tts-1-hd)",
|
||||
},
|
||||
{
|
||||
"id": "elevenlabs",
|
||||
"name": "ElevenLabs TTS",
|
||||
"env_key": "ELEVENLABS_API_KEY",
|
||||
"category": "tts",
|
||||
"description": "고품질 한국어 TTS",
|
||||
},
|
||||
{
|
||||
"id": "google_tts",
|
||||
"name": "Google Cloud TTS",
|
||||
"env_key": "GOOGLE_TTS_API_KEY",
|
||||
"category": "tts",
|
||||
"description": "Google Wavenet TTS",
|
||||
},
|
||||
{
|
||||
"id": "seedance",
|
||||
"name": "Seedance AI Video",
|
||||
"env_key": "SEEDANCE_API_KEY",
|
||||
"category": "video",
|
||||
"description": "AI 영상 생성 — Seedance 2.0",
|
||||
},
|
||||
{
|
||||
"id": "runway",
|
||||
"name": "Runway Gen-3",
|
||||
"env_key": "RUNWAY_API_KEY",
|
||||
"category": "video",
|
||||
"description": "AI 영상 생성 — Gen-3 Turbo",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ApiKeyUpdate(BaseModel):
|
||||
api_key: str
|
||||
|
||||
|
||||
def _mask_key(key: str) -> str:
|
||||
if not key:
|
||||
return ""
|
||||
if len(key) <= 8:
|
||||
return "****"
|
||||
return key[:4] + "****" + key[-4:]
|
||||
|
||||
|
||||
def _get_connections():
|
||||
connections = []
|
||||
for svc in AI_SERVICES:
|
||||
key = os.getenv(svc["env_key"], "")
|
||||
connections.append({
|
||||
**svc,
|
||||
"connected": bool(key),
|
||||
"key_masked": _mask_key(key),
|
||||
})
|
||||
return connections
|
||||
|
||||
|
||||
@router.get("/connections")
|
||||
async def get_connections():
|
||||
return {"connections": _get_connections()}
|
||||
|
||||
|
||||
@router.post("/connections/{service_id}/test")
|
||||
async def test_connection(service_id: str):
|
||||
"""서비스 연결 테스트"""
|
||||
svc = next((s for s in AI_SERVICES if s["id"] == service_id), None)
|
||||
if not svc:
|
||||
raise HTTPException(status_code=404, detail="서비스를 찾을 수 없습니다.")
|
||||
|
||||
api_key = os.getenv(svc["env_key"], "")
|
||||
if not api_key:
|
||||
return {"success": False, "message": "API 키가 설정되지 않았습니다."}
|
||||
|
||||
# 간단한 연결 테스트
|
||||
try:
|
||||
if service_id == "claude":
|
||||
import anthropic
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
# 모델 목록으로 연결 테스트
|
||||
client.messages.create(
|
||||
model="claude-haiku-4-5",
|
||||
max_tokens=10,
|
||||
messages=[{"role": "user", "content": "ping"}],
|
||||
)
|
||||
return {"success": True, "message": "Claude 연결 성공"}
|
||||
|
||||
elif service_id == "openai":
|
||||
from openai import OpenAI
|
||||
client = OpenAI(api_key=api_key)
|
||||
client.models.list()
|
||||
return {"success": True, "message": "OpenAI 연결 성공"}
|
||||
|
||||
elif service_id == "gemini":
|
||||
import google.generativeai as genai
|
||||
genai.configure(api_key=api_key)
|
||||
model = genai.GenerativeModel("gemini-2.0-flash")
|
||||
model.generate_content("ping", generation_config={"max_output_tokens": 5})
|
||||
return {"success": True, "message": "Gemini 연결 성공"}
|
||||
|
||||
elif service_id in ("elevenlabs", "seedance", "runway", "google_tts"):
|
||||
import requests
|
||||
test_urls = {
|
||||
"elevenlabs": "https://api.elevenlabs.io/v1/models",
|
||||
"google_tts": f"https://texttospeech.googleapis.com/v1/voices?key={api_key}",
|
||||
"seedance": "https://api.seedance2.ai/v1/models",
|
||||
"runway": "https://api.runwayml.com/v1/organization",
|
||||
}
|
||||
headers_map = {
|
||||
"elevenlabs": {"xi-api-key": api_key},
|
||||
"runway": {"Authorization": f"Bearer {api_key}"},
|
||||
}
|
||||
url = test_urls.get(service_id, "")
|
||||
headers = headers_map.get(service_id, {})
|
||||
if url:
|
||||
resp = requests.get(url, headers=headers, timeout=10)
|
||||
if resp.status_code < 400:
|
||||
return {"success": True, "message": f"{svc['name']} 연결 성공"}
|
||||
else:
|
||||
return {"success": False, "message": f"HTTP {resp.status_code}"}
|
||||
|
||||
return {"success": True, "message": "키 존재 확인됨 (심층 테스트 미지원)"}
|
||||
|
||||
except ImportError as e:
|
||||
return {"success": False, "message": f"라이브러리 미설치: {e}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)[:200]}
|
||||
|
||||
|
||||
@router.put("/connections/{service_id}")
|
||||
async def update_api_key(service_id: str, req: ApiKeyUpdate):
|
||||
"""API 키를 .env 파일에 저장"""
|
||||
svc = next((s for s in AI_SERVICES if s["id"] == service_id), None)
|
||||
if not svc:
|
||||
raise HTTPException(status_code=404, detail="서비스를 찾을 수 없습니다.")
|
||||
|
||||
env_key = svc["env_key"]
|
||||
api_key = req.api_key.strip()
|
||||
|
||||
try:
|
||||
# .env 파일 읽기
|
||||
if ENV_PATH.exists():
|
||||
lines = ENV_PATH.read_text(encoding="utf-8").splitlines()
|
||||
else:
|
||||
lines = []
|
||||
|
||||
# 기존 키 교체 또는 추가
|
||||
updated = False
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
if line.startswith(f"{env_key}=") or line.startswith(f"{env_key} ="):
|
||||
new_lines.append(f"{env_key}={api_key}")
|
||||
updated = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not updated:
|
||||
new_lines.append(f"{env_key}={api_key}")
|
||||
|
||||
ENV_PATH.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
|
||||
|
||||
# 현재 프로세스 환경 변수도 업데이트
|
||||
os.environ[env_key] = api_key
|
||||
|
||||
return {"success": True, "message": f"{env_key} 키 저장 완료"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"키 저장 실패: {e}")
|
||||
Reference in New Issue
Block a user