사이드프로젝트

[EggChatter] DB 설계와 SQLAlchemy로 모델 정의

코줍 2025. 3. 4. 21:03

MVP

  • 사용자 등록/로그인 : 카카오 로그인만 지원
  • 채팅 기능 :
    • 1:1 실시간 채팅가능
    • 초대된 유저 혹은 친구관계인 유저와 채팅가능
  • 이스터에그 등록 : 최대 3개의 이스터에그 생성가능. ( 수정 및 삭제도 가능 )
  • 이스터에그 힌트 기능 : 유저가 힌트를 받을 수 있는 기능.
  • 대화 중 이스터에그 트리거 : 채팅 도중 특정 단어가 입력되면 이스터에그가 트리거됨.

ERD 다이어그램

Users 테이블

  • 카카오 로그인 외에도 다른 소셜 로그인 및 자체 로그인을 지원할 수 있도록 설계

ChatroomUsers 테이블

  • 채팅방과 유저를 다대다로 매칭하기 위해 필요한 관계테이블

MessageReadStatus 테이블

  • 다대일 채팅 설계를 대비하여, 읽은 사람을 추적하는 로직에 필요한 테이블 설계

Friend 테이블

  • 채티방 초대로 가입하게 된 유저가 초대자와 친구가 될 수 있도록 설계

EasterEggHistory & EasterEggHintHistory

  • 추후 이스터에그 관련한 대시보드 조회할 수 있도록 설계

ORM 모델 정의

CommonFields 클래스 ( models/base.py )

 

모든 테이블에 created_at과 updated_at 필드를 포함하고 있어, 다른 테이블 모델을 정의할 때 이를 상속하기 위해 만든 클래스이다.

from datetime import datetime

from pydantic import Base
from sqlalchemy import Column, DateTime, func


class CommonFields(Base):
    """공통 필드 클래스"""

    __abstract__ = True

    created_at = Column(DateTime, default=func.current_timestamp(), nullable=False)
    updated_at = Column(
        DateTime,
        default=func.current_timestamp(),
        onupdate=func.current_timestamp(),
        nullable=False,
    )

    class Config:
        orm_mode = True

 

해당 클래스는 상속을 목적으로만 만든 클래스 때문에  __abstract__ = True 를 이용해 추상클래스로 정의했다.

 

Users ( models/user.py ), ChatRoomsUsers ChatRoos ( models/chats.py )
# models/users.py
class Users(CommonFields):
    """유저 테이블"""

    __tablename__ = "users"
    id = Column(Integer, primary_key=True, autoincrement=True)
    email = Column(String(128), unique=True, nullable=False, comment="회원 이메일")
    social_id = Column(String(64), nullable=True, comment="소셜로그인 key")
    login_type = Column(
        String(40), nullable=False, comment="로그인 타입 (KAKAO, EMAIL, ...)"
    )
    password = Column(String(64), nullable=True, comment="비밀번호")
    profile = Column(String(64), nullable=True, comment="프로필 이미지")
    nickname = Column(String(40), nullable=False, comment="닉네임")
    is_admin = Column(Integer, default=0, nullable=False, comment="어드민 여부")

    chatrooms = relationship("ChatRooms", secondary="chat_rooms_users")

# models/chats.py
class ChatRooms(CommonFields):
    __tablename__ = "chat_rooms"

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(64), nullable=False)
    status = Column(
        String(4),
        nullable=False,
        comment="I : 초대는 했는데, 초대수락 안됨, O : 대화중, L: 대화 후 상대방이 나감.",
    )

    users = relationship(
        "Users", secondary="chat_rooms_users", back_populates="chatrooms"
    )


class ChatRoomsUsers(CommonFields):
    __tablename__ = "chat_rooms_users"

    user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
    chatroom_id = Column(Integer, ForeignKey("chat_rooms.id"), primary_key=True)

    user = relationship("Users", back_populates="chatrooms")
    chatroom = relationship("ChatRooms", back_populates="users")

 

secondary는 SqlAlchemy에서 다대다 관계를 정의하는 옵션이다. 

다대다 관계를 맽기 위해 중간에서 처리해줄 ChatroomsUsers 정의하기 위해 Users와 ChatRooms 모델에 이 옵션을 이용하여 중간테이블을 명시해줬다.

 

