o2o-castad-backend/app/video/schemas/video_schema.py
hbyang 1ef8e82638 feat(crawling): 업체 공식 링크 수집·저장 및 official_site_url 응답 연동
- 네이버 placeDetail GraphQL base에 homepages 필드 추가, 대표(repr)
  링크 우선으로 살아있는 URL을 추출한다 (실서비스 응답 구조 확인 완료)
- marketing 테이블에 official_site_url 컬럼 추가: 크롤링 시 플레이스
  홈페이지 항목을 저장하고, 없으면 유저가 입력한 크롤링 소스 URL을 저장
- GET /video/{id}, /video/all의 official_site_url은 저장값 우선,
  컬럼 도입 전 기존 행은 place_id 기반 플레이스 URL로 폴백
- 기존 테이블에는 수동 마이그레이션 필요:
  ALTER TABLE marketing ADD COLUMN official_site_url VARCHAR(2048) NULL
  COMMENT '업체 공식 링크' AFTER place_id;

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVSeUT2pEmeHc6hG6gVXUS
2026-08-21 10:49:46 +09:00

230 lines
9.5 KiB
Python

"""
Video API Schemas
영상 생성 관련 Pydantic 스키마를 정의합니다.
"""
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field
# =============================================================================
# Response Schemas
# =============================================================================
class GenerateVideoResponse(BaseModel):
"""영상 생성 응답 스키마
Usage:
GET /video/generate/{task_id}
Returns the task IDs for tracking video generation.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"success": True,
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"creatomate_render_id": "render-id-123456",
"message": "영상 생성 요청이 접수되었습니다. creatomate_render_id로 상태를 조회하세요.",
"error_message": None,
}
}
)
success: bool = Field(..., description="요청 성공 여부")
status: Optional[str] = Field(None, description="처리 상태 (subtitle_pending: 자막 미완료, completed: 정상 접수)")
task_id: Optional[str] = Field(None, description="내부 작업 ID (Project task_id)")
creatomate_render_id: Optional[str] = Field(None, description="Creatomate 렌더 ID")
message: str = Field(..., description="응답 메시지")
error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)")
class VideoRenderData(BaseModel):
"""Creatomate 렌더링 결과 데이터"""
id: Optional[str] = Field(None, description="렌더 ID")
status: Optional[str] = Field(None, description="렌더 상태")
url: Optional[str] = Field(None, description="영상 URL")
snapshot_url: Optional[str] = Field(None, description="스냅샷 URL")
video_id: Optional[int] = Field(None, description="Video id(DB)")
class PollingVideoResponse(BaseModel):
"""영상 생성 상태 조회 응답 스키마
Usage:
GET /video/status/{creatomate_render_id}
Creatomate API 작업 상태를 조회합니다.
Note:
상태 값:
- planned: 예약됨
- waiting: 대기 중
- transcribing: 트랜스크립션 중
- rendering: 렌더링 중
- succeeded: 성공
- failed: 실패
Example Response (Success):
{
"success": true,
"status": "succeeded",
"message": "영상 생성이 완료되었습니다.",
"render_data": {
"id": "render-id",
"status": "succeeded",
"url": "https://...",
"snapshot_url": "https://..."
},
"raw_response": {...},
"error_message": null
}
"""
success: bool = Field(..., description="조회 성공 여부")
status: Optional[str] = Field(
None, description="작업 상태 (planned, waiting, rendering, succeeded, failed)"
)
message: str = Field(..., description="상태 메시지")
render_data: Optional[VideoRenderData] = Field(None, description="렌더링 결과 데이터")
raw_response: Optional[Dict[str, Any]] = Field(None, description="Creatomate API 원본 응답")
error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)")
class DownloadVideoResponse(BaseModel):
"""영상 다운로드 응답 스키마
Usage:
GET /video/download/{task_id}
Polls for video completion and returns project info with video URL.
Note:
상태 값:
- processing: 영상 생성 진행 중 (result_movie_url은 null)
- completed: 영상 생성 완료 (result_movie_url 포함)
- failed: 영상 생성 실패
- not_found: task_id에 해당하는 Video 없음
- error: 조회 중 오류 발생
Example Response (Completed):
{
"success": true,
"status": "completed",
"message": "영상 다운로드가 완료되었습니다.",
"store_name": "스테이 머뭄",
"region": "군산",
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
"result_movie_url": "http://localhost:8000/media/2025-01-15/video.mp4",
"created_at": "2025-01-15T12:00:00",
"error_message": null
}
"""
success: bool = Field(..., description="다운로드 성공 여부")
status: str = Field(..., description="처리 상태 (processing, completed, failed, not_found, error)")
message: str = Field(..., description="응답 메시지")
store_name: Optional[str] = Field(None, description="업체명")
region: Optional[str] = Field(None, description="지역명")
task_id: Optional[str] = Field(None, description="작업 고유 식별자")
result_movie_url: Optional[str] = Field(None, description="영상 결과 URL")
created_at: Optional[datetime] = Field(None, description="생성 일시")
error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)")
class VideoListItem(BaseModel):
"""영상 목록 아이템 스키마
Usage:
GET /videos 응답의 개별 영상 정보
Example:
{
"video_id": 1,
"store_name": "스테이 머뭄",
"region": "군산",
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
"result_movie_url": "http://localhost:8000/media/2025-01-15/video.mp4",
"poster_url": "http://localhost:8000/media/2025-01-15/video.jpg",
"created_at": "2025-01-15T12:00:00"
}
"""
video_id: int = Field(..., description="영상 고유 ID")
store_name: Optional[str] = Field(None, description="업체명")
region: Optional[str] = Field(None, description="지역명")
task_id: str = Field(..., description="작업 고유 식별자")
result_movie_url: Optional[str] = Field(None, description="영상 결과 URL")
poster_url: Optional[str] = Field(None, description="영상 첫 프레임 포스터 이미지 URL")
title: Optional[str] = Field(None, description="SNS 업로드 제목")
description: Optional[str] = Field(None, description="SNS 업로드 설명")
hashtags: Optional[List[str]] = Field(None, description="SNS 해시태그 목록")
created_at: Optional[datetime] = Field(None, description="생성 일시")
like_count: int = Field(0, description="좋아요 수")
comment_count: int = Field(0, description="댓글 수 (대댓글 포함)")
is_liked_by_me: bool = Field(
False,
description="현재 로그인 사용자가 좋아요를 눌렀는지",
)
class VideoThumbnailItem(BaseModel):
"""ADO2 콘텐츠 갤러리용 최소 영상 정보 (썸네일 표시 + 상세 페이지 이동용)
Usage:
GET /video/all 응답의 개별 영상 정보
"""
video_id: int = Field(..., description="영상 고유 ID (상세 페이지 라우팅 키)")
store_name: str = Field(..., description="업체명")
result_movie_url: str = Field(..., description="영상 URL")
poster_url: Optional[str] = Field(None, description="영상 첫 프레임 포스터 이미지 URL (썸네일 표시용)")
created_at: datetime = Field(..., description="생성 일시")
like_count: int = Field(..., description="좋아요 수")
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지 (비로그인은 항상 false)")
comment_count: int = Field(..., description="댓글 수 (대댓글 포함)")
official_site_url: Optional[str] = Field(
None,
description="업체 공식 링크 (플레이스 홈페이지 항목 우선, 없으면 크롤링 소스 URL; 직접 입력 생성 영상만 null)",
)
class VideoDetailResponse(BaseModel):
"""단일 영상 상세 응답
Usage:
GET /video/{video_id}
"""
video_id: int = Field(..., description="영상 고유 ID")
result_movie_url: str = Field(..., description="영상 URL")
poster_url: Optional[str] = Field(None, description="영상 첫 프레임 포스터 이미지 URL")
store_name: Optional[str] = Field(None, description="업체명")
region: Optional[str] = Field(None, description="지역명")
title: Optional[str] = Field(None, description="SNS 업로드 제목 (공유 시 og:title 및 공유 제목으로 사용)")
description: Optional[str] = Field(None, description="SNS 업로드 설명")
created_at: datetime = Field(..., description="생성 일시")
like_count: int = Field(..., description="좋아요 수")
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지 (비로그인은 항상 false)")
official_site_url: Optional[str] = Field(
None,
description="업체 공식 링크 (플레이스 홈페이지 항목 우선, 없으면 크롤링 소스 URL; 직접 입력 생성 영상만 null)",
)
class LikeToggleResponse(BaseModel):
"""좋아요 토글 응답
Usage:
POST /video/{video_id}/like
"""
video_id: int = Field(..., description="영상 고유 ID")
is_liked: bool = Field(..., description="토글 후 상태 (true=좋아요 누름, false=취소됨)")
like_count: int = Field(..., description="토글 후 전체 좋아요 수")