finish user model definition

insta
Dohyun Lim 2026-01-15 13:09:29 +09:00
parent bf7b53c8e8
commit d7120bb0ba
5 changed files with 242 additions and 16 deletions

View File

@ -72,7 +72,9 @@ async def create_db_tables():
import asyncio
# 모델 import (테이블 메타데이터 등록용)
from app.home.models import Image, Project # noqa: F401
# 주의: User를 먼저 import해야 UserProject가 User를 참조할 수 있음
from app.user.models import User # noqa: F401
from app.home.models import Image, Project, UserProject # noqa: F401
from app.lyric.models import Lyric # noqa: F401
from app.song.models import Song # noqa: F401
from app.video.models import Video # noqa: F401

View File

@ -4,19 +4,138 @@ Home 모듈 SQLAlchemy 모델 정의
모듈은 영상 제작 파이프라인의 핵심 데이터 모델을 정의합니다.
- Project: 프로젝트(사용자 입력 이력) 관리
- Image: 업로드된 이미지 URL 관리
- UserProject: User와 Project M:N 관계 중계 테이블
"""
from datetime import datetime
from typing import TYPE_CHECKING, List, Optional
from sqlalchemy import DateTime, Index, Integer, String, Text, func
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
# =============================================================================
# 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"<UserProject("
f"id={self.id}, "
f"user_id={self.user_id}, "
f"project_id={self.project_id}, "
f"role='{self.role}'"
f")>"
)
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
@ -39,6 +158,8 @@ class Project(Base):
lyrics: 생성된 가사 목록
songs: 생성된 노래 목록
videos: 최종 영상 결과 목록
user_projects: User와의 M:N 관계 (중계 테이블 통한 연결)
users: 프로젝트에 참여한 사용자 목록 (Association Proxy)
"""
__tablename__ = "project"
@ -123,6 +244,20 @@ class Project(Base):
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:

View File

