119 lines
4.4 KiB
Python
119 lines
4.4 KiB
Python
from datetime import datetime
|
|
from typing import TYPE_CHECKING, List, Optional
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
Boolean,
|
|
CheckConstraint,
|
|
DateTime,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
String,
|
|
func,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database.session import Base
|
|
|
|
if TYPE_CHECKING:
|
|
from app.ssulbox.models import SsulContent
|
|
from app.user.models import User
|
|
from app.video.models import Video
|
|
|
|
|
|
class Comment(Base):
|
|
"""
|
|
영상 댓글 테이블
|
|
|
|
2-depth 구조 (최상위 댓글 + 대댓글 1단계).
|
|
parent_id가 NULL이면 최상위 댓글, 값이 있으면 대댓글.
|
|
작성자 닉네임은 카카오 로그인 정보를 작성 시점에 그대로 저장한 스냅샷이며,
|
|
프로필 이미지는 별도 컬럼 없이 응답 시 User 테이블을 조인해 최신값을 조회한다.
|
|
|
|
**ADO2 영상과 썰박스 콘텐츠를 모두 담는다.** 대상은 `video_id` 또는 `content_id`
|
|
중 **정확히 하나**만 채워지며, 이를 DB `CHECK` 로 강제한다. MySQL 은 하나의 FK 가
|
|
두 테이블을 조건부로 가리키게 할 수 없어 컬럼을 나눠 둔 것이다.
|
|
별도의 종류 컬럼은 두지 않는다 — 어느 FK 가 채워졌는지가 곧 종류이고,
|
|
컬럼을 하나 더 두면 둘이 어긋날 수 있다.
|
|
"""
|
|
|
|
__tablename__ = "comment"
|
|
__table_args__ = (
|
|
CheckConstraint(
|
|
"(video_id IS NULL) <> (content_id IS NULL)",
|
|
name="ck_comment_one_target",
|
|
),
|
|
Index("idx_comment_video_id", "video_id"),
|
|
Index("idx_comment_content_id", "content_id"),
|
|
Index("idx_comment_user_uuid", "user_uuid"),
|
|
Index("idx_comment_parent_id", "parent_id"),
|
|
Index("idx_comment_is_deleted", "is_deleted"),
|
|
{
|
|
"mysql_engine": "InnoDB",
|
|
"mysql_charset": "utf8mb4",
|
|
"mysql_collate": "utf8mb4_unicode_ci",
|
|
},
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(
|
|
Integer, primary_key=True, autoincrement=True, comment="고유 식별자"
|
|
)
|
|
# 대상은 아래 둘 중 **정확히 하나**만 채워진다 (ck_comment_one_target).
|
|
video_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer,
|
|
ForeignKey("video.id", ondelete="CASCADE"),
|
|
nullable=True,
|
|
comment="ADO2 영상 id (썰박스 댓글이면 NULL)",
|
|
)
|
|
content_id: Mapped[Optional[int]] = mapped_column(
|
|
# ssul_content.id 는 BIGINT 다. INT 로 두면 FK 타입 불일치(errno 3780).
|
|
BigInteger,
|
|
ForeignKey("ssul_content.id", ondelete="CASCADE"),
|
|
nullable=True,
|
|
comment="썰박스 콘텐츠 id (ADO2 댓글이면 NULL)",
|
|
)
|
|
user_uuid: Mapped[str] = mapped_column(
|
|
ForeignKey("user.user_uuid", ondelete="CASCADE"),
|
|
nullable=False,
|
|
comment="작성자 UUID (응답 미노출, 권한 검증용)",
|
|
)
|
|
parent_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer,
|
|
ForeignKey("comment.id", ondelete="CASCADE"),
|
|
nullable=True,
|
|
comment="NULL=최상위 댓글, 값=대댓글의 부모 id",
|
|
)
|
|
nickname: Mapped[Optional[str]] = mapped_column(
|
|
String(50), nullable=True, comment="댓글 작성자 카카오 닉네임 스냅샷 (null이면 익명)"
|
|
)
|
|
content: Mapped[str] = mapped_column(
|
|
String(100), nullable=False, comment="댓글 본문 (한글 기준 100자 이내)"
|
|
)
|
|
is_deleted: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=False, comment="소프트 삭제 여부"
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
nullable=False,
|
|
server_default=func.now(),
|
|
comment="작성 일시",
|
|
)
|
|
|
|
# 썰박스 댓글이면 None 이다. 접근하는 쪽에서 반드시 방어할 것.
|
|
video: Mapped[Optional["Video"]] = relationship(
|
|
"Video", foreign_keys=[video_id], back_populates="comments"
|
|
)
|
|
content_ref: Mapped[Optional["SsulContent"]] = relationship(
|
|
"SsulContent", foreign_keys=[content_id], lazy="noload"
|
|
)
|
|
user: Mapped["User"] = relationship("User", back_populates="comments")
|
|
parent: Mapped[Optional["Comment"]] = relationship(
|
|
"Comment", remote_side=[id], back_populates="replies"
|
|
)
|
|
replies: Mapped[List["Comment"]] = relationship(
|
|
"Comment",
|
|
back_populates="parent",
|
|
cascade="all, delete-orphan",
|
|
)
|