608 lines
20 KiB
Python
608 lines
20 KiB
Python
"""
|
|
User 모듈 SQLAlchemy 모델 정의
|
|
|
|
카카오 소셜 로그인 기반 사용자 관리 모델입니다.
|
|
"""
|
|
|
|
from datetime import date, datetime
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING, List, Optional
|
|
|
|
from sqlalchemy import BigInteger, Boolean, Date, DateTime, ForeignKey, Index, Integer, String, Text, func
|
|
from sqlalchemy.dialects.mysql import JSON
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database.session import Base
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from app.home.models import Project
|
|
|
|
|
|
class User(Base):
|
|
"""
|
|
사용자 테이블 (카카오 소셜 로그인)
|
|
|
|
카카오 로그인을 통해 인증된 사용자 정보를 저장합니다.
|
|
|
|
Attributes:
|
|
id: 고유 식별자 (자동 증가)
|
|
kakao_id: 카카오 고유 ID (필수, 유니크)
|
|
user_uuid: 사용자 식별을 위한 UUID7 (필수, 유니크)
|
|
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:
|
|
projects: 사용자가 소유한 프로젝트 목록 (1:N 관계)
|
|
|
|
카카오 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"),
|
|
Index("idx_user_uuid", "user_uuid"),
|
|
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 (회원번호)",
|
|
)
|
|
|
|
user_uuid: Mapped[str] = mapped_column(
|
|
String(36),
|
|
nullable=False,
|
|
unique=True,
|
|
comment="사용자 식별을 위한 UUID7 (시간순 정렬 가능한 UUID)",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# 카카오에서 제공하는 사용자 정보 (선택적)
|
|
# ==========================================================================
|
|
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 1:N 관계 (한 사용자가 여러 프로젝트를 소유)
|
|
# ==========================================================================
|
|
# back_populates: Project.owner와 양방향 연결
|
|
# cascade: 사용자 삭제 시 프로젝트는 유지 (owner가 NULL로 설정됨)
|
|
# lazy="selectin": N+1 문제 방지
|
|
# ==========================================================================
|
|
projects: Mapped[List["Project"]] = relationship(
|
|
"Project",
|
|
back_populates="owner",
|
|
lazy="selectin",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# RefreshToken 1:N 관계
|
|
# ==========================================================================
|
|
# 한 사용자는 여러 리프레시 토큰을 가질 수 있음 (다중 기기 로그인)
|
|
# ==========================================================================
|
|
refresh_tokens: Mapped[List["RefreshToken"]] = relationship(
|
|
"RefreshToken",
|
|
back_populates="user",
|
|
cascade="all, delete-orphan",
|
|
lazy="selectin",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# SocialAccount 1:N 관계
|
|
# ==========================================================================
|
|
# 한 사용자는 여러 소셜 계정을 연동할 수 있음 (YouTube, Instagram, Facebook)
|
|
# ==========================================================================
|
|
social_accounts: Mapped[List["SocialAccount"]] = relationship(
|
|
"SocialAccount",
|
|
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")>"
|
|
)
|
|
|
|
|
|
class RefreshToken(Base):
|
|
"""
|
|
리프레시 토큰 테이블
|
|
|
|
JWT 리프레시 토큰을 DB에 저장하여 관리합니다.
|
|
토큰 폐기(로그아웃), 다중 기기 로그인 관리 등에 활용됩니다.
|
|
|
|
Attributes:
|
|
id: 고유 식별자 (자동 증가)
|
|
user_id: 사용자 외래키 (User.id 참조)
|
|
user_uuid: 사용자 UUID (User.user_uuid 참조)
|
|
token_hash: 리프레시 토큰의 SHA-256 해시값 (원본 저장 X)
|
|
expires_at: 토큰 만료 일시
|
|
is_revoked: 토큰 폐기 여부 (로그아웃 시 True)
|
|
created_at: 토큰 생성 일시
|
|
revoked_at: 토큰 폐기 일시
|
|
user_agent: 접속 기기 정보 (브라우저, OS 등)
|
|
ip_address: 접속 IP 주소
|
|
|
|
보안 고려사항:
|
|
- 토큰 원본은 저장하지 않고 해시값만 저장
|
|
- 토큰 검증 시 해시값 비교로 유효성 확인
|
|
- 로그아웃 시 is_revoked=True로 설정하여 재사용 방지
|
|
"""
|
|
|
|
__tablename__ = "refresh_token"
|
|
__table_args__ = (
|
|
Index("idx_refresh_token_user_id", "user_id"),
|
|
Index("idx_refresh_token_user_uuid", "user_uuid"),
|
|
Index("idx_refresh_token_token_hash", "token_hash", unique=True),
|
|
Index("idx_refresh_token_expires_at", "expires_at"),
|
|
Index("idx_refresh_token_is_revoked", "is_revoked"),
|
|
{
|
|
"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_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
ForeignKey("user.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
comment="사용자 외래키 (User.id 참조)",
|
|
)
|
|
|
|
user_uuid: Mapped[str] = mapped_column(
|
|
String(36),
|
|
nullable=False,
|
|
comment="사용자 UUID (User.user_uuid 참조)",
|
|
)
|
|
|
|
token_hash: Mapped[str] = mapped_column(
|
|
String(64),
|
|
nullable=False,
|
|
comment="리프레시 토큰 SHA-256 해시값",
|
|
)
|
|
|
|
expires_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
nullable=False,
|
|
comment="토큰 만료 일시",
|
|
)
|
|
|
|
is_revoked: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
comment="토큰 폐기 여부 (True: 폐기됨)",
|
|
)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
nullable=False,
|
|
server_default=func.now(),
|
|
comment="토큰 생성 일시",
|
|
)
|
|
|
|
revoked_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime,
|
|
nullable=True,
|
|
comment="토큰 폐기 일시",
|
|
)
|
|
|
|
user_agent: Mapped[Optional[str]] = mapped_column(
|
|
String(500),
|
|
nullable=True,
|
|
comment="접속 기기 정보 (브라우저, OS 등)",
|
|
)
|
|
|
|
ip_address: Mapped[Optional[str]] = mapped_column(
|
|
String(45),
|
|
nullable=True,
|
|
comment="접속 IP 주소 (IPv4/IPv6)",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# User 관계
|
|
# ==========================================================================
|
|
user: Mapped["User"] = relationship(
|
|
"User",
|
|
back_populates="refresh_tokens",
|
|
lazy="selectin", # lazy loading 방지
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"<RefreshToken("
|
|
f"id={self.id}, "
|
|
f"user_id={self.user_id}, "
|
|
f"is_revoked={self.is_revoked}, "
|
|
f"expires_at={self.expires_at}"
|
|
f")>"
|
|
)
|
|
|
|
|
|
class Platform(str, Enum):
|
|
"""소셜 플랫폼 구분"""
|
|
|
|
YOUTUBE = "youtube"
|
|
INSTAGRAM = "instagram"
|
|
FACEBOOK = "facebook"
|
|
TIKTOK = "tiktok"
|
|
|
|
|
|
class SocialAccount(Base):
|
|
"""
|
|
소셜 계정 연동 테이블
|
|
|
|
사용자가 연동한 외부 소셜 플랫폼 계정 정보를 저장합니다.
|
|
YouTube, Instagram, Facebook 계정 연동을 지원합니다.
|
|
|
|
Attributes:
|
|
id: 고유 식별자 (자동 증가)
|
|
user_id: 사용자 외래키 (User.id 참조)
|
|
platform: 플랫폼 구분 (youtube, instagram, facebook)
|
|
access_token: OAuth 액세스 토큰
|
|
refresh_token: OAuth 리프레시 토큰 (선택)
|
|
token_expires_at: 토큰 만료 일시
|
|
scope: 허용된 권한 범위
|
|
platform_user_id: 플랫폼 내 사용자 고유 ID
|
|
platform_username: 플랫폼 내 사용자명/핸들
|
|
platform_data: 플랫폼별 추가 정보 (JSON)
|
|
is_active: 연동 활성화 상태
|
|
connected_at: 연동 일시
|
|
updated_at: 정보 수정 일시
|
|
|
|
플랫폼별 platform_data 예시:
|
|
- YouTube: {"channel_id": "UC...", "channel_title": "채널명"}
|
|
- Instagram: {"business_account_id": "...", "facebook_page_id": "..."}
|
|
- Facebook: {"page_id": "...", "page_access_token": "..."}
|
|
|
|
Relationships:
|
|
user: 연동된 사용자 (User 테이블 참조)
|
|
"""
|
|
|
|
__tablename__ = "social_account"
|
|
__table_args__ = (
|
|
Index("idx_social_account_user_uuid", "user_uuid"),
|
|
Index("idx_social_account_platform", "platform"),
|
|
Index("idx_social_account_is_active", "is_active"),
|
|
Index("idx_social_account_is_deleted", "is_deleted"),
|
|
Index(
|
|
"uq_user_platform_account",
|
|
"user_uuid",
|
|
"platform",
|
|
"platform_user_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_uuid: Mapped[str] = mapped_column(
|
|
String(36),
|
|
ForeignKey("user.user_uuid", ondelete="CASCADE"),
|
|
nullable=False,
|
|
comment="사용자 외래키 (User.user_uuid 참조)",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# 플랫폼 구분
|
|
# ==========================================================================
|
|
platform: Mapped[Platform] = mapped_column(
|
|
String(20),
|
|
nullable=False,
|
|
comment="플랫폼 구분 (youtube, instagram, facebook, tiktok)",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# OAuth 토큰 정보
|
|
# ==========================================================================
|
|
access_token: Mapped[str] = mapped_column(
|
|
Text,
|
|
nullable=False,
|
|
comment="OAuth 액세스 토큰",
|
|
)
|
|
|
|
refresh_token: Mapped[Optional[str]] = mapped_column(
|
|
Text,
|
|
nullable=True,
|
|
comment="OAuth 리프레시 토큰 (플랫폼에 따라 선택적)",
|
|
)
|
|
|
|
token_expires_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime,
|
|
nullable=True,
|
|
comment="토큰 만료 일시",
|
|
)
|
|
|
|
scope: Mapped[Optional[str]] = mapped_column(
|
|
Text,
|
|
nullable=True,
|
|
comment="허용된 권한 범위 (OAuth scope)",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# 플랫폼 계정 식별 정보
|
|
# ==========================================================================
|
|
platform_user_id: Mapped[Optional[str]] = mapped_column(
|
|
String(100),
|
|
nullable=True,
|
|
comment="플랫폼 내 사용자 고유 ID",
|
|
)
|
|
|
|
platform_username: Mapped[Optional[str]] = mapped_column(
|
|
String(100),
|
|
nullable=True,
|
|
comment="플랫폼 내 사용자명/핸들 (@username)",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# 플랫폼별 추가 정보 (JSON)
|
|
# ==========================================================================
|
|
platform_data: Mapped[Optional[dict]] = mapped_column(
|
|
JSON,
|
|
nullable=True,
|
|
comment="플랫폼별 추가 정보 (채널ID, 페이지ID 등)",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# 연동 상태
|
|
# ==========================================================================
|
|
is_active: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=True,
|
|
comment="활성화 상태 (비활성화 시 사용 중지)",
|
|
)
|
|
|
|
is_deleted: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
comment="소프트 삭제 여부 (True: 삭제됨)",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# 시간 정보
|
|
# ==========================================================================
|
|
connected_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
nullable=True,
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
comment="연결 일시",
|
|
)
|
|
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
nullable=False,
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
comment="정보 수정 일시",
|
|
)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
nullable=False,
|
|
server_default=func.now(),
|
|
comment="생성 일시",
|
|
)
|
|
|
|
# ==========================================================================
|
|
# User 관계
|
|
# ==========================================================================
|
|
user: Mapped["User"] = relationship(
|
|
"User",
|
|
back_populates="social_accounts",
|
|
lazy="selectin", # lazy loading 방지
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"<SocialAccount("
|
|
f"id={self.id}, "
|
|
f"user_uuid='{self.user_uuid}', "
|
|
f"platform='{self.platform}', "
|
|
f"platform_username='{self.platform_username}', "
|
|
f"is_active={self.is_active}"
|
|
f")>"
|
|
)
|