""" SNS API 라우터 Instagram 업로드 관련 엔드포인트를 제공합니다. """ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database.session import get_session from app.sns.schemas.sns_schema import InstagramUploadRequest, InstagramUploadResponse # ============================================================================= # SNS 예외 클래스 정의 # ============================================================================= class SNSException(HTTPException): """SNS 관련 기본 예외""" def __init__(self, status_code: int, code: str, message: str): super().__init__(status_code=status_code, detail={"code": code, "message": message}) class SocialAccountNotFoundError(SNSException): """소셜 계정 없음""" def __init__(self, message: str = "연동된 소셜 계정을 찾을 수 없습니다."): super().__init__(status.HTTP_404_NOT_FOUND, "SOCIAL_ACCOUNT_NOT_FOUND", message) class VideoNotFoundError(SNSException): """비디오 없음""" def __init__(self, message: str = "해당 작업 ID에 대한 비디오를 찾을 수 없습니다."): super().__init__(status.HTTP_404_NOT_FOUND, "VIDEO_NOT_FOUND", message) class VideoUrlNotReadyError(SNSException): """비디오 URL 미준비""" def __init__(self, message: str = "비디오가 아직 준비되지 않았습니다."): super().__init__(status.HTTP_400_BAD_REQUEST, "VIDEO_URL_NOT_READY", message) class InstagramUploadError(SNSException): """Instagram 업로드 실패""" def __init__(self, message: str = "Instagram 업로드에 실패했습니다."): super().__init__(status.HTTP_500_INTERNAL_SERVER_ERROR, "INSTAGRAM_UPLOAD_ERROR", message) class InstagramRateLimitError(SNSException): """Instagram API Rate Limit""" def __init__(self, message: str = "Instagram API 호출 제한을 초과했습니다.", retry_after: int = 60): super().__init__( status.HTTP_429_TOO_MANY_REQUESTS, "INSTAGRAM_RATE_LIMIT", f"{message} {retry_after}초 후 다시 시도해주세요.", ) class InstagramAuthError(SNSException): """Instagram 인증 오류""" def __init__(self, message: str = "Instagram 인증에 실패했습니다. 계정을 다시 연동해주세요."): super().__init__(status.HTTP_401_UNAUTHORIZED, "INSTAGRAM_AUTH_ERROR", message) class InstagramContainerTimeoutError(SNSException): """Instagram 미디어 처리 타임아웃""" def __init__(self, message: str = "Instagram 미디어 처리 시간이 초과되었습니다."): super().__init__(status.HTTP_504_GATEWAY_TIMEOUT, "INSTAGRAM_CONTAINER_TIMEOUT", message) class InstagramContainerError(SNSException): """Instagram 미디어 컨테이너 오류""" def __init__(self, message: str = "Instagram 미디어 처리에 실패했습니다."): super().__init__(status.HTTP_500_INTERNAL_SERVER_ERROR, "INSTAGRAM_CONTAINER_ERROR", message) from app.user.dependencies.auth import get_current_user from app.user.models import Platform, SocialAccount, User from app.utils.instagram import ErrorState, InstagramClient, parse_instagram_error from app.utils.logger import get_logger from app.video.models import Video logger = get_logger(__name__) router = APIRouter(prefix="/sns", tags=["SNS"]) @router.post( "/instagram/upload/{task_id}", summary="Instagram 비디오 업로드", description=""" ## 개요 task_id에 해당하는 비디오를 Instagram에 업로드합니다. ## 경로 파라미터 - **task_id**: 비디오 생성 작업 고유 식별자 ## 요청 본문 - **caption**: 게시물 캡션 (선택, 최대 2200자) - **share_to_feed**: 피드에 공유 여부 (기본값: true) ## 인증 - Bearer 토큰 필요 (Authorization: Bearer ) - 사용자의 Instagram 계정이 연동되어 있어야 합니다. ## 반환 정보 - **task_id**: 작업 고유 식별자 - **state**: 업로드 상태 (completed, failed) - **message**: 상태 메시지 - **media_id**: Instagram 미디어 ID (성공 시) - **permalink**: Instagram 게시물 URL (성공 시) - **error**: 에러 메시지 (실패 시) """, response_model=InstagramUploadResponse, responses={ 200: {"description": "업로드 성공"}, 400: {"description": "비디오 URL 미준비"}, 401: {"description": "인증 실패"}, 404: {"description": "비디오 또는 소셜 계정 없음"}, 429: {"description": "Instagram API Rate Limit"}, 500: {"description": "업로드 실패"}, 504: {"description": "타임아웃"}, }, ) async def upload_to_instagram( task_id: str, request: InstagramUploadRequest, current_user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session), ) -> InstagramUploadResponse: """Instagram에 비디오를 업로드합니다.""" logger.info(f"[upload_to_instagram] START - task_id: {task_id}, user_uuid: {current_user.user_uuid}") # Step 1: 사용자의 Instagram 소셜 계정 조회 social_account_result = await session.execute( select(SocialAccount).where( SocialAccount.user_uuid == current_user.user_uuid, SocialAccount.platform == Platform.INSTAGRAM, SocialAccount.is_active == True, # noqa: E712 SocialAccount.is_deleted == False, # noqa: E712 ) ) social_account = social_account_result.scalar_one_or_none() if social_account is None: logger.warning(f"[upload_to_instagram] Instagram 계정 없음 - user_uuid: {current_user.user_uuid}") raise SocialAccountNotFoundError("연동된 Instagram 계정을 찾을 수 없습니다.") logger.info(f"[upload_to_instagram] 소셜 계정 확인 - social_account_id: {social_account.id}") # Step 2: task_id로 비디오 조회 (가장 최근 것) video_result = await session.execute( select(Video) .where( Video.task_id == task_id, Video.is_deleted == False, # noqa: E712 ) .order_by(Video.created_at.desc()) .limit(1) ) video = video_result.scalar_one_or_none() if video is None: logger.warning(f"[upload_to_instagram] 비디오 없음 - task_id: {task_id}") raise VideoNotFoundError(f"task_id '{task_id}'에 해당하는 비디오를 찾을 수 없습니다.") if video.result_movie_url is None: logger.warning(f"[upload_to_instagram] 비디오 URL 미준비 - task_id: {task_id}, status: {video.status}") raise VideoUrlNotReadyError("비디오가 아직 처리 중입니다. 잠시 후 다시 시도해주세요.") logger.info(f"[upload_to_instagram] 비디오 확인 - video_id: {video.id}, url: {video.result_movie_url[:50]}...") # Step 3: Instagram 업로드 try: async with InstagramClient(access_token=social_account.access_token) as client: # 접속 테스트 (계정 ID 조회) await client.get_account_id() logger.info("[upload_to_instagram] Instagram 접속 확인 완료") # 비디오 업로드 media = await client.publish_video( video_url=video.result_movie_url, caption=request.caption, share_to_feed=request.share_to_feed, ) logger.info( f"[upload_to_instagram] SUCCESS - task_id: {task_id}, " f"media_id: {media.id}, permalink: {media.permalink}" ) return InstagramUploadResponse( task_id=task_id, state="completed", message="Instagram 업로드 완료", media_id=media.id, permalink=media.permalink, error=None, ) except Exception as e: error_state, message, extra_info = parse_instagram_error(e) logger.error(f"[upload_to_instagram] FAILED - task_id: {task_id}, error_state: {error_state}, message: {message}") match error_state: case ErrorState.RATE_LIMIT: retry_after = extra_info.get("retry_after", 60) raise InstagramRateLimitError(retry_after=retry_after) case ErrorState.AUTH_ERROR: raise InstagramAuthError() case ErrorState.CONTAINER_TIMEOUT: raise InstagramContainerTimeoutError() case ErrorState.CONTAINER_ERROR: status = extra_info.get("status", "UNKNOWN") raise InstagramContainerError(f"미디어 처리 실패: {status}") case _: raise InstagramUploadError(f"Instagram 업로드 실패: {message}")