# -*- 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"" ) 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"" )