@ -2,22 +2,19 @@
User 모듈 SQLAlchemy 모델 정의
카카오 소셜 로그인 기반 사용자 관리 모델입니다.
주의: 모델은 현재 개발 중이므로 create_db_tables()에서 import하지 않습니다.
테이블 생성이 필요할 app/database/session.py의 create_db_tables()
아래 import를 추가하세요:
from app.user.models import User # noqa: F401
"""
from datetime import datetime
from typing import Optional
from datetime import date, datetime
from typing import TYPE_CHECKING, List, Optional
from sqlalchemy import BigInteger, Boolean, DateTime, Index, String, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import BigInteger, Boolean, Date, DateTime, Index, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database.session import Base
if TYPE_CHECKING:
from app.home.models import UserProject
class User(Base):
"""
@ -32,25 +29,50 @@ class User(Base):
nickname: 카카오 닉네임 (선택)
profile_image_url: 카카오 프로필 이미지 URL (선택)
thumbnail_image_url: 카카오 썸네일 이미지 URL (선택)
phone: 전화번호 (선택)
name: 실명 (선택)
birth_date: 생년월일 (선택)
gender: 성별 (선택)
is_active: 계정 활성화 상태 (기본 True)
is_admin: 관리자 여부 (기본 False)
role: 사용자 권한 (user, manager, admin )
is_deleted: 소프트 삭제 여부 (기본 False)
deleted_at: 삭제 일시
last_login_at: 마지막 로그인 일시
created_at: 계정 생성 일시
updated_at: 계정 정보 수정 일시
권한 체계:
- user: 일반 사용자 (기본값)
- manager: 매니저 (일부 관리 권한)
- admin: 관리자 (is_admin=True와 동일)
소프트 삭제:
- is_deleted=True로 설정 삭제된 것으로 처리
- 실제 데이터는 DB에 유지됨
Relationships:
user_projects: Project와의 M:N 관계 (중계 테이블 통한 연결)
projects: 사용자가 참여한 프로젝트 목록 (Association Proxy)
카카오 API 응답 필드 매핑:
- kakao_id: id (카카오 회원번호)
- email: kakao_account.email
- nickname: kakao_account.profile.nickname 또는 properties.nickname
- profile_image_url: kakao_account.profile.profile_image_url
- thumbnail_image_url: kakao_account.profile.thumbnail_image_url
- birth_date: kakao_account.birthday 또는 kakao_account.birthyear
- gender: kakao_account.gender (male/female)
"""
__tablename__ = "user"
__table_args__ = (
Index("idx_user_kakao_id", "kakao_id", unique=True),
Index("idx_user_email", "email"),
Index("idx_user_phone", "phone"),
Index("idx_user_is_active", "is_active"),
Index("idx_user_is_deleted", "is_deleted"),
Index("idx_user_role", "role"),
Index("idx_user_created_at", "created_at"),
{
"mysql_engine": "InnoDB",
@ -107,6 +129,33 @@ class User(Base):
comment="카카오 썸네일 이미지 URL",
)
# ==========================================================================
# 추가 사용자 정보
# ==========================================================================
phone: Mapped[Optional[str]] = mapped_column(
String(20),
nullable=True,
comment="전화번호 (본인인증, 알림용)",
)
name: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
comment="실명 (법적 실명, 결제/계약 시 사용)",
)
birth_date: Mapped[Optional[date]] = mapped_column(
Date,
nullable=True,
comment="생년월일 (카카오 제공 또는 직접 입력)",
)
gender: Mapped[Optional[str]] = mapped_column(
String(10),
nullable=True,
comment="성별 (male: 남성, female: 여성)",
)
# ==========================================================================
# 계정 상태 관리
# ==========================================================================
@ -124,6 +173,29 @@ class User(Base):
comment="관리자 권한 여부",
)
role: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="user",
comment="사용자 권한 (user: 일반, manager: 매니저, admin: 관리자)",
)
# ==========================================================================
# 소프트 삭제
# ==========================================================================
is_deleted: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
comment="소프트 삭제 여부 (True: 삭제됨)",
)
deleted_at: Mapped[Optional[datetime]] = mapped_column(
DateTime,
nullable=True,
comment="삭제 일시",
)
# ==========================================================================
# 시간 정보
# ==========================================================================
@ -148,12 +220,28 @@ class User(Base):
comment="계정 정보 수정 일시",
)
# ==========================================================================
# Project M:N 관계 (중계 테이블 UserProject 통한 연결)
# ==========================================================================
# back_populates: UserProject.user와 양방향 연결
# cascade: User 삭제 시 UserProject 레코드도 삭제 (Project는 유지)
# lazy="selectin": N+1 문제 방지
# ==========================================================================
user_projects: Mapped[List["UserProject"]] = relationship(
"UserProject",
back_populates="user",
cascade="all, delete-orphan",
lazy="selectin",
)
def __repr__(self) -> str:
return (
f"<User("
f"id={self.id}, "
f"kakao_id={self.kakao_id}, "
f"nickname='{self.nickname}', "
f"is_active={self.is_active}"
f"role='{self.role}', "
f"is_active={self.is_active}, "
f"is_deleted={self.is_deleted}"
f")>"
)

View File

@ -340,9 +340,6 @@ class LogSettings(BaseSettings):
prj_settings = ProjectSettings()
apikey_settings = APIKeySettings()
db_settings = DatabaseSettings()
security_settings = SecuritySettings()
kakao_settings = KakaoSettings()
notification_settings = NotificationSettings()
cors_settings = CORSSettings()
crawler_settings = CrawlerSettings()
azure_blob_settings = AzureBlobSettings()

View File

@ -6,6 +6,10 @@ from scalar_fastapi import get_scalar_api_reference
from app.admin_manager import init_admin
from app.core.common import lifespan
from app.database.session import engine
# 주의: User 모델을 먼저 import해야 UserProject가 User를 참조할 수 있음
from app.user.models import User # noqa: F401
from app.home.api.routers.v1.home import router as home_router
from app.lyric.api.routers.v1.lyric import router as lyric_router
from app.song.api.routers.v1.song import router as song_router