Compare commits

...

2 Commits

Author SHA1 Message Date
hbyang 6e70c416b8 스케줄 작업 완료 . 2026-03-05 15:24:06 +09:00
hbyang 7a887153ab 내부 youtube 업로드 endpoint 적용 . 2026-03-03 16:16:23 +09:00
7 changed files with 137 additions and 16 deletions

View File

@ -0,0 +1,39 @@
"""
내부 전용 소셜 업로드 API
스케줄러 서버에서만 호출하는 내부 엔드포인트입니다.
X-Internal-Secret 헤더로 인증합니다.
"""
import logging
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, status
from app.social.worker.upload_task import process_social_upload
from config import internal_settings
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/internal/social", tags=["Internal"])
@router.post(
"/upload/{upload_id}",
summary="[내부] 예약 업로드 실행",
description="스케줄러 서버에서 호출하는 내부 전용 엔드포인트입니다.",
)
async def trigger_scheduled_upload(
upload_id: int,
background_tasks: BackgroundTasks,
x_internal_secret: str = Header(...),
) -> dict:
if x_internal_secret != internal_settings.INTERNAL_SECRET_KEY:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid internal secret",
)
logger.info(f"[INTERNAL] 예약 업로드 실행 - upload_id: {upload_id}")
background_tasks.add_task(process_social_upload, upload_id)
return {"success": True, "upload_id": upload_id, "message": "업로드 작업이 시작되었습니다."}

View File

@ -4,7 +4,8 @@
소셜 미디어 영상 업로드 관련 엔드포인트를 제공합니다.
"""
import logging, json
import logging
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, BackgroundTasks, Depends, Query
@ -158,6 +159,7 @@ async def upload_to_social(
description=body.description,
tags=body.tags,
privacy_status=body.privacy_status.value,
scheduled_at=body.scheduled_at,
platform_options={
**(body.platform_options or {}),
"scheduled_at": body.scheduled_at.isoformat() if body.scheduled_at else None,
@ -175,15 +177,19 @@ async def upload_to_social(
f"account_id: {account.id}, upload_seq: {next_seq}, platform: {account.platform}"
)
# 6. 백그라운드 태스크 등록
# 6. 백그라운드 태스크 등록 (즉시 업로드 or 예약 없을 때만)
from app.utils.timezone import now as utcnow
is_scheduled = body.scheduled_at and body.scheduled_at > utcnow().replace(tzinfo=None)
if not is_scheduled:
background_tasks.add_task(process_social_upload, social_upload.id)
message = "예약 업로드가 등록되었습니다." if is_scheduled else "업로드 요청이 접수되었습니다."
return SocialUploadResponse(
success=True,
upload_id=social_upload.id,
platform=account.platform,
status=social_upload.status,
message="업로드 요청이 접수되었습니다.",
message=message,
)
@ -241,42 +247,81 @@ async def get_upload_status(
"/history",
response_model=SocialUploadHistoryResponse,
summary="업로드 이력 조회",
description="사용자의 소셜 미디어 업로드 이력을 조회합니다.",
description="""
사용자의 소셜 미디어 업로드 이력을 조회합니다.
## tab 파라미터
- `all`: 전체 (기본값)
- `completed`: 완료된 업로드
- `scheduled`: 예약 업로드 (pending + scheduled_at 있음)
- `failed`: 실패한 업로드
""",
)
async def get_upload_history(
current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
tab: str = Query("all", description="탭 필터 (all/completed/scheduled/failed)"),
platform: Optional[SocialPlatform] = Query(None, description="플랫폼 필터"),
status: Optional[UploadStatus] = Query(None, description="상태 필터"),
year: Optional[int] = Query(None, description="조회 연도 (없으면 현재 연도)"),
month: Optional[int] = Query(None, ge=1, le=12, description="조회 월 (없으면 현재 월)"),
page: int = Query(1, ge=1, description="페이지 번호"),
size: int = Query(20, ge=1, le=100, description="페이지 크기"),
) -> SocialUploadHistoryResponse:
"""
업로드 이력 조회
"""
now = datetime.now(timezone.utc)
target_year = year or now.year
target_month = month or now.month
logger.info(
f"[UPLOAD_API] 이력 조회 - "
f"user_uuid: {current_user.user_uuid}, page: {page}, size: {size}"
f"user_uuid: {current_user.user_uuid}, tab: {tab}, "
f"year: {target_year}, month: {target_month}, page: {page}, size: {size}"
)
# 기본 쿼리
# 월 범위 계산
from calendar import monthrange
last_day = monthrange(target_year, target_month)[1]
month_start = datetime(target_year, target_month, 1, 0, 0, 0)
month_end = datetime(target_year, target_month, last_day, 23, 59, 59)
# 기본 쿼리 (cancelled 제외)
query = select(SocialUpload).where(
SocialUpload.user_uuid == current_user.user_uuid
SocialUpload.user_uuid == current_user.user_uuid,
SocialUpload.created_at >= month_start,
SocialUpload.created_at <= month_end,
SocialUpload.status != UploadStatus.CANCELLED.value,
)
count_query = select(func.count(SocialUpload.id)).where(
SocialUpload.user_uuid == current_user.user_uuid
SocialUpload.user_uuid == current_user.user_uuid,
SocialUpload.created_at >= month_start,
SocialUpload.created_at <= month_end,
SocialUpload.status != UploadStatus.CANCELLED.value,
)
# 필터 적용
# 탭 필터 적용
if tab == "completed":
query = query.where(SocialUpload.status == UploadStatus.COMPLETED.value)
count_query = count_query.where(SocialUpload.status == UploadStatus.COMPLETED.value)
elif tab == "scheduled":
query = query.where(
SocialUpload.status == UploadStatus.PENDING.value,
SocialUpload.scheduled_at.isnot(None),
)
count_query = count_query.where(
SocialUpload.status == UploadStatus.PENDING.value,
SocialUpload.scheduled_at.isnot(None),
)
elif tab == "failed":
query = query.where(SocialUpload.status == UploadStatus.FAILED.value)
count_query = count_query.where(SocialUpload.status == UploadStatus.FAILED.value)
# 플랫폼 필터 적용
if platform:
query = query.where(SocialUpload.platform == platform.value)
count_query = count_query.where(SocialUpload.platform == platform.value)
if status:
query = query.where(SocialUpload.status == status.value)
count_query = count_query.where(SocialUpload.status == status.value)
# 총 개수 조회
total_result = await session.execute(count_query)
total = total_result.scalar() or 0
@ -301,6 +346,8 @@ async def get_upload_history(
status=upload.status,
title=upload.title,
platform_url=upload.platform_url,
error_message=upload.error_message,
scheduled_at=upload.scheduled_at,
created_at=upload.created_at,
uploaded_at=upload.uploaded_at,
)

View File

@ -190,6 +190,15 @@ class SocialUpload(Base):
comment="플랫폼별 추가 옵션 (JSON)",
)
# ==========================================================================
# 예약 게시 시간
# ==========================================================================
scheduled_at: Mapped[Optional[datetime]] = mapped_column(
DateTime,
nullable=True,
comment="예약 게시 시간 (스케줄러가 이 시간 이후에 업로드 실행)",
)
# ==========================================================================
# 에러 정보
# ==========================================================================

View File

@ -203,6 +203,7 @@ class SocialUploadStatusResponse(BaseModel):
platform_url: Optional[str] = Field(None, description="플랫폼 영상 URL")
error_message: Optional[str] = Field(None, description="에러 메시지")
retry_count: int = Field(default=0, description="재시도 횟수")
scheduled_at: Optional[datetime] = Field(None, description="예약 게시 시간 (있으면 예약 업로드)")
created_at: datetime = Field(..., description="생성 일시")
uploaded_at: Optional[datetime] = Field(None, description="업로드 완료 일시")
@ -240,6 +241,8 @@ class SocialUploadHistoryItem(BaseModel):
status: str = Field(..., description="업로드 상태")
title: str = Field(..., description="영상 제목")
platform_url: Optional[str] = Field(None, description="플랫폼 영상 URL")
error_message: Optional[str] = Field(None, description="에러 메시지")
scheduled_at: Optional[datetime] = Field(None, description="예약 게시 시간")
created_at: datetime = Field(..., description="생성 일시")
uploaded_at: Optional[datetime] = Field(None, description="업로드 완료 일시")

View File

@ -566,6 +566,17 @@ class SocialOAuthSettings(BaseSettings):
model_config = _base_config
class InternalSettings(BaseSettings):
"""내부 서버 간 통신 설정"""
INTERNAL_SECRET_KEY: str = Field(
default="change-me-internal-secret-key",
description="스케줄러 서버 → 백엔드 내부 API 인증 키",
)
model_config = _base_config
class SocialUploadSettings(BaseSettings):
"""소셜 미디어 업로드 설정
@ -613,4 +624,5 @@ kakao_settings = KakaoSettings()
jwt_settings = JWTSettings()
recovery_settings = RecoverySettings()
social_oauth_settings = SocialOAuthSettings()
internal_settings = InternalSettings()
social_upload_settings = SocialUploadSettings()

View File

@ -0,0 +1,9 @@
-- ============================================================
-- Migration: social_upload 테이블에 scheduled_at 컬럼 추가
-- Date: 2026-03-05
-- Description: SNS 예약 업로드 스케줄러를 위한 예약 게시 시간 컬럼 추가
-- ============================================================
ALTER TABLE social_upload
ADD COLUMN scheduled_at DATETIME NULL COMMENT '예약 게시 시간 (스케줄러가 이 시간 이후에 업로드 실행)'
AFTER privacy_status;

View File

@ -22,6 +22,7 @@ from app.video.api.routers.v1.video import router as video_router
from app.social.api.routers.v1.oauth import router as social_oauth_router
from app.social.api.routers.v1.upload import router as social_upload_router
from app.social.api.routers.v1.seo import router as social_seo_router
from app.social.api.routers.v1.internal import router as social_internal_router
from app.utils.cors import CustomCORSMiddleware
from config import prj_settings
@ -362,6 +363,7 @@ app.include_router(archive_router) # Archive API 라우터 추가
app.include_router(social_oauth_router, prefix="/social") # Social OAuth 라우터 추가
app.include_router(social_upload_router, prefix="/social") # Social Upload 라우터 추가
app.include_router(social_seo_router, prefix="/social") # Social Upload 라우터 추가
app.include_router(social_internal_router) # 내부 스케줄러 전용 라우터
app.include_router(sns_router) # SNS API 라우터 추가
# DEBUG 모드에서만 테스트 라우터 등록