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 fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database.session import get_session
|
from app.database.session import get_session
|
||||||
|
from app.dependencies.pagination import (
|
||||||
|
PaginationParams,
|
||||||
|
get_pagination_params,
|
||||||
|
)
|
||||||
from app.home.models import Project
|
from app.home.models import Project
|
||||||
from app.lyric.models import Lyric
|
from app.lyric.models import Lyric
|
||||||
from app.song.models import Song
|
from app.song.models import Song
|
||||||
|
|
@ -26,8 +30,10 @@ from app.song.schemas.song_schema import (
|
||||||
GenerateSongRequest,
|
GenerateSongRequest,
|
||||||
GenerateSongResponse,
|
GenerateSongResponse,
|
||||||
PollingSongResponse,
|
PollingSongResponse,
|
||||||
|
SongListItem,
|
||||||
)
|
)
|
||||||
from app.song.worker.song_task import download_and_save_song
|
from app.song.worker.song_task import download_and_save_song
|
||||||
|
from app.utils.pagination import PaginatedResponse
|
||||||
from app.utils.suno import SunoService
|
from app.utils.suno import SunoService
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -392,3 +398,117 @@ async def download_song(
|
||||||
message="노래 다운로드 조회에 실패했습니다.",
|
message="노래 다운로드 조회에 실패했습니다.",
|
||||||
error_message=str(e),
|
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="에러 메시지 (실패 시)")
|
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):
|
class DownloadSongResponse(BaseModel):
|
||||||
"""노래 다운로드 응답 스키마
|
"""노래 다운로드 응답 스키마
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue