Files
web-page-backend/docs/superpowers/specs/2026-05-15-insta-agent-design.md

14 KiB
Raw Permalink Blame History

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_fetched ON news_articles(category, fetched_at DESC)
  • idx_tk_score ON trending_keywords(category, score DESC)
  • idx_cs_created ON card_slates(created_at DESC)
  • idx_ca_slate ON 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)

  1. Playwright async chromium browser 1회 launch
  2. browser.new_context(viewport={"width": 1080, "height": 1350}) → page
  3. 10번 반복:
    • Jinja 렌더 → temp HTML 파일 저장
    • page.goto(file://...)
    • page.screenshot(path=f"{page_no:02}.png", omit_background=False)
  4. 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_collector mocked HTTP, JSON 파싱 검증
    • keyword_extractor 빈도 추출 단위 + Claude mock
    • card_writer Claude mock, JSON 스키마 검증
    • card_renderer 작은 fixture HTML로 PNG 1장 생성 (실제 Playwright 통합 테스트 1건)
  • agent-office 통합: agents/insta.py mocked 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 전체 통과