From 7d1857c8a4f1ff84f1f8112b435f09b18a860c5f Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 02:23:33 +0900 Subject: [PATCH] feat(insta-lab): GET /keywords/ranked + POST /slates/{id}/decision Co-Authored-By: Claude Opus 4.8 (1M context) --- insta-lab/app/main.py | 33 +++++++++++++++- insta-lab/tests/test_ranked_decision_api.py | 42 +++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 insta-lab/tests/test_ranked_decision_api.py diff --git a/insta-lab/app/main.py b/insta-lab/app/main.py index b2fbe2e..29d885d 100644 --- a/insta-lab/app/main.py +++ b/insta-lab/app/main.py @@ -6,6 +6,7 @@ import json import logging import os import zipfile +from datetime import datetime, timezone from typing import Optional from fastapi import FastAPI, HTTPException, BackgroundTasks, Body, Query @@ -21,7 +22,7 @@ from .config import ( ) import redis.asyncio as aioredis -from . import db, news_collector, keyword_extractor, card_writer, trend_collector +from . import db, news_collector, keyword_extractor, card_writer, trend_collector, selection, selection_judge from .internal_router import router as internal_router logger = logging.getLogger(__name__) @@ -152,6 +153,22 @@ def list_keywords( return {"items": db.list_trending_keywords(category=category, used=used)} +@app.get("/api/insta/keywords/ranked") +def ranked_keywords(limit: int = Query(20, ge=1, le=100), threshold: float = Query(0.6, ge=0.0, le=1.0)): + candidates = db.list_trending_keywords(used=False) + if not candidates: + return {"items": []} + issued = db.list_recent_issued_topics(window_days=14) + prefs = {p["category"]: p["weight"] for p in db.get_preferences()} + claude_scores = selection_judge.judge_candidates(candidates) + now_iso = datetime.now(timezone.utc).isoformat() + scored = selection.score_candidates( + candidates, issued, prefs, claude_scores=claude_scores, + threshold=threshold, now_iso=now_iso, + ) + return {"items": scored[:limit]} + + # ── Slates ─────────────────────────────────────────────────────── class SlateRequest(BaseModel): keyword: str @@ -282,6 +299,20 @@ def download_package(slate_id: int): }) +class DecisionBody(BaseModel): + decision: str # "approved" | "rejected" + + +@app.post("/api/insta/slates/{slate_id}/decision") +def slate_decision(slate_id: int, body: DecisionBody): + if not db.get_card_slate(slate_id): + raise HTTPException(404, "slate not found") + if body.decision not in ("approved", "rejected"): + raise HTTPException(400, "decision must be approved|rejected") + db.set_slate_decision(slate_id, body.decision) + return db.get_card_slate(slate_id) + + @app.delete("/api/insta/slates/{slate_id}") def delete_slate(slate_id: int): if not db.get_card_slate(slate_id): diff --git a/insta-lab/tests/test_ranked_decision_api.py b/insta-lab/tests/test_ranked_decision_api.py new file mode 100644 index 0000000..9ca95d8 --- /dev/null +++ b/insta-lab/tests/test_ranked_decision_api.py @@ -0,0 +1,42 @@ +import pytest +from fastapi.testclient import TestClient +from app import db, config, selection_judge + + +@pytest.fixture +def client(tmp_path, monkeypatch): + monkeypatch.setattr(config, "DB_PATH", str(tmp_path / "insta.db")) + monkeypatch.setattr(db, "DB_PATH", str(tmp_path / "insta.db")) + monkeypatch.setattr(selection_judge, "judge_candidates", lambda c: {}) + db.init_db() + from app.main import app + return TestClient(app) + + +def test_ranked_returns_sorted_eligible(client, monkeypatch): + db.add_trending_keyword({"keyword": "금리", "category": "economy", "score": 0.9}) + r = client.get("/api/insta/keywords/ranked?threshold=0.0&limit=10") + assert r.status_code == 200 + items = r.json()["items"] + assert len(items) >= 1 + assert "final_score" in items[0] and "eligible" in items[0] + + +def test_decision_approve_publishes(client): + sid = db.add_card_slate({"keyword": "금리", "category": "economy"}) + r = client.post(f"/api/insta/slates/{sid}/decision", json={"decision": "approved"}) + assert r.status_code == 200 + assert db.get_card_slate(sid)["status"] == "published" + + +def test_decision_reject(client): + sid = db.add_card_slate({"keyword": "환율", "category": "economy"}) + r = client.post(f"/api/insta/slates/{sid}/decision", json={"decision": "rejected"}) + assert r.status_code == 200 + assert db.get_card_slate(sid)["status"] == "rejected" + + +def test_decision_invalid_400(client): + sid = db.add_card_slate({"keyword": "x", "category": "economy"}) + r = client.post(f"/api/insta/slates/{sid}/decision", json={"decision": "maybe"}) + assert r.status_code == 400