From 41225b33376e5483b8ccb806b519fa27e2d4c4ef Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 16 May 2026 17:54:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(insta-lab):=20main.py=20=E2=80=94=20trends?= =?UTF-8?q?=20+=20preferences=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/insta/trends/collect — background trend collection via trend_collector.collect_all - GET /api/insta/trends — list external trends with source/category/days filters - GET /api/insta/preferences — return category weights (defaults seeded on init_db) - PUT /api/insta/preferences — upsert category weights - Modified GET /api/insta/keywords to accept source= filter (source present → list_trends, else existing list_trending_keywords, backward compatible) Co-Authored-By: Claude Sonnet 4.6 --- insta-lab/app/main.py | 59 +++++++++++++++++++- insta-lab/tests/test_main_trends.py | 83 +++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 insta-lab/tests/test_main_trends.py diff --git a/insta-lab/app/main.py b/insta-lab/app/main.py index fe0ef80..9cd4f1c 100644 --- a/insta-lab/app/main.py +++ b/insta-lab/app/main.py @@ -15,7 +15,7 @@ from .config import ( CORS_ALLOW_ORIGINS, NAVER_CLIENT_ID, ANTHROPIC_API_KEY, INSTA_DATA_PATH, DB_PATH, DEFAULT_CATEGORY_SEEDS, KEYWORDS_PER_CATEGORY, ) -from . import db, news_collector, keyword_extractor, card_writer, card_renderer +from . import db, news_collector, keyword_extractor, card_writer, card_renderer, trend_collector logger = logging.getLogger(__name__) app = FastAPI() @@ -119,7 +119,13 @@ def extract_keywords(req: ExtractRequest, bg: BackgroundTasks): @app.get("/api/insta/keywords") -def list_keywords(category: Optional[str] = None, used: Optional[bool] = None): +def list_keywords( + category: Optional[str] = None, + used: Optional[bool] = None, + source: Optional[str] = None, +): + if source: + return {"items": db.list_trends(source=source, category=category, days=30)} return {"items": db.list_trending_keywords(category=category, used=used)} @@ -243,3 +249,52 @@ def get_prompt(name: str): def upsert_prompt(name: str, body: TemplateBody): db.upsert_prompt_template(name, body.template, body.description) return db.get_prompt_template(name) + + +# ── Trends ─────────────────────────────────────────────────────── +class TrendsCollectRequest(BaseModel): + categories: Optional[list[str]] = None + + +async def _bg_collect_trends(task_id: str, categories: list[str]): + try: + db.update_task(task_id, "processing", 10, "외부 트렌드 수집 중") + result = trend_collector.collect_all(categories) + msg = f"naver:{result['naver_popular']}, google:{result['google_trends']}" + db.update_task(task_id, "succeeded", 100, msg, result_id=sum(result.values())) + except Exception as e: + logger.exception("trends collect failed") + db.update_task(task_id, "failed", 0, "", error=str(e)) + + +@app.post("/api/insta/trends/collect") +def collect_trends(req: TrendsCollectRequest, bg: BackgroundTasks): + cats = req.categories or list(DEFAULT_CATEGORY_SEEDS.keys()) + tid = db.create_task("trends_collect", {"categories": cats}) + bg.add_task(_bg_collect_trends, tid, cats) + return {"task_id": tid, "categories": cats} + + +@app.get("/api/insta/trends") +def list_trends_endpoint( + source: Optional[str] = None, + category: Optional[str] = None, + days: int = Query(1, ge=1, le=90), +): + return {"items": db.list_trends(source=source, category=category, days=days)} + + +# ── Preferences ────────────────────────────────────────────────── +class PreferencesBody(BaseModel): + categories: dict[str, float] + + +@app.get("/api/insta/preferences") +def get_preferences_endpoint(): + return {"categories": db.get_preferences()} + + +@app.put("/api/insta/preferences") +def put_preferences_endpoint(body: PreferencesBody): + db.upsert_preferences(body.categories) + return {"categories": db.get_preferences()} diff --git a/insta-lab/tests/test_main_trends.py b/insta-lab/tests/test_main_trends.py new file mode 100644 index 0000000..c642881 --- /dev/null +++ b/insta-lab/tests/test_main_trends.py @@ -0,0 +1,83 @@ +import os +import gc +import tempfile + +import pytest +from fastapi.testclient import TestClient + +from app import db as db_module + + +@pytest.fixture +def client(monkeypatch): + fd, path = tempfile.mkstemp(suffix=".db") + os.close(fd) + monkeypatch.setattr(db_module, "DB_PATH", path) + db_module.init_db() + from app import main + monkeypatch.setattr(main, "DB_PATH", path) + with TestClient(main.app) as c: + yield c + gc.collect() + for ext in ("", "-wal", "-shm"): + try: + os.remove(path + ext) + except OSError: + pass + + +def test_get_preferences_returns_defaults(client): + resp = client.get("/api/insta/preferences") + assert resp.status_code == 200 + cats = {p["category"]: p["weight"] for p in resp.json()["categories"]} + assert cats == {"economy": 1.0, "psychology": 1.0, "celebrity": 1.0} + + +def test_put_preferences_upsert(client): + resp = client.put("/api/insta/preferences", + json={"categories": {"economy": 0.7, "psychology": 0.2, "tech": 0.5}}) + assert resp.status_code == 200 + cats = {p["category"]: p["weight"] for p in resp.json()["categories"]} + assert cats["economy"] == 0.7 + assert cats["tech"] == 0.5 + + +def test_list_trends_filter(client): + db_module.add_external_trend({"keyword": "A", "category": "economy", + "source": "naver_popular", "score": 1.0}) + db_module.add_external_trend({"keyword": "B", "category": "celebrity", + "source": "google_trends", "score": 0.8}) + resp = client.get("/api/insta/trends?source=naver_popular") + items = resp.json()["items"] + assert {it["keyword"] for it in items} == {"A"} + + +def test_collect_trends_kicks_background(client, monkeypatch): + from app import main, trend_collector + + captured = {"called": False} + + def fake_collect_all(cats): + captured["called"] = True + return {"naver_popular": 3, "google_trends": 2} + + monkeypatch.setattr(trend_collector, "collect_all", fake_collect_all) + resp = client.post("/api/insta/trends/collect", json={}) + assert resp.status_code == 200 + task_id = resp.json()["task_id"] + for _ in range(20): + st = client.get(f"/api/insta/tasks/{task_id}").json() + if st["status"] in ("succeeded", "failed"): + break + assert st["status"] == "succeeded" + assert captured["called"] is True + + +def test_list_keywords_filters_by_source(client): + db_module.add_trending_keyword({"keyword": "M", "category": "economy", + "score": 0.4, "articles_count": 1, "source": "manual"}) + db_module.add_external_trend({"keyword": "N", "category": "economy", + "source": "naver_popular", "score": 0.9}) + resp = client.get("/api/insta/keywords?source=manual") + items = resp.json()["items"] + assert {it["keyword"] for it in items} == {"M"}