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:
2026-06-11 01:17:54 +09:00
parent 8b6b251225
commit 0d1b04d322
2 changed files with 90 additions and 2 deletions

View File

@@ -10,18 +10,50 @@ from __future__ import annotations
import json import json
import logging import logging
import os
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from . import db from . import config, db
from .auth import verify_internal_key from .auth import verify_internal_key
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() 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): class UpdatePayload(BaseModel):
task_id: str task_id: str
status: str = Field(..., description="processing|succeeded|failed") status: str = Field(..., description="processing|succeeded|failed")
@@ -56,12 +88,16 @@ def insta_update(payload: UpdatePayload):
result_id=result_id, result_id=result_id,
error=payload.error, 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: if payload.status == "succeeded" and result_id is not None:
try: try:
db.update_slate_status(result_id, "rendered") db.update_slate_status(result_id, "rendered")
except Exception: except Exception:
logger.exception("update_slate_status %s 실패 (무시)", result_id) 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( logger.info(
"internal/insta/update task=%s status=%s progress=%d", "internal/insta/update task=%s status=%s progress=%d",
payload.task_id, payload.status, payload.progress, payload.task_id, payload.status, payload.progress,

View File

@@ -78,3 +78,55 @@ def test_update_failed_records_error(client):
task = db.get_task(tid) task = db.get_task(tid)
assert task["status"] == "failed" assert task["status"] == "failed"
assert "Chromium" in (task.get("error") or "") 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) == []