o2o-castad-backend/app/user/services/social_account.py

260 lines
8.4 KiB
Python

"""
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)