o2o-castad-backend/app/p2v/models.py
2026-08-26 13:52:18 +09:00

266 lines
9.4 KiB
Python

# -*- coding: utf-8 -*-
"""P2V 잡 모델.
⚠️ 이 프로젝트는 Alembic 마이그레이션을 쓰지 않는다. 스키마 변경은
docs/database-schema/migration_2026-08-25_p2v.sql 을 수동 실행한다.
테이블명은 산출물 종류를 따른다(p2v_video / p2v_poster). 클래스명의 F1/F2 는
P2V 서버 API 경로(/api/f1/*, /api/f2/*)와의 대응 관계를 가리킨다.
여기에는 P2V 서버가 못 가진 것만 둔다 — 소유권, 크레딧 앵커, Blob URL,
그리고 "P2V 없이도 이력이 완결"되기 위한 확정 메타데이터 스냅샷.
진행 중 잡의 상세(stages, 검수 중간값)는 P2V 가 진실 공급원이다.
"""
from datetime import datetime
from decimal import Decimal
from typing import Optional
from sqlalchemy import (
JSON,
BigInteger,
DateTime,
ForeignKey,
Index,
Integer,
Numeric,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.database.session import Base
class P2vF1Job(Base):
"""무빙 포스터(포스터→영상) 잡 — 테이블 p2v_video"""
__tablename__ = "p2v_video"
__table_args__ = (
UniqueConstraint("p2v_job_id", name="uq_p2v_video_ref"),
Index("idx_p2v_video_user_created", "user_uuid", "created_at"),
Index("idx_p2v_video_status", "status"),
{
"mysql_engine": "InnoDB",
"mysql_charset": "utf8mb4",
"mysql_collate": "utf8mb4_unicode_ci",
},
)
id: Mapped[int] = mapped_column(
BigInteger,
primary_key=True,
autoincrement=True,
comment="고유 식별자 (크레딧 원장 job_ref 앵커)",
)
user_uuid: Mapped[str] = mapped_column(
String(36),
ForeignKey("user.user_uuid", ondelete="CASCADE"),
nullable=False,
comment="생성 요청한 사용자 UUID",
)
p2v_job_id: Mapped[Optional[str]] = mapped_column(
String(64),
nullable=True,
comment="P2V 서버가 발급한 잡 id(F1). 서버 호출 성공 후 채워진다",
)
name: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
comment="사용자가 입력한 행사명 (비우면 서버가 포스터에서 추출)",
)
status: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="queued",
server_default="queued",
comment="상태 (queued/running/awaiting_review/archiving/done/failed)",
)
credit_amount: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=0,
server_default="0",
comment="차감한 크레딧 수량 (환불 금액 결정)",
)
# ==========================================================================
# 산출물 (Blob 업로드 후 채워짐). 명명은 ssul_content 관례:
# poster_url = 커버/썸네일(og:image 역할), 원본은 source_*, 결과물은 p2v_ 접두.
# ==========================================================================
p2v_video_url: Mapped[Optional[str]] = mapped_column(
String(500), nullable=True, comment="완성 영상 Blob URL"
)
poster_url: Mapped[Optional[str]] = mapped_column(
String(500),
nullable=True,
comment="썸네일 Blob URL (SNS 공유 og:image 역할, ssul_content.poster_url 관례)",
)
source_image_url: Mapped[Optional[str]] = mapped_column(
String(500), nullable=True, comment="원본 업로드 포스터 Blob URL"
)
# ==========================================================================
# 확정 메타데이터 (검수에서 사용자가 수정한 최종본만 보관)
# ==========================================================================
event_name: Mapped[Optional[str]] = mapped_column(
String(200), nullable=True, comment="행사명"
)
date_text: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True, comment="일시 표기"
)
place: Mapped[Optional[str]] = mapped_column(
String(200), nullable=True, comment="장소"
)
keywords: Mapped[Optional[list]] = mapped_column(
JSON, nullable=True, comment="키워드 목록"
)
narration: Mapped[Optional[list]] = mapped_column(
JSON, nullable=True, comment="나레이션 3문장"
)
duration: Mapped[Optional[Decimal]] = mapped_column(
Numeric(6, 2), nullable=True, comment="완성 영상 길이(초)"
)
error: Mapped[Optional[str]] = mapped_column(
String(1000), nullable=True, comment="실패 사유 (스테이지 + detail)"
)
archived_at: Mapped[Optional[datetime]] = mapped_column(
DateTime,
nullable=True,
comment="Blob 업로드 완료 일시. NULL 이면 아직 P2V 에만 있음",
)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.now(), comment="생성 일시"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
onupdate=func.now(),
comment="수정 일시",
)
def __repr__(self) -> str:
return (
f"<P2vF1Job(id={self.id}, user_uuid='{self.user_uuid}', "
f"p2v_job_id='{self.p2v_job_id}', status='{self.status}')>"
)
class P2vF2Job(Base):
"""포스터 스타일링 잡 — 테이블 p2v_poster"""
__tablename__ = "p2v_poster"
__table_args__ = (
UniqueConstraint("p2v_job_id", name="uq_p2v_poster_ref"),
Index("idx_p2v_poster_user_created", "user_uuid", "created_at"),
Index("idx_p2v_poster_status", "status"),
Index("idx_p2v_poster_source", "source_video_id"),
{
"mysql_engine": "InnoDB",
"mysql_charset": "utf8mb4",
"mysql_collate": "utf8mb4_unicode_ci",
},
)
id: Mapped[int] = mapped_column(
BigInteger,
primary_key=True,
autoincrement=True,
comment="고유 식별자 (크레딧 원장 job_ref 앵커)",
)
user_uuid: Mapped[str] = mapped_column(
String(36),
ForeignKey("user.user_uuid", ondelete="CASCADE"),
nullable=False,
comment="생성 요청한 사용자 UUID",
)
p2v_job_id: Mapped[Optional[str]] = mapped_column(
String(64), nullable=True, comment="P2V 서버가 발급한 잡 id(F2)"
)
name: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
comment="포스터 이름 (업로드 파일명에서 추출). 내 콘텐츠 카드 제목에 쓴다",
)
source_video_id: Mapped[Optional[int]] = mapped_column(
BigInteger,
ForeignKey("p2v_video.id", ondelete="SET NULL"),
nullable=True,
comment="원본 p2v_video 잡 (P2V source_slug 대응). 독립 잡이면 NULL",
)
status: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="queued",
server_default="queued",
comment="상태 (queued/running/archiving/done/failed)",
)
credit_amount: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=0,
server_default="0",
comment="차감한 크레딧 수량",
)
# ==========================================================================
# 입력 스냅샷 (템플릿이 삭제돼도 이력이 남아야 한다)
# ==========================================================================
template_id: Mapped[str] = mapped_column(
String(64), nullable=False, comment="사용한 스타일 템플릿 id"
)
template_name: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True, comment="템플릿 이름 스냅샷"
)
license: Mapped[Optional[str]] = mapped_column(
String(20),
nullable=True,
comment=(
"템플릿 배포 등급 (public-domain/internal-only/user-uploaded). "
"외부 공개 가부 판단"
),
)
format: Mapped[str] = mapped_column(
String(16),
nullable=False,
default="poster",
server_default="poster",
comment="출력 포맷 (poster/story/feed/square)",
)
# ==========================================================================
# 산출물 (명명은 p2v_video 와 동일 관례)
# ==========================================================================
p2v_poster_url: Mapped[Optional[str]] = mapped_column(
String(500), nullable=True, comment="스타일 변환 결과 Blob URL"
)
source_image_url: Mapped[Optional[str]] = mapped_column(
String(500), nullable=True, comment="원본 업로드 포스터 Blob URL"
)
error: Mapped[Optional[str]] = mapped_column(
String(1000), nullable=True, comment="실패 사유"
)
archived_at: Mapped[Optional[datetime]] = mapped_column(
DateTime, nullable=True, comment="Blob 업로드 완료 일시"
)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.now(), comment="생성 일시"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
onupdate=func.now(),
comment="수정 일시",
)
def __repr__(self) -> str:
return (
f"<P2vF2Job(id={self.id}, user_uuid='{self.user_uuid}', "
f"template_id='{self.template_id}', status='{self.status}')>"
)