""" SocialAccount 서비스 레이어 소셜 계정 연동 관련 비즈니스 로직을 처리합니다. """ import logging from typing import Optional from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession from app.user.models import Platform, SocialAccount, User from app.user.schemas.social_account_schema import ( SocialAccountCreateRequest, SocialAccountUpdateRequest, ) logger = logging.getLogger(__name__) class SocialAccountService: """소셜 계정 서비스""" def __init__(self, session: AsyncSession): self.session = session async def get_list(self, user: User) -> list[SocialAccount]: """ 사용자의 소셜 계정 목록 조회 Args: user: 현재 로그인한 사용자 Returns: list[SocialAccount]: 소셜 계정 목록 """ logger.debug(f"[SocialAccountService.get_list] START - user_uuid: {user.user_uuid}") result = await self.session.execute( select(SocialAccount).where( and_( SocialAccount.user_uuid == user.user_uuid, SocialAccount.is_deleted == False, # noqa: E712 ) ).order_by(SocialAccount.created_at.desc()) ) accounts = list(result.scalars().all()) logger.debug(f"[SocialAccountService.get_list] SUCCESS - count: {len(accounts)}") return accounts async def get_by_id(self, user: User, account_id: int) -> Optional[SocialAccount]: """ ID로 소셜 계정 조회 Args: user: 현재 로그인한 사용자 account_id: 소셜 계정 ID Returns: SocialAccount | None: 소셜 계정 또는 None """ logger.debug(f"[SocialAccountService.get_by_id] START - user_uuid: {user.user_uuid}, account_id: {account_id}") result = await self.session.execute( select(SocialAccount).where( and_( SocialAccount.id == account_id, SocialAccount.user_uuid == user.user_uuid, SocialAccount.is_deleted == False, # noqa: E712 ) ) ) account = result.scalar_one_or_none() if account: logger.debug(f"[SocialAccountService.get_by_id] SUCCESS - platform: {account.platform}") else: logger.debug(f"[SocialAccountService.get_by_id] NOT_FOUND - account_id: {account_id}") return account async def get_by_platform( self, user: User, platform: Platform, platform_user_id: Optional[str] = None, ) -> Optional[SocialAccount]: """ 플랫폼별 소셜 계정 조회 Args: user: 현재 로그인한 사용자 platform: 플랫폼 platform_user_id: 플랫폼 사용자 ID (선택) Returns: SocialAccount | None: 소셜 계정 또는 None """ logger.debug( f"[SocialAccountService.get_by_platform] START - user_uuid: {user.user_uuid}, " f"platform: {platform}, platform_user_id: {platform_user_id}" ) conditions = [ SocialAccount.user_uuid == user.user_uuid, SocialAccount.platform == platform, SocialAccount.is_deleted == False, # noqa: E712 ] if platform_user_id: conditions.append(SocialAccount.platform_user_id == platform_user_id) result = await self.session.execute( select(SocialAccount).where(and_(*conditions)) ) account = result.scalar_one_or_none() if account: logger.debug(f"[SocialAccountService.get_by_platform] SUCCESS - id: {account.id}") else: logger.debug(f"[SocialAccountService.get_by_platform] NOT_FOUND") return account async def create( self, user: User, data: SocialAccountCreateRequest, ) -> SocialAccount: """ 소셜 계정 생성 Args: user: 현재 로그인한 사용자 data: 생성 요청 데이터 Returns: SocialAccount: 생성된 소셜 계정 Raises: ValueError: 이미 연동된 계정이 존재하는 경우 """ logger.debug( f"[SocialAccountService.create] START - user_uuid: {user.user_uuid}, " f"platform: {data.platform}, platform_user_id: {data.platform_user_id}" ) # 중복 확인 existing = await self.get_by_platform(user, data.platform, data.platform_user_id) if existing: logger.warning( f"[SocialAccountService.create] DUPLICATE - " f"platform: {data.platform}, platform_user_id: {data.platform_user_id}" ) raise ValueError(f"이미 연동된 {data.platform.value} 계정입니다.") account = SocialAccount( user_uuid=user.user_uuid, platform=data.platform, access_token=data.access_token, refresh_token=data.refresh_token, token_expires_at=data.token_expires_at, scope=data.scope, platform_user_id=data.platform_user_id, platform_username=data.platform_username, platform_data=data.platform_data, is_active=True, is_deleted=False, ) self.session.add(account) await self.session.commit() await self.session.refresh(account) logger.info( f"[SocialAccountService.create] SUCCESS - id: {account.id}, " f"platform: {account.platform}, platform_username: {account.platform_username}" ) return account async def update( self, user: User, account_id: int, data: SocialAccountUpdateRequest, ) -> Optional[SocialAccount]: """ 소셜 계정 수정 Args: user: 현재 로그인한 사용자 account_id: 소셜 계정 ID data: 수정 요청 데이터 Returns: SocialAccount | None: 수정된 소셜 계정 또는 None """ logger.debug( f"[SocialAccountService.update] START - user_uuid: {user.user_uuid}, account_id: {account_id}" ) account = await self.get_by_id(user, account_id) if not account: logger.warning(f"[SocialAccountService.update] NOT_FOUND - account_id: {account_id}") return None # 변경된 필드만 업데이트 update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): if value is not None: setattr(account, field, value) await self.session.commit() await self.session.refresh(account) logger.info( f"[SocialAccountService.update] SUCCESS - id: {account.id}, " f"updated_fields: {list(update_data.keys())}" ) return account async def delete(self, user: User, account_id: int) -> Optional[int]: """ 소셜 계정 소프트 삭제 Args: user: 현재 로그인한 사용자 account_id: 소셜 계정 ID Returns: int | None: 삭제된 계정 ID 또는 None """ logger.debug( f"[SocialAccountService.delete] START - user_uuid: {user.user_uuid}, account_id: {account_id}" ) account = await self.get_by_id(user, account_id) if not account: logger.warning(f"[SocialAccountService.delete] NOT_FOUND - account_id: {account_id}") return None account.is_deleted = True account.is_active = False await self.session.commit() logger.info( f"[SocialAccountService.delete] SUCCESS - id: {account_id}, platform: {account.platform}" ) return account_id # ============================================================================= # 의존성 주입용 함수 # ============================================================================= async def get_social_account_service(session: AsyncSession) -> SocialAccountService: """SocialAccountService 인스턴스 반환""" return SocialAccountService(session)