""" Home 모듈 SQLAlchemy 모델 정의 이 모듈은 영상 제작 파이프라인의 핵심 데이터 모델을 정의합니다. - Project: 프로젝트(사용자 입력 이력) 관리 - Image: 업로드된 이미지 URL 관리 - UserProject: User와 Project 간 M:N 관계 중계 테이블 """ from datetime import datetime from typing import TYPE_CHECKING, List, Optional from sqlalchemy import BigInteger, DateTime, ForeignKey, Index, Integer, String, Text, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.session import Base if TYPE_CHECKING: from app.lyric.models import Lyric from app.song.models import Song from app.user.models import User from app.video.models import Video # ============================================================================= # User-Project M:N 관계 중계 테이블 # ============================================================================= # # 설계 의도: # - User와 Project는 다대다(M:N) 관계입니다. # - 한 사용자는 여러 프로젝트에 참여할 수 있습니다. # - 한 프로젝트에는 여러 사용자가 참여할 수 있습니다. # # 중계 테이블 역할: # - UserProject 테이블이 두 테이블 간의 관계를 연결합니다. # - 각 레코드는 특정 사용자와 특정 프로젝트의 연결을 나타냅니다. # - 추가 속성(role, joined_at)으로 관계의 메타데이터를 저장합니다. # # 외래키 설정: # - user_id: User 테이블의 id를 참조 (ON DELETE CASCADE) # - project_id: Project 테이블의 id를 참조 (ON DELETE CASCADE) # - CASCADE 설정으로 부모 레코드 삭제 시 중계 레코드도 자동 삭제됩니다. # # 관계 방향: # - User.projects → UserProject → Project (사용자가 참여한 프로젝트 목록) # - Project.users → UserProject → User (프로젝트에 참여한 사용자 목록) # ============================================================================= class UserProject(Base): """ User-Project M:N 관계 중계 테이블 사용자와 프로젝트 간의 다대다 관계를 관리합니다. 한 사용자는 여러 프로젝트에 참여할 수 있고, 한 프로젝트에는 여러 사용자가 참여할 수 있습니다. Attributes: id: 고유 식별자 (자동 증가) user_id: 사용자 외래키 (User.id 참조) project_id: 프로젝트 외래키 (Project.id 참조) role: 프로젝트 내 사용자 역할 (owner: 소유자, member: 멤버, viewer: 조회자) joined_at: 프로젝트 참여 일시 외래키 제약조건: - user_id → user.id (ON DELETE CASCADE) - project_id → project.id (ON DELETE CASCADE) 유니크 제약조건: - (user_id, project_id) 조합은 유일해야 함 (중복 참여 방지) """ __tablename__ = "user_project" __table_args__ = ( Index("idx_user_project_user_id", "user_id"), Index("idx_user_project_project_id", "project_id"), Index("idx_user_project_user_project", "user_id", "project_id", unique=True), { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci", }, ) id: Mapped[int] = mapped_column( Integer, primary_key=True, nullable=False, autoincrement=True, comment="고유 식별자", ) # 외래키: User 테이블 참조 # - BigInteger 사용 (User.id가 BigInteger이므로 타입 일치 필요) # - ondelete="CASCADE": User 삭제 시 연결된 UserProject 레코드도 삭제 user_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("user.id", ondelete="CASCADE"), nullable=False, comment="사용자 외래키 (User.id 참조)", ) # 외래키: Project 테이블 참조 # - Integer 사용 (Project.id가 Integer이므로 타입 일치 필요) # - ondelete="CASCADE": Project 삭제 시 연결된 UserProject 레코드도 삭제 project_id: Mapped[int] = mapped_column( Integer, ForeignKey("project.id", ondelete="CASCADE"), nullable=False, comment="프로젝트 외래키 (Project.id 참조)", ) # ========================================================================== # Relationships (관계 설정) # ========================================================================== # back_populates: 양방향 관계 설정 (User.user_projects, Project.user_projects) # lazy="selectin": N+1 문제 방지를 위한 즉시 로딩 # ========================================================================== user: Mapped["User"] = relationship( "User", back_populates="user_projects", lazy="selectin", ) project: Mapped["Project"] = relationship( "Project", back_populates="user_projects", lazy="selectin", ) def __repr__(self) -> str: return ( f"" ) class Project(Base): """ 프로젝트 테이블 (사용자 입력 이력) 영상 제작 요청의 시작점으로, 고객 정보와 지역 정보를 저장합니다. 하위 테이블(Lyric, Song, Video)의 부모 테이블 역할을 합니다. Attributes: id: 고유 식별자 (자동 증가) store_name: 고객명 (필수) region: 지역명 (필수, 예: 서울, 부산, 대구 등) task_id: 작업 고유 식별자 (UUID 형식, 36자) detail_region_info: 상세 지역 정보 (선택, JSON 또는 텍스트 형식) created_at: 생성 일시 (자동 설정) Relationships: lyrics: 생성된 가사 목록 songs: 생성된 노래 목록 videos: 최종 영상 결과 목록 user_projects: User와의 M:N 관계 (중계 테이블 통한 연결) users: 프로젝트에 참여한 사용자 목록 (Association Proxy) """ __tablename__ = "project" __table_args__ = ( Index("idx_project_task_id", "task_id"), Index("idx_project_store_name", "store_name"), Index("idx_project_region", "region"), { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci", }, ) id: Mapped[int] = mapped_column( Integer, primary_key=True, nullable=False, autoincrement=True, comment="고유 식별자", ) store_name: Mapped[str] = mapped_column( String(255), nullable=False, index=True, comment="가게명", ) region: Mapped[str] = mapped_column( String(100), nullable=False, index=True, comment="지역명 (예: 군산)", ) task_id: Mapped[str] = mapped_column( String(36), nullable=False, comment="프로젝트 작업 고유 식별자 (UUID)", ) detail_region_info: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="상세 지역 정보", ) language: Mapped[str] = mapped_column( String(50), nullable=False, default="Korean", comment="출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)", ) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now(), comment="생성 일시", ) # Relationships lyrics: Mapped[List["Lyric"]] = relationship( "Lyric", back_populates="project", cascade="all, delete-orphan", lazy="selectin", ) songs: Mapped[List["Song"]] = relationship( "Song", back_populates="project", cascade="all, delete-orphan", lazy="selectin", ) videos: Mapped[List["Video"]] = relationship( "Video", back_populates="project", cascade="all, delete-orphan", lazy="selectin", ) # ========================================================================== # User M:N 관계 (중계 테이블 UserProject 통한 연결) # ========================================================================== # back_populates: UserProject.project와 양방향 연결 # cascade: Project 삭제 시 UserProject 레코드도 삭제 (User는 유지) # lazy="selectin": N+1 문제 방지 # ========================================================================== user_projects: Mapped[List["UserProject"]] = relationship( "UserProject", back_populates="project", cascade="all, delete-orphan", lazy="selectin", ) def __repr__(self) -> str: def truncate(value: str | None, max_len: int = 10) -> str: if value is None: return "None" return (value[:max_len] + "...") if len(value) > max_len else value return ( f"" ) class Image(Base): """ 업로드 이미지 테이블 사용자가 업로드한 이미지의 URL을 저장합니다. 독립적으로 관리되며 Project와 직접적인 관계가 없습니다. Attributes: id: 고유 식별자 (자동 증가) task_id: 이미지 업로드 작업 고유 식별자 (UUID) img_name: 이미지명 img_url: 이미지 URL (S3, CDN 등의 경로) created_at: 생성 일시 (자동 설정) """ __tablename__ = "image" __table_args__ = ( { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci", }, ) id: Mapped[int] = mapped_column( Integer, primary_key=True, nullable=False, autoincrement=True, comment="고유 식별자", ) task_id: Mapped[str] = mapped_column( String(36), nullable=False, comment="이미지 업로드 작업 고유 식별자 (UUID)", ) img_name: Mapped[str] = mapped_column( String(255), nullable=False, comment="이미지명", ) img_url: Mapped[str] = mapped_column( String(2048), nullable=False, comment="이미지 URL (blob, CDN 경로)", ) img_order: Mapped[int] = mapped_column( Integer, nullable=False, default=0, comment="이미지 순서", ) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now(), comment="생성 일시", ) def __repr__(self) -> str: task_id_str = ( (self.task_id[:10] + "...") if len(self.task_id) > 10 else self.task_id ) img_name_str = ( (self.img_name[:10] + "...") if len(self.img_name) > 10 else self.img_name ) return ( f"" )