Files
web-page-backend/docs/superpowers/specs/2026-05-16-insta-trends-design.md

11 KiB
Raw Permalink Blame History

insta-lab Trends 탭 설계 — 외부 트렌드 수집 + 카테고리 가중치

작성일: 2026-05-16 상태: 사용자 승인 대기 → writing-plans 진입 예정 연관 문서: 2026-05-15-insta-agent-design.md (insta-lab 기본 설계)


1. 목적·배경

insta-lab 운영 첫 사이클(2026-05-16 머지·배포 완료)에서 다음 두 가지 한계가 드러남:

  1. 키워드 발견 소스가 사용자 시드 키워드에만 의존 — 진짜 "지금 뜨고 있는" 화제를 잡지 못함. 카테고리당 5개 시드를 고정해두고 거기에 매칭되는 기사만 모음.
  2. 계정 정체성을 시스템이 모름 — 사용자가 "내 인스타 계정은 경제 위주"라고 정해도 시스템은 모든 카테고리를 균등하게 처리.

이 spec은 두 한계를 해소하기 위해:

  • 외부 트렌드 소스(NAVER 인기 + Google Trends)를 추가해 "발견" 단계를 보강
  • 계정 카테고리 가중치 모델을 도입해 자동 추출 알고리즘이 계정 정체성을 반영

2. 스코프

포함

  • 신규 백엔드 모듈 trend_collector.py (NAVER 인기 + Google Trends 두 source)
  • 신규 백엔드 모듈 변경: keyword_extractor.py에 가중치 기반 extract_with_weights() 추가
  • DB 마이그레이션: trending_keywords 테이블에 source 컬럼 추가, account_preferences 신규 테이블
  • 신규 API 4개 (POST /trends/collect, GET /trends, GET/PUT /preferences)
  • 09:00 매일 cron 추가 (트렌드 수집), 09:30 cron 가중치 적용
  • 프론트엔드: InstaCards 페이지에 탭 네비게이션 추가, Trends 탭 신규 3개 패널

제외

  • pytrends 외 외부 SaaS 트렌드 API (BuzzSumo 등)
  • 트렌드 시계열 차트
  • 카테고리 자동 학습 (사용자 카드 생성 이력에서 선호도 추론)
  • 트렌드 알림 (특정 키워드 등장 시 push)

3. 데이터 소스

  • NAVER news.json API 재사용. 카테고리당 시드 키워드로 sort=sim (정확도 정렬 = 인기 시그널) 30건 수집
  • 응답 기사 묶음에서 빈도어 추출 → 카테고리 매핑 (기존 keyword_extractor의 _count_nouns + _top_candidates 재사용)
  • 상위 N개를 trending_keywords 테이블에 source='naver_popular'로 저장
  • 라이브러리: pytrends (PyPI, MIT)
  • TrendReq(hl='ko-KR', tz=540).trending_searches(pn='south_korea') 호출 → 일일 트렌딩 키워드 리스트
  • 각 키워드에 대해 Claude Haiku 1회 호출로 카테고리 분류 (economy / psychology / celebrity / 사용자 추가 카테고리 / uncategorized)
  • LLM 분류 비용 절감을 위해 분류 결과를 1일 캐시 — trend_collector 모듈 레벨 _category_cache: dict[str, tuple[str, float]] (keyword → (category, expires_ts)), 컨테이너 lifetime 동안 유효. 같은 키워드 재요청 시 cache hit. 캐시는 영속화하지 않음 (재시작 시 첫 호출은 LLM 재분류)
  • trending_keywords 테이블에 source='google_trends', score=traffic 정규화값

3-3. 통합 저장

기존 trending_keywords 스키마에 한 컬럼 추가:

ALTER TABLE trending_keywords ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';
-- 기존 row 모두 'manual'로 마킹됨 (시드 키워드에서 추출된 것)
-- 신규 source: 'naver_popular' | 'google_trends'

source별 추가 인덱스:

CREATE INDEX idx_tk_source ON trending_keywords(source, suggested_at DESC);

4. 카테고리 가중치 모델

4-1. 신규 테이블 account_preferences