back_populates는 SqlAlchemy에서 양방향으로 접근가능하게 해주는 옵션이다.

양쪽 모델에 모두 정의해줄 필요없이 한쪽 모델에만 정의해도 SQLAlchemy에서 알아서 매핑해준다.

나는 ChatRooms 모델에만 명시해줬다.

 

Messages ( models/chats.py )
class ChatRoomsUsers(CommonFields):
    """채팅방-유저 관계 테이블"""

    __tablename__ = "chat_rooms_users"

    user_id = Column(BigInteger, ForeignKey("users.id"), primary_key=True)
    chatroom_id = Column(BigInteger, ForeignKey("chat_rooms.id"), primary_key=True)

    user = relationship("Users", back_populates="chatrooms")
    chatroom = relationship("ChatRooms", back_populates="users")


class Messages(CommonFields):
    """메세지 테이블"""

    __tablename__ = "messages"

    id = Column(BigInteger, primary_key=True, autoincrement=True)
    chatroom_id = Column(BigInteger, ForeignKey("chatrooms.id"), nullable=False)
    sender_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
    content = Column(Text, nullable=False)


class MessageReadStatus(CommonFields):
    """메세지읽음 상태 테이블"""

    __tablename__ = "message_read_status"

    message_id = Column(BigInteger, ForeignKey("messages.id"), primary_key=True)
    reader_id = Column(BigInteger, ForeignKey("users.id"), primary_key=True)
    read_at = Column(DateTime, nullable=False, default=func.current_timestamp())

 

Friend, Invitation ( models/friends.py ) 
from sqlalchemy import BigInteger, Column, ForeignKey, Integer, String

from app.models.base import CommonFields


class Invitation(CommonFields):
    """초대 테이블"""

    __tablename__ = "invitations"

    id = Column(BigInteger, primary_key=True, autoincrement=True)
    link = Column(String(256), nullable=False)
    chatroom_id = Column(BigInteger, ForeignKey("chatrooms.id"), nullable=False)
    inviter_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
    invitee_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
    is_valid = Column(Integer, default=1)

class Friend(CommonFields):
    """친구 테이블"""

    __tablename__ = "friends"

    invitation_id = Column(BigInteger, ForeignKey("invitations.id"), primary_key=True)
    request_from = Column(BigInteger, ForeignKey("users.id"), nullable=False)
    request_to = Column(BigInteger, ForeignKey("users.id"), nullable=False)
    status = Column(String(4), nullable=False, comment="A : 친구, I : 친구삭제, B : 차단")
EasterEggs, EasterEggHintHistory, EasterEggHistory ( models/easter_eggs.py )
from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, Integer, String, func

from app.models.base import CommonFields


class EasterEgg(CommonFields):
    """이스터에그 테이블"""

    __tablename__ = "easter_eggs"

    id = Column(BigInteger, primary_key=True, autoincrement=True)
    user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
    trigger_word = Column(String(16), nullable=False)
    gif_url = Column(String(256), nullable=False)
    hint = Column(String(16), nullable=False)


class EasterEggHintHistory(CommonFields):
    """이스터에그 힌트 히스토리"""

    __tablename__ = "easter_egg_hint_history"

    id = Column(BigInteger, primary_key=True, autoincrement=True)
    user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
    chatroom_id = Column(BigInteger, ForeignKey("chatrooms.id"), nullable=False)
    easter_egg_id = Column(BigInteger, ForeignKey("easter_eggs.id"), nullable=False)
    is_used = Column(Integer, default=0)
    used_at = Column(DateTime, nullable=False, default=func.current_timestamp())


class EasterEggHistory(CommonFields):
    """이스터에그 히스토리 테이블"""

    __tablename__ = "easter_egg_history"

    id = Column(BigInteger, primary_key=True, autoincrement=True)
    trigger_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
    chatroom_id = Column(BigInteger, ForeignKey("chatrooms.id"), nullable=False)
    easter_egg_id = Column(BigInteger, ForeignKey("easter_eggs.id"), nullable=False)
    is_used = Column(Integer, default=0)
    triggered_at = Column(DateTime, nullable=False, default=func.current_timestamp())