feat(insta-lab): GET /keywords/ranked + POST /slates/{id}/decision

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 02:23:33 +09:00
parent c3a6e78954
commit 7d1857c8a4
2 changed files with 74 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ import json
import logging import logging
import os import os
import zipfile import zipfile
from datetime import datetime, timezone
from typing import Optional from typing import Optional
from fastapi import FastAPI, HTTPException, BackgroundTasks, Body, Query from fastapi import FastAPI, HTTPException, BackgroundTasks, Body, Query
@@ -21,7 +22,7 @@ from .config import (
) )
import redis.asyncio as aioredis 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 from .internal_router import router as internal_router
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -152,6 +153,22 @@ def list_keywords(
return {"items": db.list_trending_keywords(category=category, used=used)} 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 ─────────────────────────────────────────────────────── # ── Slates ───────────────────────────────────────────────────────
class SlateRequest(BaseModel): class SlateRequest(BaseModel):
keyword: str 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}") @app.delete("/api/insta/slates/{slate_id}")
def delete_slate(slate_id: int): def delete_slate(slate_id: int):
if not db.get_card_slate(slate_id): if not db.get_card_slate(slate_id):

View File

@@ -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