From 7a887153ab766ec28c27483927a92dfd8f3ab79b Mon Sep 17 00:00:00 2001 From: hbyang Date: Tue, 3 Mar 2026 16:16:23 +0900 Subject: [PATCH] =?UTF-8?q?=EB=82=B4=EB=B6=80=20youtube=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20endpoint=20=EC=A0=81=EC=9A=A9=20.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/social/api/routers/v1/internal.py | 43 +++++++++++++++++++++++++++ app/social/api/routers/v1/upload.py | 13 +++++--- app/social/models.py | 9 ++++++ config.py | 12 ++++++++ main.py | 2 ++ 5 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 app/social/api/routers/v1/internal.py diff --git a/app/social/api/routers/v1/internal.py b/app/social/api/routers/v1/internal.py new file mode 100644 index 0000000..e079f9d --- /dev/null +++ b/app/social/api/routers/v1/internal.py @@ -0,0 +1,43 @@ +""" +내부 전용 소셜 업로드 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"]) + + +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( + "/upload/{upload_id}", + summary="[내부] 예약 업로드 실행", + description="스케줄러 서버에서 호출하는 내부 전용 엔드포인트입니다.", +) +async def trigger_scheduled_upload( + upload_id: int, + background_tasks: BackgroundTasks, + x_internal_secret: str = Header(...), +) -> dict: + _verify_secret(x_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": "업로드 작업이 시작되었습니다."} diff --git a/app/social/api/routers/v1/upload.py b/app/social/api/routers/v1/upload.py index 781d19b..ca85740 100644 --- a/app/social/api/routers/v1/upload.py +++ b/app/social/api/routers/v1/upload.py @@ -4,7 +4,7 @@ 소셜 미디어 영상 업로드 관련 엔드포인트를 제공합니다. """ -import logging, json +import logging from typing import Optional from fastapi import APIRouter, BackgroundTasks, Depends, Query @@ -158,6 +158,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 +176,19 @@ async def upload_to_social( f"account_id: {account.id}, upload_seq: {next_seq}, platform: {account.platform}" ) - # 6. 백그라운드 태스크 등록 - background_tasks.add_task(process_social_upload, social_upload.id) + # 6. 백그라운드 태스크 등록 (즉시 업로드 or 예약 없을 때만) + from app.utils.timezone import now as utcnow + is_scheduled = body.scheduled_at and body.scheduled_at > utcnow() + 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, ) diff --git a/app/social/models.py b/app/social/models.py index a5e7569..185fa8e 100644 --- a/app/social/models.py +++ b/app/social/models.py @@ -190,6 +190,15 @@ class SocialUpload(Base): comment="플랫폼별 추가 옵션 (JSON)", ) + # ========================================================================== + # 예약 게시 시간 + # ========================================================================== + scheduled_at: Mapped[Optional[datetime]] = mapped_column( + DateTime, + nullable=True, + comment="예약 게시 시간 (스케줄러가 이 시간 이후에 업로드 실행)", + ) + # ========================================================================== # 에러 정보 # ========================================================================== diff --git a/config.py b/config.py index 8aa00b5..12b87c1 100644 --- a/config.py +++ b/config.py @@ -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() diff --git a/main.py b/main.py index 453b6df..9b7b491 100644 --- a/main.py +++ b/main.py @@ -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 모드에서만 테스트 라우터 등록