Compare commits

...

5 Commits

Author SHA1 Message Date
Dohyun Lim f6da65044a update code 2026-01-22 11:57:56 +09:00
Dohyun Lim 4a06bfdde4 modify song process flow 2026-01-22 11:43:13 +09:00
Dohyun Lim 7038faaf74 add loggers for kakao login 2026-01-21 17:24:07 +09:00
Dohyun Lim 219d8798ad modify import error 2026-01-21 17:12:09 +09:00
Dohyun Lim cea23efac3 update redirect path format 2026-01-21 16:35:48 +09:00
28 changed files with 300 additions and 76 deletions

0
app/archive/__init__.py Normal file
View File

View File

View File

View File

View File

View File

View File

0
app/archive/models.py Normal file
View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@ -92,18 +92,18 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
pool = engine.pool pool = engine.pool
# 커넥션 풀 상태 로깅 (디버깅용) # 커넥션 풀 상태 로깅 (디버깅용)
logger.debug( # logger.debug(
f"[get_session] ACQUIRE - pool_size: {pool.size()}, " # f"[get_session] ACQUIRE - pool_size: {pool.size()}, "
f"in: {pool.checkedin()}, out: {pool.checkedout()}, " # f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
f"overflow: {pool.overflow()}" # f"overflow: {pool.overflow()}"
) # )
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
acquire_time = time.perf_counter() acquire_time = time.perf_counter()
logger.debug( # logger.debug(
f"[get_session] Session acquired in " # f"[get_session] Session acquired in "
f"{(acquire_time - start_time)*1000:.1f}ms" # f"{(acquire_time - start_time)*1000:.1f}ms"
) # )
try: try:
yield session yield session
except Exception as e: except Exception as e:
@ -115,10 +115,10 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
raise e raise e
finally: finally:
total_time = time.perf_counter() - start_time total_time = time.perf_counter() - start_time
logger.debug( # logger.debug(
f"[get_session] RELEASE - duration: {total_time*1000:.1f}ms, " # f"[get_session] RELEASE - duration: {total_time*1000:.1f}ms, "
f"pool_out: {pool.checkedout()}" # f"pool_out: {pool.checkedout()}"
) # )
# 백그라운드 태스크용 세션 제너레이터 # 백그라운드 태스크용 세션 제너레이터
@ -126,18 +126,18 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
start_time = time.perf_counter() start_time = time.perf_counter()
pool = background_engine.pool pool = background_engine.pool
logger.debug( # logger.debug(
f"[get_background_session] ACQUIRE - pool_size: {pool.size()}, " # f"[get_background_session] ACQUIRE - pool_size: {pool.size()}, "
f"in: {pool.checkedin()}, out: {pool.checkedout()}, " # f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
f"overflow: {pool.overflow()}" # f"overflow: {pool.overflow()}"
) # )
async with BackgroundSessionLocal() as session: async with BackgroundSessionLocal() as session:
acquire_time = time.perf_counter() acquire_time = time.perf_counter()
logger.debug( # logger.debug(
f"[get_background_session] Session acquired in " # f"[get_background_session] Session acquired in "
f"{(acquire_time - start_time)*1000:.1f}ms" # f"{(acquire_time - start_time)*1000:.1f}ms"
) # )
try: try:
yield session yield session
except Exception as e: except Exception as e:
@ -150,11 +150,11 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
raise e raise e
finally: finally:
total_time = time.perf_counter() - start_time total_time = time.perf_counter() - start_time
logger.debug( # logger.debug(
f"[get_background_session] RELEASE - " # f"[get_background_session] RELEASE - "
f"duration: {total_time*1000:.1f}ms, " # f"duration: {total_time*1000:.1f}ms, "
f"pool_out: {pool.checkedout()}" # f"pool_out: {pool.checkedout()}"
) # )
# 앱 종료 시 엔진 리소스 정리 함수 # 앱 종료 시 엔진 리소스 정리 함수

View File

