o2o-castad-backend/app/user/api/routers/v1/social_account.py

308 lines
10 KiB
Python

"""
SocialAccount API 라우터
소셜 계정 연동 CRUD 엔드포인트를 제공합니다.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_session
from app.user.dependencies import get_current_user
from app.user.models import User
from app.user.schemas.social_account_schema import (
SocialAccountCreateRequest,
SocialAccountDeleteResponse,
SocialAccountListResponse,
SocialAccountResponse,
SocialAccountUpdateRequest,
)
from app.user.services.social_account import SocialAccountService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/social-accounts", tags=["Social Account"])
# =============================================================================
# 소셜 계정 목록 조회
# =============================================================================
@router.get(
"",
response_model=SocialAccountListResponse,
summary="소셜 계정 목록 조회",
description="""
## 개요
현재 로그인한 사용자의 연동된 소셜 계정 목록을 조회합니다.
## 인증
- Bearer 토큰 필수
## 반환 정보
- **items**: 소셜 계정 목록
- **total**: 총 계정 수
""",
)
async def get_social_accounts(
current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> SocialAccountListResponse:
"""소셜 계정 목록 조회"""
logger.info(f"[get_social_accounts] START - user_uuid: {current_user.user_uuid}")
try:
service = SocialAccountService(session)
accounts = await service.get_list(current_user)
response = SocialAccountListResponse(
items=[SocialAccountResponse.model_validate(acc) for acc in accounts],
total=len(accounts),
)
logger.info(f"[get_social_accounts] SUCCESS - user_uuid: {current_user.user_uuid}, count: {len(accounts)}")
return response
except Exception as e:
logger.error(f"[get_social_accounts] ERROR - user_uuid: {current_user.user_uuid}, error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="소셜 계정 목록 조회 중 오류가 발생했습니다.",
)
# =============================================================================
# 소셜 계정 상세 조회
# =============================================================================
@router.get(
"/{account_id}",
response_model=SocialAccountResponse,
summary="소셜 계정 상세 조회",
description="""
## 개요
특정 소셜 계정의 상세 정보를 조회합니다.
## 인증
- Bearer 토큰 필수
- 본인 소유의 계정만 조회 가능
## 경로 파라미터
- **account_id**: 소셜 계정 ID
""",
responses={
404: {"description": "소셜 계정을 찾을 수 없음"},
},
)
async def get_social_account(
account_id: int,
current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> SocialAccountResponse:
"""소셜 계정 상세 조회"""
logger.info(f"[get_social_account] START - user_uuid: {current_user.user_uuid}, account_id: {account_id}")
try:
service = SocialAccountService(session)
account = await service.get_by_id(current_user, account_id)
if not account:
logger.warning(f"[get_social_account] NOT_FOUND - account_id: {account_id}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="소셜 계정을 찾을 수 없습니다.",
)
logger.info(f"[get_social_account] SUCCESS - account_id: {account_id}, platform: {account.platform}")
return SocialAccountResponse.model_validate(account)
except HTTPException:
raise
except Exception as e:
logger.error(f"[get_social_account] ERROR - account_id: {account_id}, error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="소셜 계정 조회 중 오류가 발생했습니다.",
)
# =============================================================================
# 소셜 계정 생성
# =============================================================================
@router.post(
"",
response_model=SocialAccountResponse,
status_code=status.HTTP_201_CREATED,
summary="소셜 계정 연동",
description="""
## 개요
새로운 소셜 계정을 연동합니다.
## 인증
- Bearer 토큰 필수
## 요청 본문
- **platform**: 플랫폼 구분 (youtube, instagram, facebook, tiktok)
- **access_token**: OAuth 액세스 토큰
- **platform_user_id**: 플랫폼 내 사용자 고유 ID
- 기타 선택 필드
## 주의사항
- 동일한 플랫폼의 동일한 계정은 중복 연동할 수 없습니다.
""",
responses={
400: {"description": "이미 연동된 계정"},
},
)
async def create_social_account(
data: SocialAccountCreateRequest,
current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> SocialAccountResponse:
"""소셜 계정 연동"""
logger.info(
f"[create_social_account] START - user_uuid: {current_user.user_uuid}, "
f"platform: {data.platform}, platform_user_id: {data.platform_user_id}"
)
try:
service = SocialAccountService(session)
account = await service.create(current_user, data)
logger.info(
f"[create_social_account] SUCCESS - account_id: {account.id}, "
f"platform: {account.platform}"
)
return SocialAccountResponse.model_validate(account)
except ValueError as e:
logger.warning(f"[create_social_account] DUPLICATE - error: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
except Exception as e:
logger.error(f"[create_social_account] ERROR - error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="소셜 계정 연동 중 오류가 발생했습니다.",
)
# =============================================================================
# 소셜 계정 수정
# =============================================================================
@router.patch(
"/{account_id}",
response_model=SocialAccountResponse,
summary="소셜 계정 정보 수정",
description="""
## 개요
소셜 계정 정보를 수정합니다. (토큰 갱신 등)
## 인증
- Bearer 토큰 필수
- 본인 소유의 계정만 수정 가능
## 경로 파라미터
- **account_id**: 소셜 계정 ID
## 요청 본문
- 수정할 필드만 전송 (PATCH 방식)
""",
responses={
404: {"description": "소셜 계정을 찾을 수 없음"},
},
)
async def update_social_account(
account_id: int,
data: SocialAccountUpdateRequest,
current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> SocialAccountResponse:
"""소셜 계정 정보 수정"""
logger.info(
f"[update_social_account] START - user_uuid: {current_user.user_uuid}, "
f"account_id: {account_id}, data: {data.model_dump(exclude_unset=True)}"
)
try:
service = SocialAccountService(session)
account = await service.update(current_user, account_id, data)
if not account:
logger.warning(f"[update_social_account] NOT_FOUND - account_id: {account_id}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="소셜 계정을 찾을 수 없습니다.",
)
logger.info(f"[update_social_account] SUCCESS - account_id: {account_id}")
return SocialAccountResponse.model_validate(account)
except HTTPException:
raise
except Exception as e:
logger.error(f"[update_social_account] ERROR - account_id: {account_id}, error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="소셜 계정 수정 중 오류가 발생했습니다.",
)
# =============================================================================
# 소셜 계정 삭제
# =============================================================================
@router.delete(
"/{account_id}",
response_model=SocialAccountDeleteResponse,
summary="소셜 계정 연동 해제",
description="""
## 개요
소셜 계정 연동을 해제합니다. (소프트 삭제)
## 인증
- Bearer 토큰 필수
- 본인 소유의 계정만 삭제 가능
## 경로 파라미터
- **account_id**: 소셜 계정 ID
""",
responses={
404: {"description": "소셜 계정을 찾을 수 없음"},
},
)
async def delete_social_account(
account_id: int,
current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> SocialAccountDeleteResponse:
"""소셜 계정 연동 해제"""
logger.info(f"[delete_social_account] START - user_uuid: {current_user.user_uuid}, account_id: {account_id}")
try:
service = SocialAccountService(session)
deleted_id = await service.delete(current_user, account_id)
if not deleted_id:
logger.warning(f"[delete_social_account] NOT_FOUND - account_id: {account_id}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="소셜 계정을 찾을 수 없습니다.",
)
logger.info(f"[delete_social_account] SUCCESS - deleted_id: {deleted_id}")
return SocialAccountDeleteResponse(
message="소셜 계정이 삭제되었습니다.",
deleted_id=deleted_id,
)
except HTTPException:
raise
except Exception as e:
logger.error(f"[delete_social_account] ERROR - account_id: {account_id}, error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="소셜 계정 삭제 중 오류가 발생했습니다.",
)