fix: 배포환경에 맞게 공유URL 적용되도록 수정

This commit is contained in:
김성경 2026-08-18 17:23:50 +09:00
parent 51ef659f16
commit c7149deb1f
6 changed files with 84 additions and 50 deletions

7
.gitignore vendored
View File

@ -32,8 +32,11 @@ media/
*.ipynb_checkpoint*
# Static files
static/
# Static files (공유 기본 이미지는 예외로 추적)
static/*
!static/images/
static/images/*
!static/images/ado2_image.png
# Log files
*.log

View File

@ -69,8 +69,8 @@ PROJECT_DOMAIN=localhost:8000 # 프로젝트 도메인 (호스트:포
PROJECT_VERSION=0.1.0 # 프로젝트 버전
DESCRIPTION=FastAPI 기반 CastAD 프로젝트 # 프로젝트 설명
ADMIN_BASE_URL=/admin # 관리자 페이지 기본 URL
SHARE_FRONTEND_URL=https://ado2.o2osolution.ai # 공유 링크가 이동할 프론트엔드 공개 URL
SHARE_DEFAULT_IMAGE_URL=https://ado2.o2osolution.ai/assets/images/hero-background.png # 포스터가 없을 때의 절대 이미지 URL
SHARE_FRONTEND_URL=https://ado2.o2osolution.ai # 공유 페이지 → 영상 상세 이동 프론트 URL (로컬: http://localhost:3000, 테스트: https://dev.castad.net)
SHARE_DEFAULT_IMAGE_URL= # 포스터 없을 때 OG 이미지 (비우면 API /static/images/ado2_image.png)
DEBUG=True # 디버그 모드 (True: 개발, False: 운영)
# ================================

View File

@ -17,8 +17,8 @@ import json
from collections import defaultdict
from typing import Literal
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request
from fastapi.responses import HTMLResponse
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request
from fastapi.responses import HTMLResponse
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
@ -58,11 +58,11 @@ from app.video.schemas.video_schema import (
VideoRenderData,
VideoThumbnailItem,
)
from app.video.services.share_page import build_video_share_html, get_video_share_data
from app.video.services.share_page import build_video_share_html, get_video_share_data
from app.video.worker.video_task import download_and_upload_video_to_blob
from config import creatomate_settings, prj_settings
from config import creatomate_settings, prj_settings
logger = get_logger("video")
@ -1093,43 +1093,43 @@ async def toggle_like(
raise HTTPException(status_code=500, detail=f"좋아요 처리에 실패했습니다: {str(e)}")
@router.get(
"/share/{video_id}",
response_class=HTMLResponse,
summary="영상 공유용 Open Graph 페이지",
description="영상별 제목, 설명, 포스터 메타데이터가 포함된 공개 HTML을 반환합니다.",
responses={
200: {"description": "공유 메타데이터 HTML 반환"},
404: {"description": "공유 가능한 완료 영상을 찾을 수 없음"},
},
)
async def get_video_share_page(
video_id: int,
request: Request,
session: AsyncSession = Depends(get_session),
) -> HTMLResponse:
"""공개 공유 페이지를 반환하고 일반 브라우저는 영상 상세로 이동시킵니다."""
share_data = await get_video_share_data(session, video_id)
if share_data is None:
raise HTTPException(status_code=404, detail="공유 가능한 영상을 찾을 수 없습니다.")
share_url = str(request.url).split("?", maxsplit=1)[0]
html = build_video_share_html(
share_data,
share_url=share_url,
frontend_base_url=prj_settings.SHARE_FRONTEND_URL,
configured_default_image_url=prj_settings.SHARE_DEFAULT_IMAGE_URL,
)
return HTMLResponse(
content=html,
headers={
"Cache-Control": "public, max-age=300",
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
},
)
@router.get(
"/share/{video_id}",
response_class=HTMLResponse,
summary="영상 공유용 Open Graph 페이지",
description="영상별 제목, 설명, 포스터 메타데이터가 포함된 공개 HTML을 반환합니다.",
responses={
200: {"description": "공유 메타데이터 HTML 반환"},
404: {"description": "공유 가능한 완료 영상을 찾을 수 없음"},
},
)
async def get_video_share_page(
video_id: int,
request: Request,
session: AsyncSession = Depends(get_session),
) -> HTMLResponse:
"""공개 공유 페이지를 반환하고 일반 브라우저는 영상 상세로 이동시킵니다."""
share_data = await get_video_share_data(session, video_id)
if share_data is None:
raise HTTPException(status_code=404, detail="공유 가능한 영상을 찾을 수 없습니다.")
share_url = str(request.url).split("?", maxsplit=1)[0]
html = build_video_share_html(
share_data,
share_url=share_url,
frontend_base_url=prj_settings.SHARE_FRONTEND_URL,
configured_default_image_url=prj_settings.SHARE_DEFAULT_IMAGE_URL,
)
return HTMLResponse(
content=html,
headers={
"Cache-Control": "public, max-age=300",
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
},
)
@router.get(
"/{video_id}",
summary="단일 영상 상세 조회",

View File

@ -12,6 +12,7 @@ from app.video.models import Video
FALLBACK_FRONTEND_URL = "https://ado2.o2osolution.ai"
DEFAULT_SHARE_IMAGE_PATH = "/assets/images/ado2_image.png"
DEFAULT_SHARE_IMAGE_STATIC_PATH = "/static/images/ado2_image.png"
@dataclass(frozen=True, slots=True)
class VideoShareData:
@ -68,6 +69,7 @@ def build_video_share_html(
fallback_image_url = _resolve_default_image_url(
configured_default_image_url,
frontend_base,
share_url=share_url,
)
image_url = _absolute_http_url(data.poster_url) or fallback_image_url
@ -136,14 +138,40 @@ def _normalise_frontend_base_url(value: str) -> str:
return urlunsplit((parts.scheme, parts.netloc, path, "", ""))
def _resolve_default_image_url(configured_url: str, frontend_base: str) -> str:
"""설정된 기본 이미지 또는 프론트엔드의 공개 기본 이미지를 반환합니다."""
def _resolve_default_image_url(
configured_url: str,
frontend_base: str,
*,
share_url: str = "",
) -> str:
"""포스터가 없을 때 사용할 기본 OG 이미지 URL을 반환합니다.
우선순위:
1. ``SHARE_DEFAULT_IMAGE_URL`` (.env)
2. 공유 API 호스트 ``/static/images/ado2_image.png``
3. ``SHARE_FRONTEND_URL`` + ``/assets/images/ado2_image.png``
"""
configured_absolute_url = _absolute_http_url(configured_url)
if configured_absolute_url:
return configured_absolute_url
share_origin = _origin_from_url(share_url)
if share_origin:
return f"{share_origin}{DEFAULT_SHARE_IMAGE_STATIC_PATH}"
return f"{frontend_base}{DEFAULT_SHARE_IMAGE_PATH}"
def _origin_from_url(value: str) -> str | None:
"""URL에서 scheme + host(+port) origin만 추출합니다."""
absolute_url = _absolute_http_url(value)
if not absolute_url:
return None
parts = urlsplit(absolute_url)
return urlunsplit((parts.scheme, parts.netloc, "", "", ""))
def _absolute_http_url(value: str | None) -> str | None:
"""값이 절대 HTTP(S) URL인 경우에만 정리된 문자열을 반환합니다."""
candidate = (value or "").strip()

View File

@ -35,11 +35,14 @@ class ProjectSettings(BaseSettings):
ADMIN_SESSION_MAX_AGE: int = Field(default=60 * 60 * 8)
SHARE_FRONTEND_URL: str = Field(
default="https://ado2.o2osolution.ai",
description="영상 공유 링크가 이동할 프론트엔드 공개 기준 URL",
description="공유 페이지에서 영상 상세로 이동할 프론트엔드 공개 기준 URL (.env: SHARE_FRONTEND_URL)",
)
SHARE_DEFAULT_IMAGE_URL: str = Field(
default="https://ado2.o2osolution.ai/assets/images/ado2_image.png",
description="포스터가 없는 영상 공유 시 사용할 절대 이미지 URL",
default="",
description=(
"포스터가 없는 영상 공유 시 사용할 절대 이미지 URL (.env: SHARE_DEFAULT_IMAGE_URL). "
"비우면 공유 API /static/images/ado2_image.png, 없으면 SHARE_FRONTEND_URL/assets 경로 사용"
),
)
DEBUG: bool = Field(default=True)
TIMEZONE: str = Field(

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB