o2o-castad-backend/app/user/models.py

248 lines
8.3 KiB
Python

"""
User 모듈 SQLAlchemy 모델 정의
카카오 소셜 로그인 기반 사용자 관리 모델입니다.
"""
from datetime import date, datetime
from typing import TYPE_CHECKING, List, Optional
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):
"""
사용자 테이블 (카카오 소셜 로그인)
카카오 로그인을 통해 인증된 사용자 정보를 저장합니다.
Attributes:
id: 고유 식별자 (자동 증가)
kakao_id: 카카오 고유 ID (필수, 유니크)
email: 이메일 주소 (선택, 카카오에서 제공 시)
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",
"mysql_charset": "utf8mb4",
"mysql_collate": "utf8mb4_unicode_ci",
},
)
# ==========================================================================
# 기본 식별자
# ==========================================================================
id: Mapped[int] = mapped_column(
BigInteger,
primary_key=True,
nullable=False,
autoincrement=True,
comment="고유 식별자",
)
# ==========================================================================
# 카카오 소셜 로그인 필수 정보
# ==========================================================================
kakao_id: Mapped[int] = mapped_column(
BigInteger,
nullable=False,
unique=True,
comment="카카오 고유 ID (회원번호)",
)
# ==========================================================================
# 카카오에서 제공하는 사용자 정보 (선택적)
# ==========================================================================
email: Mapped[Optional[str]] = mapped_column(
String(255),
nullable=True,
comment="이메일 주소 (카카오 계정 이메일, 동의 시 제공)",
)
nickname: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
comment="카카오 닉네임",
)
profile_image_url: Mapped[Optional[str]] = mapped_column(
String(2048),
nullable=True,
comment="카카오 프로필 이미지 URL",
)
thumbnail_image_url: Mapped[Optional[str]] = mapped_column(
String(2048),
nullable=True,
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: 여성)",
)
# ==========================================================================
# 계정 상태 관리
# ==========================================================================
is_active: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=True,
comment="계정 활성화 상태 (비활성화 시 로그인 차단)",
)
is_admin: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
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="삭제 일시",
)
# ==========================================================================
# 시간 정보
# ==========================================================================
last_login_at: Mapped[Optional[datetime]] = mapped_column(
DateTime,
nullable=True,
comment="마지막 로그인 일시",
)
created_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
comment="계정 생성 일시",
)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
onupdate=func.now(),
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"role='{self.role}', "
f"is_active={self.is_active}, "
f"is_deleted={self.is_deleted}"
f")>"
)