parent
5f06c3c59d
commit
03a80afc71
|
|
@ -0,0 +1,121 @@
|
|||
"""
|
||||
Pagination Dependencies
|
||||
|
||||
페이지네이션 관련 의존성 주입을 정의합니다.
|
||||
|
||||
사용 예시:
|
||||
from app.dependencies.pagination import PaginationParams, get_pagination_params, paginated_query
|
||||
|
||||
# 방법 1: 기본 파라미터만 사용
|
||||
@router.get("/items", response_model=PaginatedResponse[ItemModel])
|
||||
async def get_items(
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
...
|
||||
|
||||
# 방법 2: paginated_query 사용 (get_paginated 래핑)
|
||||
@router.get("/items", response_model=PaginatedResponse[ItemModel])
|
||||
async def get_items(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
):
|
||||
return await paginated_query(
|
||||
session=session,
|
||||
model=Item,
|
||||
item_schema=ItemModel,
|
||||
pagination=pagination,
|
||||
filters={"status": "completed"},
|
||||
)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated, Any, Callable, Dict, Optional, Type, TypeVar
|
||||
|
||||
from fastapi import Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.utils.pagination import PaginatedResponse, get_paginated
|
||||
|
||||
T = TypeVar("T")
|
||||
ModelT = TypeVar("ModelT")
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaginationParams:
|
||||
"""페이지네이션 파라미터를 담는 데이터 클래스"""
|
||||
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
def get_pagination_params(
|
||||
page: Annotated[int, Query(ge=1, description="페이지 번호 (1부터 시작)")] = 1,
|
||||
page_size: Annotated[
|
||||
int, Query(ge=1, le=100, description="페이지당 데이터 수 (최대 100)")
|
||||
] = 10,
|
||||
) -> PaginationParams:
|
||||
"""페이지네이션 파라미터를 주입하는 의존성 함수
|
||||
|
||||
Args:
|
||||
page: 페이지 번호 (1부터 시작, 기본값: 1)
|
||||
page_size: 페이지당 데이터 수 (기본값: 10, 최대: 100)
|
||||
|
||||
Returns:
|
||||
PaginationParams: 페이지네이션 파라미터 객체
|
||||
"""
|
||||
return PaginationParams(page=page, page_size=page_size)
|
||||
|
||||
|
||||
async def paginated_query(
|
||||
session: AsyncSession,
|
||||
model: Type[ModelT],
|
||||
item_schema: Type[T],
|
||||
pagination: PaginationParams,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
order_by: Optional[str] = "created_at",
|
||||
order_desc: bool = True,
|
||||
transform_fn: Optional[Callable[[ModelT], T]] = None,
|
||||
) -> PaginatedResponse[T]:
|
||||
"""페이지네이션 쿼리를 실행하는 헬퍼 함수
|
||||
|
||||
PaginationParams를 받아서 get_paginated를 호출합니다.
|
||||
|
||||
Args:
|
||||
session: SQLAlchemy AsyncSession
|
||||
model: SQLAlchemy 모델 클래스 (예: Song, Lyric, Video)
|
||||
item_schema: Pydantic 스키마 클래스 (예: SongListItem)
|
||||
pagination: 페이지네이션 파라미터 (get_pagination_params에서 주입)
|
||||
filters: 필터 조건 딕셔너리 (예: {"status": "completed"})
|
||||
order_by: 정렬 기준 컬럼명 (기본값: "created_at")
|
||||
order_desc: 내림차순 정렬 여부 (기본값: True)
|
||||
transform_fn: 모델을 스키마로 변환하는 함수 (None이면 자동 변환)
|
||||
|
||||
Returns:
|
||||
PaginatedResponse[T]: 페이지네이션된 응답
|
||||
|
||||
Usage:
|
||||
@router.get("/songs", response_model=PaginatedResponse[SongListItem])
|
||||
async def get_songs(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
):
|
||||
return await paginated_query(
|
||||
session=session,
|
||||
model=Song,
|
||||
item_schema=SongListItem,
|
||||
pagination=pagination,
|
||||
filters={"status": "completed"},
|
||||
)
|
||||
"""
|
||||
return await get_paginated(
|
||||
session=session,
|
||||
model=model,
|
||||
item_schema=item_schema,
|
||||
page=pagination.page,
|
||||
page_size=pagination.page_size,
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
order_desc=order_desc,
|
||||
transform_fn=transform_fn,
|
||||
)
|
||||
|
|
@ -14,10 +14,14 @@ Song API Router
|
|||
"""
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, 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.home.models import Project
|
||||
from app.lyric.models import Lyric
|
||||
from app.song.models import Song
|
||||
|
|
@ -26,8 +30,10 @@ from app.song.schemas.song_schema import (
|
|||
GenerateSongRequest,
|
||||
GenerateSongResponse,
|
||||
PollingSongResponse,
|
||||
SongListItem,
|
||||
)
|
||||
from app.song.worker.song_task import download_and_save_song
|
||||
from app.utils.pagination import PaginatedResponse
|
||||
from app.utils.suno import SunoService
|
||||
|
||||
|
||||
|
|
@ -392,3 +398,117 @@ async def download_song(
|
|||
message="노래 다운로드 조회에 실패했습니다.",
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"s/",
|
||||
summary="생성된 노래 목록 조회",
|
||||
description="""
|
||||
완료된 노래 목록을 페이지네이션하여 조회합니다.
|
||||
|
||||
## 쿼리 파라미터
|
||||
- **page**: 페이지 번호 (1부터 시작, 기본값: 1)
|
||||
- **page_size**: 페이지당 데이터 수 (기본값: 10, 최대: 100)
|
||||
|
||||
## 반환 정보
|
||||
- **items**: 노래 목록 (store_name, region, task_id, language, song_result_url, created_at)
|
||||
- **total**: 전체 데이터 수
|
||||
- **page**: 현재 페이지
|
||||
- **page_size**: 페이지당 데이터 수
|
||||
- **total_pages**: 전체 페이지 수
|
||||
- **has_next**: 다음 페이지 존재 여부
|
||||
- **has_prev**: 이전 페이지 존재 여부
|
||||
|
||||
## 사용 예시
|
||||
```
|
||||
GET /songs/?page=1&page_size=10
|
||||
```
|
||||
|
||||
## 참고
|
||||
- status가 'completed'인 노래만 반환됩니다.
|
||||
- created_at 기준 내림차순 정렬됩니다.
|
||||
""",
|
||||
response_model=PaginatedResponse[SongListItem],
|
||||
responses={
|
||||
200: {"description": "노래 목록 조회 성공"},
|
||||
500: {"description": "조회 실패"},
|
||||
},
|
||||
)
|
||||
async def get_songs(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
) -> PaginatedResponse[SongListItem]:
|
||||
"""완료된 노래 목록을 페이지네이션하여 반환합니다."""
|
||||
print(f"[get_songs] START - page: {pagination.page}, page_size: {pagination.page_size}")
|
||||
try:
|
||||
offset = (pagination.page - 1) * pagination.page_size
|
||||
|
||||
# 서브쿼리: task_id별 최신 Song의 id 조회 (completed 상태만)
|
||||
subquery = (
|
||||
select(func.max(Song.id).label("max_id"))
|
||||
.where(Song.status == "completed")
|
||||
.group_by(Song.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(Song)
|
||||
.where(Song.id.in_(select(subquery.c.max_id)))
|
||||
.order_by(Song.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(pagination.page_size)
|
||||
)
|
||||
result = await session.execute(query)
|
||||
songs = result.scalars().all()
|
||||
|
||||
# Project 정보와 함께 SongListItem으로 변환
|
||||
items = []
|
||||
for song in songs:
|
||||
# Project 조회 (song.project_id 직접 사용)
|
||||
project_result = await session.execute(
|
||||
select(Project).where(Project.id == song.project_id)
|
||||
)
|
||||
project = project_result.scalar_one_or_none()
|
||||
|
||||
item = SongListItem(
|
||||
store_name=project.store_name if project else None,
|
||||
region=project.region if project else None,
|
||||
task_id=song.task_id,
|
||||
language=song.language,
|
||||
song_result_url=song.song_result_url,
|
||||
created_at=song.created_at,
|
||||
)
|
||||
items.append(item)
|
||||
|
||||
# 개별 아이템 로그
|
||||
print(
|
||||
f"[get_songs] Item - store_name: {item.store_name}, region: {item.region}, "
|
||||
f"task_id: {item.task_id}, language: {item.language}, "
|
||||
f"song_result_url: {item.song_result_url}, created_at: {item.created_at}"
|
||||
)
|
||||
|
||||
response = PaginatedResponse.create(
|
||||
items=items,
|
||||
total=total,
|
||||
page=pagination.page,
|
||||
page_size=pagination.page_size,
|
||||
)
|
||||
|
||||
print(
|
||||
f"[get_songs] SUCCESS - total: {total}, page: {pagination.page}, "
|
||||
f"page_size: {pagination.page_size}, items_count: {len(items)}"
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
print(f"[get_songs] EXCEPTION - error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"노래 목록 조회에 실패했습니다: {str(e)}",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -184,6 +184,31 @@ class PollingSongResponse(BaseModel):
|
|||
error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)")
|
||||
|
||||
|
||||
class SongListItem(BaseModel):
|
||||
"""노래 목록 아이템 스키마
|
||||
|
||||
Usage:
|
||||
GET /songs 응답의 개별 노래 정보
|
||||
|
||||
Example:
|
||||
{
|
||||
"store_name": "스테이 머뭄",
|
||||
"region": "군산",
|
||||
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
|
||||
"language": "Korean",
|
||||
"song_result_url": "http://localhost:8000/media/2025-01-15/스테이머뭄.mp3",
|
||||
"created_at": "2025-01-15T12:00:00"
|
||||
}
|
||||
"""
|
||||
|
||||
store_name: Optional[str] = Field(None, description="업체명")
|
||||
region: Optional[str] = Field(None, description="지역명")
|
||||
task_id: str = Field(..., description="작업 고유 식별자")
|
||||
language: Optional[str] = Field(None, description="언어")
|
||||
song_result_url: Optional[str] = Field(None, description="노래 결과 URL")
|
||||
created_at: Optional[datetime] = Field(None, description="생성 일시")
|
||||
|
||||
|
||||
class DownloadSongResponse(BaseModel):
|
||||
"""노래 다운로드 응답 스키마
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue