From 6ac7469f26f15b454d299f1b25a505c861aed45a Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 15 May 2026 08:42:03 +0900 Subject: [PATCH] =?UTF-8?q?docs(insta-agent):=20blog-lab=20=ED=8F=90?= =?UTF-8?q?=EA=B8=B0=20=EB=B0=8F=20insta-lab=20=EC=84=A4=EA=B3=84=20(1080x?= =?UTF-8?q?1350=20=EC=B9=B4=EB=93=9C=20=ED=94=BC=EB=93=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 뉴스 수집 → 키워드 추출 → 10페이지 카드 카피·PNG 생성 → 텔레그램 푸시 → 사용자 수동 인스타 업로드 파이프라인. blog-lab 디렉토리·DB 폐기, 포트 18700 재활용, agents/blog.py → agents/insta.py, Playwright 기반 카드 렌더. --- .../specs/2026-05-15-insta-agent-design.md | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-15-insta-agent-design.md diff --git a/docs/superpowers/specs/2026-05-15-insta-agent-design.md b/docs/superpowers/specs/2026-05-15-insta-agent-design.md new file mode 100644 index 0000000..44d1a19 --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-insta-agent-design.md @@ -0,0 +1,357 @@ +# 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장 + 추천 캡션·해시태그 + ─ 사용자 다운로드 후 인스타 수동 업로드 +``` + +### 자동 모드 (옵션) + +- `auto_select=true` 설정 시 키워드 추출 직후 카테고리당 score 1위 키워드를 자동 선택해 4~6 단계까지 즉시 진행 +- 사용자가 텔레그램에서 결과만 확인 + +--- + +## 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 # 5테이블 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 / list_categories + - on_callback: 텔레그램 inline button "render_" → 카피·렌더·푸시 + +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 전체 통과