266 lines
9.4 KiB
Python
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}')>"
|
|
)
|