blog-lab: 블로그 마케팅 수익화 서비스 추가

네이버 검색 API 키워드 분석 + Claude AI 글 생성 + 품질 리뷰 + 수익 추적
- blog-lab/ 서비스 전체 (FastAPI, SQLite 5테이블, 18 엔드포인트)
- docker-compose.yml: blog-lab 서비스 (port 18700)
- nginx: /api/blog-marketing/ 라우팅 추가
- .env.example: NAVER_CLIENT_ID/SECRET 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-05 19:59:25 +09:00
parent bb76e62774
commit ba33e00ce3
14 changed files with 1460 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
"""Claude API 기반 콘텐츠 생성 — 트렌드 브리프 + 블로그 글 작성."""
import json
import logging
from typing import Any, Dict, Optional
import anthropic
from .config import ANTHROPIC_API_KEY, CLAUDE_MODEL
from .db import get_template
logger = logging.getLogger(__name__)
_client: Optional[anthropic.Anthropic] = None
def _get_client() -> anthropic.Anthropic:
global _client
if _client is None:
_client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
return _client
def _call_claude(prompt: str, max_tokens: int = 4096) -> str:
"""Claude API 호출. 단일 user 메시지."""
client = _get_client()
resp = client.messages.create(
model=CLAUDE_MODEL,
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
return resp.content[0].text
def generate_trend_brief(analysis: Dict[str, Any]) -> str:
"""키워드 분석 데이터를 바탕으로 트렌드 브리프 생성."""
template = get_template("trend_brief")
if not template:
raise RuntimeError("trend_brief 템플릿이 없습니다")
top_blogs_text = "\n".join(
f"- {b.get('title', '')}" for b in analysis.get("top_blogs", [])
) or "없음"
top_products_text = "\n".join(
f"- {p.get('title', '')} ({p.get('lprice', '?')}원, {p.get('mallName', '')})"
for p in analysis.get("top_products", [])
) or "없음"
prompt = template.format(
keyword=analysis.get("keyword", ""),
competition=analysis.get("competition", 0),
opportunity=analysis.get("opportunity", 0),
top_blogs=top_blogs_text,
top_products=top_products_text,
)
return _call_claude(prompt)
def generate_blog_post(analysis: Dict[str, Any], trend_brief: str) -> Dict[str, str]:
"""트렌드 브리프를 바탕으로 블로그 글 작성.
Returns:
{"title": str, "body": str, "excerpt": str, "tags": [...]}
"""
template = get_template("blog_write")
if not template:
raise RuntimeError("blog_write 템플릿이 없습니다")
top_products_text = "\n".join(
f"- {p.get('title', '')} ({p.get('lprice', '?')}원, {p.get('mallName', '')})"
for p in analysis.get("top_products", [])
) or "없음"
prompt = template.format(
keyword=analysis.get("keyword", ""),
trend_brief=trend_brief,
top_products=top_products_text,
)
# 구조화된 응답을 위한 추가 지시
prompt += (
"\n\n---\n"
"응답은 반드시 아래 JSON 형식으로 해주세요 (JSON만 출력, 다른 텍스트 없이):\n"
'{"title": "블로그 제목", "body": "HTML 본문", "excerpt": "2줄 요약", '
'"tags": ["태그1", "태그2", ...]}'
)
raw = _call_claude(prompt, max_tokens=8192)
# JSON 파싱 시도
try:
# ```json ... ``` 블록 제거
text = raw.strip()
if text.startswith("```"):
lines = text.split("\n")
lines = [l for l in lines if not l.strip().startswith("```")]
text = "\n".join(lines)
result = json.loads(text)
return {
"title": result.get("title", ""),
"body": result.get("body", ""),
"excerpt": result.get("excerpt", ""),
"tags": result.get("tags", []),
}
except (json.JSONDecodeError, KeyError):
# JSON 파싱 실패 시 원본 텍스트를 body로
logger.warning("Blog post JSON parse failed, using raw text")
return {
"title": f"{analysis.get('keyword', '')} 추천 리뷰",
"body": raw,
"excerpt": raw[:200],
"tags": [analysis.get("keyword", "")],
}
def regenerate_blog_post(
analysis: Dict[str, Any],
trend_brief: str,
previous_body: str,
feedback: str,
) -> Dict[str, str]:
"""피드백을 반영하여 블로그 글 재생성."""
prompt = (
"당신은 네이버 블로그에서 월 100만 이상 수익을 올리는 전문 블로거입니다.\n"
f"키워드: {analysis.get('keyword', '')}\n\n"
f"이전에 작성한 글:\n{previous_body[:3000]}\n\n"
f"리뷰어 피드백:\n{feedback}\n\n"
"위 피드백을 반영하여 글을 개선해주세요.\n"
"작성 규칙: 1인칭 체험기, 1,500자 이상, 자연스러운 구어체, "
"제품 비교표 포함, 광고 고지 문구 포함.\n"
"HTML 형식으로 작성하되, 네이버 블로그에서 바로 붙여넣기 가능한 형태로.\n\n"
"---\n"
"응답은 반드시 아래 JSON 형식으로 해주세요 (JSON만 출력):\n"
'{"title": "블로그 제목", "body": "HTML 본문", "excerpt": "2줄 요약", '
'"tags": ["태그1", "태그2", ...]}'
)
raw = _call_claude(prompt, max_tokens=8192)
try:
text = raw.strip()
if text.startswith("```"):
lines = text.split("\n")
lines = [l for l in lines if not l.strip().startswith("```")]
text = "\n".join(lines)
result = json.loads(text)
return {
"title": result.get("title", ""),
"body": result.get("body", ""),
"excerpt": result.get("excerpt", ""),
"tags": result.get("tags", []),
}
except (json.JSONDecodeError, KeyError):
logger.warning("Regenerate JSON parse failed, using raw text")
return {
"title": f"{analysis.get('keyword', '')} 추천 리뷰 (개선)",
"body": raw,
"excerpt": raw[:200],
"tags": [analysis.get("keyword", "")],
}