From 7f0ae81351cc9260359d7a7be19b4a7bc6a9b9df Mon Sep 17 00:00:00 2001 From: Dohyun Lim Date: Thu, 12 Feb 2026 17:52:51 +0900 Subject: [PATCH] remove endpoint at the video of get_videos --- app/home/schemas/home_schema.py | 129 ---------------------- app/sns/schemas/sns_schema.py | 16 +-- app/song/schemas/song_schema.py | 111 +------------------ app/user/api/routers/v1/auth.py | 1 - app/user/schemas/__init__.py | 2 - app/user/schemas/user_schema.py | 18 --- app/user/services/auth.py | 1 - app/utils/prompts/schemas/youtube_desc.py | 16 --- app/video/api/routers/v1/video.py | 129 +--------------------- 9 files changed, 3 insertions(+), 420 deletions(-) delete mode 100644 app/utils/prompts/schemas/youtube_desc.py diff --git a/app/home/schemas/home_schema.py b/app/home/schemas/home_schema.py index 45b0126..e227da2 100644 --- a/app/home/schemas/home_schema.py +++ b/app/home/schemas/home_schema.py @@ -3,112 +3,6 @@ from typing import Literal, Optional from pydantic import BaseModel, ConfigDict, Field from app.utils.prompts.schemas import MarketingPromptOutput -class AttributeInfo(BaseModel): - """음악 속성 정보""" - - genre: str = Field(..., description="음악 장르") - vocal: str = Field(..., description="보컬 스타일") - tempo: str = Field(..., description="템포") - mood: str = Field(..., description="분위기") - - -class GenerateRequestImg(BaseModel): - """이미지 URL 스키마""" - - url: str = Field(..., description="이미지 URL") - name: Optional[str] = Field(None, description="이미지명 (없으면 URL에서 추출)") - - -class GenerateRequestInfo(BaseModel): - """생성 요청 정보 스키마 (이미지 제외)""" - - customer_name: str = Field(..., description="고객명/가게명") - region: str = Field(..., description="지역명") - detail_region_info: Optional[str] = Field(None, description="상세 지역 정보") - attribute: AttributeInfo = Field(..., description="음악 속성 정보") - language: str = Field( - default="Korean", - description="가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)", - ) - - -class GenerateRequest(GenerateRequestInfo): - """기본 생성 요청 스키마 (이미지 없음, JSON body) - - 이미지 없이 프로젝트 정보만 전달합니다. - """ - - model_config = ConfigDict( - json_schema_extra={ - "example": { - "customer_name": "스테이 머뭄", - "region": "군산", - "detail_region_info": "군산 신흥동 말랭이 마을", - "attribute": { - "genre": "K-Pop", - "vocal": "Raspy", - "tempo": "110 BPM", - "mood": "happy", - }, - "language": "Korean", - } - } - ) - - -class GenerateUrlsRequest(GenerateRequestInfo): - """URL 기반 생성 요청 스키마 (JSON body) - - GenerateRequestInfo를 상속받아 이미지 목록을 추가합니다. - """ - - model_config = ConfigDict( - json_schema_extra={ - "example": { - "customer_name": "스테이 머뭄", - "region": "군산", - "detail_region_info": "군산 신흥동 말랭이 마을", - "attribute": { - "genre": "K-Pop", - "vocal": "Raspy", - "tempo": "110 BPM", - "mood": "happy", - }, - "language": "Korean", - "images": [ - {"url": "https://example.com/images/image_001.jpg"}, - {"url": "https://example.com/images/image_002.jpg", "name": "외관"}, - ], - } - } - ) - - images: list[GenerateRequestImg] = Field( - ..., description="이미지 URL 목록", min_length=1 - ) - - -class GenerateUploadResponse(BaseModel): - """파일 업로드 기반 생성 응답 스키마""" - - task_id: str = Field(..., description="작업 고유 식별자 (UUID7)") - status: Literal["processing", "completed", "failed"] = Field( - ..., description="작업 상태" - ) - message: str = Field(..., description="응답 메시지") - uploaded_count: int = Field(..., description="업로드된 이미지 개수") - - -class GenerateResponse(BaseModel): - """생성 응답 스키마""" - - task_id: str = Field(..., description="작업 고유 식별자 (UUID7)") - status: Literal["processing", "completed", "failed"] = Field( - ..., description="작업 상태" - ) - message: str = Field(..., description="응답 메시지") - - class CrawlingRequest(BaseModel): """크롤링 요청 스키마""" @@ -371,29 +265,6 @@ class ImageUrlItem(BaseModel): name: Optional[str] = Field(None, description="이미지명 (없으면 URL에서 추출)") -class ImageUploadRequest(BaseModel): - """이미지 업로드 요청 스키마 (JSON body 부분) - - URL 이미지 목록을 전달합니다. - 바이너리 파일은 multipart/form-data로 별도 전달됩니다. - """ - - model_config = ConfigDict( - json_schema_extra={ - "example": { - "images": [ - {"url": "https://example.com/images/image_001.jpg"}, - {"url": "https://example.com/images/image_002.jpg", "name": "외관"}, - ] - } - } - ) - - images: Optional[list[ImageUrlItem]] = Field( - None, description="외부 이미지 URL 목록" - ) - - class ImageUploadResultItem(BaseModel): """업로드된 이미지 결과 아이템""" diff --git a/app/sns/schemas/sns_schema.py b/app/sns/schemas/sns_schema.py index 51fc960..ae15aee 100644 --- a/app/sns/schemas/sns_schema.py +++ b/app/sns/schemas/sns_schema.py @@ -4,7 +4,7 @@ SNS API Schemas Instagram 업로드 관련 Pydantic 스키마를 정의합니다. """ from datetime import datetime -from typing import Any, Optional +from typing import Optional from pydantic import BaseModel, ConfigDict, Field @@ -98,20 +98,6 @@ class Media(BaseModel): children: Optional[list["Media"]] = None -class MediaList(BaseModel): - """미디어 목록 응답""" - - data: list[Media] = Field(default_factory=list) - paging: Optional[dict[str, Any]] = None - - @property - def next_cursor(self) -> Optional[str]: - """다음 페이지 커서""" - if self.paging and "cursors" in self.paging: - return self.paging["cursors"].get("after") - return None - - class MediaContainer(BaseModel): """미디어 컨테이너 상태""" diff --git a/app/song/schemas/song_schema.py b/app/song/schemas/song_schema.py index 2656646..eb2d420 100644 --- a/app/song/schemas/song_schema.py +++ b/app/song/schemas/song_schema.py @@ -1,8 +1,5 @@ -from dataclasses import dataclass, field -from datetime import datetime -from typing import Dict, List, Optional +from typing import Optional -from fastapi import Request from pydantic import BaseModel, Field @@ -107,21 +104,6 @@ class GenerateSongResponse(BaseModel): error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)") -class PollingSongRequest(BaseModel): - """노래 생성 상태 조회 요청 스키마 (Legacy) - - Note: - 현재 사용되지 않음. GET /song/status/{song_id} 엔드포인트 사용. - - Example Request: - { - "task_id": "abc123..." - } - """ - - task_id: str = Field(..., description="Suno 작업 ID") - - class SongClipData(BaseModel): """생성된 노래 클립 정보""" @@ -234,94 +216,3 @@ class PollingSongResponse(BaseModel): song_result_url: Optional[str] = Field( None, description="노래 결과 URL (Song 테이블 status가 completed일 때 반환)" ) - - -# ============================================================================= -# Dataclass Schemas (Legacy) -# ============================================================================= - - -@dataclass -class StoreData: - id: int - created_at: datetime - store_name: str - store_category: str | None = None - store_region: str | None = None - store_address: str | None = None - store_phone_number: str | None = None - store_info: str | None = None - - -@dataclass -class AttributeData: - id: int - attr_category: str - attr_value: str - created_at: datetime - - -@dataclass -class SongSampleData: - id: int - ai: str - ai_model: str - sample_song: str - season: str | None = None - num_of_people: int | None = None - people_category: str | None = None - genre: str | None = None - - -@dataclass -class PromptTemplateData: - id: int - prompt: str - description: str | None = None - - -@dataclass -class SongFormData: - store_name: str - store_id: str - prompts: str - attributes: Dict[str, str] = field(default_factory=dict) - attributes_str: str = "" - lyrics_ids: List[int] = field(default_factory=list) - llm_model: str = "gpt-5-mini" - - @classmethod - async def from_form(cls, request: Request): - """Request의 form 데이터로부터 dataclass 인스턴스 생성""" - form_data = await request.form() - - # 고정 필드명들 - fixed_keys = {"store_info_name", "store_id", "llm_model", "prompts"} - - # lyrics-{id} 형태의 모든 키를 찾아서 ID 추출 - lyrics_ids = [] - attributes = {} - - for key, value in form_data.items(): - if key.startswith("lyrics-"): - lyrics_id = key.split("-")[1] - lyrics_ids.append(int(lyrics_id)) - elif key not in fixed_keys: - attributes[key] = value - - # attributes를 문자열로 변환 - attributes_str = ( - "\r\n\r\n".join([f"{key} : {value}" for key, value in attributes.items()]) - if attributes - else "" - ) - - return cls( - store_name=form_data.get("store_info_name", ""), - store_id=form_data.get("store_id", ""), - attributes=attributes, - attributes_str=attributes_str, - lyrics_ids=lyrics_ids, - llm_model=form_data.get("llm_model", "gpt-5-mini"), - prompts=form_data.get("prompts", ""), - ) diff --git a/app/user/api/routers/v1/auth.py b/app/user/api/routers/v1/auth.py index c8b4b6e..a553a7b 100644 --- a/app/user/api/routers/v1/auth.py +++ b/app/user/api/routers/v1/auth.py @@ -23,7 +23,6 @@ logger = logging.getLogger(__name__) from app.user.dependencies import get_current_user from app.user.models import RefreshToken, User from app.user.schemas.user_schema import ( - AccessTokenResponse, KakaoCodeRequest, KakaoLoginResponse, LoginResponse, diff --git a/app/user/schemas/__init__.py b/app/user/schemas/__init__.py index 6841f87..4709e8a 100644 --- a/app/user/schemas/__init__.py +++ b/app/user/schemas/__init__.py @@ -1,5 +1,4 @@ from app.user.schemas.user_schema import ( - AccessTokenResponse, KakaoCodeRequest, KakaoLoginResponse, KakaoTokenResponse, @@ -12,7 +11,6 @@ from app.user.schemas.user_schema import ( ) __all__ = [ - "AccessTokenResponse", "KakaoCodeRequest", "KakaoLoginResponse", "KakaoTokenResponse", diff --git a/app/user/schemas/user_schema.py b/app/user/schemas/user_schema.py index e0c6337..d10fd13 100644 --- a/app/user/schemas/user_schema.py +++ b/app/user/schemas/user_schema.py @@ -64,24 +64,6 @@ class TokenResponse(BaseModel): } -class AccessTokenResponse(BaseModel): - """액세스 토큰 갱신 응답""" - - access_token: str = Field(..., description="액세스 토큰") - token_type: str = Field(default="Bearer", description="토큰 타입") - expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)") - - model_config = { - "json_schema_extra": { - "example": { - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzA1MzE1MjAwfQ.new_token", - "token_type": "Bearer", - "expires_in": 3600 - } - } - } - - class RefreshTokenRequest(BaseModel): """토큰 갱신 요청""" diff --git a/app/user/services/auth.py b/app/user/services/auth.py index 43e7bec..9f964e8 100644 --- a/app/user/services/auth.py +++ b/app/user/services/auth.py @@ -81,7 +81,6 @@ class AdminRequiredError(AuthException): from app.user.models import RefreshToken, User from app.utils.common import generate_uuid from app.user.schemas.user_schema import ( - AccessTokenResponse, KakaoUserInfo, LoginResponse, TokenResponse, diff --git a/app/utils/prompts/schemas/youtube_desc.py b/app/utils/prompts/schemas/youtube_desc.py deleted file mode 100644 index d81b97d..0000000 --- a/app/utils/prompts/schemas/youtube_desc.py +++ /dev/null @@ -1,16 +0,0 @@ -from pydantic import BaseModel, Field -from typing import List, Optional - -# Input 정의 -class YTUploadPromptInput(BaseModel): - customer_name : str = Field(..., description = "마케팅 대상 사업체 이름") - detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세") - marketing_intelligence_summary : Optional[str] = Field(None, description = "마케팅 분석 정보 보고서") - language : str= Field(..., description = "영상 언어") - target_keywords: List[str] = Field(..., description="태그 키워드 리스트") - -# Output 정의 -class YTUploadPromptOutput(BaseModel): - title:str = Field(..., description="유튜브 영상 제목 - SEO/AEO 최적화") - description: str = Field(..., description = "유튜브 영상 설명 - SEO/AEO 최적화") - diff --git a/app/video/api/routers/v1/video.py b/app/video/api/routers/v1/video.py index b203082..94e39ff 100644 --- a/app/video/api/routers/v1/video.py +++ b/app/video/api/routers/v1/video.py @@ -7,7 +7,6 @@ Video API Router - POST /video/generate/{task_id}: 영상 생성 요청 (task_id로 Project/Lyric/Song 연결) - GET /video/status/{creatomate_render_id}: Creatomate API 영상 생성 상태 조회 - GET /video/download/{task_id}: 영상 다운로드 상태 조회 (DB polling) - - GET /video/list: 완료된 영상 목록 조회 (페이지네이션) 사용 예시: from app.video.api.routers.v1.video import router @@ -18,11 +17,10 @@ import json from typing import Literal from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query -from sqlalchemy import func, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database.session import get_session -from app.dependencies.pagination import PaginationParams, get_pagination_params from app.user.dependencies.auth import get_current_user from app.user.models import User from app.home.models import Image, Project @@ -30,13 +28,11 @@ from app.lyric.models import Lyric from app.song.models import Song, SongTimestamp from app.utils.creatomate import CreatomateService from app.utils.logger import get_logger -from app.utils.pagination import PaginatedResponse from app.video.models import Video from app.video.schemas.video_schema import ( DownloadVideoResponse, GenerateVideoResponse, PollingVideoResponse, - VideoListItem, VideoRenderData, ) from app.video.worker.video_task import download_and_upload_video_to_blob @@ -738,126 +734,3 @@ async def download_video( message="영상 다운로드 조회에 실패했습니다.", error_message=str(e), ) - - -@router.get( - "/list", - summary="생성된 영상 목록 조회", - description=""" -완료된 영상 목록을 페이지네이션하여 조회합니다. - -## 인증 -**Bearer 토큰 필수** - `Authorization: Bearer {access_token}` 헤더를 포함해야 합니다. - -## 쿼리 파라미터 -- **page**: 페이지 번호 (1부터 시작, 기본값: 1) -- **page_size**: 페이지당 데이터 수 (기본값: 10, 최대: 100) - -## 반환 정보 -- **items**: 영상 목록 (store_name, region, task_id, result_movie_url, created_at) -- **total**: 전체 데이터 수 -- **page**: 현재 페이지 -- **page_size**: 페이지당 데이터 수 -- **total_pages**: 전체 페이지 수 -- **has_next**: 다음 페이지 존재 여부 -- **has_prev**: 이전 페이지 존재 여부 - -## 사용 예시 (cURL) -```bash -curl -X GET "http://localhost:8000/video/list?page=1&page_size=10" \\ - -H "Authorization: Bearer {access_token}" -``` - -## 참고 -- status가 'completed'인 영상만 반환됩니다. -- 동일한 task_id가 있는 경우 가장 최근에 생성된 1개만 반환됩니다. -- created_at 기준 내림차순 정렬됩니다. - """, - response_model=PaginatedResponse[VideoListItem], - responses={ - 200: {"description": "영상 목록 조회 성공"}, - 401: {"description": "인증 실패 (토큰 없음/만료)"}, - 500: {"description": "조회 실패"}, - }, -) -async def get_videos( - current_user: User = Depends(get_current_user), - session: AsyncSession = Depends(get_session), - pagination: PaginationParams = Depends(get_pagination_params), -) -> PaginatedResponse[VideoListItem]: - """완료된 영상 목록을 페이지네이션하여 반환합니다.""" - logger.info( - f"[get_videos] START - page: {pagination.page}, page_size: {pagination.page_size}" - ) - try: - offset = (pagination.page - 1) * pagination.page_size - - # 서브쿼리: task_id별 최신 Video의 id 조회 (completed 상태만) - subquery = ( - select(func.max(Video.id).label("max_id")) - .where(Video.status == "completed") - .group_by(Video.task_id) - .subquery() - ) - - # 전체 개수 조회 (task_id별 최신 1개만) - count_query = select(func.count()).select_from(subquery) - total_result = await session.execute(count_query) - total = total_result.scalar() or 0 - - # 데이터 조회 (completed 상태, task_id별 최신 1개만, 최신순) - query = ( - select(Video) - .where(Video.id.in_(select(subquery.c.max_id))) - .order_by(Video.created_at.desc()) - .offset(offset) - .limit(pagination.page_size) - ) - result = await session.execute(query) - videos = result.scalars().all() - - # Project 정보 일괄 조회 (N+1 문제 해결) - project_ids = [v.project_id for v in videos if v.project_id] - projects_map: dict = {} - if project_ids: - projects_result = await session.execute( - select(Project).where(Project.id.in_(project_ids)) - ) - projects_map = {p.id: p for p in projects_result.scalars().all()} - - # VideoListItem으로 변환 - items = [] - for video in videos: - project = projects_map.get(video.project_id) - - item = VideoListItem( - video_id=video.id, - store_name=project.store_name if project else None, - region=project.region if project else None, - task_id=video.task_id, - result_movie_url=video.result_movie_url, - created_at=video.created_at, - ) - items.append(item) - - response = PaginatedResponse.create( - items=items, - total=total, - page=pagination.page, - page_size=pagination.page_size, - ) - - logger.info( - f"[get_videos] SUCCESS - total: {total}, page: {pagination.page}, " - f"page_size: {pagination.page_size}, items_count: {len(items)}" - ) - return response - - except Exception as e: - logger.error(f"[get_videos] EXCEPTION - error: {e}") - raise HTTPException( - status_code=500, - detail=f"영상 목록 조회에 실패했습니다: {str(e)}", - ) - -