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
|
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()}"
|
||||||
# )
|
)
|
||||||
|
|
||||||
|
|
||||||
# 앱 종료 시 엔진 리소스 정리 함수
|
# 앱 종료 시 엔진 리소스 정리 함수
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ 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
|
from app.song.models import Song, SongTimestamp
|
||||||
|
|
||||||
from app.song.schemas.song_schema import (
|
from app.song.schemas.song_schema import (
|
||||||
DownloadSongResponse,
|
DownloadSongResponse,
|
||||||
GenerateSongRequest,
|
GenerateSongRequest,
|
||||||
|
|
@ -343,13 +344,14 @@ 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로 업데이트합니다.
|
||||||
"""
|
"""
|
||||||
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:
|
try:
|
||||||
suno_service = SunoService()
|
suno_service = SunoService()
|
||||||
result = await suno_service.get_task_status(song_id)
|
result = await suno_service.get_task_status(suno_task_id)
|
||||||
logger.debug(f"[get_song_status] Suno API raw response - song_id: {song_id}, result: {result}")
|
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)
|
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 업로드 진행
|
# SUCCESS 상태인 경우 백그라운드에서 MP3 다운로드 및 Blob 업로드 진행
|
||||||
if parsed_response.status == "SUCCESS" and result:
|
if parsed_response.status == "SUCCESS" and result:
|
||||||
|
|
@ -369,7 +371,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 == song_id)
|
.where(Song.suno_task_id == suno_task_id)
|
||||||
.order_by(Song.created_at.desc())
|
.order_by(Song.created_at.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
|
|
@ -388,26 +390,51 @@ 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: {song_id}")
|
logger.info(f"[get_song_status] Song status changed to uploading - song_id: {suno_task_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=song_id,
|
suno_task_id=suno_task_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: {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":
|
elif song and song.status == "uploading":
|
||||||
logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {song_id}")
|
logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {suno_task_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: {song_id}")
|
logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {suno_task_id}")
|
||||||
parsed_response.status == "SUCCESS"
|
|
||||||
|
|
||||||
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
|
return parsed_response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -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"{prj_settings.PROJECT_DOMAIN}"
|
base_url = f"http://{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}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,13 @@
|
||||||
카카오 로그인, 토큰 갱신, 로그아웃, 내 정보 조회 엔드포인트를 제공합니다.
|
카카오 로그인, 토큰 갱신, 로그아웃, 내 정보 조회 엔드포인트를 제공합니다.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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 (
|
||||||
|
|
@ -43,9 +39,7 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,8 +62,6 @@ 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
|
||||||
|
|
||||||
|
|
@ -78,8 +70,6 @@ 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,
|
||||||
|
|
@ -89,11 +79,10 @@ async def kakao_callback(
|
||||||
|
|
||||||
# 프론트엔드로 토큰과 함께 리다이렉트
|
# 프론트엔드로 토큰과 함께 리다이렉트
|
||||||
redirect_url = (
|
redirect_url = (
|
||||||
f"https://{prj_settings.PROJECT_DOMAIN}"
|
f"http://localhost:3000"
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -129,8 +118,6 @@ 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
|
||||||
|
|
||||||
|
|
@ -139,18 +126,13 @@ async def kakao_verify(
|
||||||
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}")
|
return await auth_service.kakao_login(
|
||||||
|
|
||||||
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",
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ 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, Text, func
|
from sqlalchemy import BigInteger, Boolean, Date, DateTime, ForeignKey, Index, Integer, String, 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
|
||||||
|
|
@ -247,18 +246,6 @@ 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("
|
||||||
|
|
@ -386,179 +373,3 @@ 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")>"
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ 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 (
|
||||||
|
|
@ -71,36 +69,24 @@ 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,
|
||||||
|
|
@ -108,16 +94,11 @@ 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,
|
||||||
|
|
@ -130,7 +111,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=redirect_url,
|
redirect_url="http://localhost:3000",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def refresh_tokens(
|
async def refresh_tokens(
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,10 @@
|
||||||
카카오 로그인 인증 흐름을 처리하는 클라이언트입니다.
|
카카오 로그인 인증 흐름을 처리하는 클라이언트입니다.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -43,15 +39,12 @@ class KakaoOAuthClient:
|
||||||
Returns:
|
Returns:
|
||||||
카카오 OAuth 인증 페이지 URL
|
카카오 OAuth 인증 페이지 URL
|
||||||
"""
|
"""
|
||||||
auth_url = (
|
return (
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
|
|
@ -67,7 +60,6 @@ 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 = {
|
||||||
|
|
@ -80,27 +72,20 @@ 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:
|
||||||
|
|
@ -117,31 +102,21 @@ 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)}")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ class SunoService:
|
||||||
)
|
)
|
||||||
|
|
||||||
code = result.get("code", 0)
|
code = result.get("code", 0)
|
||||||
data = result.get("data") or {}
|
data = result.get("data", {})
|
||||||
|
|
||||||
if code != 200:
|
if code != 200:
|
||||||
return PollingSongResponse(
|
return PollingSongResponse(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue