14 KiB
14 KiB
insta-agent 설계 — blog-lab 폐기, 인스타 카드 피드 파이프라인 신설
작성일: 2026-05-15 상태: 사용자 승인 대기 → writing-plans 진입 예정
1. 목적·배경
기존 blog-lab 서비스(네이버 블로그 마케팅 수익화)를 폐기하고, 인스타그램 프로페셔널 계정에 올릴 카드 형식 피드(1080×1350, 10페이지)를 자동 생산하는 insta-lab 서비스로 대체한다.
핵심 가치 제안:
- 매일 경제·심리학·연예 등 카테고리에서 화제 키워드를 자동 발견
- 사용자가 키워드 1개를 선택하면 10페이지 카드 카피 + PNG 자동 생성
- 텔레그램으로 카드 묶음 미디어 그룹 + 추천 캡션·해시태그 푸시
- 사용자는 카드 다운로드 → 인스타 수동 업로드 (Graph API 미사용)
블로그 발행 자동화의 운영 부담(네이버 SEO, 브랜드커넥트 링크 관리, 커미션 추적)을 제거하고 카드 콘텐츠 생산에 집중한다.
2. 스코프
포함
- 신규 컨테이너
insta-lab(포트 18700 재활용) - 신규 에이전트
insta-agent(agent-office/app/agents/insta.py) - 뉴스 수집 → 키워드 추출 → 카드 카피 생성 → 카드 PNG 렌더 → 텔레그램 푸시 파이프라인
- HTML/CSS 카드 템플릿 골격 (사용자가 디자인 직접 수정)
- 카드 슬레이트·기사·키워드·자산 5테이블 (
insta.db) - nginx 라우팅 변경 (
/api/blog-marketing/제거 →/api/insta/) - CLAUDE.md (workspace + web-backend) 갱신
제외
- 인스타그램 Graph API 자동 발행 (수동 업로드 사용)
- 카드 디자인 비주얼 완성 (사용자가 직접 작업)
- blog_marketing.db 데이터 마이그레이션 (clean slate)
- 다국어 번역, A/B 테스트, 성과 추적
3. 서비스 구성·폐기 범위
폐기
| 대상 | 처리 |
|---|---|
blog-lab/ 디렉토리 |
git rm 통째로 삭제 |
blog_marketing.db |
운영·로컬 모두 삭제 (clean slate) |
agent-office/app/agents/blog.py |
삭제 |
service_proxy.py의 blog_* 함수 |
삭제 |
agent-office의 blog 라우팅·텔레그램 명령 |
삭제 |
docker-compose의 blog-lab 서비스 정의 |
교체 |
nginx의 /api/blog-marketing/ location |
교체 |
환경변수 BLOG_DATA_PATH |
제거 |
신규
| 대상 | 비고 |
|---|---|
insta-lab/ 디렉토리 |
신규 생성 |
insta-lab 컨테이너 (포트 18700) |
blog-lab 자리 재활용 |
agents/insta.py |
신규 에이전트 |
nginx /api/insta/ → insta-lab:8000 |
신규 |
환경변수 INSTA_DATA_PATH, CARD_TEMPLATE_DIR |
신규 |
재사용 자산 (코드 패턴 차용)
naver_search.py— 엔드포인트만news.json으로 교체generation_tasks테이블 + BackgroundTask 폴링 패턴prompt_templates테이블 + DB 저장 프롬프트 패턴- agent-office의 텔레그램 인라인 키보드·승인 패턴 (
realestate_message.py참고)
4. 데이터 흐름
일일 사이클
[09:30 매일 cron — agent-office 스케줄러]
1. 뉴스 수집 ─ 카테고리별 시드 키워드로 NAVER news.json 검색
─ 카테고리당 상위 30건 메타 + 본문 일부 → news_articles
2. 키워드 추출 ─ 카테고리당 빈도 상위 + Claude Haiku 정제
─ trending_keywords (score 내림차순)
3. 텔레그램 푸시 ─ 카테고리별 후보 5개씩 인라인 키보드
─ 사용자 선택 대기
[사용자가 텔레그램 인라인 버튼 선택]
4. 카피 생성 ─ Claude로 10페이지 카피 (1=훅/커버, 2~9=본문 8장, 10=요약/CTA)
─ card_slates 저장 (status='draft')
5. 카드 렌더 ─ Jinja → HTML 1080×1350 → Playwright headless 스크린샷 10장
─ /app/data/insta_cards/{slate_id}/01.png ~ 10.png
6. 텔레그램 ─ 미디어 그룹 10장 + 추천 캡션·해시태그
─ 사용자 다운로드 후 인스타 수동 업로드
자동 모드 (옵션)
- agent-office의
agent_config.custom_config.auto_select(bool) 플래그로 제어 auto_select=true설정 시 키워드 추출 직후 카테고리당 score 1위 키워드를 자동 선택해 4~6 단계까지 즉시 진행- 사용자가 텔레그램에서 결과만 확인 (인라인 후보 푸시 단계 skip)
5. 컴포넌트
insta-lab (FastAPI 서비스)
insta-lab/
├── Dockerfile # python:3.12-slim + playwright install chromium --with-deps
├── requirements.txt
├── pytest.ini
├── tests/
└── app/
├── main.py # FastAPI 라우터
├── config.py # NAVER_*, ANTHROPIC_API_KEY, INSTA_DATA_PATH, CARD_TEMPLATE_DIR
├── db.py # 6테이블 init + CRUD
├── news_collector.py # 네이버 뉴스 API + 본문 정리
├── keyword_extractor.py # 빈도 + LLM 정제
├── card_writer.py # Claude 10페이지 카피 생성
├── card_renderer.py # Jinja → Playwright 스크린샷
└── templates/ # 사용자가 직접 수정 (rsync로 NAS 배포)
└── default/
└── card.html.j2
agent-office 변경
agent-office/app/agents/insta.py (신규)
- on_schedule: 09:30 → news collect → keyword extract → 텔레그램 후보 푸시
- on_command: extract / render <keyword> / list_categories
- on_callback: 텔레그램 inline button "render_<keyword_id>" → 카피·렌더·푸시
agent-office/app/service_proxy.py
- blog_* 함수 모두 제거
- insta_* 함수 신규 (collect, extract, list_keywords, create_slate, render_slate, get_slate, get_asset)
agent-office/app/telegram/agent_registry.py
- blog 명령 등록 제거 → insta 명령 등록
6. DB 스키마 (insta.db)
| 테이블 | 핵심 컬럼 | 설명 |
|---|---|---|
news_articles |
id PK, category, title, link UNIQUE, summary, pub_date, fetched_at | 일일 수집 기사 메타 |
trending_keywords |
id PK, keyword, category, score REAL, articles_count, suggested_at, used INTEGER | 카테고리별 화제 키워드 (used=1이면 이미 슬레이트 생성됨) |
card_slates |
id PK, keyword, category, status (draft/rendered/sent/failed), cover_copy TEXT, body_copies TEXT(JSON 8개), cta_copy TEXT, suggested_caption TEXT, hashtags TEXT(JSON), created_at | 10페이지 카피 묶음 |
card_assets |
id PK, slate_id FK→card_slates(id), page_index INTEGER 1~10, file_path, file_hash, created_at | 렌더된 PNG 자산 |
generation_tasks |
id TEXT PK, type, status, progress, message, result_id INTEGER, error TEXT, params TEXT, created_at, updated_at | blog-lab 패턴 그대로 (collect/extract/write/render 통합) |
prompt_templates |
id PK, name UNIQUE, description, template TEXT, updated_at | slate_writer, keyword_extractor 두 개 시드 |
인덱스:
idx_na_category_fetchedON news_articles(category, fetched_at DESC)idx_tk_scoreON trending_keywords(category, score DESC)idx_cs_createdON card_slates(created_at DESC)idx_ca_slateON card_assets(slate_id, page_index)
7. 카드 렌더 (Playwright)
템플릿
templates/default/card.html.j2 — Jinja 변수:
| 변수 | 타입 | 설명 |
|---|---|---|
page_type |
str | "cover" / "body" / "cta" |
headline |
str | 페이지 헤드라인 |
body |
str | 본문 (markdown-lite 허용 — 줄바꿈 보존) |
accent_color |
str | hex (예: "#FF5733") |
page_no |
int | 1~10 |
total_pages |
int | 10 |
컨테이너 CSS: width: 1080px; height: 1350px; overflow: hidden;
렌더 로직 (card_renderer.py)
- Playwright async chromium browser 1회 launch
- browser.new_context(viewport={"width": 1080, "height": 1350}) → page
- 10번 반복:
- Jinja 렌더 → temp HTML 파일 저장
- page.goto(
file://...) - page.screenshot(path=f"{page_no:02}.png", omit_background=False)
- browser.close
Dockerfile
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
fonts-noto-cjk fonts-noto-cjk-extra \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN playwright install chromium --with-deps
COPY app ./app
ENV PYTHONUNBUFFERED=1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
이미지 사이즈 +500MB 예상. NAS Celeron J4025에서 카드 10장 렌더 ≤ 30초 목표.
8. API (insta-lab)
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/insta/status |
서비스 상태 (NAVER/ANTHROPIC 키 여부) |
| POST | /api/insta/news/collect |
뉴스 수집 수동 트리거 → BackgroundTask |
| GET | /api/insta/news/articles |
수집 기사 목록 (category, days 필터) |
| POST | /api/insta/keywords/extract |
키워드 추출 수동 트리거 → BackgroundTask |
| GET | /api/insta/keywords |
트렌딩 키워드 (category, used 필터) |
| POST | /api/insta/slates |
슬레이트 생성 (keyword, category) → BackgroundTask |
| GET | /api/insta/slates |
슬레이트 목록 |
| GET | /api/insta/slates/{id} |
슬레이트 상세 (카피 + 자산 경로) |
| POST | /api/insta/slates/{id}/render |
카드 렌더 재시도 |
| GET | /api/insta/slates/{id}/assets/{page} |
카드 PNG 다운로드 (1~10) |
| DELETE | /api/insta/slates/{id} |
삭제 (slate + assets) |
| GET | /api/insta/tasks/{task_id} |
BackgroundTask 상태 폴링 |
| GET/PUT | /api/insta/templates/prompts/{name} |
프롬프트 템플릿 조회·수정 |
9. 키워드 추출 알고리즘
def extract_keywords(category: str, articles: list[Article]) -> list[Keyword]:
# 1. 빈도 기반 후보 추출
# - 명사 추출 (간단: 한글 2~6자 정규식 + 불용어 제거)
# - 카테고리 시드 키워드와 코사인 유사도 ≥ 0.3 이상만
raw_freq = count_nouns(articles)
candidates = top_n(raw_freq, n=20)
# 2. Claude Haiku로 정제
# - 시스템 프롬프트: "{category} 인스타 카드용 키워드"
# - 입력: 후보 20개 + 각 후보가 등장한 기사 제목 3개
# - 출력 JSON: [{"keyword": str, "score": 0~1, "reason": str}]
refined = claude_haiku_refine(category, candidates, articles)
# 3. score 내림차순 → 상위 5개 trending_keywords로 저장
return refined[:5]
score는 LLM이 평가한 "카드 콘텐츠 적합도" (호기심 유발성 + 시의성 + 구체성)- 시드 키워드는
prompt_templates.name='category_seeds'에서 카테고리별 JSON으로 관리
10. 카드 카피 생성 (slate_writer)
Claude 호출 1회로 10페이지 카피 생성:
시스템 프롬프트 (DB 저장, 사용자가 수정 가능):
- 너는 인스타그램 카드 뉴스 카피라이터다.
- {category} 카테고리, 키워드: {keyword}
- 출력은 JSON 객체:
{
"cover_copy": {"headline": str, "body": str, "accent_color": "#hex"},
"body_copies": [
{"headline": str, "body": str},
... (8개)
],
"cta_copy": {"headline": str, "body": str, "cta": str},
"suggested_caption": str,
"hashtags": ["#tag1", ...]
}
입력:
- 키워드 + 관련 기사 제목·요약 5건
accent_color는 카테고리별 기본값(경제=#0F62FE, 심리학=#A66CFF, 연예=#FF5C8A) 사용, LLM이 더 어울리면 override.
11. 에러 처리
| 단계 | 실패 시 |
|---|---|
| 뉴스 수집 | 카테고리별 try/except, 한 카테고리 빈 결과여도 다른 카테고리 진행. 모두 실패 시 텔레그램 알림 |
| 키워드 추출 | LLM 실패 시 빈도 기반 결과만 사용 (degrade). LLM 타임아웃 60s |
| 카피 생성 | LLM 실패 시 BackgroundTask failed, 텔레그램 알림. JSON 파싱 실패 시 1회 retry |
| 카드 렌더 | Playwright 크래시 시 retry 1회. 실패 시 slate.status='failed' + 텔레그램 알림. 일부 페이지만 실패 시 해당 페이지만 재렌더 가능 |
| 텔레그램 미디어 그룹 | 텔레그램 API 10MB/장 제한 → PNG quality 90, 평균 < 500KB 예상. 초과 시 압축 후 재시도 |
12. 테스트
- pytest 단위 테스트:
news_collectormocked HTTP, JSON 파싱 검증keyword_extractor빈도 추출 단위 + Claude mockcard_writerClaude mock, JSON 스키마 검증card_renderer작은 fixture HTML로 PNG 1장 생성 (실제 Playwright 통합 테스트 1건)
- agent-office 통합:
agents/insta.pymocked service_proxy로 on_schedule·on_command·on_callback 분기 검증
13. 운영·환경
환경변수 (insta-lab)
| 변수 | 기본값 | 설명 |
|---|---|---|
NAVER_CLIENT_ID |
(필수) | 네이버 검색 API 키 |
NAVER_CLIENT_SECRET |
(필수) | 네이버 검색 API 시크릿 |
ANTHROPIC_API_KEY |
(필수) | Claude API 키 |
INSTA_DATA_PATH |
./data/insta |
DB + 카드 PNG 저장 경로 |
CARD_TEMPLATE_DIR |
/app/app/templates |
HTML/CSS 템플릿 디렉토리 |
CORS_ALLOW_ORIGINS |
* |
CORS 설정 |
docker-compose.yml 변경
blog-lab서비스 블록 →insta-lab서비스 블록 (포트 18700:8000 그대로)- 볼륨:
./data/insta:/app/data/insta
nginx default.conf 변경
location /api/blog-marketing/ { # 제거
...
}
location /api/insta/ { # 신규
proxy_pass http://insta-lab:8000;
...
}
CLAUDE.md 갱신
- workspace/CLAUDE.md: blog-lab 표 행 제거 → insta-lab 추가,
/api/blog-marketing/행 제거 →/api/insta/추가, 컨테이너 이름·역할 업데이트 - web-backend/CLAUDE.md: 9.x 섹션 blog-lab 통째로 → insta-lab 섹션, 4·5 표 갱신
14. 완료 정의
- blog-lab 디렉토리·DB 삭제, 컨테이너에서 더 이상 빌드 안 됨
- insta-lab 컨테이너 빌드 및 헬스체크 통과
POST /api/insta/news/collect→ news_articles에 카테고리당 30건 저장 확인POST /api/insta/keywords/extract→ trending_keywords 카테고리당 5개 저장POST /api/insta/slates→ 카피 생성 + 카드 PNG 10장 렌더 (수동 호출)- agent-office의 insta-agent 09:30 cron 등록, 텔레그램 인라인 키보드 후보 푸시 작동
- 텔레그램 인라인 버튼 클릭 → 미디어 그룹 10장 발송 성공
- CLAUDE.md 양쪽 갱신 후 커밋
- pytest 전체 통과