import os import uuid import logging from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import List, Optional from .config import CORS_ALLOW_ORIGINS, NAVER_CLIENT_ID, ANTHROPIC_API_KEY from .db import ( init_db, get_keyword_analyses, get_keyword_analysis, delete_keyword_analysis, add_keyword_analysis, get_posts, get_post, add_post, update_post, delete_post, get_commissions, add_commission, update_commission, delete_commission, get_dashboard_stats, get_task, create_task, update_task, ) from .naver_search import analyze_keyword from .content_generator import generate_trend_brief, generate_blog_post, regenerate_blog_post from .quality_reviewer import review_post logger = logging.getLogger(__name__) app = FastAPI() _cors_origins = CORS_ALLOW_ORIGINS.split(",") app.add_middleware( CORSMiddleware, allow_origins=[o.strip() for o in _cors_origins], allow_credentials=False, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Content-Type"], ) @app.on_event("startup") def on_startup(): init_db() os.makedirs("/app/data", exist_ok=True) @app.get("/health") def health(): return {"ok": True} @app.get("/api/blog-marketing/status") def service_status(): """서비스 상태 및 설정 현황.""" return { "ok": True, "naver_api": bool(NAVER_CLIENT_ID), "claude_api": bool(ANTHROPIC_API_KEY), } # ── 키워드 분석 API ────────────────────────────────────────────────────────── class ResearchRequest(BaseModel): keyword: str def _run_research(task_id: str, keyword: str): """BackgroundTask: 네이버 검색 → 키워드 분석 → DB 저장.""" try: update_task(task_id, "processing", 30, "네이버 검색 중...") result = analyze_keyword(keyword) update_task(task_id, "processing", 80, "분석 결과 저장 중...") saved = add_keyword_analysis(result) update_task(task_id, "succeeded", 100, "분석 완료", result_id=saved["id"]) except Exception as e: logger.exception("Research failed for keyword=%s", keyword) update_task(task_id, "failed", 0, "", error=str(e)) @app.post("/api/blog-marketing/research") def start_research(req: ResearchRequest, background_tasks: BackgroundTasks): """키워드 분석 시작 (BackgroundTask). task_id 즉시 반환.""" if not NAVER_CLIENT_ID: raise HTTPException(status_code=400, detail="Naver API 키가 설정되지 않았습니다") if not req.keyword.strip(): raise HTTPException(status_code=400, detail="키워드를 입력하세요") task_id = str(uuid.uuid4()) create_task(task_id, "research", {"keyword": req.keyword.strip()}) background_tasks.add_task(_run_research, task_id, req.keyword.strip()) return {"task_id": task_id} @app.get("/api/blog-marketing/research/history") def list_research(limit: int = 30): return {"analyses": get_keyword_analyses(limit)} @app.get("/api/blog-marketing/research/{analysis_id}") def get_research(analysis_id: int): result = get_keyword_analysis(analysis_id) if not result: raise HTTPException(status_code=404, detail="Analysis not found") return result @app.delete("/api/blog-marketing/research/{analysis_id}") def remove_research(analysis_id: int): if not delete_keyword_analysis(analysis_id): raise HTTPException(status_code=404, detail="Analysis not found") return {"ok": True} # ── 작업 상태 폴링 API ────────────────────────────────────────────────────── @app.get("/api/blog-marketing/task/{task_id}") def get_task_status(task_id: str): task = get_task(task_id) if not task: raise HTTPException(status_code=404, detail="Task not found") return task # ── AI 글 생성 API ────────────────────────────────────────────────────────── class GenerateRequest(BaseModel): keyword_id: int # keyword_analyses.id def _run_generate(task_id: str, keyword_id: int): """BackgroundTask: 트렌드 브리프 → 블로그 글 생성 → DB 저장.""" try: analysis = get_keyword_analysis(keyword_id) if not analysis: update_task(task_id, "failed", 0, "", error="키워드 분석 결과를 찾을 수 없습니다") return update_task(task_id, "processing", 20, "트렌드 브리프 생성 중...") trend_brief = generate_trend_brief(analysis) update_task(task_id, "processing", 60, "블로그 글 작성 중...") post_data = generate_blog_post(analysis, trend_brief) update_task(task_id, "processing", 90, "저장 중...") saved = add_post({ "keyword_id": keyword_id, "title": post_data["title"], "body": post_data["body"], "excerpt": post_data["excerpt"], "tags": post_data["tags"], "status": "draft", "trend_brief": trend_brief, }) update_task(task_id, "succeeded", 100, "글 생성 완료", result_id=saved["id"]) except Exception as e: logger.exception("Generate failed for keyword_id=%s", keyword_id) update_task(task_id, "failed", 0, "", error=str(e)) @app.post("/api/blog-marketing/generate") def start_generate(req: GenerateRequest, background_tasks: BackgroundTasks): """AI 블로그 글 생성 시작. task_id 즉시 반환.""" if not ANTHROPIC_API_KEY: raise HTTPException(status_code=400, detail="Claude API 키가 설정되지 않았습니다") analysis = get_keyword_analysis(req.keyword_id) if not analysis: raise HTTPException(status_code=404, detail="키워드 분석 결과를 찾을 수 없습니다") task_id = str(uuid.uuid4()) create_task(task_id, "generate", {"keyword_id": req.keyword_id}) background_tasks.add_task(_run_generate, task_id, req.keyword_id) return {"task_id": task_id} # ── 품질 리뷰 API ─────────────────────────────────────────────────────────── def _run_review(task_id: str, post_id: int): """BackgroundTask: 블로그 글 품질 리뷰.""" try: post = get_post(post_id) if not post: update_task(task_id, "failed", 0, "", error="포스트를 찾을 수 없습니다") return update_task(task_id, "processing", 50, "품질 리뷰 중...") result = review_post(post["title"], post["body"]) update_post(post_id, { "review_score": result["total"], "review_detail": result, "status": "reviewed" if result["pass"] else "draft", }) update_task(task_id, "succeeded", 100, "리뷰 완료", result_id=post_id) except Exception as e: logger.exception("Review failed for post_id=%s", post_id) update_task(task_id, "failed", 0, "", error=str(e)) @app.post("/api/blog-marketing/review/{post_id}") def start_review(post_id: int, background_tasks: BackgroundTasks): """블로그 글 품질 리뷰 시작. task_id 즉시 반환.""" if not ANTHROPIC_API_KEY: raise HTTPException(status_code=400, detail="Claude API 키가 설정되지 않았습니다") post = get_post(post_id) if not post: raise HTTPException(status_code=404, detail="Post not found") task_id = str(uuid.uuid4()) create_task(task_id, "review", {"post_id": post_id}) background_tasks.add_task(_run_review, task_id, post_id) return {"task_id": task_id} # ── 재생성 API ─────────────────────────────────────────────────────────────── def _run_regenerate(task_id: str, post_id: int): """BackgroundTask: 피드백 기반 블로그 글 재생성.""" try: post = get_post(post_id) if not post: update_task(task_id, "failed", 0, "", error="포스트를 찾을 수 없습니다") return analysis = get_keyword_analysis(post["keyword_id"]) if post["keyword_id"] else {} feedback = post.get("review_detail", {}).get("feedback", "개선이 필요합니다") update_task(task_id, "processing", 50, "글 재생성 중...") result = regenerate_blog_post( analysis or {"keyword": ""}, post.get("trend_brief", ""), post["body"], feedback, ) update_post(post_id, { "title": result["title"], "body": result["body"], "excerpt": result["excerpt"], "tags": result["tags"], "status": "draft", "review_score": None, "review_detail": {}, }) update_task(task_id, "succeeded", 100, "재생성 완료", result_id=post_id) except Exception as e: logger.exception("Regenerate failed for post_id=%s", post_id) update_task(task_id, "failed", 0, "", error=str(e)) @app.post("/api/blog-marketing/regenerate/{post_id}") def start_regenerate(post_id: int, background_tasks: BackgroundTasks): """피드백 기반 블로그 글 재생성. task_id 즉시 반환.""" if not ANTHROPIC_API_KEY: raise HTTPException(status_code=400, detail="Claude API 키가 설정되지 않았습니다") post = get_post(post_id) if not post: raise HTTPException(status_code=404, detail="Post not found") task_id = str(uuid.uuid4()) create_task(task_id, "regenerate", {"post_id": post_id}) background_tasks.add_task(_run_regenerate, task_id, post_id) return {"task_id": task_id} # ── 포스트 CRUD API ────────────────────────────────────────────────────────── @app.get("/api/blog-marketing/posts") def list_posts(status: str = None, limit: int = 50): return {"posts": get_posts(status=status, limit=limit)} @app.get("/api/blog-marketing/posts/{post_id}") def get_post_detail(post_id: int): post = get_post(post_id) if not post: raise HTTPException(status_code=404, detail="Post not found") return post @app.put("/api/blog-marketing/posts/{post_id}") def edit_post(post_id: int, data: dict): result = update_post(post_id, data) if not result: raise HTTPException(status_code=404, detail="Post not found") return result @app.delete("/api/blog-marketing/posts/{post_id}") def remove_post(post_id: int): if not delete_post(post_id): raise HTTPException(status_code=404, detail="Post not found") return {"ok": True} @app.post("/api/blog-marketing/posts/{post_id}/publish") def publish_post(post_id: int, data: dict = None): """네이버 URL 등록 + 상태를 published로 변경.""" naver_url = (data or {}).get("naver_url", "") result = update_post(post_id, {"status": "published", "naver_url": naver_url}) if not result: raise HTTPException(status_code=404, detail="Post not found") return result # ── 수익 추적 API ──────────────────────────────────────────────────────────── @app.get("/api/blog-marketing/commissions") def list_commissions(post_id: int = None, limit: int = 100): return {"commissions": get_commissions(post_id=post_id, limit=limit)} @app.post("/api/blog-marketing/commissions", status_code=201) def create_commission(data: dict): return add_commission(data) @app.put("/api/blog-marketing/commissions/{comm_id}") def edit_commission(comm_id: int, data: dict): result = update_commission(comm_id, data) if not result: raise HTTPException(status_code=404, detail="Commission not found") return result @app.delete("/api/blog-marketing/commissions/{comm_id}") def remove_commission(comm_id: int): if not delete_commission(comm_id): raise HTTPException(status_code=404, detail="Commission not found") return {"ok": True} # ── 대시보드 API ───────────────────────────────────────────────────────────── @app.get("/api/blog-marketing/dashboard") def dashboard(): return get_dashboard_stats()