fix(insta-lab): webhook이 렌더 PNG를 card_assets로 등록 (cutover 누락 복구)
2026-05-19 cutover(렌더를 Windows insta-render 워커로 이관)에서 card_assets 등록 단계가 새 설계에 누락됨. 구 card_renderer.render_slate가 NAS DB에 등록하던 것을, webhook은 task/slate status만 갱신하도록 만들어 card_assets가 영구 빈 상태 → /assets 404, /package 409, get_slate assets=0.
insta_update가 succeeded 시 워커 출력 디렉토리를 스캔해 실제 PNG만 card_assets에 등록(_register_rendered_assets). CARDS_DIR/{id}, INSTA_DATA_PATH/{id} 두 후보를 순서대로 스캔 → 경로 정합 전환기에도 견고. 신규 테스트 2건(등록 성공 / 파일 없으면 미등록).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,18 +10,50 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from . import db
|
||||
from . import config, db
|
||||
from .auth import verify_internal_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _register_rendered_assets(slate_id: int) -> int:
|
||||
"""워커가 저장한 10장 PNG를 card_assets로 등록.
|
||||
|
||||
cutover(2026-05-19) 후 렌더는 Windows insta-render 워커가 NAS SMB 볼륨에
|
||||
직접 쓰지만, NAS DB에 card_assets를 등록하는 단계가 누락됐었다. 이 함수가
|
||||
그 갭을 메운다. 워커 출력 경로 후보를 순서대로 스캔해 실제 파일만 등록한다
|
||||
(경로 정합 가드: CARDS_DIR 하위 / INSTA_DATA_PATH 직하 둘 다 수용).
|
||||
|
||||
저장하는 file_path는 insta-lab 컨테이너 내부 절대경로 →
|
||||
get_asset(FileResponse) / package(zip)가 그대로 읽는다.
|
||||
"""
|
||||
candidates = [
|
||||
os.path.join(config.CARDS_DIR, str(slate_id)), # /app/data/insta_cards/{id}
|
||||
os.path.join(config.INSTA_DATA_PATH, str(slate_id)), # /app/data/{id}
|
||||
]
|
||||
for base in candidates:
|
||||
if not os.path.isdir(base):
|
||||
continue
|
||||
count = 0
|
||||
for page in range(1, 11):
|
||||
fp = os.path.join(base, f"{page:02d}.png")
|
||||
if os.path.exists(fp) and os.path.getsize(fp) > 0:
|
||||
db.add_card_asset(slate_id, page, fp)
|
||||
count += 1
|
||||
if count:
|
||||
logger.info("card_assets 등록: slate=%s pages=%d dir=%s", slate_id, count, base)
|
||||
return count
|
||||
logger.warning("렌더 PNG를 찾지 못함: slate=%s (후보=%s)", slate_id, candidates)
|
||||
return 0
|
||||
|
||||
|
||||
class UpdatePayload(BaseModel):
|
||||
task_id: str
|
||||
status: str = Field(..., description="processing|succeeded|failed")
|
||||
@@ -56,12 +88,16 @@ def insta_update(payload: UpdatePayload):
|
||||
result_id=result_id,
|
||||
error=payload.error,
|
||||
)
|
||||
# succeeded 시 slate_status도 'rendered'로 갱신 (cutover 후 NAS가 처리)
|
||||
# succeeded 시 slate_status도 'rendered'로 갱신 + card_assets 등록 (cutover 후 NAS가 처리)
|
||||
if payload.status == "succeeded" and result_id is not None:
|
||||
try:
|
||||
db.update_slate_status(result_id, "rendered")
|
||||
except Exception:
|
||||
logger.exception("update_slate_status %s 실패 (무시)", result_id)
|
||||
try:
|
||||
_register_rendered_assets(result_id)
|
||||
except Exception:
|
||||
logger.exception("card_assets 등록 %s 실패 (무시)", result_id)
|
||||
logger.info(
|
||||
"internal/insta/update task=%s status=%s progress=%d",
|
||||
payload.task_id, payload.status, payload.progress,
|
||||
|
||||
@@ -78,3 +78,55 @@ def test_update_failed_records_error(client):
|
||||
task = db.get_task(tid)
|
||||
assert task["status"] == "failed"
|
||||
assert "Chromium" in (task.get("error") or "")
|
||||
|
||||
|
||||
def test_succeeded_registers_card_assets(client, tmp_path, monkeypatch):
|
||||
"""succeeded 시 워커가 쓴 PNG들을 card_assets로 등록 (cutover 후 누락된 단계)."""
|
||||
from app import config
|
||||
|
||||
# FK 충족용 실제 슬레이트
|
||||
sid = db.add_card_slate({"keyword": "금리", "category": "economy"})
|
||||
# 워커가 PNG 10장을 쓴 디렉토리 시뮬 (CARDS_DIR/{sid})
|
||||
cards_root = tmp_path / "insta_cards"
|
||||
sdir = cards_root / str(sid)
|
||||
sdir.mkdir(parents=True)
|
||||
for p in range(1, 11):
|
||||
(sdir / f"{p:02d}.png").write_bytes(b"\x89PNG\r\n\x1a\n" + b"x" * 100)
|
||||
monkeypatch.setattr(config, "CARDS_DIR", str(cards_root))
|
||||
monkeypatch.setattr(config, "INSTA_DATA_PATH", str(tmp_path))
|
||||
|
||||
tid = db.create_task("slate_render", {"slate_id": sid})
|
||||
r = client.post(
|
||||
"/api/internal/insta/update",
|
||||
headers={"X-Internal-Key": "test-secret"},
|
||||
json={
|
||||
"task_id": tid,
|
||||
"status": "succeeded",
|
||||
"progress": 100,
|
||||
"result_path": f"/media/insta/{sid}/01.png",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assets = db.list_card_assets(sid)
|
||||
assert len(assets) == 10
|
||||
assert assets[0]["page_index"] == 1
|
||||
assert assets[0]["file_path"].endswith("01.png")
|
||||
assert db.get_card_slate(sid)["status"] == "rendered"
|
||||
|
||||
|
||||
def test_succeeded_no_files_registers_nothing(client, tmp_path, monkeypatch):
|
||||
"""워커 출력이 없으면(파일 미존재) 잘못된 asset 등록 금지 — 200은 유지."""
|
||||
from app import config
|
||||
|
||||
sid = db.add_card_slate({"keyword": "환율", "category": "economy"})
|
||||
monkeypatch.setattr(config, "CARDS_DIR", str(tmp_path / "insta_cards"))
|
||||
monkeypatch.setattr(config, "INSTA_DATA_PATH", str(tmp_path))
|
||||
|
||||
tid = db.create_task("slate_render", {"slate_id": sid})
|
||||
r = client.post(
|
||||
"/api/internal/insta/update",
|
||||
headers={"X-Internal-Key": "test-secret"},
|
||||
json={"task_id": tid, "status": "succeeded", "progress": 100},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert db.list_card_assets(sid) == []
|
||||
|
||||
Reference in New Issue
Block a user