""" 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 typing import Optional from fastapi import APIRouter, 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.utils.chatgpt_prompt import ChatgptService from app.utils.common import generate_task_id 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)) 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)) 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를 이용하여 가사를 생성합니다. ## 요청 필드 - **customer_name**: 고객명/가게명 (필수) - **region**: 지역명 (필수) - **detail_region_info**: 상세 지역 정보 (선택) - **language**: 가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese) ## 반환 정보 - **success**: 생성 성공 여부 - **task_id**: 작업 고유 식별자 - **lyric**: 생성된 가사 (성공 시) - **language**: 가사 언어 - **error_message**: 에러 메시지 (실패 시) ## 실패 조건 - ChatGPT API 오류 - ChatGPT 거부 응답 (I'm sorry, I cannot 등) - 응답에 ERROR: 포함 ## 사용 예시 ``` POST /lyric/generate { "customer_name": "스테이 머뭄", "region": "군산", "detail_region_info": "군산 신흥동 말랭이 마을", "language": "Korean" } ``` ## 응답 예시 (성공) ```json { "success": true, "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", "lyric": "인스타 감성의 스테이 머뭄...", "language": "Korean", "error_message": null } ``` ## 응답 예시 (실패) ```json { "success": false, "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", "lyric": null, "language": "Korean", "error_message": "I'm sorry, I can't comply with that request." } ``` """, response_model=GenerateLyricResponse, responses={ 200: {"description": "가사 생성 성공 또는 실패 (success 필드로 구분)"}, 500: {"description": "서버 내부 오류"}, }, ) async def generate_lyric( request_body: GenerateLyricRequest, session: AsyncSession = Depends(get_session), ) -> GenerateLyricResponse: """고객 정보를 기반으로 가사를 생성합니다.""" task_id = await generate_task_id(session=session, table_name=Project) print( f"[generate_lyric] START - task_id: {task_id}, customer_name: {request_body.customer_name}, 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) # commit 후 project.id 동기화 print( f"[generate_lyric] Project saved - 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() ) # processing 상태를 확실히 저장 (다른 트랜잭션에서 조회 가능) await session.refresh(lyric) # commit 후 객체 상태 동기화 print( f"[generate_lyric] Lyric saved (processing) - lyric_id: {lyric.id}, task_id: {task_id}" ) # 4. ChatGPT를 통해 가사 생성 print(f"[generate_lyric] ChatGPT generation started - task_id: {task_id}") result = await service.generate(prompt=prompt) print(f"[generate_lyric] ChatGPT generation completed - task_id: {task_id}") # 5. 실패 응답 검사 (ERROR 또는 ChatGPT 거부 응답) failure_patterns = [ "ERROR:", "I'm sorry", "I cannot", "I can't", "I apologize", "I'm unable", "I am unable", "I'm not able", "I am not able", ] is_failure = any( pattern.lower() in result.lower() for pattern in failure_patterns ) if is_failure: print(f"[generate_lyric] FAILED - task_id: {task_id}, error: {result}") lyric.status = "failed" lyric.lyric_result = result await session.commit() return GenerateLyricResponse( success=False, task_id=task_id, lyric=None, language=request_body.language, error_message=result, ) # 6. 성공 시 Lyric 테이블 업데이트 (status: completed) lyric.status = "completed" lyric.lyric_result = result await session.commit() print(f"[generate_lyric] SUCCESS - task_id: {task_id}") return GenerateLyricResponse( success=True, task_id=task_id, lyric=result, 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)