CREATE TABLE account_preferences (
    category    TEXT PRIMARY KEY,
    weight      REAL NOT NULL DEFAULT 1.0,
    updated_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
  • 초기 시드: economy=1.0, psychology=1.0, celebrity=1.0 (균등)
  • 사용자는 010 자유 범위 (UI는 0100 정수%로 노출, 백엔드에서 0~1 정규화)
  • 합계 강제 없음. 알고리즘 내부에서 비율 정규화
  • 카테고리 추가 자유. 단 추가 시 prompt_templates.category_seeds에도 시드 키워드 함께 정의해야 자동 추출에 반영됨 (UI에서 안내)

4-2. 가중치 기반 추출 알고리즘

기존 keyword_extractor.extract_for_category(category, limit) 유지. 신규:

def extract_with_weights(weights: dict[str, float], total_limit: int) -> list[Keyword]:
    """카테고리 가중치 비율대로 키워드를 분배 추출."""
    if not weights or sum(weights.values()) == 0:
        # fallback: 균등 가중치
        cats = list(DEFAULT_CATEGORY_SEEDS.keys())
        weights = {c: 1.0 for c in cats}

    total_weight = sum(weights.values())
    saved = []
    for category, w in weights.items():
        if w <= 0:
            continue
        per_cat = round(total_limit * w / total_weight)
        if per_cat <= 0:
            continue
        saved.extend(extract_for_category(category, limit=per_cat))
    return saved
  • total_limit 기본 15 (3 카테고리 × 5 시드 시절 합계와 동일)
  • weight=0 카테고리는 skip (분류는 유지하되 자동 추출에서 제외하고 싶을 때)

5. API (insta-lab)

메서드 경로 설명
POST /api/insta/trends/collect 두 source 모두 수집 (BackgroundTask) → {task_id}
GET /api/insta/trends 트렌드 조회. query: source (naver_popular/google_trends/all), category, days (default 1, 의미: suggested_at >= now() - days*24h). 정렬 suggested_at DESC, score DESC
GET /api/insta/preferences 가중치 조회 → {categories: [{category, weight, updated_at}]}
PUT /api/insta/preferences body {categories: {economy: 0.6, ...}} → upsert

기존 /api/insta/keywords는 source 필터 추가 (?source=manual 등). 미지정 시 모든 source 반환 (default behavior 유지).


6. 스케줄러 변경 (agent-office InstaAgent)

기존:

  • 09:30 — 키워드 추출 → 텔레그램 푸시

신규:

  • 09:00 — 외부 트렌드 수집 (NAVER 인기 + Google Trends) — _run_insta_trends_collect() 신규 cron
  • 09:30 — 키워드 추출 (기존 + 가중치 적용) — InstaAgent가 get_preferences() 호출 후 extract_with_weights() 사용

수동 트리거: InstaAgent에 on_command("collect_trends", {}) 신규 액션. 텔레그램에서 /insta collect_trends 슬래시 명령 또는 Insta 페이지 버튼에서 호출.


7. 프론트엔드 변경 (web-ui InstaCards.jsx)

7-1. 탭 네비게이션

기존 5개 패널을 두 탭으로 재구성:

패널
Cards (기본) Trigger, Trending Keywords, Slates, SlateDetail, PromptEditor (기존 그대로)
Trends (신규) AccountFocusPanel, ExternalTrendsPanel, PreferenceImpactPanel

탭 컴포넌트: <TabBar> 단순 buttons (activeTab state), URL에 ?tab=trends 쿼리로 deep-link 지원.

7-2. AccountFocusPanel

  • 카테고리별 가중치 슬라이더 (0~100 정수%) + 우측 막대 차트 (분포 시각화)
  • + 카테고리 추가 버튼 → 모달로 카테고리명 + 시드 키워드 N개 입력 (시드는 category_seeds 프롬프트 템플릿에 머지)
  • 저장 버튼 → PUT /preferences (debounce 1초)

7-3. ExternalTrendsPanel

  • 상단: 🔄 수동 수집 버튼 + "마지막 수집: HH:MM" 라벨 + 진행 task box
  • 두 컬럼 (반응형 → 모바일은 세로):
    • 🔥 NAVER 인기 — 카테고리별 그룹핑, 각 카드: keyword + score + 카테고리 배지
    • 🌐 Google Trends — 단순 리스트, 각 카드: keyword + 카테고리 배지 + traffic
  • 각 카드 우측에 🎴 버튼 → 즉시 POST /slates (기존 흐름)
  • 색상 매핑: economy=#0F62FE, psychology=#A66CFF, celebrity=#FF5C8A, custom=#6B7280

7-4. PreferenceImpactPanel (작은 박스)

  • "현재 가중치 기준 다음 자동 추출 결과 미리보기: economy 3 / psychology 2 / celebrity 0"
  • 가중치 슬라이더 변경 시 즉시 클라이언트에서 계산해 갱신
  • 컴팩트 1줄 표시

7-5. 신규 API 헬퍼 (src/api.js)

export function getInstaTrends({ source, category, days = 1 } = {}) { ... }
export function instaCollectTrends() { ... }
export function getInstaPreferences() { ... }
export function putInstaPreferences(categories) { ... }

8. 에러 처리

상황 처리
pytrends rate limit / 차단 try/except → 빈 결과로 graceful degrade. NAVER 인기는 정상 수집
LLM 분류 실패 uncategorized 카테고리로 폴백, 사용자가 UI에서 수동 재분류 가능
가중치 합계 0 균등 가중치 (1/N)로 폴백, 로그 warning
카테고리 추가했는데 시드 없음 자동 추출에서 자연스럽게 skip (NAVER 검색에 시드 필요), UI에서 "시드 키워드 추가 필요" 경고
Google Trends 한국 region 부재 hl='ko-KR' + pn='south_korea' 명시. 실패 시 빈 결과

9. 테스트

insta-lab pytest

  • test_trend_collector.py (4): fetch_naver_popular mocked, fetch_google_trends pytrends mocked, 카테고리 매핑, 캐시 hit
  • test_extract_with_weights.py (3): 균등 가중치, 한쪽 0 가중치, fallback 빈 가중치
  • test_preferences_crud.py (2): GET 기본값, PUT upsert
  • test_main_trends.py (3): 신규 4개 엔드포인트 통합

agent-office pytest

  • test_insta_agent_trends.py (2): on_schedule_trends mocked, weight-applied extract

10. 마이그레이션 절차

  1. db.init_db()ALTER TABLE trending_keywords ADD COLUMN source ... 추가 — PRAGMA table_info로 컬럼 존재 여부 확인 후 idempotent하게 실행
  2. account_preferences 테이블 신규 생성
  3. 초기 시드: 기존 카테고리 economy/psychology/celebrity 모두 weight=1.0
  4. 기존 trending_keywords row는 자동으로 source='manual' (컬럼 DEFAULT)
  5. requirements.txtpytrends>=4.9 추가
  6. 배포 후 사용자가 Trends 탭에서 가중치 조정 (필수 아님, 균등이 디폴트 동작)

11. 운영 영향

항목 영향
Anthropic 토큰 비용 +미미 (Google Trends 1회당 ~20 키워드 × Haiku 분류 1콜 ≈ 600 토큰/일)
DB 크기 +미미 (트렌드 row 일일 ~50개, 카테고리당 30 + Google 20)
NAS CPU +낮음 (pytrends + NAVER API 호출만, LLM은 외부)
카드 생성 흐름 변경 없음. 트렌드는 "발견" 단계만 보강

12. 완료 정의

  • trending_keywords.source 컬럼 마이그레이션 적용, 기존 row 모두 'manual'로 표시됨
  • account_preferences 테이블 생성, 초기 3개 카테고리 weight=1.0
  • POST /api/insta/trends/collect 호출 시 NAVER 인기 + Google Trends 모두 수집되어 DB 저장
  • GET /api/insta/trends?source=google_trends 결과 카테고리 분류됨
  • PUT /api/insta/preferences 후 09:30 cron이 가중치 비율대로 추출
  • 09:00 cron 등록, 매일 자동 트렌드 수집
  • Insta 페이지에 Cards/Trends 탭 전환 작동
  • Trends 탭의 AccountFocusPanel에서 가중치 변경·저장 가능
  • ExternalTrendsPanel에서 NAVER 인기 + Google Trends 한 눈에 표시, 각 카드 생성 트리거 작동
  • PreferenceImpactPanel 미리보기 갱신
  • insta-lab pytest 전체 통과 (기존 21 + 신규 12 = 33)
  • agent-office pytest 전체 통과