@ -24,8 +24,7 @@ from app.dependencies.pagination import (
) )
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, SongTimestamp from app.song.models import Song
from app.song.schemas.song_schema import ( from app.song.schemas.song_schema import (
DownloadSongResponse, DownloadSongResponse,
GenerateSongRequest, GenerateSongRequest,
@ -344,14 +343,13 @@ async def get_song_status(
Azure Blob Storage에 업로드한 Song 테이블의 status를 completed로, Azure Blob Storage에 업로드한 Song 테이블의 status를 completed로,
song_result_url을 Blob URL로 업데이트합니다. song_result_url을 Blob URL로 업데이트합니다.
""" """
suno_task_id = song_id # 임시방편 / 외부 suno 노출 방지 logger.info(f"[get_song_status] START - song_id: {song_id}")
logger.info(f"[get_song_status] START - song_id: {suno_task_id}")
try: try:
suno_service = SunoService() suno_service = SunoService()
result = await suno_service.get_task_status(suno_task_id) result = await suno_service.get_task_status(song_id)
logger.debug(f"[get_song_status] Suno API raw response - song_id: {suno_task_id}, result: {result}") logger.debug(f"[get_song_status] Suno API raw response - song_id: {song_id}, result: {result}")
parsed_response = suno_service.parse_status_response(result) parsed_response = suno_service.parse_status_response(result)
logger.info(f"[get_song_status] Suno API response - song_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 상태인 경우 백그라운드에서 MP3 다운로드 및 Blob 업로드 진행 # SUCCESS 상태인 경우 백그라운드에서 MP3 다운로드 및 Blob 업로드 진행
if parsed_response.status == "SUCCESS" and result: if parsed_response.status == "SUCCESS" and result:
@ -371,7 +369,7 @@ async def get_song_status(
# song_id로 Song 조회 # song_id로 Song 조회
song_result = await session.execute( song_result = await session.execute(
select(Song) select(Song)
.where(Song.suno_task_id == suno_task_id) .where(Song.suno_task_id == song_id)
.order_by(Song.created_at.desc()) .order_by(Song.created_at.desc())
.limit(1) .limit(1)
) )
@ -390,51 +388,26 @@ async def get_song_status(
song.status = "uploading" song.status = "uploading"
song.suno_audio_id = first_clip.get('id') song.suno_audio_id = first_clip.get('id')
await session.commit() await session.commit()
logger.info(f"[get_song_status] Song status changed to uploading - song_id: {suno_task_id}") logger.info(f"[get_song_status] Song status changed to uploading - song_id: {song_id}")
# 백그라운드 태스크로 MP3 다운로드 및 Blob 업로드 실행 # 백그라운드 태스크로 MP3 다운로드 및 Blob 업로드 실행
background_tasks.add_task( background_tasks.add_task(
download_and_upload_song_by_suno_task_id, download_and_upload_song_by_suno_task_id,
suno_task_id=suno_task_id, suno_task_id=song_id,
audio_url=audio_url, audio_url=audio_url,
store_name=store_name, store_name=store_name,
duration=clip_duration, duration=clip_duration,
) )
logger.info(f"[get_song_status] Background task scheduled - song_id: {suno_task_id}, store_name: {store_name}") logger.info(f"[get_song_status] Background task scheduled - song_id: {song_id}, store_name: {store_name}")
suno_audio_id = first_clip.get('id')
word_data = await suno_service.get_lyric_timestamp(suno_task_id, suno_audio_id)
lyric_result = await session.execute(
select(Lyric)
.where(Lyric.task_id == song.task_id)
.order_by(Lyric.created_at.desc())
.limit(1)
)
lyric = lyric_result.scalar_one_or_none()
gt_lyric = lyric.lyric_result
lyric_line_list = gt_lyric.split("\n")
sentences = [lyric_line.strip(',. ') for lyric_line in lyric_line_list if lyric_line and lyric_line != "---"]
timestamped_lyrics = suno_service.align_lyrics(word_data, sentences)
# TODO : DB upload timestamped_lyrics
for order_idx, timestamped_lyric in enumerate(timestamped_lyrics):
song_timestamp = SongTimestamp(
suno_audio_id = suno_audio_id,
order_idx = order_idx,
lyric_line = timestamped_lyric["text"],
start_time = timestamped_lyric["start_sec"],
end_time = timestamped_lyric["end_sec"]
)
session.add(song_timestamp)
await session.commit()
elif song and song.status == "uploading": elif song and song.status == "uploading":
logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {suno_task_id}") logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {song_id}")
parsed_response.status == "uploading"
elif song and song.status == "completed": elif song and song.status == "completed":
logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {suno_task_id}") logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {song_id}")
parsed_response.status == "SUCCESS"
logger.info(f"[get_song_status] SUCCESS - song_id: {suno_task_id}") logger.info(f"[get_song_status] song_id: {song_id}")
return parsed_response return parsed_response
except Exception as e: except Exception as e:

View File

@ -152,7 +152,7 @@ async def download_and_save_song(
# 프론트엔드에서 접근 가능한 URL 생성 # 프론트엔드에서 접근 가능한 URL 생성
relative_path = f"/media/song/{today}/{unique_id}/{file_name}" relative_path = f"/media/song/{today}/{unique_id}/{file_name}"
base_url = f"http://{prj_settings.PROJECT_DOMAIN}" base_url = f"{prj_settings.PROJECT_DOMAIN}"
file_url = f"{base_url}{relative_path}" file_url = f"{base_url}{relative_path}"
logger.info(f"[download_and_save_song] URL generated - task_id: {task_id}, url: {file_url}") logger.info(f"[download_and_save_song] URL generated - task_id: {task_id}, url: {file_url}")

View File

@ -4,13 +4,17 @@
카카오 로그인, 토큰 갱신, 로그아웃, 정보 조회 엔드포인트를 제공합니다. 카카오 로그인, 토큰 갱신, 로그아웃, 정보 조회 엔드포인트를 제공합니다.
""" """
import logging
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, Header, Request, status from fastapi import APIRouter, Depends, Header, Request, status
from fastapi.responses import RedirectResponse, Response from fastapi.responses import RedirectResponse, Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import prj_settings
from app.database.session import get_session from app.database.session import get_session
logger = logging.getLogger(__name__)
from app.user.dependencies import get_current_user from app.user.dependencies import get_current_user
from app.user.models import User from app.user.models import User
from app.user.schemas.user_schema import ( from app.user.schemas.user_schema import (
@ -39,7 +43,9 @@ async def kakao_login() -> KakaoLoginResponse:
프론트엔드에서 URL로 사용자를 리다이렉트하면 프론트엔드에서 URL로 사용자를 리다이렉트하면
카카오 로그인 페이지가 표시됩니다. 카카오 로그인 페이지가 표시됩니다.
""" """
logger.info("[ROUTER] 카카오 로그인 URL 요청")
auth_url = kakao_client.get_authorization_url() auth_url = kakao_client.get_authorization_url()
logger.debug(f"[ROUTER] 카카오 인증 URL 생성 완료 - auth_url: {auth_url}")
return KakaoLoginResponse(auth_url=auth_url) return KakaoLoginResponse(auth_url=auth_url)
@ -62,6 +68,8 @@ async def kakao_callback(
신규 사용자인 경우 자동으로 회원가입이 처리됩니다. 신규 사용자인 경우 자동으로 회원가입이 처리됩니다.
""" """
logger.info(f"[ROUTER] 카카오 콜백 수신 - code: {code[:20]}...")
# 클라이언트 IP 추출 # 클라이언트 IP 추출
ip_address = request.client.host if request.client else None ip_address = request.client.host if request.client else None
@ -70,6 +78,8 @@ async def kakao_callback(
if forwarded_for: if forwarded_for:
ip_address = forwarded_for.split(",")[0].strip() ip_address = forwarded_for.split(",")[0].strip()
logger.debug(f"[ROUTER] 클라이언트 정보 - ip: {ip_address}, user_agent: {user_agent}")
result = await auth_service.kakao_login( result = await auth_service.kakao_login(
code=code, code=code,
session=session, session=session,
@ -79,10 +89,11 @@ async def kakao_callback(
# 프론트엔드로 토큰과 함께 리다이렉트 # 프론트엔드로 토큰과 함께 리다이렉트
redirect_url = ( redirect_url = (
f"http://localhost:3000" f"https://{prj_settings.PROJECT_DOMAIN}"
f"?access_token={result.access_token}" f"?access_token={result.access_token}"
f"&refresh_token={result.refresh_token}" f"&refresh_token={result.refresh_token}"
) )
logger.info(f"[ROUTER] 카카오 콜백 완료, 프론트엔드로 리다이렉트 - redirect_url: {redirect_url[:50]}...")
return RedirectResponse(url=redirect_url, status_code=302) return RedirectResponse(url=redirect_url, status_code=302)
@ -118,6 +129,8 @@ async def kakao_verify(
신규 사용자인 경우 자동으로 회원가입이 처리됩니다. 신규 사용자인 경우 자동으로 회원가입이 처리됩니다.
""" """
logger.info(f"[ROUTER] 카카오 인가 코드 검증 요청 - code: {body.code[:20]}...")
# 클라이언트 IP 추출 # 클라이언트 IP 추출
ip_address = request.client.host if request.client else None ip_address = request.client.host if request.client else None
@ -126,13 +139,18 @@ async def kakao_verify(
if forwarded_for: if forwarded_for:
ip_address = forwarded_for.split(",")[0].strip() ip_address = forwarded_for.split(",")[0].strip()
return await auth_service.kakao_login( logger.debug(f"[ROUTER] 클라이언트 정보 - ip: {ip_address}, user_agent: {user_agent}")
result = await auth_service.kakao_login(
code=body.code, code=body.code,
session=session, session=session,
user_agent=user_agent, user_agent=user_agent,
ip_address=ip_address, ip_address=ip_address,
) )
logger.info(f"[ROUTER] 카카오 인가 코드 검증 완료 - user_id: {result.user.id}, is_new_user: {result.user.is_new_user}")
return result
@router.post( @router.post(
"/refresh", "/refresh",

View File

@ -7,7 +7,8 @@ User 모듈 SQLAlchemy 모델 정의
from datetime import date, datetime from datetime import date, datetime
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, List, Optional
from sqlalchemy import BigInteger, Boolean, Date, DateTime, ForeignKey, Index, Integer, String, func from sqlalchemy import BigInteger, Boolean, Date, DateTime, ForeignKey, Index, Integer, String, Text, func
from sqlalchemy.dialects.mysql import JSON
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database.session import Base from app.database.session import Base
@ -246,6 +247,18 @@ class User(Base):
lazy="selectin", lazy="selectin",
) )
# ==========================================================================
# SocialAccount 1:N 관계
# ==========================================================================
# 한 사용자는 여러 소셜 계정을 연동할 수 있음 (YouTube, Instagram, Facebook)
# ==========================================================================
social_accounts: Mapped[List["SocialAccount"]] = relationship(
"SocialAccount",
back_populates="user",
cascade="all, delete-orphan",
lazy="selectin",
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<User(" f"<User("
@ -373,3 +386,179 @@ class RefreshToken(Base):
f"expires_at={self.expires_at}" f"expires_at={self.expires_at}"
f")>" f")>"
) )
class SocialAccount(Base):
"""
소셜 계정 연동 테이블
사용자가 연동한 외부 소셜 플랫폼 계정 정보를 저장합니다.
YouTube, Instagram, Facebook 계정 연동을 지원합니다.
Attributes:
id: 고유 식별자 (자동 증가)
user_id: 사용자 외래키 (User.id 참조)
platform: 플랫폼 구분 (youtube, instagram, facebook)
access_token: OAuth 액세스 토큰
refresh_token: OAuth 리프레시 토큰 (선택)
token_expires_at: 토큰 만료 일시
scope: 허용된 권한 범위
platform_user_id: 플랫폼 사용자 고유 ID
platform_username: 플랫폼 사용자명/핸들
platform_data: 플랫폼별 추가 정보 (JSON)
is_active: 연동 활성화 상태
connected_at: 연동 일시
updated_at: 정보 수정 일시
플랫폼별 platform_data 예시:
- YouTube: {"channel_id": "UC...", "channel_title": "채널명"}
- Instagram: {"business_account_id": "...", "facebook_page_id": "..."}
- Facebook: {"page_id": "...", "page_access_token": "..."}
Relationships:
user: 연동된 사용자 (User 테이블 참조)
"""
__tablename__ = "social_account"
__table_args__ = (
Index("idx_social_account_user_id", "user_id"),
Index("idx_social_account_platform", "platform"),
Index("idx_social_account_is_active", "is_active"),
Index(
"uq_user_platform_account",
"user_id",
"platform",
"platform_user_id",
unique=True,
),
{
"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="고유 식별자",
)
user_id: Mapped[int] = mapped_column(
BigInteger,
ForeignKey("user.id", ondelete="CASCADE"),
nullable=False,
comment="사용자 외래키 (User.id 참조)",
)
# ==========================================================================
# 플랫폼 구분
# ==========================================================================
platform: Mapped[str] = mapped_column(
String(20),
nullable=False,
comment="플랫폼 구분 (youtube, instagram, facebook)",
)
# ==========================================================================
# OAuth 토큰 정보
# ==========================================================================
access_token: Mapped[str] = mapped_column(
Text,
nullable=False,
comment="OAuth 액세스 토큰",
)
refresh_token: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
comment="OAuth 리프레시 토큰 (플랫폼에 따라 선택적)",
)
token_expires_at: Mapped[Optional[datetime]] = mapped_column(
DateTime,
nullable=True,
comment="토큰 만료 일시",
)
scope: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
comment="허용된 권한 범위 (OAuth scope)",
)
# ==========================================================================
# 플랫폼 계정 식별 정보
# ==========================================================================
platform_user_id: Mapped[str] = mapped_column(
String(100),
nullable=False,
comment="플랫폼 내 사용자 고유 ID",
)
platform_username: Mapped[Optional[str]] = mapped_column(
String(100),
nullable=True,
comment="플랫폼 내 사용자명/핸들 (@username)",
)
# ==========================================================================
# 플랫폼별 추가 정보 (JSON)
# ==========================================================================
platform_data: Mapped[Optional[dict]] = mapped_column(
JSON,
nullable=True,
comment="플랫폼별 추가 정보 (채널ID, 페이지ID 등)",
)
# ==========================================================================
# 연동 상태
# ==========================================================================
is_active: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=True,
comment="연동 활성화 상태 (비활성화 시 사용 중지)",
)
# ==========================================================================
# 시간 정보
# ==========================================================================
connected_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
comment="연동 일시",
)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
onupdate=func.now(),
comment="정보 수정 일시",
)
# ==========================================================================
# User 관계
# ==========================================================================
user: Mapped["User"] = relationship(
"User",
back_populates="social_accounts",
)
def __repr__(self) -> str:
return (
f"<SocialAccount("
f"id={self.id}, "
f"user_id={self.user_id}, "
f"platform='{self.platform}', "
f"platform_username='{self.platform_username}', "
f"is_active={self.is_active}"
f")>"
)

View File

@ -11,6 +11,8 @@ from typing import Optional
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import prj_settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from app.user.exceptions import ( from app.user.exceptions import (
@ -69,24 +71,36 @@ class AuthService:
Returns: Returns:
LoginResponse: 토큰 사용자 정보 LoginResponse: 토큰 사용자 정보
""" """
logger.info(f"[AUTH] 카카오 로그인 시작 - code: {code[:20]}..., ip: {ip_address}")
# 1. 카카오 토큰 획득 # 1. 카카오 토큰 획득
logger.info("[AUTH] 1단계: 카카오 토큰 획득 시작")
kakao_token = await kakao_client.get_access_token(code) kakao_token = await kakao_client.get_access_token(code)
logger.debug(f"[AUTH] 카카오 토큰 획득 완료 - token_type: {kakao_token.token_type}")
# 2. 카카오 사용자 정보 조회 # 2. 카카오 사용자 정보 조회
logger.info("[AUTH] 2단계: 카카오 사용자 정보 조회 시작")
kakao_user_info = await kakao_client.get_user_info(kakao_token.access_token) kakao_user_info = await kakao_client.get_user_info(kakao_token.access_token)
logger.debug(f"[AUTH] 카카오 사용자 정보 조회 완료 - kakao_id: {kakao_user_info.id}")
# 3. 사용자 조회 또는 생성 # 3. 사용자 조회 또는 생성
logger.info("[AUTH] 3단계: 사용자 조회/생성 시작")
user, is_new_user = await self._get_or_create_user(kakao_user_info, session) user, is_new_user = await self._get_or_create_user(kakao_user_info, session)
logger.info(f"[AUTH] 사용자 처리 완료 - user_id: {user.id}, is_new_user: {is_new_user}")
# 4. 비활성화 계정 체크 # 4. 비활성화 계정 체크
if not user.is_active: if not user.is_active:
logger.error(f"[AUTH] 비활성화 계정 접근 시도 - user_id: {user.id}")
raise UserInactiveError() raise UserInactiveError()
# 5. JWT 토큰 생성 # 5. JWT 토큰 생성
logger.info("[AUTH] 5단계: JWT 토큰 생성 시작")
access_token = create_access_token(user.id) access_token = create_access_token(user.id)
refresh_token = create_refresh_token(user.id) refresh_token = create_refresh_token(user.id)
logger.debug(f"[AUTH] JWT 토큰 생성 완료 - user_id: {user.id}")
# 6. 리프레시 토큰 DB 저장 # 6. 리프레시 토큰 DB 저장
logger.info("[AUTH] 6단계: 리프레시 토큰 저장 시작")
await self._save_refresh_token( await self._save_refresh_token(
user_id=user.id, user_id=user.id,
token=refresh_token, token=refresh_token,
@ -94,11 +108,16 @@ class AuthService:
user_agent=user_agent, user_agent=user_agent,
ip_address=ip_address, ip_address=ip_address,
) )
logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}")
# 7. 마지막 로그인 시간 업데이트 # 7. 마지막 로그인 시간 업데이트
user.last_login_at = datetime.now(timezone.utc) user.last_login_at = datetime.now(timezone.utc)
await session.commit() await session.commit()
redirect_url = f"https://{prj_settings.PROJECT_DOMAIN}"
logger.info(f"[AUTH] 카카오 로그인 완료 - user_id: {user.id}, redirect_url: {redirect_url}")
logger.debug(f"[AUTH] 응답 토큰 정보 - access_token: {access_token[:30]}..., refresh_token: {refresh_token[:30]}...")
return LoginResponse( return LoginResponse(
access_token=access_token, access_token=access_token,
refresh_token=refresh_token, refresh_token=refresh_token,
@ -111,7 +130,7 @@ class AuthService:
profile_image_url=user.profile_image_url, profile_image_url=user.profile_image_url,
is_new_user=is_new_user, is_new_user=is_new_user,
), ),
redirect_url="http://localhost:3000", redirect_url=redirect_url,
) )
async def refresh_tokens( async def refresh_tokens(

View File

@ -4,10 +4,14 @@
카카오 로그인 인증 흐름을 처리하는 클라이언트입니다. 카카오 로그인 인증 흐름을 처리하는 클라이언트입니다.
""" """
import logging
import aiohttp import aiohttp
from config import kakao_settings from config import kakao_settings
logger = logging.getLogger(__name__)
from app.user.exceptions import KakaoAPIError, KakaoAuthFailedError from app.user.exceptions import KakaoAPIError, KakaoAuthFailedError
from app.user.schemas.user_schema import KakaoTokenResponse, KakaoUserInfo from app.user.schemas.user_schema import KakaoTokenResponse, KakaoUserInfo
@ -39,12 +43,15 @@ class KakaoOAuthClient:
Returns: Returns:
카카오 OAuth 인증 페이지 URL 카카오 OAuth 인증 페이지 URL
""" """
return ( auth_url = (
f"{self.AUTH_URL}" f"{self.AUTH_URL}"
f"?client_id={self.client_id}" f"?client_id={self.client_id}"
f"&redirect_uri={self.redirect_uri}" f"&redirect_uri={self.redirect_uri}"
f"&response_type=code" f"&response_type=code"
) )
logger.info(f"[KAKAO] 인증 URL 생성 - redirect_uri: {self.redirect_uri}")
logger.debug(f"[KAKAO] 인증 URL 상세 - auth_url: {auth_url}")
return auth_url
async def get_access_token(self, code: str) -> KakaoTokenResponse: async def get_access_token(self, code: str) -> KakaoTokenResponse:
""" """
@ -60,6 +67,7 @@ class KakaoOAuthClient:
KakaoAuthFailedError: 토큰 발급 실패 KakaoAuthFailedError: 토큰 발급 실패
KakaoAPIError: API 호출 오류 KakaoAPIError: API 호출 오류
""" """
logger.info(f"[KAKAO] 액세스 토큰 요청 시작 - code: {code[:20]}...")
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
data = { data = {
@ -72,20 +80,27 @@ class KakaoOAuthClient:
if self.client_secret: if self.client_secret:
data["client_secret"] = self.client_secret data["client_secret"] = self.client_secret
logger.debug(f"[KAKAO] 토큰 요청 데이터 - redirect_uri: {self.redirect_uri}, client_id: {self.client_id[:10]}...")
async with session.post(self.TOKEN_URL, data=data) as response: async with session.post(self.TOKEN_URL, data=data) as response:
result = await response.json() result = await response.json()
logger.debug(f"[KAKAO] 토큰 응답 상태 - status: {response.status}")
if "error" in result: if "error" in result:
error_desc = result.get( error_desc = result.get(
"error_description", result.get("error", "알 수 없는 오류") "error_description", result.get("error", "알 수 없는 오류")
) )
logger.error(f"[KAKAO] 토큰 발급 실패 - error: {result.get('error')}, description: {error_desc}")
raise KakaoAuthFailedError(f"카카오 토큰 발급 실패: {error_desc}") raise KakaoAuthFailedError(f"카카오 토큰 발급 실패: {error_desc}")
logger.info("[KAKAO] 액세스 토큰 발급 성공")
logger.debug(f"[KAKAO] 토큰 정보 - token_type: {result.get('token_type')}, expires_in: {result.get('expires_in')}")
return KakaoTokenResponse(**result) return KakaoTokenResponse(**result)
except KakaoAuthFailedError: except KakaoAuthFailedError:
raise raise
except Exception as e: except Exception as e:
logger.error(f"[KAKAO] API 호출 오류 - error: {str(e)}")
raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}") raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}")
async def get_user_info(self, access_token: str) -> KakaoUserInfo: async def get_user_info(self, access_token: str) -> KakaoUserInfo:
@ -102,21 +117,31 @@ class KakaoOAuthClient:
KakaoAuthFailedError: 사용자 정보 조회 실패 KakaoAuthFailedError: 사용자 정보 조회 실패
KakaoAPIError: API 호출 오류 KakaoAPIError: API 호출 오류
""" """
logger.info("[KAKAO] 사용자 정보 조회 시작")
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
headers = {"Authorization": f"Bearer {access_token}"} headers = {"Authorization": f"Bearer {access_token}"}
async with session.get(self.USER_INFO_URL, headers=headers) as response: async with session.get(self.USER_INFO_URL, headers=headers) as response:
result = await response.json() result = await response.json()
logger.debug(f"[KAKAO] 사용자 정보 응답 상태 - status: {response.status}")
if "id" not in result: if "id" not in result:
logger.error(f"[KAKAO] 사용자 정보 조회 실패 - response: {result}")
raise KakaoAuthFailedError("카카오 사용자 정보를 가져올 수 없습니다.") raise KakaoAuthFailedError("카카오 사용자 정보를 가져올 수 없습니다.")
kakao_id = result.get("id")
kakao_account = result.get("kakao_account", {})
profile = kakao_account.get("profile", {})
logger.info(f"[KAKAO] 사용자 정보 조회 성공 - kakao_id: {kakao_id}")
logger.debug(f"[KAKAO] 사용자 상세 정보 - nickname: {profile.get('nickname')}, email: {kakao_account.get('email')}")
return KakaoUserInfo(**result) return KakaoUserInfo(**result)
except KakaoAuthFailedError: except KakaoAuthFailedError:
raise raise
except Exception as e: except Exception as e:
logger.error(f"[KAKAO] API 호출 오류 - error: {str(e)}")
raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}") raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}")

View File

@ -234,7 +234,7 @@ class SunoService:
) )
code = result.get("code", 0) code = result.get("code", 0)
data = result.get("data", {}) data = result.get("data") or {}
if code != 200: if code != 200:
return PollingSongResponse( return PollingSongResponse(