- 네이버 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
230 lines
9.5 KiB
Python
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="토글 후 전체 좋아요 수")
|
|
|
|
|