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

359 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```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. 키워드 추출 알고리즘
```python
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 전체 통과