{escaped_title}
{escaped_description}
콘텐츠 보기"""콘텐츠 공유 링크용 Open Graph HTML 생성 서비스.""" from collections.abc import Mapping from dataclasses import dataclass from html import escape from urllib.parse import urlsplit, urlunsplit from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.home.models import Project from app.ssulbox.models import SsulContent 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: """공유 페이지에 필요한 영상 및 프로젝트 정보.""" video_id: int poster_url: str | None store_name: str region: str title: str | None = None description: str | None = None @dataclass(frozen=True, slots=True) class SsulShareData: """공유 페이지에 필요한 썰박스 콘텐츠 정보.""" content_id: int poster_url: str | None store_name: str region: str title: str | None = None description: str | None = None async def get_video_share_data( session: AsyncSession, video_id: int, ) -> VideoShareData | None: """공유 가능한 완료 영상을 프로젝트 정보와 함께 조회합니다.""" result = await session.execute( select( Video.id, Video.poster_url, Video.title, Video.description, Project.store_name, Project.region, ) .join(Project, Video.project_id == Project.id) .where( Video.id == video_id, Video.status == "completed", Video.is_deleted.is_(False), Project.is_deleted.is_(False), ) ) row = result.one_or_none() if row is None: return None return VideoShareData( video_id=row.id, poster_url=row.poster_url, store_name=row.store_name, region=row.region, title=row.title, description=row.description, ) async def get_ssul_share_data( session: AsyncSession, content_id: int, ) -> SsulShareData | None: """공유 가능한 완료 썰박스 콘텐츠를 조회합니다.""" result = await session.execute( select( SsulContent.id, SsulContent.poster_url, SsulContent.title, SsulContent.description, SsulContent.store_name, SsulContent.region, ).where( SsulContent.id == content_id, SsulContent.status == "done", SsulContent.is_deleted.is_(False), SsulContent.video_url.is_not(None), SsulContent.video_url != "", ) ) row = result.one_or_none() if row is None: return None return SsulShareData( content_id=row.id, poster_url=row.poster_url, store_name=row.store_name, region=row.region or "", title=row.title, description=row.description, ) def build_video_share_html( data: VideoShareData, *, share_url: str, frontend_base_url: str, configured_default_image_url: str = "", ) -> str: """영상별 OG 메타데이터와 상세 화면 이동 기능을 포함한 HTML을 생성합니다.""" return _build_share_html( detail_path=f"/video/{data.video_id}", poster_url=data.poster_url, title=_share_title(data.title, data.store_name, fallback_store="ADO2 영상"), description=_share_description(data.description), share_url=share_url, frontend_base_url=frontend_base_url, configured_default_image_url=configured_default_image_url, ) def build_ssul_share_html( data: SsulShareData, *, share_url: str, frontend_base_url: str, configured_default_image_url: str = "", ) -> str: """썰박스 OG 메타데이터와 상세 화면 이동 기능을 포함한 HTML을 생성합니다.""" return _build_share_html( detail_path=f"/ssul/{data.content_id}", poster_url=data.poster_url, title=_share_title(data.title, data.store_name, fallback_store="ADO2 썰"), description=_share_description(data.description), share_url=share_url, frontend_base_url=frontend_base_url, configured_default_image_url=configured_default_image_url, ) def _build_share_html( *, detail_path: str, poster_url: str | None, title: str, description: str, share_url: str, frontend_base_url: str, configured_default_image_url: str, ) -> str: """크롤러용 OG 메타와, 사람용 프론트 상세 이동 링크를 포함한 HTML을 만듭니다.""" frontend_base = _normalise_frontend_base_url(frontend_base_url) detail_url = f"{frontend_base}{detail_path}" fallback_image_url = _resolve_default_image_url( configured_default_image_url, frontend_base, share_url=share_url, ) image_url = _absolute_http_url(poster_url) or fallback_image_url canonical_url = _absolute_http_url(share_url) or detail_url image_size_tags = _og_image_size_tags(image_url, fallback_image_url) escaped_title = escape(title, quote=True) escaped_description = escape(description, quote=True) escaped_image_url = escape(image_url, quote=True) escaped_canonical_url = escape(canonical_url, quote=True) escaped_detail_url = escape(detail_url, quote=True) return f"""
{escaped_description}
콘텐츠 보기