스케줄 작업 완료 .

feature-scheduler
hbyang 2026-03-05 15:24:06 +09:00
parent 7a887153ab
commit 6e70c416b8
4 changed files with 72 additions and 22 deletions

View File

@ -17,14 +17,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/internal/social", tags=["Internal"]) router = APIRouter(prefix="/internal/social", tags=["Internal"])
def _verify_secret(x_internal_secret: str = Header(...)) -> None:
if x_internal_secret != internal_settings.INTERNAL_SECRET_KEY:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid internal secret",
)
@router.post( @router.post(
"/upload/{upload_id}", "/upload/{upload_id}",
summary="[내부] 예약 업로드 실행", summary="[내부] 예약 업로드 실행",
@ -35,7 +27,11 @@ async def trigger_scheduled_upload(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
x_internal_secret: str = Header(...), x_internal_secret: str = Header(...),
) -> dict: ) -> dict:
_verify_secret(x_internal_secret) 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}") logger.info(f"[INTERNAL] 예약 업로드 실행 - upload_id: {upload_id}")
background_tasks.add_task(process_social_upload, upload_id) background_tasks.add_task(process_social_upload, upload_id)

View File

@ -5,6 +5,7 @@
""" """
import logging import logging
from datetime import datetime, timezone
from typing import Optional from typing import Optional
from fastapi import APIRouter, BackgroundTasks, Depends, Query from fastapi import APIRouter, BackgroundTasks, Depends, Query
@ -178,7 +179,7 @@ async def upload_to_social(
# 6. 백그라운드 태스크 등록 (즉시 업로드 or 예약 없을 때만) # 6. 백그라운드 태스크 등록 (즉시 업로드 or 예약 없을 때만)
from app.utils.timezone import now as utcnow from app.utils.timezone import now as utcnow
is_scheduled = body.scheduled_at and body.scheduled_at > utcnow() is_scheduled = body.scheduled_at and body.scheduled_at > utcnow().replace(tzinfo=None)
if not is_scheduled: if not is_scheduled:
background_tasks.add_task(process_social_upload, social_upload.id) background_tasks.add_task(process_social_upload, social_upload.id)
@ -246,42 +247,81 @@ async def get_upload_status(
"/history", "/history",
response_model=SocialUploadHistoryResponse, response_model=SocialUploadHistoryResponse,
summary="업로드 이력 조회", summary="업로드 이력 조회",
description="사용자의 소셜 미디어 업로드 이력을 조회합니다.", description="""
사용자의 소셜 미디어 업로드 이력을 조회합니다.
## tab 파라미터
- `all`: 전체 (기본값)
- `completed`: 완료된 업로드
- `scheduled`: 예약 업로드 (pending + scheduled_at 있음)
- `failed`: 실패한 업로드
""",
) )
async def get_upload_history( async def get_upload_history(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
tab: str = Query("all", description="탭 필터 (all/completed/scheduled/failed)"),
platform: Optional[SocialPlatform] = Query(None, description="플랫폼 필터"), 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="페이지 번호"), page: int = Query(1, ge=1, description="페이지 번호"),
size: int = Query(20, ge=1, le=100, description="페이지 크기"), size: int = Query(20, ge=1, le=100, description="페이지 크기"),
) -> SocialUploadHistoryResponse: ) -> SocialUploadHistoryResponse:
""" """
업로드 이력 조회 업로드 이력 조회
""" """
now = datetime.now(timezone.utc)
target_year = year or now.year
target_month = month or now.month
logger.info( logger.info(
f"[UPLOAD_API] 이력 조회 - " 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( 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( 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: if platform:
query = query.where(SocialUpload.platform == platform.value) query = query.where(SocialUpload.platform == platform.value)
count_query = count_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_result = await session.execute(count_query)
total = total_result.scalar() or 0 total = total_result.scalar() or 0
@ -306,6 +346,8 @@ async def get_upload_history(
status=upload.status, status=upload.status,
title=upload.title, title=upload.title,
platform_url=upload.platform_url, platform_url=upload.platform_url,
error_message=upload.error_message,
scheduled_at=upload.scheduled_at,
created_at=upload.created_at, created_at=upload.created_at,
uploaded_at=upload.uploaded_at, uploaded_at=upload.uploaded_at,
) )

View File

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

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;