142 lines
5.9 KiB
Python
142 lines
5.9 KiB
Python
"""썰박스 API 요청/응답 스키마 (Pydantic v2)."""
|
|
|
|
from datetime import datetime
|
|
from typing import Literal, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.ssulbox.constants import SCENARIOS
|
|
|
|
ScenarioLiteral = Literal["joseon", "samgukji", "greek", "odyssey"]
|
|
|
|
|
|
# 장소 검색 스키마는 두지 않는다 — 업장 검색은 ADO2 와 동일하게
|
|
# `/search/accommodation`(네이버 검색 API)을 쓰고, place URL 은 생성 요청 시
|
|
# 서버가 해석한다(2026-07-31 전환). 썰박스 전용 검색 엔드포인트는 제거됐다.
|
|
|
|
|
|
# =============================================================================
|
|
# 생성 요청
|
|
# =============================================================================
|
|
class SsulCreateRequest(BaseModel):
|
|
"""생성 요청.
|
|
|
|
`scenes`/`seconds` 의 기본값과 허용 범위는 **여기서만** 강제한다.
|
|
DB 에 기본값을 두면 진실이 두 곳에 생기므로 모델에는 두지 않았다.
|
|
"""
|
|
|
|
scenario: ScenarioLiteral = Field(..., description="시나리오 코드")
|
|
input: str = Field(
|
|
...,
|
|
min_length=2,
|
|
max_length=500,
|
|
description=(
|
|
"네이버 지도 place URL 또는 업장명. "
|
|
"자동완성으로 고른 경우 store_name/address 가 함께 오며, "
|
|
"그때는 서버가 ADO2 와 동일한 방식으로 place URL 을 해석한다."
|
|
),
|
|
)
|
|
scenes: int = Field(default=9, ge=4, le=20, description="장면 수")
|
|
seconds: int = Field(default=30, ge=20, le=90, description="장면당 초 길이")
|
|
|
|
# 자동완성(`/search/accommodation`)으로 업장을 고른 경우 프론트가 함께 보낸다.
|
|
# 세 가지에 쓰인다: ① place URL 해석 ② 통합 목록의 업장명 표시
|
|
# ③ store_name/region 필터.
|
|
# place URL 을 직접 붙여넣은 경우에는 없으며, 그때 store_name 은 생성 로그의
|
|
# `■ 가게:` 마커로 뒤늦게 채운다(region 은 주소가 없어 채울 수 없다).
|
|
store_name: str | None = Field(
|
|
default=None, max_length=200, description="업장명 (검색 선택 시)"
|
|
)
|
|
# 도로명·지번을 모두 받는다. castad `/home/crawl` 과 같이 도로명에서 시/군 추출이
|
|
# 실패하면 지번으로 재시도해야 지역이 비는 경우를 줄인다.
|
|
road_address: str | None = Field(
|
|
default=None,
|
|
max_length=300,
|
|
description="도로명 주소 (검색 선택 시). region 추출에만 쓰고 저장하지 않는다",
|
|
)
|
|
address: str | None = Field(
|
|
default=None,
|
|
max_length=300,
|
|
description="지번 주소 (검색 선택 시). 도로명 추출 실패 시 폴백",
|
|
)
|
|
|
|
|
|
class SsulCreateResponse(BaseModel):
|
|
id: int = Field(..., description="생성 잡 ID (폴링·크레딧 원장 앵커)")
|
|
status: str = Field(..., description="queued")
|
|
poll_interval_seconds: int = Field(
|
|
..., description="권장 폴링 간격(초). 클라이언트가 참고한다"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# 진행 상태 (폴링)
|
|
# =============================================================================
|
|
class SsulTaskStatus(BaseModel):
|
|
"""`GET /ssul/tasks/{id}` 응답. 프론트가 3초마다 폴링한다."""
|
|
|
|
id: int
|
|
scenario: str
|
|
status: Literal["queued", "running", "done", "error"]
|
|
step: int = Field(..., ge=0, le=4, description="완료한 단계 수 (0=준비, 4=합성 완료)")
|
|
error: Optional[str] = None
|
|
video_url: Optional[str] = Field(None, description="완료 시에만 채워진다")
|
|
# 완료 화면의 파일명·표시에 쓴다(ADO2 가 업장명으로 파일명을 만드는 것과 동일).
|
|
#
|
|
# **`status='done'` 이면 채워져 있다.** 확보 경로가 3중이라서다:
|
|
# ① 자동완성으로 고른 경우 생성 요청에 실려 온다
|
|
# ② 아니면 워커가 place 상세를 크롤링해 채운다(`set_place_info`)
|
|
# ③ 그래도 비어 있으면 엔진 로그의 `■ 가게:` 마커로 finalize 시 채운다
|
|
# 다만 생성 **중**(queued/running)에는 아직 비어 있을 수 있다 — 행은 요청 즉시
|
|
# 만들어지기 때문이다(크레딧 선차감). 진행 화면에서 쓰려면 그 점을 감안할 것.
|
|
store_name: str = Field(
|
|
"", description="대상 업장명. 완료(done) 시점에는 항상 채워져 있다"
|
|
)
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class SsulActiveTasksResponse(BaseModel):
|
|
"""진행 중인 내 잡. 새로고침·새 탭 복구에 쓴다"""
|
|
|
|
items: list[SsulTaskStatus]
|
|
|
|
|
|
class SsulDetailResponse(BaseModel):
|
|
"""`GET /ssul/{content_id}` 공개 상세 응답.
|
|
|
|
castad `VideoDetailResponse` 와 같은 필드 구성에 `scenario` 만 더했다
|
|
(프론트가 시나리오 표지·라벨을 그리는 데 쓴다). 공유 링크로 들어온
|
|
**비로그인 사용자도 볼 수 있다** — `is_liked_by_me` 는 비로그인이면 항상 False.
|
|
"""
|
|
|
|
content_id: int = Field(..., description="콘텐츠 고유 ID")
|
|
scenario: str = Field(..., description="시나리오 코드")
|
|
video_url: str = Field(..., description="완성 영상 URL")
|
|
store_name: Optional[str] = Field(None, description="업장명")
|
|
region: Optional[str] = Field(None, description="지역명")
|
|
created_at: datetime = Field(..., description="생성 일시")
|
|
like_count: int = Field(..., description="좋아요 수")
|
|
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지")
|
|
|
|
|
|
class SsulDeleteResponse(BaseModel):
|
|
"""`DELETE /ssul/{content_id}` 응답. castad 삭제 응답과 같은 형태."""
|
|
|
|
success: bool
|
|
content_id: int
|
|
message: str
|
|
|
|
|
|
__all__ = [
|
|
"SCENARIOS",
|
|
"ScenarioLiteral",
|
|
"SsulActiveTasksResponse",
|
|
"SsulCreateRequest",
|
|
"SsulCreateResponse",
|
|
"SsulDeleteResponse",
|
|
"SsulDetailResponse",
|
|
"SsulTaskStatus",
|
|
]
|