From 25f4f1f98b848f4ff04e896d87d97db444cabfb9 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 7 Apr 2026 00:51:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(blog-lab):=20=EB=B8=8C=EB=9E=9C=EB=93=9C?= =?UTF-8?q?=EC=BB=A4=EB=84=A5=ED=8A=B8=20=EB=A7=81=ED=81=AC=20CRUD=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- blog-lab/app/main.py | 37 ++++++++++++++ blog-lab/tests/test_api_links.py | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 blog-lab/tests/test_api_links.py diff --git a/blog-lab/app/main.py b/blog-lab/app/main.py index b5b019b..318fada 100644 --- a/blog-lab/app/main.py +++ b/blog-lab/app/main.py @@ -15,6 +15,7 @@ from .db import ( get_commissions, add_commission, update_commission, delete_commission, get_dashboard_stats, get_task, create_task, update_task, + add_brand_link, get_brand_links, update_brand_link, delete_brand_link, ) from .naver_search import analyze_keyword_with_crawling from .content_generator import generate_trend_brief, generate_blog_post, regenerate_blog_post @@ -126,6 +127,15 @@ class GenerateRequest(BaseModel): keyword_id: int # keyword_analyses.id +class LinkRequest(BaseModel): + url: str + product_name: str + keyword_id: Optional[int] = None + post_id: Optional[int] = None + description: str = "" + placement_hint: str = "" + + def _run_generate(task_id: str, keyword_id: int): """BackgroundTask: 트렌드 브리프 → 블로그 글 생성 → DB 저장.""" try: @@ -304,6 +314,33 @@ def publish_post(post_id: int, data: dict = None): return result +# ── 브랜드커넥트 링크 API ────────────────────────────────────────────────── + +@app.post("/api/blog-marketing/links", status_code=201) +def create_link(req: LinkRequest): + return add_brand_link(req.model_dump()) + + +@app.get("/api/blog-marketing/links") +def list_links(post_id: int = None, keyword_id: int = None): + return {"links": get_brand_links(post_id=post_id, keyword_id=keyword_id)} + + +@app.put("/api/blog-marketing/links/{link_id}") +def edit_link(link_id: int, data: dict): + result = update_brand_link(link_id, data) + if not result: + raise HTTPException(status_code=404, detail="Link not found") + return result + + +@app.delete("/api/blog-marketing/links/{link_id}") +def remove_link(link_id: int): + if not delete_brand_link(link_id): + raise HTTPException(status_code=404, detail="Link not found") + return {"ok": True} + + # ── 수익 추적 API ──────────────────────────────────────────────────────────── @app.get("/api/blog-marketing/commissions") diff --git a/blog-lab/tests/test_api_links.py b/blog-lab/tests/test_api_links.py new file mode 100644 index 0000000..810059e --- /dev/null +++ b/blog-lab/tests/test_api_links.py @@ -0,0 +1,85 @@ +"""브랜드커넥트 링크 API 테스트.""" +import os +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture(autouse=True) +def setup_db(tmp_path): + test_db = str(tmp_path / "test.db") + import app.config as config + config.DB_PATH = test_db + from app import db + db.DB_PATH = test_db + db.init_db() + yield + + +@pytest.fixture +def client(): + from app.main import app + return TestClient(app) + + +def test_create_link(client): + resp = client.post("/api/blog-marketing/links", json={ + "keyword_id": 1, + "url": "https://link.coupang.com/abc", + "product_name": "테스트 상품", + "description": "상품 설명", + }) + assert resp.status_code == 201 + data = resp.json() + assert data["url"] == "https://link.coupang.com/abc" + assert data["product_name"] == "테스트 상품" + + +def test_create_link_requires_url(client): + resp = client.post("/api/blog-marketing/links", json={ + "product_name": "상품", + }) + assert resp.status_code == 422 + + +def test_create_link_requires_product_name(client): + resp = client.post("/api/blog-marketing/links", json={ + "url": "https://a.com", + }) + assert resp.status_code == 422 + + +def test_list_links_by_keyword_id(client): + client.post("/api/blog-marketing/links", json={ + "keyword_id": 1, "url": "https://a.com", "product_name": "A", + }) + client.post("/api/blog-marketing/links", json={ + "keyword_id": 2, "url": "https://b.com", "product_name": "B", + }) + resp = client.get("/api/blog-marketing/links?keyword_id=1") + assert resp.status_code == 200 + assert len(resp.json()["links"]) == 1 + + +def test_update_link(client): + create_resp = client.post("/api/blog-marketing/links", json={ + "url": "https://a.com", "product_name": "원래", + }) + link_id = create_resp.json()["id"] + resp = client.put(f"/api/blog-marketing/links/{link_id}", json={ + "product_name": "새이름", + }) + assert resp.status_code == 200 + assert resp.json()["product_name"] == "새이름" + + +def test_delete_link(client): + create_resp = client.post("/api/blog-marketing/links", json={ + "url": "https://a.com", "product_name": "삭제", + }) + link_id = create_resp.json()["id"] + resp = client.delete(f"/api/blog-marketing/links/{link_id}") + assert resp.status_code == 200 + assert resp.json()["ok"] is True + + resp = client.delete(f"/api/blog-marketing/links/{link_id}") + assert resp.status_code == 404