From 56069a04a171ae66e4d36564803273a9506c2286 Mon Sep 17 00:00:00 2001 From: Dohyun Lim Date: Mon, 19 Jan 2026 16:59:25 +0900 Subject: [PATCH] modify url parameter --- app/auth/__init__.py | 0 app/auth/api/__init__.py | 0 app/auth/api/routers/__init__.py | 0 app/auth/api/routers/v1/__init__.py | 0 app/auth/api/routers/v1/auth.py | 125 ---------------------------- app/auth/dependencies.py | 71 ---------------- app/auth/models.py | 88 -------------------- app/auth/schemas.py | 36 -------- app/auth/services/__init__.py | 0 app/auth/services/jwt.py | 34 -------- app/auth/services/kakao.py | 58 ------------- app/lyric/api/routers/v1/lyric.py | 4 +- app/song/api/routers/v1/song.py | 94 +++++++++++---------- app/song/schemas/song_schema.py | 32 ++----- app/user/services/auth.py | 10 +++ app/utils/suno.py | 6 -- main.py | 2 +- 17 files changed, 68 insertions(+), 492 deletions(-) delete mode 100644 app/auth/__init__.py delete mode 100644 app/auth/api/__init__.py delete mode 100644 app/auth/api/routers/__init__.py delete mode 100644 app/auth/api/routers/v1/__init__.py delete mode 100644 app/auth/api/routers/v1/auth.py delete mode 100644 app/auth/dependencies.py delete mode 100644 app/auth/models.py delete mode 100644 app/auth/schemas.py delete mode 100644 app/auth/services/__init__.py delete mode 100644 app/auth/services/jwt.py delete mode 100644 app/auth/services/kakao.py diff --git a/app/auth/__init__.py b/app/auth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/auth/api/__init__.py b/app/auth/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/auth/api/routers/__init__.py b/app/auth/api/routers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/auth/api/routers/v1/__init__.py b/app/auth/api/routers/v1/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/auth/api/routers/v1/auth.py b/app/auth/api/routers/v1/auth.py deleted file mode 100644 index 21485b5..0000000 --- a/app/auth/api/routers/v1/auth.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -카카오 로그인 API 라우터 -""" - -from datetime import datetime, timezone - -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.responses import RedirectResponse -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.auth.dependencies import get_current_user_optional -from app.auth.models import User -from app.auth.schemas import AuthStatusResponse, TokenResponse, UserResponse -from app.auth.services.jwt import create_access_token -from app.auth.services.kakao import kakao_client -from app.database.session import get_session - -router = APIRouter(tags=["Auth"]) - - -@router.get("/auth/kakao/login") -async def kakao_login(): - """ - 카카오 로그인 페이지로 리다이렉트 - - 프론트엔드에서 이 URL을 호출하면 카카오 로그인 페이지로 이동합니다. - """ - auth_url = kakao_client.get_authorization_url() - return RedirectResponse(url=auth_url) - - -@router.get("/kakao/callback", response_model=TokenResponse) -async def kakao_callback( - code: str, - session: AsyncSession = Depends(get_session), -): - """ - 카카오 로그인 콜백 - - 카카오 로그인 성공 후 인가 코드를 받아 JWT 토큰을 발급합니다. - """ - # 1. 인가 코드로 액세스 토큰 획득 - token_data = await kakao_client.get_access_token(code) - - if "error" in token_data: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"카카오 토큰 발급 실패: {token_data.get('error_description', token_data.get('error'))}", - ) - - access_token = token_data.get("access_token") - if not access_token: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="액세스 토큰을 받지 못했습니다", - ) - - # 2. 액세스 토큰으로 사용자 정보 조회 - user_info = await kakao_client.get_user_info(access_token) - - if "id" not in user_info: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="사용자 정보를 가져오지 못했습니다", - ) - - kakao_id = str(user_info["id"]) - kakao_account = user_info.get("kakao_account", {}) - profile = kakao_account.get("profile", {}) - - nickname = profile.get("nickname") - email = kakao_account.get("email") - profile_image = profile.get("profile_image_url") - - # 3. 기존 회원 확인 또는 신규 가입 - result = await session.execute(select(User).where(User.kakao_id == kakao_id)) - user = result.scalar_one_or_none() - - if user is None: - # 신규 가입 - user = User( - kakao_id=kakao_id, - nickname=nickname, - email=email, - profile_image=profile_image, - ) - session.add(user) - await session.commit() - await session.refresh(user) - else: - # 기존 회원 - 마지막 로그인 시간 및 정보 업데이트 - user.nickname = nickname - user.email = email - user.profile_image = profile_image - user.last_login_at = datetime.now(timezone.utc) - await session.commit() - await session.refresh(user) - - # 4. JWT 토큰 발급 - jwt_token = create_access_token({"sub": str(user.id)}) - - return TokenResponse( - access_token=jwt_token, - user=UserResponse.model_validate(user), - ) - - -@router.get("/auth/me", response_model=AuthStatusResponse) -async def get_auth_status( - current_user: User | None = Depends(get_current_user_optional), -): - """ - 현재 인증 상태 확인 - - 프론트엔드에서 로그인 상태를 확인할 때 사용합니다. - 토큰이 유효하면 사용자 정보를, 아니면 is_authenticated=False를 반환합니다. - """ - if current_user is None: - return AuthStatusResponse(is_authenticated=False) - - return AuthStatusResponse( - is_authenticated=True, - user=UserResponse.model_validate(current_user), - ) diff --git a/app/auth/dependencies.py b/app/auth/dependencies.py deleted file mode 100644 index 85507b2..0000000 --- a/app/auth/dependencies.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Auth 모듈 의존성 주입 -""" - -from fastapi import Depends, HTTPException, status -from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.auth.models import User -from app.auth.services.jwt import decode_access_token -from app.database.session import get_session - -security = HTTPBearer(auto_error=False) - - -async def get_current_user( - credentials: HTTPAuthorizationCredentials | None = Depends(security), - session: AsyncSession = Depends(get_session), -) -> User: - """현재 로그인한 사용자 반환 (필수 인증)""" - if credentials is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="인증이 필요합니다", - ) - - payload = decode_access_token(credentials.credentials) - if payload is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="유효하지 않은 토큰입니다", - ) - - user_id = payload.get("sub") - if user_id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="유효하지 않은 토큰입니다", - ) - - result = await session.execute(select(User).where(User.id == int(user_id))) - user = result.scalar_one_or_none() - - if user is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="사용자를 찾을 수 없습니다", - ) - - return user - - -async def get_current_user_optional( - credentials: HTTPAuthorizationCredentials | None = Depends(security), - session: AsyncSession = Depends(get_session), -) -> User | None: - """현재 로그인한 사용자 반환 (선택적 인증)""" - if credentials is None: - return None - - payload = decode_access_token(credentials.credentials) - if payload is None: - return None - - user_id = payload.get("sub") - if user_id is None: - return None - - result = await session.execute(select(User).where(User.id == int(user_id))) - return result.scalar_one_or_none() diff --git a/app/auth/models.py b/app/auth/models.py deleted file mode 100644 index 25a7150..0000000 --- a/app/auth/models.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Auth 모듈 SQLAlchemy 모델 정의 - -카카오 로그인 사용자 정보를 저장합니다. -""" - -from datetime import datetime - -from sqlalchemy import DateTime, Index, Integer, String, func -from sqlalchemy.orm import Mapped, mapped_column - -from app.database.session import Base - - -class User(Base): - """ - 사용자 테이블 (카카오 로그인) - - Attributes: - id: 고유 식별자 (자동 증가) - kakao_id: 카카오 고유 ID - nickname: 카카오 닉네임 - email: 카카오 이메일 (선택) - profile_image: 프로필 이미지 URL - created_at: 가입 일시 - last_login_at: 마지막 로그인 일시 - """ - - __tablename__ = "user" - __table_args__ = ( - Index("idx_user_kakao_id", "kakao_id"), - { - "mysql_engine": "InnoDB", - "mysql_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - }, - ) - - id: Mapped[int] = mapped_column( - Integer, - primary_key=True, - nullable=False, - autoincrement=True, - comment="고유 식별자", - ) - - kakao_id: Mapped[str] = mapped_column( - String(50), - nullable=False, - unique=True, - comment="카카오 고유 ID", - ) - - nickname: Mapped[str | None] = mapped_column( - String(100), - nullable=True, - comment="카카오 닉네임", - ) - - email: Mapped[str | None] = mapped_column( - String(255), - nullable=True, - comment="카카오 이메일", - ) - - profile_image: Mapped[str | None] = mapped_column( - String(500), - nullable=True, - comment="프로필 이미지 URL", - ) - - created_at: Mapped[datetime] = mapped_column( - DateTime, - nullable=False, - server_default=func.now(), - comment="가입 일시", - ) - - last_login_at: Mapped[datetime] = mapped_column( - DateTime, - nullable=False, - server_default=func.now(), - onupdate=func.now(), - comment="마지막 로그인 일시", - ) - - def __repr__(self) -> str: - return f"" diff --git a/app/auth/schemas.py b/app/auth/schemas.py deleted file mode 100644 index 2b06b0e..0000000 --- a/app/auth/schemas.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Auth 모듈 Pydantic 스키마 -""" - -from datetime import datetime - -from pydantic import BaseModel - - -class UserResponse(BaseModel): - """사용자 정보 응답""" - - id: int - kakao_id: str - nickname: str | None - email: str | None - profile_image: str | None - created_at: datetime - last_login_at: datetime - - model_config = {"from_attributes": True} - - -class TokenResponse(BaseModel): - """토큰 응답""" - - access_token: str - token_type: str = "bearer" - user: UserResponse - - -class AuthStatusResponse(BaseModel): - """인증 상태 응답 (프론트엔드용)""" - - is_authenticated: bool - user: UserResponse | None = None diff --git a/app/auth/services/__init__.py b/app/auth/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/auth/services/jwt.py b/app/auth/services/jwt.py deleted file mode 100644 index 88bcc6a..0000000 --- a/app/auth/services/jwt.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -JWT 토큰 유틸리티 -""" - -from datetime import datetime, timedelta, timezone - -from jose import JWTError, jwt - -from config import security_settings - - -def create_access_token(data: dict) -> str: - """JWT 액세스 토큰 생성""" - to_encode = data.copy() - expire = datetime.now(timezone.utc) + timedelta(minutes=security_settings.JWT_EXPIRE_MINUTES) - to_encode.update({"exp": expire}) - return jwt.encode( - to_encode, - security_settings.JWT_SECRET, - algorithm=security_settings.JWT_ALGORITHM, - ) - - -def decode_access_token(token: str) -> dict | None: - """JWT 액세스 토큰 디코딩""" - try: - payload = jwt.decode( - token, - security_settings.JWT_SECRET, - algorithms=[security_settings.JWT_ALGORITHM], - ) - return payload - except JWTError: - return None diff --git a/app/auth/services/kakao.py b/app/auth/services/kakao.py deleted file mode 100644 index 3b96a69..0000000 --- a/app/auth/services/kakao.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -카카오 OAuth API 클라이언트 -""" - -import aiohttp - -from app.utils.logger import get_logger -from config import kakao_settings - -logger = get_logger("kakao") - - -class KakaoOAuthClient: - """카카오 OAuth API 클라이언트""" - - AUTH_URL = "https://kauth.kakao.com/oauth/authorize" - TOKEN_URL = "https://kauth.kakao.com/oauth/token" - USER_INFO_URL = "https://kapi.kakao.com/v2/user/me" - - def __init__(self): - self.client_id = kakao_settings.KAKAO_CLIENT_ID - self.client_secret = kakao_settings.KAKAO_CLIENT_SECRET - self.redirect_uri = kakao_settings.KAKAO_REDIRECT_URI - - def get_authorization_url(self) -> str: - """카카오 로그인 페이지 URL 반환""" - return ( - f"{self.AUTH_URL}" - f"?client_id={self.client_id}" - f"&redirect_uri={self.redirect_uri}" - f"&response_type=code" - ) - - async def get_access_token(self, code: str) -> dict: - """인가 코드로 액세스 토큰 획득""" - async with aiohttp.ClientSession() as session: - data = { - "grant_type": "authorization_code", - "client_id": self.client_id, - "client_secret": self.client_secret, - "redirect_uri": self.redirect_uri, - "code": code, - } - logger.debug(f"Token request - client_id: {self.client_id}, redirect_uri: {self.redirect_uri}") - async with session.post(self.TOKEN_URL, data=data) as response: - result = await response.json() - logger.debug(f"Token response: {result}") - return result - - async def get_user_info(self, access_token: str) -> dict: - """액세스 토큰으로 사용자 정보 조회""" - async with aiohttp.ClientSession() as session: - headers = {"Authorization": f"Bearer {access_token}"} - async with session.get(self.USER_INFO_URL, headers=headers) as response: - return await response.json() - - -kakao_client = KakaoOAuthClient() diff --git a/app/lyric/api/routers/v1/lyric.py b/app/lyric/api/routers/v1/lyric.py index 60576de..7eea279 100644 --- a/app/lyric/api/routers/v1/lyric.py +++ b/app/lyric/api/routers/v1/lyric.py @@ -49,7 +49,7 @@ import traceback as tb # 로거 설정 logger = get_logger("lyric") -router = APIRouter(prefix="/lyric", tags=["lyric"]) +router = APIRouter(prefix="/lyric", tags=["Lyric"]) # ============================================================================= @@ -398,7 +398,7 @@ async def get_lyric_status( @router.get( - "s", + "s/", summary="가사 목록 조회 (페이지네이션)", description=""" 생성 완료된 가사를 페이지네이션으로 조회합니다. diff --git a/app/song/api/routers/v1/song.py b/app/song/api/routers/v1/song.py index a29b1b6..42d0861 100644 --- a/app/song/api/routers/v1/song.py +++ b/app/song/api/routers/v1/song.py @@ -5,7 +5,7 @@ Song API Router 엔드포인트 목록: - POST /song/generate/{task_id}: 노래 생성 요청 (task_id로 Project/Lyric 연결) - - GET /song/status/{suno_task_id}: Suno API 노래 생성 상태 조회 + - GET /song/status/{song_id}: Suno API 노래 생성 상태 조회 - GET /song/download/{task_id}: 노래 다운로드 상태 조회 (DB polling) 사용 예시: @@ -58,7 +58,7 @@ Suno API를 통해 노래 생성을 요청합니다. ## 반환 정보 - **success**: 요청 성공 여부 - **task_id**: 내부 작업 ID (Project/Lyric task_id) -- **suno_task_id**: Suno API 작업 ID (상태 조회에 사용) +- **song_id**: Suno API 작업 ID (상태 조회에 사용) - **message**: 응답 메시지 ## 사용 예시 @@ -73,7 +73,7 @@ POST /song/generate/019123ab-cdef-7890-abcd-ef1234567890 ## 참고 - 생성되는 노래는 약 1분 이내 길이입니다. -- suno_task_id를 사용하여 /status/{suno_task_id} 엔드포인트에서 생성 상태를 확인할 수 있습니다. +- song_id를 사용하여 /status/{song_id} 엔드포인트에서 생성 상태를 확인할 수 있습니다. - Song 테이블에 데이터가 저장되며, project_id와 lyric_id가 자동으로 연결됩니다. """, response_model=GenerateSongResponse, @@ -193,7 +193,7 @@ async def generate_song( return GenerateSongResponse( success=False, task_id=task_id, - suno_task_id=None, + song_id=None, message="노래 생성 요청에 실패했습니다.", error_message=str(e), ) @@ -237,7 +237,7 @@ async def generate_song( return GenerateSongResponse( success=False, task_id=task_id, - suno_task_id=None, + song_id=None, message="노래 생성 요청에 실패했습니다.", error_message=str(e), ) @@ -273,8 +273,8 @@ async def generate_song( return GenerateSongResponse( success=True, task_id=task_id, - suno_task_id=suno_task_id, - message="노래 생성 요청이 접수되었습니다. suno_task_id로 상태를 조회하세요.", + song_id=suno_task_id, + message="노래 생성 요청이 접수되었습니다. song_id로 상태를 조회하세요.", error_message=None, ) @@ -286,28 +286,26 @@ async def generate_song( return GenerateSongResponse( success=False, task_id=task_id, - suno_task_id=suno_task_id, + song_id=suno_task_id, message="노래 생성은 요청되었으나 DB 업데이트에 실패했습니다.", error_message=str(e), ) @router.get( - "/status/{suno_task_id}", + "/status/{song_id}", summary="노래 생성 상태 조회", description=""" Suno API를 통해 노래 생성 작업의 상태를 조회합니다. SUCCESS 상태인 경우 백그라운드에서 MP3 파일을 다운로드하고 Azure Blob Storage에 업로드한 뒤 Song 테이블을 업데이트합니다. ## 경로 파라미터 -- **suno_task_id**: 노래 생성 시 반환된 Suno API 작업 ID (필수) +- **song_id**: 노래 생성 시 반환된 작업 ID (필수) ## 반환 정보 - **success**: 조회 성공 여부 - **status**: 작업 상태 (PENDING, processing, SUCCESS, failed) - **message**: 상태 메시지 -- **clips**: 생성된 노래 클립 목록 (완료 시) -- **raw_response**: Suno API 원본 응답 ## 사용 예시 ``` @@ -334,64 +332,68 @@ GET /song/status/abc123... }, ) async def get_song_status( - suno_task_id: str, + song_id: str, session: AsyncSession = Depends(get_session), ) -> PollingSongResponse: - """suno_task_id로 노래 생성 작업의 상태를 조회합니다. + """song_id로 노래 생성 작업의 상태를 조회합니다. SUCCESS 상태인 경우 백그라운드에서 MP3 파일을 다운로드하고 Azure Blob Storage에 업로드한 뒤 Song 테이블의 status를 completed로, song_result_url을 Blob URL로 업데이트합니다. """ - logger.info(f"[get_song_status] START - suno_task_id: {suno_task_id}") + logger.info(f"[get_song_status] START - song_id: {song_id}") try: suno_service = SunoService() - result = await suno_service.get_task_status(suno_task_id) + result = await suno_service.get_task_status(song_id) parsed_response = suno_service.parse_status_response(result) - logger.info(f"[get_song_status] Suno API response - suno_task_id: {suno_task_id}, status: {parsed_response.status}") + logger.info(f"[get_song_status] Suno API response - song_id: {song_id}, status: {parsed_response.status}") # SUCCESS 상태인 경우 첫 번째 클립 정보를 DB에 직접 저장 - if parsed_response.status == "SUCCESS" and parsed_response.clips: - # 첫 번째 클립(clips[0])의 audioUrl과 duration 사용 - first_clip = parsed_response.clips[0] - audio_url = first_clip.audio_url - clip_duration = first_clip.duration - logger.debug(f"[get_song_status] Using first clip - id: {first_clip.id}, audio_url: {audio_url}, duration: {clip_duration}") + if parsed_response.status == "SUCCESS" and result: + # result에서 직접 clips 데이터 추출 + data = result.get("data", {}) + response_data = data.get("response") or {} + clips_data = response_data.get("sunoData") or [] - if audio_url: - # suno_task_id로 Song 조회 - song_result = await session.execute( - select(Song) - .where(Song.suno_task_id == suno_task_id) - .order_by(Song.created_at.desc()) - .limit(1) - ) - song = song_result.scalar_one_or_none() + if clips_data: + # 첫 번째 클립(clips[0])의 audioUrl과 duration 사용 + first_clip = clips_data[0] + audio_url = first_clip.get("audioUrl") + clip_duration = first_clip.get("duration") + logger.debug(f"[get_song_status] Using first clip - id: {first_clip.get('id')}, audio_url: {audio_url}, duration: {clip_duration}") - if song and song.status != "completed": - # 첫 번째 클립의 audio_url과 duration을 직접 DB에 저장 - song.status = "completed" - song.song_result_url = audio_url - if clip_duration is not None: - song.duration = clip_duration - await session.commit() - logger.info(f"[get_song_status] Song updated - suno_task_id: {suno_task_id}, status: completed, song_result_url: {audio_url}, duration: {clip_duration}") - elif song and song.status == "completed": - logger.info(f"[get_song_status] SKIPPED - Song already completed, suno_task_id: {suno_task_id}") + if audio_url: + # song_id로 Song 조회 + song_result = await session.execute( + select(Song) + .where(Song.suno_task_id == song_id) + .order_by(Song.created_at.desc()) + .limit(1) + ) + song = song_result.scalar_one_or_none() - logger.info(f"[get_song_status] SUCCESS - suno_task_id: {suno_task_id}") + if song and song.status != "completed": + # 첫 번째 클립의 audio_url과 duration을 직접 DB에 저장 + song.status = "completed" + song.song_result_url = audio_url + if clip_duration is not None: + song.duration = clip_duration + await session.commit() + logger.info(f"[get_song_status] Song updated - song_id: {song_id}, status: completed, song_result_url: {audio_url}, duration: {clip_duration}") + elif song and song.status == "completed": + logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {song_id}") + + logger.info(f"[get_song_status] SUCCESS - song_id: {song_id}") return parsed_response except Exception as e: import traceback - logger.error(f"[get_song_status] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}") + logger.error(f"[get_song_status] EXCEPTION - song_id: {song_id}, error: {e}") return PollingSongResponse( success=False, status="error", message="상태 조회에 실패했습니다.", - clips=None, - raw_response=None, error_message=f"{type(e).__name__}: {e}\n{traceback.format_exc()}", ) diff --git a/app/song/schemas/song_schema.py b/app/song/schemas/song_schema.py index c5d2126..b5daac2 100644 --- a/app/song/schemas/song_schema.py +++ b/app/song/schemas/song_schema.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional from fastapi import Request from pydantic import BaseModel, Field @@ -64,8 +64,8 @@ class GenerateSongResponse(BaseModel): { "success": true, "task_id": "019123ab-cdef-7890-abcd-ef1234567890", - "suno_task_id": "abc123...", - "message": "노래 생성 요청이 접수되었습니다. suno_task_id로 상태를 조회하세요.", + "song_id": "abc123...", + "message": "노래 생성 요청이 접수되었습니다. song_id로 상태를 조회하세요.", "error_message": null } @@ -73,7 +73,7 @@ class GenerateSongResponse(BaseModel): { "success": false, "task_id": "019123ab-cdef-7890-abcd-ef1234567890", - "suno_task_id": null, + "song_id": null, "message": "노래 생성 요청에 실패했습니다.", "error_message": "Suno API connection error" } @@ -81,7 +81,7 @@ class GenerateSongResponse(BaseModel): success: bool = Field(..., description="요청 성공 여부") task_id: Optional[str] = Field(None, description="내부 작업 ID (Project/Lyric task_id)") - suno_task_id: Optional[str] = Field(None, description="Suno API 작업 ID") + song_id: Optional[str] = Field(None, description="Suno API 작업 ID") message: str = Field(..., description="응답 메시지") error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)") @@ -90,7 +90,7 @@ class PollingSongRequest(BaseModel): """노래 생성 상태 조회 요청 스키마 (Legacy) Note: - 현재 사용되지 않음. GET /song/status/{suno_task_id} 엔드포인트 사용. + 현재 사용되지 않음. GET /song/status/{song_id} 엔드포인트 사용. Example Request: { @@ -117,7 +117,7 @@ class PollingSongResponse(BaseModel): """노래 생성 상태 조회 응답 스키마 Usage: - GET /song/status/{suno_task_id} + GET /song/status/{song_id} Suno API 작업 상태를 조회합니다. Note: @@ -138,8 +138,6 @@ class PollingSongResponse(BaseModel): "success": true, "status": "processing", "message": "노래를 생성하고 있습니다.", - "clips": null, - "raw_response": {...}, "error_message": null } @@ -148,18 +146,6 @@ class PollingSongResponse(BaseModel): "success": true, "status": "SUCCESS", "message": "노래 생성이 완료되었습니다.", - "clips": [ - { - "id": "clip-id", - "audio_url": "https://...", - "stream_audio_url": "https://...", - "image_url": "https://...", - "title": "Song Title", - "status": "complete", - "duration": 60.0 - } - ], - "raw_response": {...}, "error_message": null } @@ -168,8 +154,6 @@ class PollingSongResponse(BaseModel): "success": false, "status": "error", "message": "상태 조회에 실패했습니다.", - "clips": null, - "raw_response": null, "error_message": "ConnectionError: ..." } """ @@ -179,8 +163,6 @@ class PollingSongResponse(BaseModel): None, description="작업 상태 (PENDING, processing, SUCCESS, failed)" ) message: str = Field(..., description="상태 메시지") - clips: Optional[List[SongClipData]] = Field(None, description="생성된 노래 클립 목록") - raw_response: Optional[Dict[str, Any]] = Field(None, description="Suno API 원본 응답") error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)") diff --git a/app/user/services/auth.py b/app/user/services/auth.py index df83706..011d2fd 100644 --- a/app/user/services/auth.py +++ b/app/user/services/auth.py @@ -4,12 +4,15 @@ 카카오 로그인, JWT 토큰 관리, 사용자 인증 관련 비즈니스 로직을 처리합니다. """ +import logging from datetime import datetime, timezone from typing import Optional from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession +logger = logging.getLogger(__name__) + from app.user.exceptions import ( InvalidTokenError, TokenExpiredError, @@ -221,14 +224,19 @@ class AuthService: kakao_account = kakao_user_info.kakao_account profile = kakao_account.profile if kakao_account else None + logger.info(f"[AUTH] 사용자 조회 시작 - kakao_id: {kakao_id}") + # 기존 사용자 조회 result = await session.execute( select(User).where(User.kakao_id == kakao_id) ) user = result.scalar_one_or_none() + logger.info(f"[AUTH] DB 조회 결과 - user_exists: {user is not None}, user_id: {user.id if user else None}") + if user is not None: # 기존 사용자: 프로필 정보 업데이트 + logger.info(f"[AUTH] 기존 사용자 발견 - user_id: {user.id}, is_new_user: False") if profile: user.nickname = profile.nickname user.profile_image_url = profile.profile_image_url @@ -239,6 +247,7 @@ class AuthService: return user, False # 신규 사용자 생성 + logger.info(f"[AUTH] 신규 사용자 생성 시작 - kakao_id: {kakao_id}") new_user = User( kakao_id=kakao_id, email=kakao_account.email if kakao_account else None, @@ -249,6 +258,7 @@ class AuthService: session.add(new_user) await session.flush() await session.refresh(new_user) + logger.info(f"[AUTH] 신규 사용자 생성 완료 - user_id: {new_user.id}, is_new_user: True") return new_user, True async def _save_refresh_token( diff --git a/app/utils/suno.py b/app/utils/suno.py index 4809300..d850543 100644 --- a/app/utils/suno.py +++ b/app/utils/suno.py @@ -199,8 +199,6 @@ class SunoService: success=False, status="error", message="Suno API 응답이 비어있습니다.", - clips=None, - raw_response=None, error_message="Suno API returned None response", ) @@ -212,8 +210,6 @@ class SunoService: success=False, status="failed", message="Suno API 응답 오류", - clips=None, - raw_response=result, error_message=result.get("msg", "Unknown error"), ) @@ -255,7 +251,5 @@ class SunoService: success=True, status=status, message=status_messages.get(status, f"상태: {status}"), - clips=clips, - raw_response=result, error_message=None, ) diff --git a/main.py b/main.py index 465d951..c158c05 100644 --- a/main.py +++ b/main.py @@ -68,7 +68,7 @@ tags_metadata = [ ## 노래 생성 흐름 1. `POST /api/v1/song/generate/{task_id}` - 노래 생성 요청 -2. `GET /api/v1/song/status/{suno_task_id}` - Suno API 상태 확인 +2. `GET /api/v1/song/status/{song_id}` - Suno API 상태 확인 3. `GET /api/v1/song/download/{task_id}` - 노래 다운로드 URL 조회 """, },