Files
web-page-backend/insta-lab/app/internal_router.py
gahusb 0d1b04d322 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>
2026-06-11 01:17:54 +09:00

106 lines
3.7 KiB
Python

"""SP-4 — Windows insta-render → NAS internal webhook.
POST /api/internal/insta/update
- X-Internal-Key 인증 필수
- task DB row update (status, progress, result_path, error)
- result_path는 nginx 서빙 경로 (예: /media/insta/{slate_id}/01.png)
- succeeded 시 params에서 slate_id 추출 → result_id 세팅
"""
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 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")
progress: int = Field(..., ge=0, le=100)
result_path: Optional[str] = None
error: Optional[str] = None
@router.post(
"/api/internal/insta/update",
dependencies=[Depends(verify_internal_key)],
)
def insta_update(payload: UpdatePayload):
task = db.get_task(payload.task_id)
if task is None:
raise HTTPException(404, f"task not found: {payload.task_id}")
result_id = None
if payload.status == "succeeded":
try:
# DB stores params (not input_data) from create_task
params_data = json.loads(task.get("params") or "{}")
result_id = params_data.get("slate_id")
except (ValueError, TypeError):
pass
db.update_task(
payload.task_id,
payload.status,
payload.progress,
message=payload.result_path or "",
result_id=result_id,
error=payload.error,
)
# 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,
)
return {"ok": True}