Compare commits
No commits in common. "f6da65044a177d25d302f9c72e36f55334428d8e" and "a6daff4e3863eed9c772014a83de8234209f43f4" have entirely different histories.
f6da65044a
...
a6daff4e38
|
|
@ -92,18 +92,18 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
pool = engine.pool
|
||||
|
||||
# 커넥션 풀 상태 로깅 (디버깅용)
|
||||
# logger.debug(
|
||||
# f"[get_session] ACQUIRE - pool_size: {pool.size()}, "
|
||||
# f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
|
||||
# f"overflow: {pool.overflow()}"
|
||||
# )
|
||||
logger.debug(
|
||||
f"[get_session] ACQUIRE - pool_size: {pool.size()}, "
|
||||
f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
|
||||
f"overflow: {pool.overflow()}"
|
||||
)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
acquire_time = time.perf_counter()
|
||||
# logger.debug(
|
||||
# f"[get_session] Session acquired in "
|
||||
# f"{(acquire_time - start_time)*1000:.1f}ms"
|
||||
# )
|
||||
logger.debug(
|
||||
f"[get_session] Session acquired in "
|
||||
f"{(acquire_time - start_time)*1000:.1f}ms"
|
||||
)
|
||||
try:
|
||||
yield session
|
||||
except Exception as e:
|
||||
|
|
@ -115,10 +115,10 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
raise e
|
||||
finally:
|
||||
total_time = time.perf_counter() - start_time
|
||||
# logger.debug(
|
||||
# f"[get_session] RELEASE - duration: {total_time*1000:.1f}ms, "
|
||||
# f"pool_out: {pool.checkedout()}"
|
||||
# )
|
||||
logger.debug(
|
||||
f"[get_session] RELEASE - duration: {total_time*1000:.1f}ms, "
|
||||
f"pool_out: {pool.checkedout()}"
|
||||
)
|
||||
|
||||
|
||||
# 백그라운드 태스크용 세션 제너레이터
|
||||
|
|
@ -126,18 +126,18 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
start_time = time.perf_counter()
|
||||
pool = background_engine.pool
|
||||
|
||||
# logger.debug(
|
||||
# f"[get_background_session] ACQUIRE - pool_size: {pool.size()}, "
|
||||
# f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
|
||||
# f"overflow: {pool.overflow()}"
|
||||
# )
|
||||
logger.debug(
|
||||
f"[get_background_session] ACQUIRE - pool_size: {pool.size()}, "
|
||||
f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
|
||||
f"overflow: {pool.overflow()}"
|
||||
)
|
||||
|
||||
async with BackgroundSessionLocal() as session:
|
||||
acquire_time = time.perf_counter()
|
||||
# logger.debug(
|
||||
# f"[get_background_session] Session acquired in "
|
||||
# f"{(acquire_time - start_time)*1000:.1f}ms"
|
||||
# )
|
||||
logger.debug(
|
||||
f"[get_background_session] Session acquired in "
|
||||
f"{(acquire_time - start_time)*1000:.1f}ms"
|
||||
)
|
||||
try:
|
||||
yield session
|
||||
except Exception as e:
|
||||
|
|
@ -150,11 +150,11 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
raise e
|
||||
finally:
|
||||
total_time = time.perf_counter() - start_time
|
||||
# logger.debug(
|
||||
# f"[get_background_session] RELEASE - "
|
||||
# f"duration: {total_time*1000:.1f}ms, "
|
||||
# f"pool_out: {pool.checkedout()}"
|
||||
# )
|
||||
logger.debug(
|
||||
f"[get_background_session] RELEASE - "
|
||||
f"duration: {total_time*1000:.1f}ms, "
|
||||
f"pool_out: {pool.checkedout()}"
|
||||
)
|
||||
|
||||
|
||||
# 앱 종료 시 엔진 리소스 정리 함수
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ from app.dependencies.pagination import (
|
|||
)
|
||||
from app.home.models import Project
|
||||
from app.lyric.models import Lyric
|
||||
from app.song.models import Song
|
||||
from app.song.models import Song, SongTimestamp
|
||||
|
||||
from app.song.schemas.song_schema import (
|
||||
DownloadSongResponse,
|
||||
GenerateSongRequest,
|
||||
|
|
@ -343,13 +344,14 @@ async def get_song_status(
|
|||
Azure Blob Storage에 업로드한 뒤 Song 테이블의 status를 completed로,
|
||||
song_result_url을 Blob URL로 업데이트합니다.
|
||||
"""
|
||||
logger.info(f"[get_song_status] START - song_id: {song_id}")
|
||||
suno_task_id = song_id # 임시방편 / 외부 suno 노출 방지
|
||||
logger.info(f"[get_song_status] START - song_id: {suno_task_id}")
|
||||
try:
|
||||
suno_service = SunoService()
|
||||
result = await suno_service.get_task_status(song_id)
|
||||
logger.debug(f"[get_song_status] Suno API raw response - song_id: {song_id}, result: {result}")
|
||||
result = await suno_service.get_task_status(suno_task_id)
|
||||
logger.debug(f"[get_song_status] Suno API raw response - song_id: {suno_task_id}, result: {result}")
|
||||
parsed_response = suno_service.parse_status_response(result)
|
||||
logger.info(f"[get_song_status] Suno API response - song_id: {song_id}, status: {parsed_response.status}")
|
||||
logger.info(f"[get_song_status] Suno API response - song_id: {suno_task_id}, status: {parsed_response.status}")
|
||||
|
||||
# SUCCESS 상태인 경우 백그라운드에서 MP3 다운로드 및 Blob 업로드 진행
|
||||
if parsed_response.status == "SUCCESS" and result:
|
||||
|
|
@ -369,12 +371,12 @@ async def get_song_status(
|
|||
# song_id로 Song 조회
|
||||
song_result = await session.execute(
|
||||
select(Song)
|
||||
.where(Song.suno_task_id == song_id)
|
||||
.where(Song.suno_task_id == suno_task_id)
|
||||
.order_by(Song.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
song = song_result.scalar_one_or_none()
|
||||
|
||||
|
||||
# processing 상태인 경우에만 백그라운드 태스크 실행 (중복 방지)
|
||||
if song and song.status == "processing":
|
||||
# store_name 조회
|
||||
|
|
@ -388,26 +390,51 @@ async def get_song_status(
|
|||
song.status = "uploading"
|
||||
song.suno_audio_id = first_clip.get('id')
|
||||
await session.commit()
|
||||
logger.info(f"[get_song_status] Song status changed to uploading - song_id: {song_id}")
|
||||
logger.info(f"[get_song_status] Song status changed to uploading - song_id: {suno_task_id}")
|
||||
|
||||
# 백그라운드 태스크로 MP3 다운로드 및 Blob 업로드 실행
|
||||
background_tasks.add_task(
|
||||
download_and_upload_song_by_suno_task_id,
|
||||
suno_task_id=song_id,
|
||||
suno_task_id=suno_task_id,
|
||||
audio_url=audio_url,
|
||||
store_name=store_name,
|
||||
duration=clip_duration,
|
||||
)
|
||||
logger.info(f"[get_song_status] Background task scheduled - song_id: {song_id}, store_name: {store_name}")
|
||||
logger.info(f"[get_song_status] Background task scheduled - song_id: {suno_task_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":
|
||||
logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {song_id}")
|
||||
parsed_response.status == "uploading"
|
||||
logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {suno_task_id}")
|
||||
elif song and song.status == "completed":
|
||||
logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {song_id}")
|
||||
parsed_response.status == "SUCCESS"
|
||||
logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {suno_task_id}")
|
||||
|
||||
logger.info(f"[get_song_status] song_id: {song_id}")
|
||||
logger.info(f"[get_song_status] SUCCESS - song_id: {suno_task_id}")
|
||||
return parsed_response
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ async def download_and_save_song(
|
|||
|
||||
# 프론트엔드에서 접근 가능한 URL 생성
|
||||
relative_path = f"/media/song/{today}/{unique_id}/{file_name}"
|
||||
base_url = f"{prj_settings.PROJECT_DOMAIN}"
|
||||
base_url = f"http://{prj_settings.PROJECT_DOMAIN}"
|
||||
file_url = f"{base_url}{relative_path}"
|
||||
logger.info(f"[download_and_save_song] URL generated - task_id: {task_id}, url: {file_url}")
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,13 @@
|
|||
카카오 로그인, 토큰 갱신, 로그아웃, 내 정보 조회 엔드포인트를 제공합니다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, Request, status
|
||||
from fastapi.responses import RedirectResponse, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import prj_settings
|
||||
from app.database.session import get_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from app.user.dependencies import get_current_user
|
||||
from app.user.models import User
|
||||
from app.user.schemas.user_schema import (
|
||||
|
|
@ -43,9 +39,7 @@ async def kakao_login() -> KakaoLoginResponse:
|
|||
프론트엔드에서 이 URL로 사용자를 리다이렉트하면
|
||||
카카오 로그인 페이지가 표시됩니다.
|
||||
"""
|
||||
logger.info("[ROUTER] 카카오 로그인 URL 요청")
|
||||
auth_url = kakao_client.get_authorization_url()
|
||||
logger.debug(f"[ROUTER] 카카오 인증 URL 생성 완료 - auth_url: {auth_url}")
|
||||
return KakaoLoginResponse(auth_url=auth_url)
|
||||
|
||||
|
||||
|
|
@ -68,8 +62,6 @@ async def kakao_callback(
|
|||
|
||||
신규 사용자인 경우 자동으로 회원가입이 처리됩니다.
|
||||
"""
|
||||
logger.info(f"[ROUTER] 카카오 콜백 수신 - code: {code[:20]}...")
|
||||
|
||||
# 클라이언트 IP 추출
|
||||
ip_address = request.client.host if request.client else None
|
||||
|
||||
|
|
@ -78,8 +70,6 @@ async def kakao_callback(
|
|||
if forwarded_for:
|
||||
ip_address = forwarded_for.split(",")[0].strip()
|
||||
|
||||
logger.debug(f"[ROUTER] 클라이언트 정보 - ip: {ip_address}, user_agent: {user_agent}")
|
||||
|
||||
result = await auth_service.kakao_login(
|
||||
code=code,
|
||||
session=session,
|
||||
|
|
@ -89,11 +79,10 @@ async def kakao_callback(
|
|||
|
||||
# 프론트엔드로 토큰과 함께 리다이렉트
|
||||
redirect_url = (
|
||||
f"https://{prj_settings.PROJECT_DOMAIN}"
|
||||
f"http://localhost:3000"
|
||||
f"?access_token={result.access_token}"
|
||||
f"&refresh_token={result.refresh_token}"
|
||||
)
|
||||
logger.info(f"[ROUTER] 카카오 콜백 완료, 프론트엔드로 리다이렉트 - redirect_url: {redirect_url[:50]}...")
|
||||
return RedirectResponse(url=redirect_url, status_code=302)
|
||||
|
||||
|
||||
|
|
@ -129,8 +118,6 @@ async def kakao_verify(
|
|||
|
||||
신규 사용자인 경우 자동으로 회원가입이 처리됩니다.
|
||||
"""
|
||||
logger.info(f"[ROUTER] 카카오 인가 코드 검증 요청 - code: {body.code[:20]}...")
|
||||
|
||||
# 클라이언트 IP 추출
|
||||
ip_address = request.client.host if request.client else None
|
||||
|
||||
|
|
@ -139,18 +126,13 @@ async def kakao_verify(
|
|||
if forwarded_for:
|
||||
ip_address = forwarded_for.split(",")[0].strip()
|
||||
|
||||
logger.debug(f"[ROUTER] 클라이언트 정보 - ip: {ip_address}, user_agent: {user_agent}")
|
||||
|
||||
result = await auth_service.kakao_login(
|
||||
return await auth_service.kakao_login(
|
||||
code=body.code,
|
||||
session=session,
|
||||
user_agent=user_agent,
|
||||
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(
|
||||
"/refresh",
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ User 모듈 SQLAlchemy 모델 정의
|
|||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from sqlalchemy import BigInteger, Boolean, Date, DateTime, ForeignKey, Index, Integer, String, Text, func
|
||||
from sqlalchemy.dialects.mysql import JSON
|
||||
from sqlalchemy import BigInteger, Boolean, Date, DateTime, ForeignKey, Index, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database.session import Base
|
||||
|
|
@ -247,18 +246,6 @@ class User(Base):
|
|||
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:
|
||||
return (
|
||||
f"<User("
|
||||
|
|
@ -386,179 +373,3 @@ class RefreshToken(Base):
|
|||
f"expires_at={self.expires_at}"
|
||||
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")>"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ from typing import Optional
|
|||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import prj_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from app.user.exceptions import (
|
||||
|
|
@ -71,36 +69,24 @@ class AuthService:
|
|||
Returns:
|
||||
LoginResponse: 토큰 및 사용자 정보
|
||||
"""
|
||||
logger.info(f"[AUTH] 카카오 로그인 시작 - code: {code[:20]}..., ip: {ip_address}")
|
||||
|
||||
# 1. 카카오 토큰 획득
|
||||
logger.info("[AUTH] 1단계: 카카오 토큰 획득 시작")
|
||||
kakao_token = await kakao_client.get_access_token(code)
|
||||
logger.debug(f"[AUTH] 카카오 토큰 획득 완료 - token_type: {kakao_token.token_type}")
|
||||
|
||||
# 2. 카카오 사용자 정보 조회
|
||||
logger.info("[AUTH] 2단계: 카카오 사용자 정보 조회 시작")
|
||||
kakao_user_info = await kakao_client.get_user_info(kakao_token.access_token)
|
||||
logger.debug(f"[AUTH] 카카오 사용자 정보 조회 완료 - kakao_id: {kakao_user_info.id}")
|
||||
|
||||
# 3. 사용자 조회 또는 생성
|
||||
logger.info("[AUTH] 3단계: 사용자 조회/생성 시작")
|
||||
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. 비활성화 계정 체크
|
||||
if not user.is_active:
|
||||
logger.error(f"[AUTH] 비활성화 계정 접근 시도 - user_id: {user.id}")
|
||||
raise UserInactiveError()
|
||||
|
||||
# 5. JWT 토큰 생성
|
||||
logger.info("[AUTH] 5단계: JWT 토큰 생성 시작")
|
||||
access_token = create_access_token(user.id)
|
||||
refresh_token = create_refresh_token(user.id)
|
||||
logger.debug(f"[AUTH] JWT 토큰 생성 완료 - user_id: {user.id}")
|
||||
|
||||
# 6. 리프레시 토큰 DB 저장
|
||||
logger.info("[AUTH] 6단계: 리프레시 토큰 저장 시작")
|
||||
await self._save_refresh_token(
|
||||
user_id=user.id,
|
||||
token=refresh_token,
|
||||
|
|
@ -108,16 +94,11 @@ class AuthService:
|
|||
user_agent=user_agent,
|
||||
ip_address=ip_address,
|
||||
)
|
||||
logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}")
|
||||
|
||||
# 7. 마지막 로그인 시간 업데이트
|
||||
user.last_login_at = datetime.now(timezone.utc)
|
||||
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(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
|
|
@ -130,7 +111,7 @@ class AuthService:
|
|||
profile_image_url=user.profile_image_url,
|
||||
is_new_user=is_new_user,
|
||||
),
|
||||
redirect_url=redirect_url,
|
||||
redirect_url="http://localhost:3000",
|
||||
)
|
||||
|
||||
async def refresh_tokens(
|
||||
|
|
|
|||
|
|
@ -4,14 +4,10 @@
|
|||
카카오 로그인 인증 흐름을 처리하는 클라이언트입니다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
|
||||
from config import kakao_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from app.user.exceptions import KakaoAPIError, KakaoAuthFailedError
|
||||
from app.user.schemas.user_schema import KakaoTokenResponse, KakaoUserInfo
|
||||
|
||||
|
|
@ -43,15 +39,12 @@ class KakaoOAuthClient:
|
|||
Returns:
|
||||
카카오 OAuth 인증 페이지 URL
|
||||
"""
|
||||
auth_url = (
|
||||
return (
|
||||
f"{self.AUTH_URL}"
|
||||
f"?client_id={self.client_id}"
|
||||
f"&redirect_uri={self.redirect_uri}"
|
||||
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:
|
||||
"""
|
||||
|
|
@ -67,7 +60,6 @@ class KakaoOAuthClient:
|
|||
KakaoAuthFailedError: 토큰 발급 실패 시
|
||||
KakaoAPIError: API 호출 오류 시
|
||||
"""
|
||||
logger.info(f"[KAKAO] 액세스 토큰 요청 시작 - code: {code[:20]}...")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
data = {
|
||||
|
|
@ -80,27 +72,20 @@ class KakaoOAuthClient:
|
|||
if 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:
|
||||
result = await response.json()
|
||||
logger.debug(f"[KAKAO] 토큰 응답 상태 - status: {response.status}")
|
||||
|
||||
if "error" in result:
|
||||
error_desc = result.get(
|
||||
"error_description", result.get("error", "알 수 없는 오류")
|
||||
)
|
||||
logger.error(f"[KAKAO] 토큰 발급 실패 - error: {result.get('error')}, description: {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)
|
||||
|
||||
except KakaoAuthFailedError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[KAKAO] API 호출 오류 - error: {str(e)}")
|
||||
raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}")
|
||||
|
||||
async def get_user_info(self, access_token: str) -> KakaoUserInfo:
|
||||
|
|
@ -117,31 +102,21 @@ class KakaoOAuthClient:
|
|||
KakaoAuthFailedError: 사용자 정보 조회 실패 시
|
||||
KakaoAPIError: API 호출 오류 시
|
||||
"""
|
||||
logger.info("[KAKAO] 사용자 정보 조회 시작")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
|
||||
async with session.get(self.USER_INFO_URL, headers=headers) as response:
|
||||
result = await response.json()
|
||||
logger.debug(f"[KAKAO] 사용자 정보 응답 상태 - status: {response.status}")
|
||||
|
||||
if "id" not in result:
|
||||
logger.error(f"[KAKAO] 사용자 정보 조회 실패 - response: {result}")
|
||||
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)
|
||||
|
||||
except KakaoAuthFailedError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[KAKAO] API 호출 오류 - error: {str(e)}")
|
||||
raise KakaoAPIError(f"카카오 API 호출 중 오류 발생: {str(e)}")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ class SunoService:
|
|||
)
|
||||
|
||||
code = result.get("code", 0)
|
||||
data = result.get("data") or {}
|
||||
data = result.get("data", {})
|
||||
|
||||
if code != 200:
|
||||
return PollingSongResponse(
|
||||
|
|
|
|||
Loading…
Reference in New Issue