스케줄 작업 완료 .
parent
6fba9c5362
commit
41087b5fda
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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="업로드 완료 일시")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue