ssulbox/backend/app/models.py

92 lines
4.9 KiB
Python

# -*- coding: utf-8 -*-
"""SQLAlchemy 모델. 사용자·콘텐츠(썰박스)·좋아요·댓글·크레딧 원장.
castad 컨벤션대로 소셜/크레딧 FK 는 숫자 PK 가 아니라 user_uuid(String36) 기준.
콘텐츠 id 는 생성 잡 폴더명(문자열)을 그대로 PK 로 쓴다.
"""
from datetime import datetime
from sqlalchemy import (
BigInteger, Boolean, DateTime, ForeignKey, Integer, String, Text,
UniqueConstraint, func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .database import Base, TimestampMixin
# SQLite 에서 BIGINT 는 rowid 별칭이 안 돼 autoincrement 가 동작하지 않는다 → sqlite 는 INTEGER 로.
BIGPK = BigInteger().with_variant(Integer, "sqlite")
class User(Base, TimestampMixin):
__tablename__ = "user"
id: Mapped[int] = mapped_column(BIGPK, primary_key=True, autoincrement=True)
user_uuid: Mapped[str] = mapped_column(String(36), unique=True, index=True)
nickname: Mapped[str] = mapped_column(String(50), default="사용자")
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
profile_image_url: Mapped[str | None] = mapped_column(Text, nullable=True) # data URL 또는 외부 URL
bio: Mapped[str | None] = mapped_column(String(200), nullable=True) # 한 줄 소개
kakao_id: Mapped[int | None] = mapped_column(BigInteger, unique=True, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
credits: Mapped[int] = mapped_column(Integer, default=10)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class Content(Base, TimestampMixin):
"""생성된 썰박스 1편. id = output 잡 폴더명."""
__tablename__ = "content"
id: Mapped[str] = mapped_column(String(120), primary_key=True)
scenario: Mapped[str] = mapped_column(String(20), index=True) # joseon|samgukji|greek
title: Mapped[str] = mapped_column(String(200), default="")
video: Mapped[str | None] = mapped_column(String(300), nullable=True) # /videos/... url
owner_uuid: Mapped[str | None] = mapped_column(String(36), index=True, nullable=True)
views: Mapped[int] = mapped_column(Integer, default=0)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
class Like(Base, TimestampMixin):
__tablename__ = "content_like"
__table_args__ = (UniqueConstraint("content_id", "user_uuid", name="uq_like_user_content"),)
id: Mapped[int] = mapped_column(BIGPK, primary_key=True, autoincrement=True)
content_id: Mapped[str] = mapped_column(String(120), ForeignKey("content.id"), index=True)
user_uuid: Mapped[str] = mapped_column(String(36), index=True)
class Comment(Base, TimestampMixin):
"""2-depth 댓글(댓글+대댓글). 익명 정책 — user_uuid 는 저장만, 응답에 노출 X."""
__tablename__ = "comment"
id: Mapped[int] = mapped_column(BIGPK, primary_key=True, autoincrement=True)
content_id: Mapped[str] = mapped_column(String(120), ForeignKey("content.id"), index=True)
user_uuid: Mapped[str] = mapped_column(String(36), index=True)
parent_id: Mapped[int | None] = mapped_column(BigInteger, ForeignKey("comment.id"), nullable=True, index=True)
nickname: Mapped[str] = mapped_column(String(50), default="익명")
body: Mapped[str] = mapped_column(String(300))
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
parent = relationship("Comment", remote_side=[id], backref="replies")
class CreditTransaction(Base, TimestampMixin):
"""크레딧 원장. amount +충전/-소모, balance_after 스냅샷."""
__tablename__ = "credit_transaction"
id: Mapped[int] = mapped_column(BIGPK, primary_key=True, autoincrement=True)
user_uuid: Mapped[str] = mapped_column(String(36), index=True)
amount: Mapped[int] = mapped_column(Integer)
balance_after: Mapped[int] = mapped_column(Integer)
type: Mapped[str] = mapped_column(String(20)) # charge|consume|refund|signup
reason: Mapped[str | None] = mapped_column(String(200), nullable=True)
class SocialAccount(Base, TimestampMixin):
"""SNS 연동 계정 (유튜브/인스타 등). OAuth 토큰은 실제 연동 시 채운다."""
__tablename__ = "social_account"
__table_args__ = (UniqueConstraint("user_uuid", "platform", name="uq_social_user_platform"),)
id: Mapped[int] = mapped_column(BIGPK, primary_key=True, autoincrement=True)
user_uuid: Mapped[str] = mapped_column(String(36), index=True)
platform: Mapped[str] = mapped_column(String(20)) # youtube|instagram|tiktok|facebook
account_name: Mapped[str] = mapped_column(String(120), default="")
access_token: Mapped[str | None] = mapped_column(Text, nullable=True)
refresh_token: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)