""" Lyric API Router 이 모듈은 가사 관련 API 엔드포인트를 정의합니다. 모든 엔드포인트는 재사용 가능하도록 설계되었습니다. 엔드포인트 목록: - POST /lyric/generate: 가사 생성 - GET /lyric/status/{task_id}: 가사 생성 상태 조회 - GET /lyric/{task_id}: 가사 상세 조회 - GET /lyrics: 가사 목록 조회 (페이지네이션) 사용 예시: from app.lyric.api.routers.v1.lyric import router app.include_router(router, prefix="/api/v1") 다른 서비스에서 재사용: # 이 파일의 헬퍼 함수들을 import하여 사용 가능 from app.lyric.api.routers.v1.lyric import ( get_lyric_status_by_task_id, get_lyric_by_task_id, ) # 페이지네이션은 pagination 모듈 사용 from app.utils.pagination import PaginatedResponse, get_paginated """ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database.session import get_session from app.home.models import Project from app.lyric.models import Lyric from app.lyric.schemas.lyric import ( GenerateLyricRequest, GenerateLyricResponse, LyricDetailResponse, LyricListItem, LyricStatusResponse, ) from app.lyric.worker.lyric_task import generate_lyric_background from app.utils.chatgpt_prompt import ChatgptService from app.utils.pagination import PaginatedResponse, get_paginated router = APIRouter(prefix="/lyric", tags=["lyric"]) # ============================================================================= # Reusable Service Functions (다른 모듈에서 import하여 사용 가능) # ============================================================================= async def get_lyric_status_by_task_id( session: AsyncSession, task_id: str ) -> LyricStatusResponse: """task_id로 가사 생성 작업의 상태를 조회합니다. Args: session: SQLAlchemy AsyncSession task_id: 작업 고유 식별자 Returns: LyricStatusResponse: 상태 정보 Raises: HTTPException: 404 - task_id에 해당하는 가사가 없는 경우 Usage: # 다른 서비스에서 사용 from app.lyric.api.routers.v1.lyric import get_lyric_status_by_task_id status_info = await get_lyric_status_by_task_id(session, "some-task-id") if status_info.status == "completed": # 완료 처리 """ print(f"[get_lyric_status_by_task_id] START - task_id: {task_id}") result = await session.execute( select(Lyric) .where(Lyric.task_id == task_id) .order_by(Lyric.created_at.desc()) .limit(1) ) lyric = result.scalar_one_or_none() if not lyric: print(f"[get_lyric_status_by_task_id] NOT FOUND - task_id: {task_id}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.", ) status_messages = { "processing": "가사 생성 중입니다.", "completed": "가사 생성이 완료되었습니다.", "failed": "가사 생성에 실패했습니다.", } print( f"[get_lyric_status_by_task_id] SUCCESS - task_id: {task_id}, status: {lyric.status}" ) return LyricStatusResponse( task_id=lyric.task_id, status=lyric.status, message=status_messages.get(lyric.status, "알 수 없는 상태입니다."), ) async def get_lyric_by_task_id( session: AsyncSession, task_id: str ) -> LyricDetailResponse: """task_id로 생성된 가사 상세 정보를 조회합니다. Args: session: SQLAlchemy AsyncSession task_id: 작업 고유 식별자 Returns: LyricDetailResponse: 가사 상세 정보 Raises: HTTPException: 404 - task_id에 해당하는 가사가 없는 경우 Usage: # 다른 서비스에서 사용 from app.lyric.api.routers.v1.lyric import get_lyric_by_task_id lyric = await get_lyric_by_task_id(session, task_id) """ print(f"[get_lyric_by_task_id] START - task_id: {task_id}") result = await session.execute( select(Lyric) .where(Lyric.task_id == task_id) .order_by(Lyric.created_at.desc()) .limit(1) ) lyric = result.scalar_one_or_none() if not lyric: print(f"[get_lyric_by_task_id] NOT FOUND - task_id: {task_id}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.", ) print(f"[get_lyric_by_task_id] SUCCESS - task_id: {task_id}, lyric_id: {lyric.id}") return LyricDetailResponse( id=lyric.id, task_id=lyric.task_id, project_id=lyric.project_id, status=lyric.status, lyric_prompt=lyric.lyric_prompt, lyric_result=lyric.lyric_result, created_at=lyric.created_at, ) # ============================================================================= # API Endpoints # ============================================================================= @router.post( "/generate", summary="가사 생성", description=""" 고객 정보를 기반으로 ChatGPT를 이용하여 가사를 생성합니다. 백그라운드에서 비동기로 처리되며, 즉시 task_id를 반환합니다. ## 요청 필드 - **task_id**: 작업 고유 식별자 (이미지 업로드 시 생성된 task_id, 필수) - **customer_name**: 고객명/가게명 (필수) - **region**: 지역명 (필수) - **detail_region_info**: 상세 지역 정보 (선택) - **language**: 가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese) ## 반환 정보 - **success**: 요청 접수 성공 여부 - **task_id**: 작업 고유 식별자 - **lyric**: null (백그라운드 처리 중) - **language**: 가사 언어 - **error_message**: 에러 메시지 (요청 접수 실패 시) ## 상태 확인 - GET /lyric/status/{task_id} 로 처리 상태 확인 - GET /lyric/{task_id} 로 생성된 가사 조회 ## 사용 예시 ``` POST /lyric/generate { "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", "customer_name": "스테이 머뭄", "region": "군산", "detail_region_info": "군산 신흥동 말랭이 마을", "language": "Korean" } ``` ## 응답 예시 ```json { "success": true, "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", "lyric": null, "language": "Korean", "error_message": null } ``` """, response_model=GenerateLyricResponse, responses={ 200: {"description": "가사 생성 요청 접수 성공"}, 500: {"description": "서버 내부 오류"}, }, ) async def generate_lyric( request_body: GenerateLyricRequest, background_tasks: BackgroundTasks, session: AsyncSession = Depends(get_session), ) -> GenerateLyricResponse: """고객 정보를 기반으로 가사를 생성합니다. (백그라운드 처리)""" task_id = request_body.task_id print( f"[generate_lyric] START - task_id: {task_id}, " f"customer_name: {request_body.customer_name}, " f"region: {request_body.region}" ) try: # 1. ChatGPT 서비스 초기화 및 프롬프트 생성 service = ChatgptService( customer_name=request_body.customer_name, region=request_body.region, detail_region_info=request_body.detail_region_info or "", language=request_body.language, ) prompt = service.build_lyrics_prompt() # 2. Project 테이블에 데이터 저장 project = Project( store_name=request_body.customer_name, region=request_body.region, task_id=task_id, detail_region_info=request_body.detail_region_info, language=request_body.language, ) session.add(project) await session.commit() await session.refresh(project) print( f"[generate_lyric] Project saved - " f"project_id: {project.id}, task_id: {task_id}" ) # 3. Lyric 테이블에 데이터 저장 (status: processing) lyric = Lyric( project_id=project.id, task_id=task_id, status="processing", lyric_prompt=prompt, lyric_result=None, language=request_body.language, ) session.add(lyric) await session.commit() await session.refresh(lyric) print( f"[generate_lyric] Lyric saved (processing) - " f"lyric_id: {lyric.id}, task_id: {task_id}" ) # 4. 백그라운드 태스크로 ChatGPT 가사 생성 실행 background_tasks.add_task( generate_lyric_background, task_id=task_id, prompt=prompt, language=request_body.language, ) print(f"[generate_lyric] Background task scheduled - task_id: {task_id}") # 5. 즉시 응답 반환 return GenerateLyricResponse( success=True, task_id=task_id, lyric=None, language=request_body.language, error_message=None, ) except Exception as e: print(f"[generate_lyric] EXCEPTION - task_id: {task_id}, error: {e}") await session.rollback() return GenerateLyricResponse( success=False, task_id=task_id, lyric=None, language=request_body.language, error_message=str(e), ) @router.get( "/status/{task_id}", summary="가사 생성 상태 조회", description=""" task_id로 가사 생성 작업의 현재 상태를 조회합니다. ## 상태 값 - **processing**: 가사 생성 중 - **completed**: 가사 생성 완료 - **failed**: 가사 생성 실패 ## 사용 예시 ``` GET /lyric/status/019123ab-cdef-7890-abcd-ef1234567890 ``` """, response_model=LyricStatusResponse, responses={ 200: {"description": "상태 조회 성공"}, 404: {"description": "해당 task_id를 찾을 수 없음"}, }, ) async def get_lyric_status( task_id: str, session: AsyncSession = Depends(get_session), ) -> LyricStatusResponse: """task_id로 가사 생성 작업 상태를 조회합니다.""" return await get_lyric_status_by_task_id(session, task_id) @router.get( "s", summary="가사 목록 조회 (페이지네이션)", description=""" 생성 완료된 가사를 페이지네이션으로 조회합니다. ## 파라미터 - **page**: 페이지 번호 (1부터 시작, 기본값: 1) - **page_size**: 페이지당 데이터 수 (기본값: 20, 최대: 100) ## 반환 정보 - **items**: 가사 목록 (completed 상태만) - **total**: 전체 데이터 수 - **page**: 현재 페이지 - **page_size**: 페이지당 데이터 수 - **total_pages**: 전체 페이지 수 - **has_next**: 다음 페이지 존재 여부 - **has_prev**: 이전 페이지 존재 여부 ## 사용 예시 ``` GET /lyrics # 기본 조회 (1페이지, 20개) GET /lyrics?page=2 # 2페이지 조회 GET /lyrics?page=1&page_size=50 # 50개씩 조회 ``` ## 참고 - 생성 완료(completed)된 가사만 조회됩니다. - processing, failed 상태의 가사는 조회되지 않습니다. """, response_model=PaginatedResponse[LyricListItem], responses={ 200: {"description": "가사 목록 조회 성공"}, }, ) async def list_lyrics( page: int = Query(1, ge=1, description="페이지 번호 (1부터 시작)"), page_size: int = Query(20, ge=1, le=100, description="페이지당 데이터 수"), session: AsyncSession = Depends(get_session), ) -> PaginatedResponse[LyricListItem]: """페이지네이션으로 완료된 가사 목록을 조회합니다.""" return await get_paginated( session=session, model=Lyric, item_schema=LyricListItem, page=page, page_size=page_size, filters={"status": "completed"}, order_by="created_at", order_desc=True, ) @router.get( "/{task_id}", summary="가사 상세 조회", description=""" task_id로 생성된 가사의 상세 정보를 조회합니다. ## 반환 정보 - **id**: 가사 ID - **task_id**: 작업 고유 식별자 - **project_id**: 프로젝트 ID - **status**: 처리 상태 - **lyric_prompt**: 가사 생성에 사용된 프롬프트 - **lyric_result**: 생성된 가사 (완료 시) - **created_at**: 생성 일시 ## 사용 예시 ``` GET /lyric/019123ab-cdef-7890-abcd-ef1234567890 ``` """, response_model=LyricDetailResponse, responses={ 200: {"description": "가사 조회 성공"}, 404: {"description": "해당 task_id를 찾을 수 없음"}, }, ) async def get_lyric_detail( task_id: str, session: AsyncSession = Depends(get_session), ) -> LyricDetailResponse: """task_id로 생성된 가사를 조회합니다.""" return await get_lyric_by_task_id(session, task_id)