youtube upload 기능 작성 .
parent
ef203dc14d
commit
8c7893d989
|
|
@ -206,9 +206,94 @@ async def get_videos(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/videos/{video_id}",
|
||||||
|
summary="개별 영상 소프트 삭제",
|
||||||
|
description="""
|
||||||
|
## 개요
|
||||||
|
video_id에 해당하는 영상만 소프트 삭제합니다.
|
||||||
|
(is_deleted=True로 설정, 실제 데이터는 DB에 유지)
|
||||||
|
|
||||||
|
## 경로 파라미터
|
||||||
|
- **video_id**: 삭제할 영상의 ID (Video.id)
|
||||||
|
|
||||||
|
## 참고
|
||||||
|
- 본인이 소유한 프로젝트의 영상만 삭제할 수 있습니다.
|
||||||
|
- 소프트 삭제 방식으로 데이터 복구가 가능합니다.
|
||||||
|
- 프로젝트나 다른 관련 데이터(Song, Lyric 등)는 삭제되지 않습니다.
|
||||||
|
""",
|
||||||
|
responses={
|
||||||
|
200: {"description": "삭제 성공"},
|
||||||
|
401: {"description": "인증 실패 (토큰 없음/만료)"},
|
||||||
|
403: {"description": "삭제 권한 없음"},
|
||||||
|
404: {"description": "영상을 찾을 수 없음"},
|
||||||
|
500: {"description": "삭제 실패"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def delete_single_video(
|
||||||
|
video_id: int,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
) -> dict:
|
||||||
|
"""video_id에 해당하는 개별 영상만 소프트 삭제합니다."""
|
||||||
|
logger.info(f"[delete_single_video] START - video_id: {video_id}, user: {current_user.user_uuid}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Video 조회 (Project와 함께)
|
||||||
|
result = await session.execute(
|
||||||
|
select(Video, Project)
|
||||||
|
.join(Project, Video.project_id == Project.id)
|
||||||
|
.where(
|
||||||
|
Video.id == video_id,
|
||||||
|
Video.is_deleted == False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row = result.one_or_none()
|
||||||
|
|
||||||
|
if row is None:
|
||||||
|
logger.warning(f"[delete_single_video] NOT FOUND - video_id: {video_id}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="영상을 찾을 수 없습니다.",
|
||||||
|
)
|
||||||
|
|
||||||
|
video, project = row
|
||||||
|
|
||||||
|
# 소유권 검증
|
||||||
|
if project.user_uuid != current_user.user_uuid:
|
||||||
|
logger.warning(
|
||||||
|
f"[delete_single_video] FORBIDDEN - video_id: {video_id}, "
|
||||||
|
f"owner: {project.user_uuid}, requester: {current_user.user_uuid}"
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="삭제 권한이 없습니다.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 소프트 삭제
|
||||||
|
video.is_deleted = True
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
logger.info(f"[delete_single_video] SUCCESS - video_id: {video_id}")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "영상이 삭제되었습니다.",
|
||||||
|
"video_id": video_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[delete_single_video] EXCEPTION - video_id: {video_id}, error: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"삭제에 실패했습니다: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/videos/delete/{task_id}",
|
"/videos/delete/{task_id}",
|
||||||
summary="아카이브 영상 소프트 삭제",
|
summary="프로젝트 전체 소프트 삭제 (task_id 기준)",
|
||||||
description="""
|
description="""
|
||||||
## 개요
|
## 개요
|
||||||
task_id에 해당하는 프로젝트와 관련된 모든 데이터를 소프트 삭제합니다.
|
task_id에 해당하는 프로젝트와 관련된 모든 데이터를 소프트 삭제합니다.
|
||||||
|
|
@ -229,6 +314,7 @@ task_id에 해당하는 프로젝트와 관련된 모든 데이터를 소프트
|
||||||
- 본인이 소유한 프로젝트만 삭제할 수 있습니다.
|
- 본인이 소유한 프로젝트만 삭제할 수 있습니다.
|
||||||
- 소프트 삭제 방식으로 데이터 복구가 가능합니다.
|
- 소프트 삭제 방식으로 데이터 복구가 가능합니다.
|
||||||
- 백그라운드에서 비동기로 처리됩니다.
|
- 백그라운드에서 비동기로 처리됩니다.
|
||||||
|
- **개별 영상만 삭제하려면 DELETE /archive/videos/{video_id}를 사용하세요.**
|
||||||
""",
|
""",
|
||||||
responses={
|
responses={
|
||||||
200: {"description": "삭제 요청 성공"},
|
200: {"description": "삭제 요청 성공"},
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,20 @@ def add_exception_handlers(app: FastAPI):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# SocialException 핸들러 추가
|
||||||
|
from app.social.exceptions import SocialException
|
||||||
|
|
||||||
|
@app.exception_handler(SocialException)
|
||||||
|
def social_exception_handler(request: Request, exc: SocialException) -> Response:
|
||||||
|
logger.debug(f"Handled SocialException: {exc.__class__.__name__} - {exc.message}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={
|
||||||
|
"detail": exc.message,
|
||||||
|
"code": exc.code,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@app.exception_handler(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
@app.exception_handler(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
def internal_server_error_handler(request, exception):
|
def internal_server_error_handler(request, exception):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|
|
||||||
|
|
@ -242,17 +242,63 @@ async def get_account_by_platform(
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/{platform}/disconnect",
|
"/accounts/{account_id}",
|
||||||
response_model=MessageResponse,
|
response_model=MessageResponse,
|
||||||
summary="소셜 계정 연동 해제",
|
summary="소셜 계정 연동 해제 (account_id)",
|
||||||
description="""
|
description="""
|
||||||
소셜 미디어 계정 연동을 해제합니다.
|
소셜 미디어 계정 연동을 해제합니다.
|
||||||
|
|
||||||
|
## 경로 파라미터
|
||||||
|
- **account_id**: 연동 해제할 소셜 계정 ID (SocialAccount.id)
|
||||||
|
|
||||||
|
## 연동 해제 시
|
||||||
|
- 해당 플랫폼으로의 업로드가 불가능해집니다
|
||||||
|
- 기존 업로드 기록은 유지됩니다
|
||||||
|
- 재연동 시 동의 화면이 스킵됩니다
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
async def disconnect_by_account_id(
|
||||||
|
account_id: int,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
) -> MessageResponse:
|
||||||
|
"""
|
||||||
|
소셜 계정 연동 해제 (account_id 기준)
|
||||||
|
|
||||||
|
account_id로 특정 소셜 계정의 연동을 해제합니다.
|
||||||
|
"""
|
||||||
|
logger.info(
|
||||||
|
f"[OAUTH_API] 소셜 연동 해제 (by account_id) - "
|
||||||
|
f"user_uuid: {current_user.user_uuid}, account_id: {account_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
platform = await social_account_service.disconnect_by_account_id(
|
||||||
|
user_uuid=current_user.user_uuid,
|
||||||
|
account_id=account_id,
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
|
|
||||||
|
return MessageResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"{platform} 계정 연동이 해제되었습니다.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{platform}/disconnect",
|
||||||
|
response_model=MessageResponse,
|
||||||
|
summary="소셜 계정 연동 해제 (platform)",
|
||||||
|
description="""
|
||||||
|
소셜 미디어 계정 연동을 해제합니다.
|
||||||
|
|
||||||
|
**주의**: 이 API는 플랫폼당 1개의 계정만 연동된 경우에 사용합니다.
|
||||||
|
여러 채널이 연동된 경우 `DELETE /accounts/{account_id}`를 사용하세요.
|
||||||
|
|
||||||
연동 해제 시:
|
연동 해제 시:
|
||||||
- 플랫폼에서 토큰이 폐기됩니다
|
|
||||||
- 해당 플랫폼으로의 업로드가 불가능해집니다
|
- 해당 플랫폼으로의 업로드가 불가능해집니다
|
||||||
- 기존 업로드 기록은 유지됩니다
|
- 기존 업로드 기록은 유지됩니다
|
||||||
""",
|
""",
|
||||||
|
deprecated=True,
|
||||||
)
|
)
|
||||||
async def disconnect(
|
async def disconnect(
|
||||||
platform: SocialPlatform,
|
platform: SocialPlatform,
|
||||||
|
|
@ -260,9 +306,9 @@ async def disconnect(
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
) -> MessageResponse:
|
) -> MessageResponse:
|
||||||
"""
|
"""
|
||||||
소셜 계정 연동 해제
|
소셜 계정 연동 해제 (platform 기준)
|
||||||
|
|
||||||
플랫폼 토큰을 폐기하고 연동을 해제합니다.
|
플랫폼으로 연동된 첫 번째 계정을 해제합니다.
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[OAUTH_API] 소셜 연동 해제 - "
|
f"[OAUTH_API] 소셜 연동 해제 - "
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,14 @@ router = APIRouter(prefix="/upload", tags=["Social Upload"])
|
||||||
- 해당 플랫폼에 계정이 연동되어 있어야 합니다
|
- 해당 플랫폼에 계정이 연동되어 있어야 합니다
|
||||||
- 영상이 completed 상태여야 합니다 (result_movie_url 필요)
|
- 영상이 completed 상태여야 합니다 (result_movie_url 필요)
|
||||||
|
|
||||||
## 지원 플랫폼
|
## 요청 필드
|
||||||
- **youtube**: YouTube
|
- **video_id**: 업로드할 영상 ID
|
||||||
|
- **social_account_id**: 업로드할 소셜 계정 ID (연동 계정 목록 조회 API에서 확인)
|
||||||
|
- **title**: 영상 제목 (최대 100자)
|
||||||
|
- **description**: 영상 설명 (최대 5000자)
|
||||||
|
- **tags**: 태그 목록
|
||||||
|
- **privacy_status**: 공개 상태 (public, unlisted, private)
|
||||||
|
- **scheduled_at**: 예약 게시 시간 (선택사항)
|
||||||
|
|
||||||
## 업로드 상태
|
## 업로드 상태
|
||||||
업로드는 백그라운드에서 처리되며, 상태를 폴링하여 확인할 수 있습니다:
|
업로드는 백그라운드에서 처리되며, 상태를 폴링하여 확인할 수 있습니다:
|
||||||
|
|
@ -72,7 +78,7 @@ async def upload_to_social(
|
||||||
f"[UPLOAD_API] 업로드 요청 - "
|
f"[UPLOAD_API] 업로드 요청 - "
|
||||||
f"user_uuid: {current_user.user_uuid}, "
|
f"user_uuid: {current_user.user_uuid}, "
|
||||||
f"video_id: {body.video_id}, "
|
f"video_id: {body.video_id}, "
|
||||||
f"platform: {body.platform.value}"
|
f"social_account_id: {body.social_account_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. 영상 조회 및 검증
|
# 1. 영상 조회 및 검증
|
||||||
|
|
@ -92,19 +98,19 @@ async def upload_to_social(
|
||||||
detail="영상이 아직 준비되지 않았습니다. 영상 생성이 완료된 후 시도해주세요.",
|
detail="영상이 아직 준비되지 않았습니다. 영상 생성이 완료된 후 시도해주세요.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. 소셜 계정 조회
|
# 2. 소셜 계정 조회 (social_account_id로 직접 조회, 소유권 검증 포함)
|
||||||
account = await social_account_service.get_account_by_platform(
|
account = await social_account_service.get_account_by_id(
|
||||||
user_uuid=current_user.user_uuid,
|
user_uuid=current_user.user_uuid,
|
||||||
platform=body.platform,
|
account_id=body.social_account_id,
|
||||||
session=session,
|
session=session,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[UPLOAD_API] 연동 계정 없음 - "
|
f"[UPLOAD_API] 연동 계정 없음 - "
|
||||||
f"user_uuid: {current_user.user_uuid}, platform: {body.platform.value}"
|
f"user_uuid: {current_user.user_uuid}, social_account_id: {body.social_account_id}"
|
||||||
)
|
)
|
||||||
raise SocialAccountNotFoundError(platform=body.platform.value)
|
raise SocialAccountNotFoundError()
|
||||||
|
|
||||||
# 3. 기존 업로드 확인 (동일 video + account 조합)
|
# 3. 기존 업로드 확인 (동일 video + account 조합)
|
||||||
existing_result = await session.execute(
|
existing_result = await session.execute(
|
||||||
|
|
@ -125,7 +131,7 @@ async def upload_to_social(
|
||||||
return SocialUploadResponse(
|
return SocialUploadResponse(
|
||||||
success=True,
|
success=True,
|
||||||
upload_id=existing_upload.id,
|
upload_id=existing_upload.id,
|
||||||
platform=body.platform.value,
|
platform=account.platform,
|
||||||
status=existing_upload.status,
|
status=existing_upload.status,
|
||||||
message="이미 업로드가 진행 중입니다.",
|
message="이미 업로드가 진행 중입니다.",
|
||||||
)
|
)
|
||||||
|
|
@ -135,14 +141,17 @@ async def upload_to_social(
|
||||||
user_uuid=current_user.user_uuid,
|
user_uuid=current_user.user_uuid,
|
||||||
video_id=body.video_id,
|
video_id=body.video_id,
|
||||||
social_account_id=account.id,
|
social_account_id=account.id,
|
||||||
platform=body.platform.value,
|
platform=account.platform, # 계정의 플랫폼 정보 사용
|
||||||
status=UploadStatus.PENDING.value,
|
status=UploadStatus.PENDING.value,
|
||||||
upload_progress=0,
|
upload_progress=0,
|
||||||
title=body.title,
|
title=body.title,
|
||||||
description=body.description,
|
description=body.description,
|
||||||
tags=body.tags,
|
tags=body.tags,
|
||||||
privacy_status=body.privacy_status.value,
|
privacy_status=body.privacy_status.value,
|
||||||
platform_options=body.platform_options,
|
platform_options={
|
||||||
|
**(body.platform_options or {}),
|
||||||
|
"scheduled_at": body.scheduled_at.isoformat() if body.scheduled_at else None,
|
||||||
|
},
|
||||||
retry_count=0,
|
retry_count=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -152,7 +161,7 @@ async def upload_to_social(
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[UPLOAD_API] 업로드 레코드 생성 - "
|
f"[UPLOAD_API] 업로드 레코드 생성 - "
|
||||||
f"upload_id: {social_upload.id}, video_id: {body.video_id}"
|
f"upload_id: {social_upload.id}, video_id: {body.video_id}, platform: {account.platform}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 5. 백그라운드 태스크 등록
|
# 5. 백그라운드 태스크 등록
|
||||||
|
|
@ -161,7 +170,7 @@ async def upload_to_social(
|
||||||
return SocialUploadResponse(
|
return SocialUploadResponse(
|
||||||
success=True,
|
success=True,
|
||||||
upload_id=social_upload.id,
|
upload_id=social_upload.id,
|
||||||
platform=body.platform.value,
|
platform=account.platform,
|
||||||
status=social_upload.status,
|
status=social_upload.status,
|
||||||
message="업로드 요청이 접수되었습니다.",
|
message="업로드 요청이 접수되었습니다.",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -132,14 +132,17 @@ class SocialUploadRequest(BaseModel):
|
||||||
"""소셜 업로드 요청"""
|
"""소셜 업로드 요청"""
|
||||||
|
|
||||||
video_id: int = Field(..., description="업로드할 영상 ID")
|
video_id: int = Field(..., description="업로드할 영상 ID")
|
||||||
platform: SocialPlatform = Field(..., description="업로드할 플랫폼")
|
social_account_id: int = Field(..., description="업로드할 소셜 계정 ID (연동 계정 목록의 id)")
|
||||||
title: str = Field(..., min_length=1, max_length=100, description="영상 제목")
|
title: str = Field(..., min_length=1, max_length=100, description="영상 제목")
|
||||||
description: Optional[str] = Field(
|
description: Optional[str] = Field(
|
||||||
None, max_length=5000, description="영상 설명"
|
None, max_length=5000, description="영상 설명"
|
||||||
)
|
)
|
||||||
tags: Optional[list[str]] = Field(None, description="태그 목록")
|
tags: Optional[list[str]] = Field(None, description="태그 목록 (쉼표로 구분된 문자열도 가능)")
|
||||||
privacy_status: PrivacyStatus = Field(
|
privacy_status: PrivacyStatus = Field(
|
||||||
default=PrivacyStatus.PRIVATE, description="공개 상태"
|
default=PrivacyStatus.PRIVATE, description="공개 상태 (public, unlisted, private)"
|
||||||
|
)
|
||||||
|
scheduled_at: Optional[datetime] = Field(
|
||||||
|
None, description="예약 게시 시간 (없으면 즉시 게시)"
|
||||||
)
|
)
|
||||||
platform_options: Optional[dict[str, Any]] = Field(
|
platform_options: Optional[dict[str, Any]] = Field(
|
||||||
None, description="플랫폼별 추가 옵션"
|
None, description="플랫폼별 추가 옵션"
|
||||||
|
|
@ -149,11 +152,12 @@ class SocialUploadRequest(BaseModel):
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
"example": {
|
"example": {
|
||||||
"video_id": 123,
|
"video_id": 123,
|
||||||
"platform": "youtube",
|
"social_account_id": 1,
|
||||||
"title": "나의 첫 영상",
|
"title": "도그앤조이 애견펜션 2026.02.02",
|
||||||
"description": "영상 설명입니다.",
|
"description": "영상 설명입니다.",
|
||||||
"tags": ["여행", "vlog"],
|
"tags": ["여행", "vlog", "애견펜션"],
|
||||||
"privacy_status": "private",
|
"privacy_status": "public",
|
||||||
|
"scheduled_at": "2026-02-02T15:00:00",
|
||||||
"platform_options": {
|
"platform_options": {
|
||||||
"category_id": "22", # YouTube 카테고리
|
"category_id": "22", # YouTube 카테고리
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,88 @@ class SocialAccountService:
|
||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_account_by_id(
|
||||||
|
self,
|
||||||
|
user_uuid: str,
|
||||||
|
account_id: int,
|
||||||
|
session: AsyncSession,
|
||||||
|
) -> Optional[SocialAccount]:
|
||||||
|
"""
|
||||||
|
account_id로 연동 계정 조회 (소유권 검증 포함)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_uuid: 사용자 UUID
|
||||||
|
account_id: 소셜 계정 ID
|
||||||
|
session: DB 세션
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SocialAccount: 소셜 계정 (없으면 None)
|
||||||
|
"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(SocialAccount).where(
|
||||||
|
SocialAccount.id == account_id,
|
||||||
|
SocialAccount.user_uuid == user_uuid,
|
||||||
|
SocialAccount.is_active == True, # noqa: E712
|
||||||
|
SocialAccount.is_deleted == False, # noqa: E712
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def disconnect_by_account_id(
|
||||||
|
self,
|
||||||
|
user_uuid: str,
|
||||||
|
account_id: int,
|
||||||
|
session: AsyncSession,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
account_id로 소셜 계정 연동 해제
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_uuid: 사용자 UUID
|
||||||
|
account_id: 소셜 계정 ID
|
||||||
|
session: DB 세션
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 연동 해제된 플랫폼 이름
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SocialAccountNotFoundError: 연동된 계정이 없는 경우
|
||||||
|
"""
|
||||||
|
logger.info(
|
||||||
|
f"[SOCIAL] 소셜 계정 연동 해제 시작 (by account_id) - "
|
||||||
|
f"user_uuid: {user_uuid}, account_id: {account_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. account_id로 계정 조회 (user_uuid 소유권 확인 포함)
|
||||||
|
result = await session.execute(
|
||||||
|
select(SocialAccount).where(
|
||||||
|
SocialAccount.id == account_id,
|
||||||
|
SocialAccount.user_uuid == user_uuid,
|
||||||
|
SocialAccount.is_active == True, # noqa: E712
|
||||||
|
SocialAccount.is_deleted == False, # noqa: E712
|
||||||
|
)
|
||||||
|
)
|
||||||
|
account = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if account is None:
|
||||||
|
logger.warning(
|
||||||
|
f"[SOCIAL] 연동된 계정 없음 - "
|
||||||
|
f"user_uuid: {user_uuid}, account_id: {account_id}"
|
||||||
|
)
|
||||||
|
raise SocialAccountNotFoundError()
|
||||||
|
|
||||||
|
# 2. 소프트 삭제
|
||||||
|
platform = account.platform
|
||||||
|
account.is_active = False
|
||||||
|
account.is_deleted = True
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[SOCIAL] 소셜 계정 연동 해제 완료 - "
|
||||||
|
f"account_id: {account.id}, platform: {platform}"
|
||||||
|
)
|
||||||
|
return platform
|
||||||
|
|
||||||
async def disconnect(
|
async def disconnect(
|
||||||
self,
|
self,
|
||||||
user_uuid: str,
|
user_uuid: str,
|
||||||
|
|
@ -269,7 +351,7 @@ class SocialAccountService:
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
소셜 계정 연동 해제
|
소셜 계정 연동 해제 (platform 기준, deprecated)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_uuid: 사용자 UUID
|
user_uuid: 사용자 UUID
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
-- ===================================================================
|
||||||
|
-- social_upload 테이블 생성 마이그레이션
|
||||||
|
-- 소셜 미디어 업로드 기록을 저장하는 테이블
|
||||||
|
-- 생성일: 2026-02-02
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
-- social_upload 테이블 생성
|
||||||
|
CREATE TABLE IF NOT EXISTS social_upload (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '고유 식별자',
|
||||||
|
|
||||||
|
-- 관계 필드
|
||||||
|
user_uuid VARCHAR(36) NOT NULL COMMENT '사용자 UUID (User.user_uuid 참조)',
|
||||||
|
video_id INT NOT NULL COMMENT 'Video 외래키',
|
||||||
|
social_account_id INT NOT NULL COMMENT 'SocialAccount 외래키',
|
||||||
|
|
||||||
|
-- 플랫폼 정보
|
||||||
|
platform VARCHAR(20) NOT NULL COMMENT '플랫폼 구분 (youtube, instagram, facebook, tiktok)',
|
||||||
|
|
||||||
|
-- 업로드 상태
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '업로드 상태 (pending, uploading, processing, completed, failed)',
|
||||||
|
upload_progress INT NOT NULL DEFAULT 0 COMMENT '업로드 진행률 (0-100)',
|
||||||
|
|
||||||
|
-- 플랫폼 결과
|
||||||
|
platform_video_id VARCHAR(100) NULL COMMENT '플랫폼에서 부여한 영상 ID',
|
||||||
|
platform_url VARCHAR(500) NULL COMMENT '플랫폼에서의 영상 URL',
|
||||||
|
|
||||||
|
-- 메타데이터
|
||||||
|
title VARCHAR(200) NOT NULL COMMENT '영상 제목',
|
||||||
|
description TEXT NULL COMMENT '영상 설명',
|
||||||
|
tags JSON NULL COMMENT '태그 목록 (JSON 배열)',
|
||||||
|
privacy_status VARCHAR(20) NOT NULL DEFAULT 'private' COMMENT '공개 상태 (public, unlisted, private)',
|
||||||
|
platform_options JSON NULL COMMENT '플랫폼별 추가 옵션 (JSON)',
|
||||||
|
|
||||||
|
-- 에러 정보
|
||||||
|
error_message TEXT NULL COMMENT '에러 메시지 (실패 시)',
|
||||||
|
retry_count INT NOT NULL DEFAULT 0 COMMENT '재시도 횟수',
|
||||||
|
|
||||||
|
-- 시간 정보
|
||||||
|
uploaded_at DATETIME NULL COMMENT '업로드 완료 시간',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시',
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 일시',
|
||||||
|
|
||||||
|
-- 외래키 제약조건
|
||||||
|
CONSTRAINT fk_social_upload_user FOREIGN KEY (user_uuid) REFERENCES user(user_uuid) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_social_upload_video FOREIGN KEY (video_id) REFERENCES video(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_social_upload_account FOREIGN KEY (social_account_id) REFERENCES social_account(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='소셜 미디어 업로드 기록 테이블';
|
||||||
|
|
||||||
|
-- 인덱스 생성
|
||||||
|
CREATE INDEX idx_social_upload_user_uuid ON social_upload(user_uuid);
|
||||||
|
CREATE INDEX idx_social_upload_video_id ON social_upload(video_id);
|
||||||
|
CREATE INDEX idx_social_upload_social_account_id ON social_upload(social_account_id);
|
||||||
|
CREATE INDEX idx_social_upload_platform ON social_upload(platform);
|
||||||
|
CREATE INDEX idx_social_upload_status ON social_upload(status);
|
||||||
|
CREATE INDEX idx_social_upload_created_at ON social_upload(created_at);
|
||||||
|
|
||||||
|
-- 유니크 인덱스 (동일 영상 + 동일 계정 조합은 하나만 존재)
|
||||||
|
CREATE UNIQUE INDEX uq_social_upload_video_platform ON social_upload(video_id, social_account_id);
|
||||||
4
main.py
4
main.py
|
|
@ -313,6 +313,10 @@ def get_scalar_docs():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 예외 핸들러 등록
|
||||||
|
from app.core.exceptions import add_exception_handlers
|
||||||
|
add_exception_handlers(app)
|
||||||
|
|
||||||
app.include_router(home_router)
|
app.include_router(home_router)
|
||||||
app.include_router(auth_router, prefix="/user") # Auth API 라우터 추가
|
app.include_router(auth_router, prefix="/user") # Auth API 라우터 추가
|
||||||
app.include_router(lyric_router)
|
app.include_router(lyric_router)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue