diff --git a/insta-lab/app/main.py b/insta-lab/app/main.py index 93d1610..e08d974 100644 --- a/insta-lab/app/main.py +++ b/insta-lab/app/main.py @@ -1,14 +1,16 @@ """FastAPI entrypoint for insta-lab.""" import asyncio +import io import json import logging import os +import zipfile from typing import Optional from fastapi import FastAPI, HTTPException, BackgroundTasks, Body, Query from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, StreamingResponse from pydantic import BaseModel from _shared.access_log import install as install_access_log @@ -247,6 +249,35 @@ def get_asset(slate_id: int, page: int): return FileResponse(match["file_path"], media_type="image/png") +@app.get("/api/insta/slates/{slate_id}/package") +def download_package(slate_id: int): + slate = db.get_card_slate(slate_id) + if not slate: + raise HTTPException(404, "slate not found") + assets = sorted(db.list_card_assets(slate_id), key=lambda a: a["page_index"]) + if not assets: + raise HTTPException(409, "아직 렌더된 카드가 없습니다") + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z: + for a in assets: + fp = a["file_path"] + if os.path.exists(fp): + z.write(fp, arcname=f"{a['page_index']:02d}.png") + caption = (slate.get("suggested_caption") or "").strip() + tags = slate.get("hashtags") or [] + if isinstance(tags, str): + try: + tags = json.loads(tags) + except Exception: + tags = [] + caption_full = caption + ("\n\n" + " ".join(tags) if tags else "") + z.writestr("caption.txt", caption_full) + buf.seek(0) + return StreamingResponse(buf, media_type="application/zip", headers={ + "Content-Disposition": f'attachment; filename="insta_slate_{slate_id}.zip"' + }) + + @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/app/test_package_api.py b/insta-lab/app/test_package_api.py new file mode 100644 index 0000000..70f83d6 --- /dev/null +++ b/insta-lab/app/test_package_api.py @@ -0,0 +1,45 @@ +import io, os, tempfile, zipfile, sys +from fastapi.testclient import TestClient + + +def _client(monkeypatch): + # Insert web-backend root (3 levels up from this file) so _shared is importable + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + from app import config, db + tmp = tempfile.mkdtemp() + monkeypatch.setattr(config, "INSTA_DATA_PATH", tmp, raising=False) + monkeypatch.setattr(db, "DB_PATH", os.path.join(tmp, "insta.db"), raising=False) + db.init_db() + from app.main import app + return TestClient(app), db, tmp + + +def test_package_zip_contains_pngs_and_caption(monkeypatch): + client, db, tmp = _client(monkeypatch) + # 슬레이트 + 2개 asset(실제 PNG 파일) 시드 + sid = db.add_card_slate({ + "keyword": "k", + "category": "economy", + "status": "rendered", + "cover_copy": {"headline": "h"}, + "body_copies": [{"headline": "b", "body": "x"}] * 8, + "cta_copy": {}, + "suggested_caption": "캡션입니다", + "hashtags": ["#a", "#b"], + }) + cards_dir = os.path.join(tmp, "insta_cards", str(sid)) + os.makedirs(cards_dir, exist_ok=True) + for pg in (1, 2): + fp = os.path.join(cards_dir, f"{pg:02d}.png") + with open(fp, "wb") as f: + f.write(b"\x89PNG\r\n" + b"0" * 2000) + db.add_card_asset(slate_id=sid, page_index=pg, file_path=fp) + r = client.get(f"/api/insta/slates/{sid}/package") + assert r.status_code == 200 + assert r.headers["content-type"] == "application/zip" + z = zipfile.ZipFile(io.BytesIO(r.content)) + names = z.namelist() + assert any(n.endswith(".png") for n in names) + assert "caption.txt" in names + cap = z.read("caption.txt").decode("utf-8") + assert "캡션입니다" in cap and "#a" in cap