add logger
parent
1acd8846ab
commit
bf7b53c8e8
|
|
@ -27,3 +27,10 @@ build/
|
|||
*.mp3
|
||||
*.mp4
|
||||
media/
|
||||
|
||||
# Static files
|
||||
static/
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
|
|
@ -4,12 +4,16 @@ from contextlib import asynccontextmanager
|
|||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("core")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""FastAPI 애플리케이션 생명주기 관리"""
|
||||
# Startup - 애플리케이션 시작 시
|
||||
print("Starting up...")
|
||||
logger.info("Starting up...")
|
||||
|
||||
try:
|
||||
from config import prj_settings
|
||||
|
|
@ -19,20 +23,20 @@ async def lifespan(app: FastAPI):
|
|||
from app.database.session import create_db_tables
|
||||
|
||||
await create_db_tables()
|
||||
print("Database tables created (DEBUG mode)")
|
||||
logger.info("Database tables created (DEBUG mode)")
|
||||
except asyncio.TimeoutError:
|
||||
print("Database initialization timed out")
|
||||
logger.error("Database initialization timed out")
|
||||
# 타임아웃 시 앱 시작 중단하려면 raise, 계속하려면 pass
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Database initialization failed: {e}")
|
||||
logger.error(f"Database initialization failed: {e}")
|
||||
# 에러 시 앱 시작 중단하려면 raise, 계속하려면 pass
|
||||
raise
|
||||
|
||||
yield # 애플리케이션 실행 중
|
||||
|
||||
# Shutdown - 애플리케이션 종료 시
|
||||
print("Shutting down...")
|
||||
logger.info("Shutting down...")
|
||||
|
||||
# 공유 HTTP 클라이언트 종료
|
||||
from app.utils.creatomate import close_shared_client
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import logging
|
||||
import traceback
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, TypeVar
|
||||
|
|
@ -7,8 +6,10 @@ from fastapi import FastAPI, HTTPException, Request, Response, status
|
|||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger("core")
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
|
@ -159,16 +160,14 @@ def handle_db_exceptions(
|
|||
raise
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[DB Error] {func.__name__}: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
print(f"[DB Error] {func.__name__}: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=error_message,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[Unexpected Error] {func.__name__}: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
print(f"[Unexpected Error] {func.__name__}: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 예기치 않은 오류가 발생했습니다.",
|
||||
|
|
@ -205,8 +204,7 @@ def handle_external_service_exceptions(
|
|||
except Exception as e:
|
||||
msg = error_message or f"{service_name} 서비스 호출 중 오류가 발생했습니다."
|
||||
logger.error(f"[{service_name} Error] {func.__name__}: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
print(f"[{service_name} Error] {func.__name__}: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=msg,
|
||||
|
|
@ -240,16 +238,14 @@ def handle_api_exceptions(
|
|||
raise
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[API DB Error] {func.__name__}: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
print(f"[API DB Error] {func.__name__}: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[API Error] {func.__name__}: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
print(f"[API Error] {func.__name__}: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=error_message,
|
||||
|
|
@ -263,16 +259,7 @@ def handle_api_exceptions(
|
|||
def _get_handler(status: int, detail: str):
|
||||
# Define
|
||||
def handler(request: Request, exception: Exception) -> Response:
|
||||
# DEBUG PRINT STATEMENT 👇
|
||||
from rich import print, panel
|
||||
print(
|
||||
panel.Panel(
|
||||
exception.__class__.__name__,
|
||||
title="Handled Exception",
|
||||
border_style="red",
|
||||
),
|
||||
)
|
||||
# DEBUG PRINT STATEMENT 👆
|
||||
logger.debug(f"Handled Exception: {exception.__class__.__name__}")
|
||||
|
||||
# Raise HTTPException with given status and detail
|
||||
# can return JSONResponse as well
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ from sqlalchemy.ext.asyncio import (
|
|||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.pool import AsyncQueuePool # 비동기 풀 클래스
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import db_settings
|
||||
|
||||
logger = get_logger("database")
|
||||
|
||||
|
||||
# Base 클래스 정의
|
||||
class Base(DeclarativeBase):
|
||||
|
|
@ -61,7 +64,7 @@ async def create_db_tables() -> None:
|
|||
async with engine.begin() as conn:
|
||||
# from app.database.models import Shipment, Seller # noqa: F401
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
print("MySQL tables created successfully")
|
||||
logger.info("MySQL tables created successfully")
|
||||
|
||||
|
||||
# 세션 제너레이터 (FastAPI Depends에 사용)
|
||||
|
|
@ -80,13 +83,13 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
# FastAPI 요청 완료 시 자동 commit (예외 발생 시 rollback)
|
||||
except Exception as e:
|
||||
await session.rollback() # 명시적 롤백 (선택적)
|
||||
print(f"Session rollback due to: {e}") # 로깅
|
||||
logger.error(f"Session rollback due to: {e}")
|
||||
raise
|
||||
finally:
|
||||
# 명시적 세션 종료 (Connection Pool에 반환)
|
||||
# context manager가 자동 처리하지만, 명시적으로 유지
|
||||
await session.close()
|
||||
print("session closed successfully")
|
||||
logger.debug("session closed successfully")
|
||||
# 또는 session.aclose() - Python 3.10+
|
||||
|
||||
|
||||
|
|
@ -94,4 +97,4 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
async def dispose_engine() -> None:
|
||||
"""애플리케이션 종료 시 모든 연결 해제"""
|
||||
await engine.dispose()
|
||||
print("Database engine disposed")
|
||||
logger.info("Database engine disposed")
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ from typing import AsyncGenerator
|
|||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import db_settings
|
||||
|
||||
logger = get_logger("database")
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
|
@ -74,7 +77,7 @@ async def create_db_tables():
|
|||
from app.song.models import Song # noqa: F401
|
||||
from app.video.models import Video # noqa: F401
|
||||
|
||||
print("Creating database tables...")
|
||||
logger.info("Creating database tables...")
|
||||
|
||||
async with asyncio.timeout(10):
|
||||
async with engine.begin() as connection:
|
||||
|
|
@ -87,7 +90,7 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
pool = engine.pool
|
||||
|
||||
# 커넥션 풀 상태 로깅 (디버깅용)
|
||||
print(
|
||||
logger.debug(
|
||||
f"[get_session] ACQUIRE - pool_size: {pool.size()}, "
|
||||
f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
|
||||
f"overflow: {pool.overflow()}"
|
||||
|
|
@ -95,7 +98,7 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
|
||||
async with AsyncSessionLocal() as session:
|
||||
acquire_time = time.perf_counter()
|
||||
print(
|
||||
logger.debug(
|
||||
f"[get_session] Session acquired in "
|
||||
f"{(acquire_time - start_time)*1000:.1f}ms"
|
||||
)
|
||||
|
|
@ -103,14 +106,14 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
yield session
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
print(
|
||||
logger.error(
|
||||
f"[get_session] ROLLBACK - error: {type(e).__name__}: {e}, "
|
||||
f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms"
|
||||
)
|
||||
raise e
|
||||
finally:
|
||||
total_time = time.perf_counter() - start_time
|
||||
print(
|
||||
logger.debug(
|
||||
f"[get_session] RELEASE - duration: {total_time*1000:.1f}ms, "
|
||||
f"pool_out: {pool.checkedout()}"
|
||||
)
|
||||
|
|
@ -121,7 +124,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
start_time = time.perf_counter()
|
||||
pool = background_engine.pool
|
||||
|
||||
print(
|
||||
logger.debug(
|
||||
f"[get_background_session] ACQUIRE - pool_size: {pool.size()}, "
|
||||
f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
|
||||
f"overflow: {pool.overflow()}"
|
||||
|
|
@ -129,7 +132,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
|
||||
async with BackgroundSessionLocal() as session:
|
||||
acquire_time = time.perf_counter()
|
||||
print(
|
||||
logger.debug(
|
||||
f"[get_background_session] Session acquired in "
|
||||
f"{(acquire_time - start_time)*1000:.1f}ms"
|
||||
)
|
||||
|
|
@ -137,7 +140,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
yield session
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
print(
|
||||
logger.error(
|
||||
f"[get_background_session] ROLLBACK - "
|
||||
f"error: {type(e).__name__}: {e}, "
|
||||
f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms"
|
||||
|
|
@ -145,7 +148,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
raise e
|
||||
finally:
|
||||
total_time = time.perf_counter() - start_time
|
||||
print(
|
||||
logger.debug(
|
||||
f"[get_background_session] RELEASE - "
|
||||
f"duration: {total_time*1000:.1f}ms, "
|
||||
f"pool_out: {pool.checkedout()}"
|
||||
|
|
@ -154,8 +157,8 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
|||
|
||||
# 앱 종료 시 엔진 리소스 정리 함수
|
||||
async def dispose_engine() -> None:
|
||||
print("[dispose_engine] Disposing database engines...")
|
||||
logger.info("[dispose_engine] Disposing database engines...")
|
||||
await engine.dispose()
|
||||
print("[dispose_engine] Main engine disposed")
|
||||
logger.info("[dispose_engine] Main engine disposed")
|
||||
await background_engine.dispose()
|
||||
print("[dispose_engine] Background engine disposed - ALL DONE")
|
||||
logger.info("[dispose_engine] Background engine disposed - ALL DONE")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import json
|
||||
import logging
|
||||
import traceback
|
||||
import time
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
|
@ -26,12 +25,12 @@ from app.home.schemas.home_schema import (
|
|||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
from app.utils.chatgpt_prompt import ChatgptService
|
||||
from app.utils.common import generate_task_id
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.nvMapScraper import NvMapScraper, GraphQLException
|
||||
from config import MEDIA_ROOT
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MEDIA_ROOT = Path("media")
|
||||
logger = get_logger("home")
|
||||
|
||||
# 전국 시 이름 목록 (roadAddress에서 region 추출용)
|
||||
# fmt: off
|
||||
|
|
@ -106,16 +105,13 @@ def _extract_region_from_address(road_address: str | None) -> str:
|
|||
)
|
||||
async def crawling(request_body: CrawlingRequest):
|
||||
"""네이버 지도 장소 크롤링"""
|
||||
import time
|
||||
|
||||
request_start = time.perf_counter()
|
||||
logger.info(f"[crawling] START - url: {request_body.url[:80]}...")
|
||||
print(f"[crawling] ========== START ==========")
|
||||
print(f"[crawling] URL: {request_body.url[:80]}...")
|
||||
logger.info("[crawling] ========== START ==========")
|
||||
logger.info(f"[crawling] URL: {request_body.url[:80]}...")
|
||||
|
||||
# ========== Step 1: 네이버 지도 크롤링 ==========
|
||||
step1_start = time.perf_counter()
|
||||
print(f"[crawling] Step 1: 네이버 지도 크롤링 시작...")
|
||||
logger.info("[crawling] Step 1: 네이버 지도 크롤링 시작...")
|
||||
|
||||
try:
|
||||
scraper = NvMapScraper(request_body.url)
|
||||
|
|
@ -123,7 +119,6 @@ async def crawling(request_body: CrawlingRequest):
|
|||
except GraphQLException as e:
|
||||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||
logger.error(f"[crawling] Step 1 FAILED - GraphQL 크롤링 실패: {e} ({step1_elapsed:.1f}ms)")
|
||||
print(f"[crawling] Step 1 FAILED - {e} ({step1_elapsed:.1f}ms)")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"네이버 지도 크롤링에 실패했습니다: {e}",
|
||||
|
|
@ -131,8 +126,7 @@ async def crawling(request_body: CrawlingRequest):
|
|||
except Exception as e:
|
||||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||
logger.error(f"[crawling] Step 1 FAILED - 크롤링 중 예기치 않은 오류: {e} ({step1_elapsed:.1f}ms)")
|
||||
print(f"[crawling] Step 1 FAILED - {e} ({step1_elapsed:.1f}ms)")
|
||||
traceback.print_exc()
|
||||
logger.exception("[crawling] Step 1 상세 오류:")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="네이버 지도 크롤링 중 오류가 발생했습니다.",
|
||||
|
|
@ -141,11 +135,10 @@ async def crawling(request_body: CrawlingRequest):
|
|||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||
image_count = len(scraper.image_link_list) if scraper.image_link_list else 0
|
||||
logger.info(f"[crawling] Step 1 완료 - 이미지 {image_count}개 ({step1_elapsed:.1f}ms)")
|
||||
print(f"[crawling] Step 1 완료 - 이미지 {image_count}개 ({step1_elapsed:.1f}ms)")
|
||||
|
||||
# ========== Step 2: 정보 가공 ==========
|
||||
step2_start = time.perf_counter()
|
||||
print(f"[crawling] Step 2: 정보 가공 시작...")
|
||||
logger.info("[crawling] Step 2: 정보 가공 시작...")
|
||||
|
||||
processed_info = None
|
||||
marketing_analysis = None
|
||||
|
|
@ -163,11 +156,10 @@ async def crawling(request_body: CrawlingRequest):
|
|||
|
||||
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
||||
logger.info(f"[crawling] Step 2 완료 - {customer_name}, {region} ({step2_elapsed:.1f}ms)")
|
||||
print(f"[crawling] Step 2 완료 - {customer_name}, {region} ({step2_elapsed:.1f}ms)")
|
||||
|
||||
# ========== Step 3: ChatGPT 마케팅 분석 ==========
|
||||
step3_start = time.perf_counter()
|
||||
print(f"[crawling] Step 3: ChatGPT 마케팅 분석 시작...")
|
||||
logger.info("[crawling] Step 3: ChatGPT 마케팅 분석 시작...")
|
||||
|
||||
try:
|
||||
# Step 3-1: ChatGPT 서비스 초기화
|
||||
|
|
@ -178,59 +170,54 @@ async def crawling(request_body: CrawlingRequest):
|
|||
detail_region_info=road_address or "",
|
||||
)
|
||||
step3_1_elapsed = (time.perf_counter() - step3_1_start) * 1000
|
||||
print(f"[crawling] Step 3-1: 서비스 초기화 완료 ({step3_1_elapsed:.1f}ms)")
|
||||
logger.debug(f"[crawling] Step 3-1: 서비스 초기화 완료 ({step3_1_elapsed:.1f}ms)")
|
||||
|
||||
# Step 3-2: 프롬프트 생성
|
||||
step3_2_start = time.perf_counter()
|
||||
prompt = chatgpt_service.build_market_analysis_prompt()
|
||||
step3_2_elapsed = (time.perf_counter() - step3_2_start) * 1000
|
||||
print(f"[crawling] Step 3-2: 프롬프트 생성 완료 - {len(prompt)}자 ({step3_2_elapsed:.1f}ms)")
|
||||
logger.debug(f"[crawling] Step 3-2: 프롬프트 생성 완료 - {len(prompt)}자 ({step3_2_elapsed:.1f}ms)")
|
||||
|
||||
# Step 3-3: GPT API 호출
|
||||
step3_3_start = time.perf_counter()
|
||||
raw_response = await chatgpt_service.generate(prompt)
|
||||
step3_3_elapsed = (time.perf_counter() - step3_3_start) * 1000
|
||||
logger.info(f"[crawling] Step 3-3: GPT API 호출 완료 - 응답 {len(raw_response)}자 ({step3_3_elapsed:.1f}ms)")
|
||||
print(f"[crawling] Step 3-3: GPT API 호출 완료 - 응답 {len(raw_response)}자 ({step3_3_elapsed:.1f}ms)")
|
||||
|
||||
# Step 3-4: 응답 파싱 (크롤링에서 가져온 facility_info 전달)
|
||||
step3_4_start = time.perf_counter()
|
||||
print(f"[crawling] Step 3-4: 응답 파싱 시작 - facility_info: {scraper.facility_info}")
|
||||
logger.debug(f"[crawling] Step 3-4: 응답 파싱 시작 - facility_info: {scraper.facility_info}")
|
||||
parsed = await chatgpt_service.parse_marketing_analysis(
|
||||
raw_response
|
||||
)
|
||||
marketing_analysis = MarketingAnalysis(**parsed)
|
||||
step3_4_elapsed = (time.perf_counter() - step3_4_start) * 1000
|
||||
print(f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)")
|
||||
logger.debug(f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)")
|
||||
|
||||
step3_elapsed = (time.perf_counter() - step3_start) * 1000
|
||||
logger.info(f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)")
|
||||
print(f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)")
|
||||
|
||||
except Exception as e:
|
||||
step3_elapsed = (time.perf_counter() - step3_start) * 1000
|
||||
logger.error(f"[crawling] Step 3 FAILED - GPT 마케팅 분석 중 오류: {e} ({step3_elapsed:.1f}ms)")
|
||||
print(f"[crawling] Step 3 FAILED - {e} ({step3_elapsed:.1f}ms)")
|
||||
traceback.print_exc()
|
||||
logger.exception("[crawling] Step 3 상세 오류:")
|
||||
# GPT 실패 시에도 크롤링 결과는 반환
|
||||
marketing_analysis = None
|
||||
else:
|
||||
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
||||
logger.warning(f"[crawling] Step 2 - base_info 없음 ({step2_elapsed:.1f}ms)")
|
||||
print(f"[crawling] Step 2 - base_info 없음, 마케팅 분석 스킵 ({step2_elapsed:.1f}ms)")
|
||||
logger.warning(f"[crawling] Step 2 - base_info 없음, 마케팅 분석 스킵 ({step2_elapsed:.1f}ms)")
|
||||
|
||||
# ========== 완료 ==========
|
||||
total_elapsed = (time.perf_counter() - request_start) * 1000
|
||||
logger.info(f"[crawling] COMPLETE - 총 소요시간: {total_elapsed:.1f}ms")
|
||||
print(f"[crawling] ========== COMPLETE ==========")
|
||||
print(f"[crawling] 총 소요시간: {total_elapsed:.1f}ms")
|
||||
print(f"[crawling] - Step 1 (크롤링): {step1_elapsed:.1f}ms")
|
||||
logger.info("[crawling] ========== COMPLETE ==========")
|
||||
logger.info(f"[crawling] 총 소요시간: {total_elapsed:.1f}ms")
|
||||
logger.info(f"[crawling] - Step 1 (크롤링): {step1_elapsed:.1f}ms")
|
||||
if scraper.base_info:
|
||||
print(f"[crawling] - Step 2 (정보가공): {step2_elapsed:.1f}ms")
|
||||
logger.info(f"[crawling] - Step 2 (정보가공): {step2_elapsed:.1f}ms")
|
||||
if 'step3_elapsed' in locals():
|
||||
print(f"[crawling] - Step 3 (GPT 분석): {step3_elapsed:.1f}ms")
|
||||
logger.info(f"[crawling] - Step 3 (GPT 분석): {step3_elapsed:.1f}ms")
|
||||
if 'step3_3_elapsed' in locals():
|
||||
print(f"[crawling] - GPT API 호출: {step3_3_elapsed:.1f}ms")
|
||||
logger.info(f"[crawling] - GPT API 호출: {step3_3_elapsed:.1f}ms")
|
||||
|
||||
return {
|
||||
"image_list": scraper.image_link_list,
|
||||
|
|
@ -612,12 +599,11 @@ async def upload_images_blob(
|
|||
- Stage 2: Azure Blob 업로드 (세션 없음)
|
||||
- Stage 3: DB 저장 (새 세션으로 빠르게 처리)
|
||||
"""
|
||||
import time
|
||||
request_start = time.perf_counter()
|
||||
|
||||
# task_id 생성
|
||||
task_id = await generate_task_id()
|
||||
print(f"[upload_images_blob] START - task_id: {task_id}")
|
||||
logger.info(f"[upload_images_blob] START - task_id: {task_id}")
|
||||
|
||||
# ========== Stage 1: 입력 검증 및 파일 데이터 준비 (세션 없음) ==========
|
||||
has_images_json = images_json is not None and images_json.strip() != ""
|
||||
|
|
@ -671,7 +657,7 @@ async def upload_images_blob(
|
|||
)
|
||||
|
||||
stage1_time = time.perf_counter()
|
||||
print(f"[upload_images_blob] Stage 1 done - urls: {len(url_images)}, "
|
||||
logger.info(f"[upload_images_blob] Stage 1 done - urls: {len(url_images)}, "
|
||||
f"files: {len(valid_files_data)}, "
|
||||
f"elapsed: {(stage1_time - request_start)*1000:.1f}ms")
|
||||
|
||||
|
|
@ -692,7 +678,7 @@ async def upload_images_blob(
|
|||
)
|
||||
filename = f"{name_without_ext}_{img_order:03d}{ext}"
|
||||
|
||||
print(f"[upload_images_blob] Uploading file {idx+1}/{total_files}: "
|
||||
logger.debug(f"[upload_images_blob] Uploading file {idx+1}/{total_files}: "
|
||||
f"{filename} ({len(file_content)} bytes)")
|
||||
|
||||
# Azure Blob Storage에 직접 업로드
|
||||
|
|
@ -702,18 +688,18 @@ async def upload_images_blob(
|
|||
blob_url = uploader.public_url
|
||||
blob_upload_results.append((original_name, blob_url))
|
||||
img_order += 1
|
||||
print(f"[upload_images_blob] File {idx+1}/{total_files} SUCCESS")
|
||||
logger.debug(f"[upload_images_blob] File {idx+1}/{total_files} SUCCESS")
|
||||
else:
|
||||
skipped_files.append(filename)
|
||||
print(f"[upload_images_blob] File {idx+1}/{total_files} FAILED")
|
||||
logger.warning(f"[upload_images_blob] File {idx+1}/{total_files} FAILED")
|
||||
|
||||
stage2_time = time.perf_counter()
|
||||
print(f"[upload_images_blob] Stage 2 done - blob uploads: "
|
||||
logger.info(f"[upload_images_blob] Stage 2 done - blob uploads: "
|
||||
f"{len(blob_upload_results)}, skipped: {len(skipped_files)}, "
|
||||
f"elapsed: {(stage2_time - stage1_time)*1000:.1f}ms")
|
||||
|
||||
# ========== Stage 3: DB 저장 (새 세션으로 빠르게 처리) ==========
|
||||
print("[upload_images_blob] Stage 3 starting - DB save...")
|
||||
logger.info("[upload_images_blob] Stage 3 starting - DB save...")
|
||||
result_images: list[ImageUploadResultItem] = []
|
||||
img_order = 0
|
||||
|
||||
|
|
@ -769,13 +755,13 @@ async def upload_images_blob(
|
|||
|
||||
await session.commit()
|
||||
stage3_time = time.perf_counter()
|
||||
print(f"[upload_images_blob] Stage 3 done - "
|
||||
logger.info(f"[upload_images_blob] Stage 3 done - "
|
||||
f"saved: {len(result_images)}, "
|
||||
f"elapsed: {(stage3_time - stage2_time)*1000:.1f}ms")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[upload_images_blob] DB Error - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.exception("[upload_images_blob] DB 상세 오류:")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="이미지 저장 중 데이터베이스 오류가 발생했습니다.",
|
||||
|
|
@ -783,7 +769,7 @@ async def upload_images_blob(
|
|||
except Exception as e:
|
||||
logger.error(f"[upload_images_blob] Stage 3 EXCEPTION - "
|
||||
f"task_id: {task_id}, error: {type(e).__name__}: {e}")
|
||||
traceback.print_exc()
|
||||
logger.exception("[upload_images_blob] Stage 3 상세 오류:")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="이미지 업로드 중 오류가 발생했습니다.",
|
||||
|
|
@ -793,7 +779,7 @@ async def upload_images_blob(
|
|||
image_urls = [img.img_url for img in result_images]
|
||||
|
||||
total_time = time.perf_counter() - request_start
|
||||
print(f"[upload_images_blob] SUCCESS - task_id: {task_id}, "
|
||||
logger.info(f"[upload_images_blob] SUCCESS - task_id: {task_id}, "
|
||||
f"total: {saved_count}, total_time: {total_time*1000:.1f}ms")
|
||||
|
||||
return ImageUploadResponse(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import pytest
|
|||
from sqlalchemy import text
|
||||
|
||||
from app.database.session import AsyncSessionLocal, engine
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
# 로거 설정
|
||||
logger = get_logger("test_db")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -27,4 +31,4 @@ async def test_database_version():
|
|||
result = await session.execute(text("SELECT VERSION()"))
|
||||
version = result.scalar()
|
||||
assert version is not None
|
||||
print(f"MySQL Version: {version}")
|
||||
logger.info(f"MySQL Version: {version}")
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ from fastapi import UploadFile
|
|||
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
|
||||
MEDIA_ROOT = Path("media")
|
||||
|
||||
|
||||
async def save_upload_file(file: UploadFile, save_path: Path) -> None:
|
||||
"""업로드 파일을 지정된 경로에 저장"""
|
||||
|
|
|
|||
|
|
@ -41,8 +41,12 @@ from app.lyric.schemas.lyric import (
|
|||
)
|
||||
from app.lyric.worker.lyric_task import generate_lyric_background
|
||||
from app.utils.chatgpt_prompt import ChatgptService
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.pagination import PaginatedResponse, get_paginated
|
||||
|
||||
# 로거 설정
|
||||
logger = get_logger("lyric")
|
||||
|
||||
router = APIRouter(prefix="/lyric", tags=["lyric"])
|
||||
|
||||
|
||||
|
|
@ -74,7 +78,7 @@ async def get_lyric_status_by_task_id(
|
|||
if status_info.status == "completed":
|
||||
# 완료 처리
|
||||
"""
|
||||
print(f"[get_lyric_status_by_task_id] START - task_id: {task_id}")
|
||||
logger.info(f"[get_lyric_status_by_task_id] START - task_id: {task_id}")
|
||||
result = await session.execute(
|
||||
select(Lyric)
|
||||
.where(Lyric.task_id == task_id)
|
||||
|
|
@ -84,7 +88,7 @@ async def get_lyric_status_by_task_id(
|
|||
lyric = result.scalar_one_or_none()
|
||||
|
||||
if not lyric:
|
||||
print(f"[get_lyric_status_by_task_id] NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[get_lyric_status_by_task_id] NOT FOUND - task_id: {task_id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.",
|
||||
|
|
@ -96,7 +100,7 @@ async def get_lyric_status_by_task_id(
|
|||
"failed": "가사 생성에 실패했습니다.",
|
||||
}
|
||||
|
||||
print(
|
||||
logger.info(
|
||||
f"[get_lyric_status_by_task_id] SUCCESS - task_id: {task_id}, status: {lyric.status}"
|
||||
)
|
||||
return LyricStatusResponse(
|
||||
|
|
@ -127,7 +131,7 @@ async def get_lyric_by_task_id(
|
|||
|
||||
lyric = await get_lyric_by_task_id(session, task_id)
|
||||
"""
|
||||
print(f"[get_lyric_by_task_id] START - task_id: {task_id}")
|
||||
logger.info(f"[get_lyric_by_task_id] START - task_id: {task_id}")
|
||||
result = await session.execute(
|
||||
select(Lyric)
|
||||
.where(Lyric.task_id == task_id)
|
||||
|
|
@ -137,13 +141,13 @@ async def get_lyric_by_task_id(
|
|||
lyric = result.scalar_one_or_none()
|
||||
|
||||
if not lyric:
|
||||
print(f"[get_lyric_by_task_id] NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[get_lyric_by_task_id] NOT FOUND - task_id: {task_id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.",
|
||||
)
|
||||
|
||||
print(f"[get_lyric_by_task_id] SUCCESS - task_id: {task_id}, lyric_id: {lyric.id}")
|
||||
logger.info(f"[get_lyric_by_task_id] SUCCESS - task_id: {task_id}, lyric_id: {lyric.id}")
|
||||
return LyricDetailResponse(
|
||||
id=lyric.id,
|
||||
task_id=lyric.task_id,
|
||||
|
|
@ -224,8 +228,8 @@ async def generate_lyric(
|
|||
request_start = time.perf_counter()
|
||||
task_id = request_body.task_id
|
||||
|
||||
print(f"[generate_lyric] ========== START ==========")
|
||||
print(
|
||||
logger.info(f"[generate_lyric] ========== START ==========")
|
||||
logger.info(
|
||||
f"[generate_lyric] task_id: {task_id}, "
|
||||
f"customer_name: {request_body.customer_name}, "
|
||||
f"region: {request_body.region}"
|
||||
|
|
@ -234,7 +238,7 @@ async def generate_lyric(
|
|||
try:
|
||||
# ========== Step 1: ChatGPT 서비스 초기화 및 프롬프트 생성 ==========
|
||||
step1_start = time.perf_counter()
|
||||
print(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...")
|
||||
logger.debug(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...")
|
||||
|
||||
service = ChatgptService(
|
||||
customer_name=request_body.customer_name,
|
||||
|
|
@ -245,11 +249,11 @@ async def generate_lyric(
|
|||
prompt = service.build_lyrics_prompt()
|
||||
|
||||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||
print(f"[generate_lyric] Step 1 완료 - 프롬프트 {len(prompt)}자 ({step1_elapsed:.1f}ms)")
|
||||
logger.debug(f"[generate_lyric] Step 1 완료 - 프롬프트 {len(prompt)}자 ({step1_elapsed:.1f}ms)")
|
||||
|
||||
# ========== Step 2: Project 테이블에 데이터 저장 ==========
|
||||
step2_start = time.perf_counter()
|
||||
print(f"[generate_lyric] Step 2: Project 저장...")
|
||||
logger.debug(f"[generate_lyric] Step 2: Project 저장...")
|
||||
|
||||
project = Project(
|
||||
store_name=request_body.customer_name,
|
||||
|
|
@ -263,11 +267,11 @@ async def generate_lyric(
|
|||
await session.refresh(project)
|
||||
|
||||
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
||||
print(f"[generate_lyric] Step 2 완료 - project_id: {project.id} ({step2_elapsed:.1f}ms)")
|
||||
logger.debug(f"[generate_lyric] Step 2 완료 - project_id: {project.id} ({step2_elapsed:.1f}ms)")
|
||||
|
||||
# ========== Step 3: Lyric 테이블에 데이터 저장 ==========
|
||||
step3_start = time.perf_counter()
|
||||
print(f"[generate_lyric] Step 3: Lyric 저장 (processing)...")
|
||||
logger.debug(f"[generate_lyric] Step 3: Lyric 저장 (processing)...")
|
||||
|
||||
lyric = Lyric(
|
||||
project_id=project.id,
|
||||
|
|
@ -282,11 +286,11 @@ async def generate_lyric(
|
|||
await session.refresh(lyric)
|
||||
|
||||
step3_elapsed = (time.perf_counter() - step3_start) * 1000
|
||||
print(f"[generate_lyric] Step 3 완료 - lyric_id: {lyric.id} ({step3_elapsed:.1f}ms)")
|
||||
logger.debug(f"[generate_lyric] Step 3 완료 - lyric_id: {lyric.id} ({step3_elapsed:.1f}ms)")
|
||||
|
||||
# ========== Step 4: 백그라운드 태스크 스케줄링 ==========
|
||||
step4_start = time.perf_counter()
|
||||
print(f"[generate_lyric] Step 4: 백그라운드 태스크 스케줄링...")
|
||||
logger.debug(f"[generate_lyric] Step 4: 백그라운드 태스크 스케줄링...")
|
||||
|
||||
background_tasks.add_task(
|
||||
generate_lyric_background,
|
||||
|
|
@ -296,17 +300,17 @@ async def generate_lyric(
|
|||
)
|
||||
|
||||
step4_elapsed = (time.perf_counter() - step4_start) * 1000
|
||||
print(f"[generate_lyric] Step 4 완료 ({step4_elapsed:.1f}ms)")
|
||||
logger.debug(f"[generate_lyric] Step 4 완료 ({step4_elapsed:.1f}ms)")
|
||||
|
||||
# ========== 완료 ==========
|
||||
total_elapsed = (time.perf_counter() - request_start) * 1000
|
||||
print(f"[generate_lyric] ========== COMPLETE ==========")
|
||||
print(f"[generate_lyric] API 응답 소요시간: {total_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric] - Step 1 (프롬프트 생성): {step1_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric] - Step 2 (Project 저장): {step2_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric] - Step 3 (Lyric 저장): {step3_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric] - Step 4 (태스크 스케줄링): {step4_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric] (GPT API 호출은 백그라운드에서 별도 진행)")
|
||||
logger.info(f"[generate_lyric] ========== COMPLETE ==========")
|
||||
logger.info(f"[generate_lyric] API 응답 소요시간: {total_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric] - Step 1 (프롬프트 생성): {step1_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric] - Step 2 (Project 저장): {step2_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric] - Step 3 (Lyric 저장): {step3_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric] - Step 4 (태스크 스케줄링): {step4_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric] (GPT API 호출은 백그라운드에서 별도 진행)")
|
||||
|
||||
# 5. 즉시 응답 반환
|
||||
return GenerateLyricResponse(
|
||||
|
|
@ -319,7 +323,7 @@ async def generate_lyric(
|
|||
|
||||
except Exception as e:
|
||||
elapsed = (time.perf_counter() - request_start) * 1000
|
||||
print(f"[generate_lyric] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)")
|
||||
logger.error(f"[generate_lyric] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)")
|
||||
await session.rollback()
|
||||
return GenerateLyricResponse(
|
||||
success=False,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ from app.lyric.schemas.lyrics_schema import (
|
|||
StoreData,
|
||||
)
|
||||
from app.utils.chatgpt_prompt import chatgpt_api
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
# 로거 설정
|
||||
logger = get_logger("lyric")
|
||||
|
||||
|
||||
async def get_store_info(conn: Connection) -> List[StoreData]:
|
||||
|
|
@ -38,13 +42,13 @@ async def get_store_info(conn: Connection) -> List[StoreData]:
|
|||
result.close()
|
||||
return all_store_info
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"Database error in get_store_info: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_store_info: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -69,13 +73,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
|
|||
result.close()
|
||||
return all_attribute
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"Database error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -100,13 +104,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
|
|||
result.close()
|
||||
return all_attribute
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"Database error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -132,13 +136,13 @@ async def get_sample_song(conn: Connection) -> List[SongSampleData]:
|
|||
result.close()
|
||||
return all_sample_song
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"Database error in get_sample_song: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_sample_song: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -162,13 +166,13 @@ async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
|
|||
result.close()
|
||||
return all_prompt_template
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"Database error in get_prompt_template: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_prompt_template: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -192,13 +196,13 @@ async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
|
|||
result.close()
|
||||
return all_prompt_template
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"Database error in get_song_result: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_song_result: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -210,11 +214,11 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
# 1. Form 데이터 파싱
|
||||
form_data = await SongFormData.from_form(request)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Store ID: {form_data.store_id}")
|
||||
print(f"Lyrics IDs: {form_data.lyrics_ids}")
|
||||
print(f"Prompt IDs: {form_data.prompts}")
|
||||
print(f"{'=' * 60}\n")
|
||||
logger.info(f"{'=' * 60}")
|
||||
logger.info(f"Store ID: {form_data.store_id}")
|
||||
logger.info(f"Lyrics IDs: {form_data.lyrics_ids}")
|
||||
logger.info(f"Prompt IDs: {form_data.prompts}")
|
||||
logger.info(f"{'=' * 60}")
|
||||
|
||||
# 2. Store 정보 조회
|
||||
store_query = """
|
||||
|
|
@ -243,7 +247,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
store_info = all_store_info[0]
|
||||
print(f"Store: {store_info.store_name}")
|
||||
logger.info(f"Store: {store_info.store_name}")
|
||||
|
||||
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||
|
||||
|
|
@ -251,7 +255,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
combined_sample_song = None
|
||||
|
||||
if form_data.lyrics_ids:
|
||||
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
|
||||
lyrics_query = """
|
||||
SELECT sample_song FROM song_sample
|
||||
|
|
@ -270,11 +274,11 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("선택된 lyrics가 없습니다")
|
||||
logger.info("선택된 lyrics가 없습니다")
|
||||
|
||||
# 5. 템플릿 가져오기
|
||||
if not form_data.prompts:
|
||||
|
|
@ -283,7 +287,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
detail="프롬프트 ID가 필요합니다",
|
||||
)
|
||||
|
||||
print("템플릿 가져오기")
|
||||
logger.info("템플릿 가져오기")
|
||||
|
||||
prompts_query = """
|
||||
SELECT * FROM prompt_template WHERE id=:id;
|
||||
|
|
@ -310,7 +314,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
prompt = prompts_info[0]
|
||||
print(f"Prompt Template: {prompt.prompt}")
|
||||
logger.debug(f"Prompt Template: {prompt.prompt}")
|
||||
|
||||
# ✅ 6. 프롬프트 조합
|
||||
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format(
|
||||
|
|
@ -329,7 +333,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
{combined_sample_song}
|
||||
"""
|
||||
|
||||
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
|
||||
logger.debug(f"[업데이트된 프롬프트]\n{updated_prompt}")
|
||||
|
||||
# 7. 모델에게 요청
|
||||
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
|
||||
|
|
@ -348,13 +352,12 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
||||
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
||||
|
||||
print("=" * 40)
|
||||
print("[translate:form_data.attributes_str:] ", form_data.attributes_str)
|
||||
print("[translate:total_chars_with_space:] ", total_chars_with_space)
|
||||
print("[translate:total_chars_without_space:] ", total_chars_without_space)
|
||||
print("[translate:final_lyrics:]")
|
||||
print(final_lyrics)
|
||||
print("=" * 40)
|
||||
logger.debug("=" * 40)
|
||||
logger.debug(f"[translate:form_data.attributes_str:] {form_data.attributes_str}")
|
||||
logger.debug(f"[translate:total_chars_with_space:] {total_chars_with_space}")
|
||||
logger.debug(f"[translate:total_chars_without_space:] {total_chars_without_space}")
|
||||
logger.debug(f"[translate:final_lyrics:]\n{final_lyrics}")
|
||||
logger.debug("=" * 40)
|
||||
|
||||
# 8. DB 저장
|
||||
insert_query = """
|
||||
|
|
@ -396,9 +399,9 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
await conn.execute(text(insert_query), insert_params)
|
||||
await conn.commit()
|
||||
|
||||
print("결과 저장 완료")
|
||||
logger.info("결과 저장 완료")
|
||||
|
||||
print("\n전체 결과 조회 중...")
|
||||
logger.info("전체 결과 조회 중...")
|
||||
|
||||
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||
select_query = """
|
||||
|
|
@ -430,26 +433,20 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
@ -490,25 +487,19 @@ async def get_song_result(conn: Connection): # 반환 타입 수정
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
except HTTPException: # HTTPException은 그대로 raise
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
@ -520,9 +511,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
# 1. Form 데이터 파싱
|
||||
form_data = await SongFormData.from_form(request)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Store ID: {form_data.store_id}")
|
||||
print(f"{'=' * 60}\n")
|
||||
logger.info(f"{'=' * 60}")
|
||||
logger.info(f"Store ID: {form_data.store_id}")
|
||||
logger.info(f"{'=' * 60}")
|
||||
|
||||
# 2. Store 정보 조회
|
||||
store_query = """
|
||||
|
|
@ -551,7 +542,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
store_info = all_store_info[0]
|
||||
print(f"Store: {store_info.store_name}")
|
||||
logger.info(f"Store: {store_info.store_name}")
|
||||
|
||||
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||
attribute_query = """
|
||||
|
|
@ -596,13 +587,13 @@ async def make_automation(request: Request, conn: Connection):
|
|||
# 최종 문자열 생성
|
||||
formatted_attributes = "\n".join(formatted_pairs)
|
||||
|
||||
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n")
|
||||
logger.debug(f"[포맷팅된 문자열 속성 정보]\n{formatted_attributes}")
|
||||
else:
|
||||
print("속성 데이터가 없습니다")
|
||||
logger.info("속성 데이터가 없습니다")
|
||||
formatted_attributes = ""
|
||||
|
||||
# 4. 템플릿 가져오기
|
||||
print("템플릿 가져오기 (ID=1)")
|
||||
logger.info("템플릿 가져오기 (ID=1)")
|
||||
|
||||
prompts_query = """
|
||||
SELECT * FROM prompt_template WHERE id=1;
|
||||
|
|
@ -624,7 +615,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
prompt=row[2],
|
||||
)
|
||||
|
||||
print(f"Prompt Template: {prompt.prompt}")
|
||||
logger.debug(f"Prompt Template: {prompt.prompt}")
|
||||
|
||||
# 5. 템플릿 조합
|
||||
|
||||
|
|
@ -635,17 +626,17 @@ async def make_automation(request: Request, conn: Connection):
|
|||
description=store_info.store_info or "",
|
||||
)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("업데이트된 프롬프트")
|
||||
print("=" * 80)
|
||||
print(updated_prompt)
|
||||
print("=" * 80 + "\n")
|
||||
logger.debug("=" * 80)
|
||||
logger.debug("업데이트된 프롬프트")
|
||||
logger.debug("=" * 80)
|
||||
logger.debug(updated_prompt)
|
||||
logger.debug("=" * 80)
|
||||
|
||||
# 4. Sample Song 조회 및 결합
|
||||
combined_sample_song = None
|
||||
|
||||
if form_data.lyrics_ids:
|
||||
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
|
||||
lyrics_query = """
|
||||
SELECT sample_song FROM song_sample
|
||||
|
|
@ -664,14 +655,14 @@ async def make_automation(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("선택된 lyrics가 없습니다")
|
||||
logger.info("선택된 lyrics가 없습니다")
|
||||
|
||||
# 1. song_sample 테이블의 모든 ID 조회
|
||||
print("\n[샘플 가사 랜덤 선택]")
|
||||
logger.info("[샘플 가사 랜덤 선택]")
|
||||
|
||||
all_ids_query = """
|
||||
SELECT id FROM song_sample;
|
||||
|
|
@ -679,7 +670,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
ids_result = await conn.execute(text(all_ids_query))
|
||||
all_ids = [row.id for row in ids_result.fetchall()]
|
||||
|
||||
print(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
||||
logger.info(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
||||
|
||||
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
|
||||
combined_sample_song = None
|
||||
|
|
@ -689,7 +680,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
sample_count = min(3, len(all_ids))
|
||||
selected_ids = random.sample(all_ids, sample_count)
|
||||
|
||||
print(f"랜덤 선택된 ID: {selected_ids}")
|
||||
logger.info(f"랜덤 선택된 ID: {selected_ids}")
|
||||
|
||||
# 3. 선택된 ID로 샘플 가사 조회
|
||||
lyrics_query = """
|
||||
|
|
@ -710,11 +701,11 @@ async def make_automation(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("song_sample 테이블에 데이터가 없습니다")
|
||||
logger.info("song_sample 테이블에 데이터가 없습니다")
|
||||
|
||||
# 5. 프롬프트에 샘플 가사 추가
|
||||
if combined_sample_song:
|
||||
|
|
@ -726,11 +717,11 @@ async def make_automation(request: Request, conn: Connection):
|
|||
|
||||
{combined_sample_song}
|
||||
"""
|
||||
print("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
||||
logger.info("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
||||
else:
|
||||
print("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
||||
logger.info("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
||||
|
||||
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
|
||||
logger.info(f"[최종 프롬프트 길이: {len(updated_prompt)} 자]")
|
||||
|
||||
# 7. 모델에게 요청
|
||||
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
|
||||
|
|
@ -763,10 +754,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
:sample_song, :result_song, NOW()
|
||||
);
|
||||
"""
|
||||
print("\n[insert_params 선택된 속성 확인]")
|
||||
print(f"Categories: {selected_categories}")
|
||||
print(f"Values: {selected_values}")
|
||||
print()
|
||||
logger.debug("[insert_params 선택된 속성 확인]")
|
||||
logger.debug(f"Categories: {selected_categories}")
|
||||
logger.debug(f"Values: {selected_values}")
|
||||
|
||||
# attr_category, attr_value
|
||||
insert_params = {
|
||||
|
|
@ -792,9 +782,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
await conn.execute(text(insert_query), insert_params)
|
||||
await conn.commit()
|
||||
|
||||
print("결과 저장 완료")
|
||||
logger.info("결과 저장 완료")
|
||||
|
||||
print("\n전체 결과 조회 중...")
|
||||
logger.info("전체 결과 조회 중...")
|
||||
|
||||
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||
select_query = """
|
||||
|
|
@ -826,26 +816,20 @@ async def make_automation(request: Request, conn: Connection):
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ Lyric Background Tasks
|
|||
가사 생성 관련 백그라운드 태스크를 정의합니다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from sqlalchemy import select
|
||||
|
|
@ -13,9 +12,10 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||
from app.database.session import BackgroundSessionLocal
|
||||
from app.lyric.models import Lyric
|
||||
from app.utils.chatgpt_prompt import ChatgptService
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger("lyric")
|
||||
|
||||
|
||||
async def _update_lyric_status(
|
||||
|
|
@ -49,20 +49,16 @@ async def _update_lyric_status(
|
|||
lyric.lyric_result = result
|
||||
await session.commit()
|
||||
logger.info(f"[Lyric] Status updated - task_id: {task_id}, status: {status}")
|
||||
print(f"[Lyric] Status updated - task_id: {task_id}, status: {status}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"[Lyric] NOT FOUND in DB - task_id: {task_id}")
|
||||
print(f"[Lyric] NOT FOUND in DB - task_id: {task_id}")
|
||||
return False
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[Lyric] DB Error while updating status - task_id: {task_id}, error: {e}")
|
||||
print(f"[Lyric] DB Error while updating status - task_id: {task_id}, error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"[Lyric] Unexpected error while updating status - task_id: {task_id}, error: {e}")
|
||||
print(f"[Lyric] Unexpected error while updating status - task_id: {task_id}, error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -82,15 +78,15 @@ async def generate_lyric_background(
|
|||
|
||||
task_start = time.perf_counter()
|
||||
logger.info(f"[generate_lyric_background] START - task_id: {task_id}")
|
||||
print(f"[generate_lyric_background] ========== START ==========")
|
||||
print(f"[generate_lyric_background] task_id: {task_id}")
|
||||
print(f"[generate_lyric_background] language: {language}")
|
||||
print(f"[generate_lyric_background] prompt length: {len(prompt)}자")
|
||||
logger.debug(f"[generate_lyric_background] ========== START ==========")
|
||||
logger.debug(f"[generate_lyric_background] task_id: {task_id}")
|
||||
logger.debug(f"[generate_lyric_background] language: {language}")
|
||||
logger.debug(f"[generate_lyric_background] prompt length: {len(prompt)}자")
|
||||
|
||||
try:
|
||||
# ========== Step 1: ChatGPT 서비스 초기화 ==========
|
||||
step1_start = time.perf_counter()
|
||||
print(f"[generate_lyric_background] Step 1: ChatGPT 서비스 초기화...")
|
||||
logger.debug(f"[generate_lyric_background] Step 1: ChatGPT 서비스 초기화...")
|
||||
|
||||
service = ChatgptService(
|
||||
customer_name="", # 프롬프트가 이미 생성되었으므로 빈 값
|
||||
|
|
@ -100,47 +96,42 @@ async def generate_lyric_background(
|
|||
)
|
||||
|
||||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||
print(f"[generate_lyric_background] Step 1 완료 ({step1_elapsed:.1f}ms)")
|
||||
logger.debug(f"[generate_lyric_background] Step 1 완료 ({step1_elapsed:.1f}ms)")
|
||||
|
||||
# ========== Step 2: ChatGPT API 호출 (가사 생성) ==========
|
||||
step2_start = time.perf_counter()
|
||||
logger.info(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작 - task_id: {task_id}")
|
||||
print(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작...")
|
||||
logger.debug(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작...")
|
||||
|
||||
result = await service.generate(prompt=prompt)
|
||||
|
||||
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
||||
logger.info(f"[generate_lyric_background] Step 2 완료 - 응답 {len(result)}자 ({step2_elapsed:.1f}ms)")
|
||||
print(f"[generate_lyric_background] Step 2 완료 - 응답 {len(result)}자 ({step2_elapsed:.1f}ms)")
|
||||
|
||||
# ========== Step 3: DB 상태 업데이트 ==========
|
||||
step3_start = time.perf_counter()
|
||||
print(f"[generate_lyric_background] Step 3: DB 상태 업데이트...")
|
||||
logger.debug(f"[generate_lyric_background] Step 3: DB 상태 업데이트...")
|
||||
|
||||
await _update_lyric_status(task_id, "completed", result)
|
||||
|
||||
step3_elapsed = (time.perf_counter() - step3_start) * 1000
|
||||
print(f"[generate_lyric_background] Step 3 완료 ({step3_elapsed:.1f}ms)")
|
||||
logger.debug(f"[generate_lyric_background] Step 3 완료 ({step3_elapsed:.1f}ms)")
|
||||
|
||||
# ========== 완료 ==========
|
||||
total_elapsed = (time.perf_counter() - task_start) * 1000
|
||||
logger.info(f"[generate_lyric_background] SUCCESS - task_id: {task_id}, 총 소요시간: {total_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric_background] ========== SUCCESS ==========")
|
||||
print(f"[generate_lyric_background] 총 소요시간: {total_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric_background] - Step 1 (서비스 초기화): {step1_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric_background] - Step 2 (GPT API 호출): {step2_elapsed:.1f}ms")
|
||||
print(f"[generate_lyric_background] - Step 3 (DB 업데이트): {step3_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric_background] ========== SUCCESS ==========")
|
||||
logger.debug(f"[generate_lyric_background] 총 소요시간: {total_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric_background] - Step 1 (서비스 초기화): {step1_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric_background] - Step 2 (GPT API 호출): {step2_elapsed:.1f}ms")
|
||||
logger.debug(f"[generate_lyric_background] - Step 3 (DB 업데이트): {step3_elapsed:.1f}ms")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
elapsed = (time.perf_counter() - task_start) * 1000
|
||||
logger.error(f"[generate_lyric_background] DB ERROR - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)")
|
||||
print(f"[generate_lyric_background] DB ERROR - {e} ({elapsed:.1f}ms)")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[generate_lyric_background] DB ERROR - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)", exc_info=True)
|
||||
await _update_lyric_status(task_id, "failed", f"Database Error: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
elapsed = (time.perf_counter() - task_start) * 1000
|
||||
logger.error(f"[generate_lyric_background] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)")
|
||||
print(f"[generate_lyric_background] EXCEPTION - {e} ({elapsed:.1f}ms)")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[generate_lyric_background] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)", exc_info=True)
|
||||
await _update_lyric_status(task_id, "failed", f"Error: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -32,9 +32,11 @@ from app.song.schemas.song_schema import (
|
|||
PollingSongResponse,
|
||||
SongListItem,
|
||||
)
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.pagination import PaginatedResponse
|
||||
from app.utils.suno import SunoService
|
||||
|
||||
logger = get_logger("song")
|
||||
|
||||
router = APIRouter(prefix="/song", tags=["song"])
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ async def generate_song(
|
|||
from app.database.session import AsyncSessionLocal
|
||||
|
||||
request_start = time.perf_counter()
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_song] START - task_id: {task_id}, "
|
||||
f"genre: {request_body.genre}, language: {request_body.language}"
|
||||
)
|
||||
|
|
@ -124,7 +126,7 @@ async def generate_song(
|
|||
project = project_result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
print(f"[generate_song] Project NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[generate_song] Project NOT FOUND - task_id: {task_id}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.",
|
||||
|
|
@ -141,7 +143,7 @@ async def generate_song(
|
|||
lyric = lyric_result.scalar_one_or_none()
|
||||
|
||||
if not lyric:
|
||||
print(f"[generate_song] Lyric NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[generate_song] Lyric NOT FOUND - task_id: {task_id}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 Lyric을 찾을 수 없습니다.",
|
||||
|
|
@ -149,7 +151,7 @@ async def generate_song(
|
|||
lyric_id = lyric.id
|
||||
|
||||
query_time = time.perf_counter()
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_song] Queries completed - task_id: {task_id}, "
|
||||
f"project_id: {project_id}, lyric_id: {lyric_id}, "
|
||||
f"elapsed: {(query_time - request_start)*1000:.1f}ms"
|
||||
|
|
@ -174,7 +176,7 @@ async def generate_song(
|
|||
song_id = song.id
|
||||
|
||||
stage1_time = time.perf_counter()
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_song] Stage 1 DONE - Song saved - "
|
||||
f"task_id: {task_id}, song_id: {song_id}, "
|
||||
f"elapsed: {(stage1_time - request_start)*1000:.1f}ms"
|
||||
|
|
@ -184,7 +186,7 @@ async def generate_song(
|
|||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(
|
||||
logger.error(
|
||||
f"[generate_song] Stage 1 EXCEPTION - "
|
||||
f"task_id: {task_id}, error: {type(e).__name__}: {e}"
|
||||
)
|
||||
|
|
@ -203,7 +205,7 @@ async def generate_song(
|
|||
suno_task_id: str | None = None
|
||||
|
||||
try:
|
||||
print(f"[generate_song] Stage 2 START - Suno API - task_id: {task_id}")
|
||||
logger.info(f"[generate_song] Stage 2 START - Suno API - task_id: {task_id}")
|
||||
suno_service = SunoService()
|
||||
suno_task_id = await suno_service.generate(
|
||||
prompt=request_body.lyrics,
|
||||
|
|
@ -211,14 +213,14 @@ async def generate_song(
|
|||
)
|
||||
|
||||
stage2_time = time.perf_counter()
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_song] Stage 2 DONE - task_id: {task_id}, "
|
||||
f"suno_task_id: {suno_task_id}, "
|
||||
f"elapsed: {(stage2_time - stage2_start)*1000:.1f}ms"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(
|
||||
logger.error(
|
||||
f"[generate_song] Stage 2 EXCEPTION - Suno API failed - "
|
||||
f"task_id: {task_id}, error: {type(e).__name__}: {e}"
|
||||
)
|
||||
|
|
@ -244,7 +246,7 @@ async def generate_song(
|
|||
# 3단계: suno_task_id 업데이트 (새 세션으로 빠르게 처리)
|
||||
# ==========================================================================
|
||||
stage3_start = time.perf_counter()
|
||||
print(f"[generate_song] Stage 3 START - DB update - task_id: {task_id}")
|
||||
logger.info(f"[generate_song] Stage 3 START - DB update - task_id: {task_id}")
|
||||
|
||||
try:
|
||||
async with AsyncSessionLocal() as update_session:
|
||||
|
|
@ -258,11 +260,11 @@ async def generate_song(
|
|||
|
||||
stage3_time = time.perf_counter()
|
||||
total_time = stage3_time - request_start
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_song] Stage 3 DONE - task_id: {task_id}, "
|
||||
f"elapsed: {(stage3_time - stage3_start)*1000:.1f}ms"
|
||||
)
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_song] SUCCESS - task_id: {task_id}, "
|
||||
f"suno_task_id: {suno_task_id}, "
|
||||
f"total_time: {total_time*1000:.1f}ms"
|
||||
|
|
@ -277,7 +279,7 @@ async def generate_song(
|
|||
)
|
||||
|
||||
except Exception as e:
|
||||
print(
|
||||
logger.error(
|
||||
f"[generate_song] Stage 3 EXCEPTION - "
|
||||
f"task_id: {task_id}, error: {type(e).__name__}: {e}"
|
||||
)
|
||||
|
|
@ -341,12 +343,12 @@ async def get_song_status(
|
|||
Azure Blob Storage에 업로드한 뒤 Song 테이블의 status를 completed로,
|
||||
song_result_url을 Blob URL로 업데이트합니다.
|
||||
"""
|
||||
print(f"[get_song_status] START - suno_task_id: {suno_task_id}")
|
||||
logger.info(f"[get_song_status] START - suno_task_id: {suno_task_id}")
|
||||
try:
|
||||
suno_service = SunoService()
|
||||
result = await suno_service.get_task_status(suno_task_id)
|
||||
parsed_response = suno_service.parse_status_response(result)
|
||||
print(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 - suno_task_id: {suno_task_id}, status: {parsed_response.status}")
|
||||
|
||||
# SUCCESS 상태인 경우 첫 번째 클립 정보를 DB에 직접 저장
|
||||
if parsed_response.status == "SUCCESS" and parsed_response.clips:
|
||||
|
|
@ -354,7 +356,7 @@ async def get_song_status(
|
|||
first_clip = parsed_response.clips[0]
|
||||
audio_url = first_clip.audio_url
|
||||
clip_duration = first_clip.duration
|
||||
print(f"[get_song_status] Using first clip - id: {first_clip.id}, audio_url: {audio_url}, duration: {clip_duration}")
|
||||
logger.debug(f"[get_song_status] Using first clip - id: {first_clip.id}, audio_url: {audio_url}, duration: {clip_duration}")
|
||||
|
||||
if audio_url:
|
||||
# suno_task_id로 Song 조회
|
||||
|
|
@ -373,17 +375,17 @@ async def get_song_status(
|
|||
if clip_duration is not None:
|
||||
song.duration = clip_duration
|
||||
await session.commit()
|
||||
print(f"[get_song_status] Song updated - suno_task_id: {suno_task_id}, status: completed, song_result_url: {audio_url}, duration: {clip_duration}")
|
||||
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":
|
||||
print(f"[get_song_status] SKIPPED - Song already completed, suno_task_id: {suno_task_id}")
|
||||
logger.info(f"[get_song_status] SKIPPED - Song already completed, suno_task_id: {suno_task_id}")
|
||||
|
||||
print(f"[get_song_status] SUCCESS - suno_task_id: {suno_task_id}")
|
||||
logger.info(f"[get_song_status] SUCCESS - suno_task_id: {suno_task_id}")
|
||||
return parsed_response
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
print(f"[get_song_status] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}")
|
||||
logger.error(f"[get_song_status] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}")
|
||||
return PollingSongResponse(
|
||||
success=False,
|
||||
status="error",
|
||||
|
|
@ -438,7 +440,7 @@ async def download_song(
|
|||
session: AsyncSession = Depends(get_session),
|
||||
) -> DownloadSongResponse:
|
||||
"""task_id로 Song 상태를 polling하고 completed 시 Project 정보와 노래 URL을 반환합니다."""
|
||||
print(f"[download_song] START - task_id: {task_id}")
|
||||
logger.info(f"[download_song] START - task_id: {task_id}")
|
||||
try:
|
||||
# task_id로 Song 조회 (여러 개 있을 경우 가장 최근 것 선택)
|
||||
song_result = await session.execute(
|
||||
|
|
@ -450,7 +452,7 @@ async def download_song(
|
|||
song = song_result.scalar_one_or_none()
|
||||
|
||||
if not song:
|
||||
print(f"[download_song] Song NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[download_song] Song NOT FOUND - task_id: {task_id}")
|
||||
return DownloadSongResponse(
|
||||
success=False,
|
||||
status="not_found",
|
||||
|
|
@ -458,11 +460,11 @@ async def download_song(
|
|||
error_message="Song not found",
|
||||
)
|
||||
|
||||
print(f"[download_song] Song found - task_id: {task_id}, status: {song.status}")
|
||||
logger.info(f"[download_song] Song found - task_id: {task_id}, status: {song.status}")
|
||||
|
||||
# processing 상태인 경우
|
||||
if song.status == "processing":
|
||||
print(f"[download_song] PROCESSING - task_id: {task_id}")
|
||||
logger.info(f"[download_song] PROCESSING - task_id: {task_id}")
|
||||
return DownloadSongResponse(
|
||||
success=True,
|
||||
status="processing",
|
||||
|
|
@ -472,7 +474,7 @@ async def download_song(
|
|||
|
||||
# failed 상태인 경우
|
||||
if song.status == "failed":
|
||||
print(f"[download_song] FAILED - task_id: {task_id}")
|
||||
logger.warning(f"[download_song] FAILED - task_id: {task_id}")
|
||||
return DownloadSongResponse(
|
||||
success=False,
|
||||
status="failed",
|
||||
|
|
@ -487,7 +489,7 @@ async def download_song(
|
|||
)
|
||||
project = project_result.scalar_one_or_none()
|
||||
|
||||
print(f"[download_song] COMPLETED - task_id: {task_id}, song_result_url: {song.song_result_url}")
|
||||
logger.info(f"[download_song] COMPLETED - task_id: {task_id}, song_result_url: {song.song_result_url}")
|
||||
return DownloadSongResponse(
|
||||
success=True,
|
||||
status="completed",
|
||||
|
|
@ -502,7 +504,7 @@ async def download_song(
|
|||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[download_song] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
logger.error(f"[download_song] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
return DownloadSongResponse(
|
||||
success=False,
|
||||
status="error",
|
||||
|
|
@ -550,7 +552,7 @@ async def get_songs(
|
|||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
) -> PaginatedResponse[SongListItem]:
|
||||
"""완료된 노래 목록을 페이지네이션하여 반환합니다."""
|
||||
print(f"[get_songs] START - page: {pagination.page}, page_size: {pagination.page_size}")
|
||||
logger.info(f"[get_songs] START - page: {pagination.page}, page_size: {pagination.page_size}")
|
||||
try:
|
||||
offset = (pagination.page - 1) * pagination.page_size
|
||||
|
||||
|
|
@ -622,14 +624,14 @@ async def get_songs(
|
|||
page_size=pagination.page_size,
|
||||
)
|
||||
|
||||
print(
|
||||
logger.info(
|
||||
f"[get_songs] SUCCESS - total: {total}, page: {pagination.page}, "
|
||||
f"page_size: {pagination.page_size}, items_count: {len(items)}"
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
print(f"[get_songs] EXCEPTION - error: {e}")
|
||||
logger.error(f"[get_songs] EXCEPTION - error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"노래 목록 조회에 실패했습니다: {str(e)}",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from fastapi.exceptions import HTTPException
|
|||
from sqlalchemy import Connection, text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from app.lyrics.schemas.lyrics_schema import (
|
||||
AttributeData,
|
||||
PromptTemplateData,
|
||||
|
|
@ -15,6 +16,8 @@ from app.lyrics.schemas.lyrics_schema import (
|
|||
)
|
||||
from app.utils.chatgpt_prompt import chatgpt_api
|
||||
|
||||
logger = get_logger("song")
|
||||
|
||||
|
||||
async def get_store_info(conn: Connection) -> List[StoreData]:
|
||||
try:
|
||||
|
|
@ -38,13 +41,13 @@ async def get_store_info(conn: Connection) -> List[StoreData]:
|
|||
result.close()
|
||||
return all_store_info
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemyError in get_store_info: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_store_info: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -69,13 +72,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
|
|||
result.close()
|
||||
return all_attribute
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemyError in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -100,13 +103,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
|
|||
result.close()
|
||||
return all_attribute
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemyError in get_attribute (duplicate): {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_attribute (duplicate): {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -132,13 +135,13 @@ async def get_sample_song(conn: Connection) -> List[SongSampleData]:
|
|||
result.close()
|
||||
return all_sample_song
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemyError in get_sample_song: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_sample_song: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -162,13 +165,13 @@ async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
|
|||
result.close()
|
||||
return all_prompt_template
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemyError in get_prompt_template: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_prompt_template: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -192,13 +195,13 @@ async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
|
|||
result.close()
|
||||
return all_prompt_template
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemyError in get_song_result (prompt_template): {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_song_result (prompt_template): {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -210,11 +213,11 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
# 1. Form 데이터 파싱
|
||||
form_data = await SongFormData.from_form(request)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Store ID: {form_data.store_id}")
|
||||
print(f"Lyrics IDs: {form_data.lyrics_ids}")
|
||||
print(f"Prompt IDs: {form_data.prompts}")
|
||||
print(f"{'=' * 60}\n")
|
||||
logger.info(f"{'=' * 60}")
|
||||
logger.info(f"Store ID: {form_data.store_id}")
|
||||
logger.info(f"Lyrics IDs: {form_data.lyrics_ids}")
|
||||
logger.info(f"Prompt IDs: {form_data.prompts}")
|
||||
logger.info(f"{'=' * 60}")
|
||||
|
||||
# 2. Store 정보 조회
|
||||
store_query = """
|
||||
|
|
@ -243,7 +246,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
store_info = all_store_info[0]
|
||||
print(f"Store: {store_info.store_name}")
|
||||
logger.info(f"Store: {store_info.store_name}")
|
||||
|
||||
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||
|
||||
|
|
@ -251,7 +254,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
combined_sample_song = None
|
||||
|
||||
if form_data.lyrics_ids:
|
||||
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
|
||||
lyrics_query = """
|
||||
SELECT sample_song FROM song_sample
|
||||
|
|
@ -270,11 +273,11 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("선택된 lyrics가 없습니다")
|
||||
logger.info("선택된 lyrics가 없습니다")
|
||||
|
||||
# 5. 템플릿 가져오기
|
||||
if not form_data.prompts:
|
||||
|
|
@ -283,7 +286,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
detail="프롬프트 ID가 필요합니다",
|
||||
)
|
||||
|
||||
print("템플릿 가져오기")
|
||||
logger.info("템플릿 가져오기")
|
||||
|
||||
prompts_query = """
|
||||
SELECT * FROM prompt_template WHERE id=:id;
|
||||
|
|
@ -310,7 +313,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
prompt = prompts_info[0]
|
||||
print(f"Prompt Template: {prompt.prompt}")
|
||||
logger.debug(f"Prompt Template: {prompt.prompt}")
|
||||
|
||||
# ✅ 6. 프롬프트 조합
|
||||
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format(
|
||||
|
|
@ -329,7 +332,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
{combined_sample_song}
|
||||
"""
|
||||
|
||||
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
|
||||
logger.debug(f"[업데이트된 프롬프트]\n{updated_prompt}")
|
||||
|
||||
# 7. 모델에게 요청
|
||||
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
|
||||
|
|
@ -348,13 +351,12 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
||||
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
||||
|
||||
print("=" * 40)
|
||||
print("[translate:form_data.attributes_str:] ", form_data.attributes_str)
|
||||
print("[translate:total_chars_with_space:] ", total_chars_with_space)
|
||||
print("[translate:total_chars_without_space:] ", total_chars_without_space)
|
||||
print("[translate:final_lyrics:]")
|
||||
print(final_lyrics)
|
||||
print("=" * 40)
|
||||
logger.debug("=" * 40)
|
||||
logger.debug(f"[translate:form_data.attributes_str:] {form_data.attributes_str}")
|
||||
logger.debug(f"[translate:total_chars_with_space:] {total_chars_with_space}")
|
||||
logger.debug(f"[translate:total_chars_without_space:] {total_chars_without_space}")
|
||||
logger.debug(f"[translate:final_lyrics:]\n{final_lyrics}")
|
||||
logger.debug("=" * 40)
|
||||
|
||||
# 8. DB 저장
|
||||
insert_query = """
|
||||
|
|
@ -396,9 +398,9 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
await conn.execute(text(insert_query), insert_params)
|
||||
await conn.commit()
|
||||
|
||||
print("결과 저장 완료")
|
||||
logger.info("make_song_result 결과 저장 완료")
|
||||
|
||||
print("\n전체 결과 조회 중...")
|
||||
logger.info("make_song_result 전체 결과 조회 중...")
|
||||
|
||||
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||
select_query = """
|
||||
|
|
@ -430,26 +432,20 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"make_song_result 전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"make_song_result Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"make_song_result Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
@ -490,25 +486,19 @@ async def get_song_result(conn: Connection): # 반환 타입 수정
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"get_song_result 전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
except HTTPException: # HTTPException은 그대로 raise
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"get_song_result Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"get_song_result Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
@ -520,9 +510,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
# 1. Form 데이터 파싱
|
||||
form_data = await SongFormData.from_form(request)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Store ID: {form_data.store_id}")
|
||||
print(f"{'=' * 60}\n")
|
||||
logger.info(f"{'=' * 60}")
|
||||
logger.info(f"make_automation Store ID: {form_data.store_id}")
|
||||
logger.info(f"{'=' * 60}")
|
||||
|
||||
# 2. Store 정보 조회
|
||||
store_query = """
|
||||
|
|
@ -551,7 +541,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
store_info = all_store_info[0]
|
||||
print(f"Store: {store_info.store_name}")
|
||||
logger.info(f"make_automation Store: {store_info.store_name}")
|
||||
|
||||
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||
attribute_query = """
|
||||
|
|
@ -596,13 +586,13 @@ async def make_automation(request: Request, conn: Connection):
|
|||
# 최종 문자열 생성
|
||||
formatted_attributes = "\n".join(formatted_pairs)
|
||||
|
||||
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n")
|
||||
logger.debug(f"[포맷팅된 문자열 속성 정보]\n{formatted_attributes}")
|
||||
else:
|
||||
print("속성 데이터가 없습니다")
|
||||
logger.info("속성 데이터가 없습니다")
|
||||
formatted_attributes = ""
|
||||
|
||||
# 4. 템플릿 가져오기
|
||||
print("템플릿 가져오기 (ID=1)")
|
||||
logger.info("템플릿 가져오기 (ID=1)")
|
||||
|
||||
prompts_query = """
|
||||
SELECT * FROM prompt_template WHERE id=1;
|
||||
|
|
@ -624,7 +614,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
prompt=row[2],
|
||||
)
|
||||
|
||||
print(f"Prompt Template: {prompt.prompt}")
|
||||
logger.debug(f"Prompt Template: {prompt.prompt}")
|
||||
|
||||
# 5. 템플릿 조합
|
||||
|
||||
|
|
@ -635,17 +625,17 @@ async def make_automation(request: Request, conn: Connection):
|
|||
description=store_info.store_info or "",
|
||||
)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("업데이트된 프롬프트")
|
||||
print("=" * 80)
|
||||
print(updated_prompt)
|
||||
print("=" * 80 + "\n")
|
||||
logger.debug("=" * 80)
|
||||
logger.debug("업데이트된 프롬프트")
|
||||
logger.debug("=" * 80)
|
||||
logger.debug(updated_prompt)
|
||||
logger.debug("=" * 80)
|
||||
|
||||
# 4. Sample Song 조회 및 결합
|
||||
combined_sample_song = None
|
||||
|
||||
if form_data.lyrics_ids:
|
||||
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
|
||||
lyrics_query = """
|
||||
SELECT sample_song FROM song_sample
|
||||
|
|
@ -664,14 +654,14 @@ async def make_automation(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("선택된 lyrics가 없습니다")
|
||||
logger.info("선택된 lyrics가 없습니다")
|
||||
|
||||
# 1. song_sample 테이블의 모든 ID 조회
|
||||
print("\n[샘플 가사 랜덤 선택]")
|
||||
logger.info("[샘플 가사 랜덤 선택]")
|
||||
|
||||
all_ids_query = """
|
||||
SELECT id FROM song_sample;
|
||||
|
|
@ -679,7 +669,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
ids_result = await conn.execute(text(all_ids_query))
|
||||
all_ids = [row.id for row in ids_result.fetchall()]
|
||||
|
||||
print(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
||||
logger.info(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
||||
|
||||
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
|
||||
combined_sample_song = None
|
||||
|
|
@ -689,7 +679,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
sample_count = min(3, len(all_ids))
|
||||
selected_ids = random.sample(all_ids, sample_count)
|
||||
|
||||
print(f"랜덤 선택된 ID: {selected_ids}")
|
||||
logger.debug(f"랜덤 선택된 ID: {selected_ids}")
|
||||
|
||||
# 3. 선택된 ID로 샘플 가사 조회
|
||||
lyrics_query = """
|
||||
|
|
@ -710,11 +700,11 @@ async def make_automation(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("song_sample 테이블에 데이터가 없습니다")
|
||||
logger.info("song_sample 테이블에 데이터가 없습니다")
|
||||
|
||||
# 5. 프롬프트에 샘플 가사 추가
|
||||
if combined_sample_song:
|
||||
|
|
@ -726,11 +716,11 @@ async def make_automation(request: Request, conn: Connection):
|
|||
|
||||
{combined_sample_song}
|
||||
"""
|
||||
print("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
||||
logger.info("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
||||
else:
|
||||
print("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
||||
logger.info("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
||||
|
||||
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
|
||||
logger.info(f"[최종 프롬프트 길이: {len(updated_prompt)} 자]")
|
||||
|
||||
# 7. 모델에게 요청
|
||||
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
|
||||
|
|
@ -763,10 +753,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
:sample_song, :result_song, NOW()
|
||||
);
|
||||
"""
|
||||
print("\n[insert_params 선택된 속성 확인]")
|
||||
print(f"Categories: {selected_categories}")
|
||||
print(f"Values: {selected_values}")
|
||||
print()
|
||||
logger.debug("[insert_params 선택된 속성 확인]")
|
||||
logger.debug(f"Categories: {selected_categories}")
|
||||
logger.debug(f"Values: {selected_values}")
|
||||
|
||||
# attr_category, attr_value
|
||||
insert_params = {
|
||||
|
|
@ -792,9 +781,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
await conn.execute(text(insert_query), insert_params)
|
||||
await conn.commit()
|
||||
|
||||
print("결과 저장 완료")
|
||||
logger.info("make_automation 결과 저장 완료")
|
||||
|
||||
print("\n전체 결과 조회 중...")
|
||||
logger.info("make_automation 전체 결과 조회 중...")
|
||||
|
||||
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||
select_query = """
|
||||
|
|
@ -826,26 +815,20 @@ async def make_automation(request: Request, conn: Connection):
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"make_automation 전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"make_automation Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"make_automation Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ Song Background Tasks
|
|||
노래 생성 관련 백그라운드 태스크를 정의합니다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
|
@ -17,11 +16,12 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||
from app.database.session import BackgroundSessionLocal
|
||||
from app.song.models import Song
|
||||
from app.utils.common import generate_task_id
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
from config import prj_settings
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger("song")
|
||||
|
||||
# HTTP 요청 설정
|
||||
REQUEST_TIMEOUT = 120.0 # 초
|
||||
|
|
@ -73,20 +73,16 @@ async def _update_song_status(
|
|||
song.duration = duration
|
||||
await session.commit()
|
||||
logger.info(f"[Song] Status updated - task_id: {task_id}, status: {status}")
|
||||
print(f"[Song] Status updated - task_id: {task_id}, status: {status}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"[Song] NOT FOUND in DB - task_id: {task_id}")
|
||||
print(f"[Song] NOT FOUND in DB - task_id: {task_id}")
|
||||
return False
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[Song] DB Error while updating status - task_id: {task_id}, error: {e}")
|
||||
print(f"[Song] DB Error while updating status - task_id: {task_id}, error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"[Song] Unexpected error while updating status - task_id: {task_id}, error: {e}")
|
||||
print(f"[Song] Unexpected error while updating status - task_id: {task_id}, error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -104,14 +100,12 @@ async def _download_audio(url: str, task_id: str) -> bytes:
|
|||
httpx.HTTPError: 다운로드 실패 시
|
||||
"""
|
||||
logger.info(f"[Download] Downloading - task_id: {task_id}")
|
||||
print(f"[Download] Downloading - task_id: {task_id}")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, timeout=REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
|
||||
logger.info(f"[Download] SUCCESS - task_id: {task_id}, size: {len(response.content)} bytes")
|
||||
print(f"[Download] SUCCESS - task_id: {task_id}, size: {len(response.content)} bytes")
|
||||
return response.content
|
||||
|
||||
|
||||
|
|
@ -128,7 +122,6 @@ async def download_and_save_song(
|
|||
store_name: 저장할 파일명에 사용할 업체명
|
||||
"""
|
||||
logger.info(f"[download_and_save_song] START - task_id: {task_id}, store_name: {store_name}")
|
||||
print(f"[download_and_save_song] START - task_id: {task_id}, store_name: {store_name}")
|
||||
|
||||
try:
|
||||
# 저장 경로 생성: media/song/{날짜}/{uuid7}/{store_name}.mp3
|
||||
|
|
@ -146,11 +139,9 @@ async def download_and_save_song(
|
|||
media_dir.mkdir(parents=True, exist_ok=True)
|
||||
file_path = media_dir / file_name
|
||||
logger.info(f"[download_and_save_song] Directory created - path: {file_path}")
|
||||
print(f"[download_and_save_song] Directory created - path: {file_path}")
|
||||
|
||||
# 오디오 파일 다운로드
|
||||
logger.info(f"[download_and_save_song] Downloading audio - task_id: {task_id}, url: {audio_url}")
|
||||
print(f"[download_and_save_song] Downloading audio - task_id: {task_id}, url: {audio_url}")
|
||||
|
||||
content = await _download_audio(audio_url, task_id)
|
||||
|
||||
|
|
@ -158,36 +149,27 @@ async def download_and_save_song(
|
|||
await f.write(content)
|
||||
|
||||
logger.info(f"[download_and_save_song] File saved - task_id: {task_id}, path: {file_path}")
|
||||
print(f"[download_and_save_song] File saved - task_id: {task_id}, path: {file_path}")
|
||||
|
||||
# 프론트엔드에서 접근 가능한 URL 생성
|
||||
relative_path = f"/media/song/{today}/{unique_id}/{file_name}"
|
||||
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}")
|
||||
print(f"[download_and_save_song] URL generated - task_id: {task_id}, url: {file_url}")
|
||||
|
||||
# Song 테이블 업데이트
|
||||
await _update_song_status(task_id, "completed", file_url)
|
||||
logger.info(f"[download_and_save_song] SUCCESS - task_id: {task_id}")
|
||||
print(f"[download_and_save_song] SUCCESS - task_id: {task_id}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"[download_and_save_song] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_save_song] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_save_song] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[download_and_save_song] DB ERROR - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_save_song] DB ERROR - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_save_song] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[download_and_save_song] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_save_song] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_save_song] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
|
||||
|
|
@ -204,7 +186,6 @@ async def download_and_upload_song_to_blob(
|
|||
store_name: 저장할 파일명에 사용할 업체명
|
||||
"""
|
||||
logger.info(f"[download_and_upload_song_to_blob] START - task_id: {task_id}, store_name: {store_name}")
|
||||
print(f"[download_and_upload_song_to_blob] START - task_id: {task_id}, store_name: {store_name}")
|
||||
temp_file_path: Path | None = None
|
||||
|
||||
try:
|
||||
|
|
@ -220,11 +201,9 @@ async def download_and_upload_song_to_blob(
|
|||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_file_path = temp_dir / file_name
|
||||
logger.info(f"[download_and_upload_song_to_blob] Temp directory created - path: {temp_file_path}")
|
||||
print(f"[download_and_upload_song_to_blob] Temp directory created - path: {temp_file_path}")
|
||||
|
||||
# 오디오 파일 다운로드
|
||||
logger.info(f"[download_and_upload_song_to_blob] Downloading audio - task_id: {task_id}, url: {audio_url}")
|
||||
print(f"[download_and_upload_song_to_blob] Downloading audio - task_id: {task_id}, url: {audio_url}")
|
||||
|
||||
content = await _download_audio(audio_url, task_id)
|
||||
|
||||
|
|
@ -232,7 +211,6 @@ async def download_and_upload_song_to_blob(
|
|||
await f.write(content)
|
||||
|
||||
logger.info(f"[download_and_upload_song_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}")
|
||||
print(f"[download_and_upload_song_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}")
|
||||
|
||||
# Azure Blob Storage에 업로드
|
||||
uploader = AzureBlobUploader(task_id=task_id)
|
||||
|
|
@ -244,29 +222,21 @@ async def download_and_upload_song_to_blob(
|
|||
# SAS 토큰이 제외된 public_url 사용
|
||||
blob_url = uploader.public_url
|
||||
logger.info(f"[download_and_upload_song_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
|
||||
print(f"[download_and_upload_song_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
|
||||
|
||||
# Song 테이블 업데이트
|
||||
await _update_song_status(task_id, "completed", blob_url)
|
||||
logger.info(f"[download_and_upload_song_to_blob] SUCCESS - task_id: {task_id}")
|
||||
print(f"[download_and_upload_song_to_blob] SUCCESS - task_id: {task_id}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"[download_and_upload_song_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_upload_song_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_song_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[download_and_upload_song_to_blob] DB ERROR - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_upload_song_to_blob] DB ERROR - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_song_to_blob] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_song_status(task_id, "failed")
|
||||
|
||||
finally:
|
||||
|
|
@ -275,10 +245,8 @@ async def download_and_upload_song_to_blob(
|
|||
try:
|
||||
temp_file_path.unlink()
|
||||
logger.info(f"[download_and_upload_song_to_blob] Temp file deleted - path: {temp_file_path}")
|
||||
print(f"[download_and_upload_song_to_blob] Temp file deleted - path: {temp_file_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[download_and_upload_song_to_blob] Failed to delete temp file: {e}")
|
||||
print(f"[download_and_upload_song_to_blob] Failed to delete temp file: {e}")
|
||||
|
||||
# 임시 디렉토리 삭제 시도
|
||||
temp_dir = Path("media") / "temp" / task_id
|
||||
|
|
@ -304,7 +272,6 @@ async def download_and_upload_song_by_suno_task_id(
|
|||
duration: 노래 재생 시간 (초)
|
||||
"""
|
||||
logger.info(f"[download_and_upload_song_by_suno_task_id] START - suno_task_id: {suno_task_id}, store_name: {store_name}, duration: {duration}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] START - suno_task_id: {suno_task_id}, store_name: {store_name}, duration: {duration}")
|
||||
temp_file_path: Path | None = None
|
||||
task_id: str | None = None
|
||||
|
||||
|
|
@ -321,12 +288,10 @@ async def download_and_upload_song_by_suno_task_id(
|
|||
|
||||
if not song:
|
||||
logger.warning(f"[download_and_upload_song_by_suno_task_id] Song NOT FOUND - suno_task_id: {suno_task_id}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] Song NOT FOUND - suno_task_id: {suno_task_id}")
|
||||
return
|
||||
|
||||
task_id = song.task_id
|
||||
logger.info(f"[download_and_upload_song_by_suno_task_id] Song found - suno_task_id: {suno_task_id}, task_id: {task_id}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] Song found - suno_task_id: {suno_task_id}, task_id: {task_id}")
|
||||
|
||||
# 파일명에 사용할 수 없는 문자 제거
|
||||
safe_store_name = "".join(
|
||||
|
|
@ -340,11 +305,9 @@ async def download_and_upload_song_by_suno_task_id(
|
|||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_file_path = temp_dir / file_name
|
||||
logger.info(f"[download_and_upload_song_by_suno_task_id] Temp directory created - path: {temp_file_path}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] Temp directory created - path: {temp_file_path}")
|
||||
|
||||
# 오디오 파일 다운로드
|
||||
logger.info(f"[download_and_upload_song_by_suno_task_id] Downloading audio - suno_task_id: {suno_task_id}, url: {audio_url}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] Downloading audio - suno_task_id: {suno_task_id}, url: {audio_url}")
|
||||
|
||||
content = await _download_audio(audio_url, task_id)
|
||||
|
||||
|
|
@ -352,7 +315,6 @@ async def download_and_upload_song_by_suno_task_id(
|
|||
await f.write(content)
|
||||
|
||||
logger.info(f"[download_and_upload_song_by_suno_task_id] File downloaded - suno_task_id: {suno_task_id}, path: {temp_file_path}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] File downloaded - suno_task_id: {suno_task_id}, path: {temp_file_path}")
|
||||
|
||||
# Azure Blob Storage에 업로드
|
||||
uploader = AzureBlobUploader(task_id=task_id)
|
||||
|
|
@ -364,7 +326,6 @@ async def download_and_upload_song_by_suno_task_id(
|
|||
# SAS 토큰이 제외된 public_url 사용
|
||||
blob_url = uploader.public_url
|
||||
logger.info(f"[download_and_upload_song_by_suno_task_id] Uploaded to Blob - suno_task_id: {suno_task_id}, url: {blob_url}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] Uploaded to Blob - suno_task_id: {suno_task_id}, url: {blob_url}")
|
||||
|
||||
# Song 테이블 업데이트
|
||||
await _update_song_status(
|
||||
|
|
@ -375,26 +336,19 @@ async def download_and_upload_song_by_suno_task_id(
|
|||
duration=duration,
|
||||
)
|
||||
logger.info(f"[download_and_upload_song_by_suno_task_id] SUCCESS - suno_task_id: {suno_task_id}, duration: {duration}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] SUCCESS - suno_task_id: {suno_task_id}, duration: {duration}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"[download_and_upload_song_by_suno_task_id] DOWNLOAD ERROR - suno_task_id: {suno_task_id}, error: {e}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] DOWNLOAD ERROR - suno_task_id: {suno_task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_song_by_suno_task_id] DOWNLOAD ERROR - suno_task_id: {suno_task_id}, error: {e}", exc_info=True)
|
||||
if task_id:
|
||||
await _update_song_status(task_id, "failed", suno_task_id=suno_task_id)
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[download_and_upload_song_by_suno_task_id] DB ERROR - suno_task_id: {suno_task_id}, error: {e}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] DB ERROR - suno_task_id: {suno_task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_song_by_suno_task_id] DB ERROR - suno_task_id: {suno_task_id}, error: {e}", exc_info=True)
|
||||
if task_id:
|
||||
await _update_song_status(task_id, "failed", suno_task_id=suno_task_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[download_and_upload_song_by_suno_task_id] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_song_by_suno_task_id] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}", exc_info=True)
|
||||
if task_id:
|
||||
await _update_song_status(task_id, "failed", suno_task_id=suno_task_id)
|
||||
|
||||
|
|
@ -404,10 +358,8 @@ async def download_and_upload_song_by_suno_task_id(
|
|||
try:
|
||||
temp_file_path.unlink()
|
||||
logger.info(f"[download_and_upload_song_by_suno_task_id] Temp file deleted - path: {temp_file_path}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] Temp file deleted - path: {temp_file_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[download_and_upload_song_by_suno_task_id] Failed to delete temp file: {e}")
|
||||
print(f"[download_and_upload_song_by_suno_task_id] Failed to delete temp file: {e}")
|
||||
|
||||
# 임시 디렉토리 삭제 시도
|
||||
if task_id:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
"""
|
||||
User 모듈 커스텀 예외 정의
|
||||
|
||||
인증 및 사용자 관련 에러를 처리하기 위한 예외 클래스들입니다.
|
||||
"""
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
class AuthException(HTTPException):
|
||||
"""인증 관련 기본 예외"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: int,
|
||||
code: str,
|
||||
message: str,
|
||||
):
|
||||
super().__init__(
|
||||
status_code=status_code,
|
||||
detail={"code": code, "message": message},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 카카오 OAuth 관련 예외
|
||||
# =============================================================================
|
||||
class InvalidAuthCodeError(AuthException):
|
||||
"""유효하지 않은 인가 코드"""
|
||||
|
||||
def __init__(self, message: str = "유효하지 않은 인가 코드입니다."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
code="INVALID_CODE",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class KakaoAuthFailedError(AuthException):
|
||||
"""카카오 인증 실패"""
|
||||
|
||||
def __init__(self, message: str = "카카오 인증에 실패했습니다."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
code="KAKAO_AUTH_FAILED",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class KakaoAPIError(AuthException):
|
||||
"""카카오 API 호출 오류"""
|
||||
|
||||
def __init__(self, message: str = "카카오 API 호출 중 오류가 발생했습니다."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
code="KAKAO_API_ERROR",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# JWT 토큰 관련 예외
|
||||
# =============================================================================
|
||||
class TokenExpiredError(AuthException):
|
||||
"""토큰 만료"""
|
||||
|
||||
def __init__(self, message: str = "토큰이 만료되었습니다. 다시 로그인해주세요."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
code="TOKEN_EXPIRED",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class InvalidTokenError(AuthException):
|
||||
"""유효하지 않은 토큰"""
|
||||
|
||||
def __init__(self, message: str = "유효하지 않은 토큰입니다."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
code="INVALID_TOKEN",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class TokenRevokedError(AuthException):
|
||||
"""취소된 토큰"""
|
||||
|
||||
def __init__(self, message: str = "취소된 토큰입니다. 다시 로그인해주세요."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
code="TOKEN_REVOKED",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class MissingTokenError(AuthException):
|
||||
"""토큰 누락"""
|
||||
|
||||
def __init__(self, message: str = "인증 토큰이 필요합니다."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
code="MISSING_TOKEN",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 사용자 관련 예외
|
||||
# =============================================================================
|
||||
class UserNotFoundError(AuthException):
|
||||
"""사용자 없음"""
|
||||
|
||||
def __init__(self, message: str = "사용자를 찾을 수 없습니다."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
code="USER_NOT_FOUND",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class UserInactiveError(AuthException):
|
||||
"""비활성화된 계정"""
|
||||
|
||||
def __init__(self, message: str = "비활성화된 계정입니다. 관리자에게 문의하세요."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
code="USER_INACTIVE",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class AdminRequiredError(AuthException):
|
||||
"""관리자 권한 필요"""
|
||||
|
||||
def __init__(self, message: str = "관리자 권한이 필요합니다."):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
code="ADMIN_REQUIRED",
|
||||
message=message,
|
||||
)
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
"""
|
||||
User 모듈 SQLAlchemy 모델 정의
|
||||
|
||||
카카오 소셜 로그인 기반 사용자 관리 모델입니다.
|
||||
|
||||
주의: 이 모델은 현재 개발 중이므로 create_db_tables()에서 import하지 않습니다.
|
||||
테이블 생성이 필요할 때 app/database/session.py의 create_db_tables()에
|
||||
아래 import를 추가하세요:
|
||||
|
||||
from app.user.models import User # noqa: F401
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import BigInteger, Boolean, DateTime, Index, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database.session import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""
|
||||
사용자 테이블 (카카오 소셜 로그인)
|
||||
|
||||
카카오 로그인을 통해 인증된 사용자 정보를 저장합니다.
|
||||
|
||||
Attributes:
|
||||
id: 고유 식별자 (자동 증가)
|
||||
kakao_id: 카카오 고유 ID (필수, 유니크)
|
||||
email: 이메일 주소 (선택, 카카오에서 제공 시)
|
||||
nickname: 카카오 닉네임 (선택)
|
||||
profile_image_url: 카카오 프로필 이미지 URL (선택)
|
||||
thumbnail_image_url: 카카오 썸네일 이미지 URL (선택)
|
||||
is_active: 계정 활성화 상태 (기본 True)
|
||||
is_admin: 관리자 여부 (기본 False)
|
||||
last_login_at: 마지막 로그인 일시
|
||||
created_at: 계정 생성 일시
|
||||
updated_at: 계정 정보 수정 일시
|
||||
|
||||
카카오 API 응답 필드 매핑:
|
||||
- kakao_id: id (카카오 회원번호)
|
||||
- email: kakao_account.email
|
||||
- nickname: kakao_account.profile.nickname 또는 properties.nickname
|
||||
- profile_image_url: kakao_account.profile.profile_image_url
|
||||
- thumbnail_image_url: kakao_account.profile.thumbnail_image_url
|
||||
"""
|
||||
|
||||
__tablename__ = "user"
|
||||
__table_args__ = (
|
||||
Index("idx_user_kakao_id", "kakao_id", unique=True),
|
||||
Index("idx_user_email", "email"),
|
||||
Index("idx_user_is_active", "is_active"),
|
||||
Index("idx_user_created_at", "created_at"),
|
||||
{
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_charset": "utf8mb4",
|
||||
"mysql_collate": "utf8mb4_unicode_ci",
|
||||
},
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# 기본 식별자
|
||||
# ==========================================================================
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger,
|
||||
primary_key=True,
|
||||
nullable=False,
|
||||
autoincrement=True,
|
||||
comment="고유 식별자",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# 카카오 소셜 로그인 필수 정보
|
||||
# ==========================================================================
|
||||
kakao_id: Mapped[int] = mapped_column(
|
||||
BigInteger,
|
||||
nullable=False,
|
||||
unique=True,
|
||||
comment="카카오 고유 ID (회원번호)",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# 카카오에서 제공하는 사용자 정보 (선택적)
|
||||
# ==========================================================================
|
||||
email: Mapped[Optional[str]] = mapped_column(
|
||||
String(255),
|
||||
nullable=True,
|
||||
comment="이메일 주소 (카카오 계정 이메일, 동의 시 제공)",
|
||||
)
|
||||
|
||||
nickname: Mapped[Optional[str]] = mapped_column(
|
||||
String(100),
|
||||
nullable=True,
|
||||
comment="카카오 닉네임",
|
||||
)
|
||||
|
||||
profile_image_url: Mapped[Optional[str]] = mapped_column(
|
||||
String(2048),
|
||||
nullable=True,
|
||||
comment="카카오 프로필 이미지 URL",
|
||||
)
|
||||
|
||||
thumbnail_image_url: Mapped[Optional[str]] = mapped_column(
|
||||
String(2048),
|
||||
nullable=True,
|
||||
comment="카카오 썸네일 이미지 URL",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# 계정 상태 관리
|
||||
# ==========================================================================
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=True,
|
||||
comment="계정 활성화 상태 (비활성화 시 로그인 차단)",
|
||||
)
|
||||
|
||||
is_admin: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
comment="관리자 권한 여부",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# 시간 정보
|
||||
# ==========================================================================
|
||||
last_login_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="마지막 로그인 일시",
|
||||
)
|
||||
|
||||
created_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="계정 정보 수정 일시",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<User("
|
||||
f"id={self.id}, "
|
||||
f"kakao_id={self.kakao_id}, "
|
||||
f"nickname='{self.nickname}', "
|
||||
f"is_active={self.is_active}"
|
||||
f")>"
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from app.user.schemas.user_schema import (
|
||||
AccessTokenResponse,
|
||||
KakaoCallbackRequest,
|
||||
KakaoLoginResponse,
|
||||
KakaoTokenResponse,
|
||||
KakaoUserInfo,
|
||||
LoginResponse,
|
||||
RefreshTokenRequest,
|
||||
TokenResponse,
|
||||
UserBriefResponse,
|
||||
UserResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AccessTokenResponse",
|
||||
"KakaoCallbackRequest",
|
||||
"KakaoLoginResponse",
|
||||
"KakaoTokenResponse",
|
||||
"KakaoUserInfo",
|
||||
"LoginResponse",
|
||||
"RefreshTokenRequest",
|
||||
"TokenResponse",
|
||||
"UserBriefResponse",
|
||||
"UserResponse",
|
||||
]
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
"""
|
||||
User 모듈 Pydantic 스키마 정의
|
||||
|
||||
API 요청/응답 검증을 위한 스키마들입니다.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 카카오 OAuth 스키마
|
||||
# =============================================================================
|
||||
class KakaoLoginResponse(BaseModel):
|
||||
"""카카오 로그인 URL 응답"""
|
||||
|
||||
auth_url: str = Field(..., description="카카오 인증 페이지 URL")
|
||||
|
||||
|
||||
class KakaoCallbackRequest(BaseModel):
|
||||
"""카카오 콜백 요청 (인가 코드)"""
|
||||
|
||||
code: str = Field(..., min_length=1, description="카카오 인가 코드")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# JWT 토큰 스키마
|
||||
# =============================================================================
|
||||
class TokenResponse(BaseModel):
|
||||
"""토큰 발급 응답"""
|
||||
|
||||
access_token: str = Field(..., description="액세스 토큰")
|
||||
refresh_token: str = Field(..., description="리프레시 토큰")
|
||||
token_type: str = Field(default="Bearer", description="토큰 타입")
|
||||
expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)")
|
||||
|
||||
|
||||
class AccessTokenResponse(BaseModel):
|
||||
"""액세스 토큰 갱신 응답"""
|
||||
|
||||
access_token: str = Field(..., description="액세스 토큰")
|
||||
token_type: str = Field(default="Bearer", description="토큰 타입")
|
||||
expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)")
|
||||
|
||||
|
||||
class RefreshTokenRequest(BaseModel):
|
||||
"""토큰 갱신 요청"""
|
||||
|
||||
refresh_token: str = Field(..., min_length=1, description="리프레시 토큰")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 사용자 정보 스키마
|
||||
# =============================================================================
|
||||
class UserResponse(BaseModel):
|
||||
"""사용자 정보 응답"""
|
||||
|
||||
id: int = Field(..., description="사용자 ID")
|
||||
kakao_id: int = Field(..., description="카카오 회원번호")
|
||||
email: Optional[str] = Field(None, description="이메일")
|
||||
nickname: Optional[str] = Field(None, description="닉네임")
|
||||
profile_image_url: Optional[str] = Field(None, description="프로필 이미지 URL")
|
||||
thumbnail_image_url: Optional[str] = Field(None, description="썸네일 이미지 URL")
|
||||
is_active: bool = Field(..., description="계정 활성화 상태")
|
||||
is_admin: bool = Field(..., description="관리자 여부")
|
||||
last_login_at: Optional[datetime] = Field(None, description="마지막 로그인 일시")
|
||||
created_at: datetime = Field(..., description="가입 일시")
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class UserBriefResponse(BaseModel):
|
||||
"""사용자 간략 정보 (토큰 응답에 포함)"""
|
||||
|
||||
id: int = Field(..., description="사용자 ID")
|
||||
nickname: Optional[str] = Field(None, description="닉네임")
|
||||
email: Optional[str] = Field(None, description="이메일")
|
||||
profile_image_url: Optional[str] = Field(None, description="프로필 이미지 URL")
|
||||
is_new_user: bool = Field(..., description="신규 가입 여부")
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
"""로그인 응답 (토큰 + 사용자 정보)"""
|
||||
|
||||
access_token: str = Field(..., description="액세스 토큰")
|
||||
refresh_token: str = Field(..., description="리프레시 토큰")
|
||||
token_type: str = Field(default="Bearer", description="토큰 타입")
|
||||
expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)")
|
||||
user: UserBriefResponse = Field(..., description="사용자 정보")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 내부 사용 스키마 (카카오 API 응답 파싱)
|
||||
# =============================================================================
|
||||
class KakaoTokenResponse(BaseModel):
|
||||
"""카카오 토큰 응답 (내부 사용)"""
|
||||
|
||||
access_token: str
|
||||
token_type: str
|
||||
refresh_token: Optional[str] = None
|
||||
expires_in: int
|
||||
scope: Optional[str] = None
|
||||
refresh_token_expires_in: Optional[int] = None
|
||||
|
||||
|
||||
class KakaoProfile(BaseModel):
|
||||
"""카카오 프로필 정보 (내부 사용)"""
|
||||
|
||||
nickname: Optional[str] = None
|
||||
profile_image_url: Optional[str] = None
|
||||
thumbnail_image_url: Optional[str] = None
|
||||
is_default_image: Optional[bool] = None
|
||||
|
||||
|
||||
class KakaoAccount(BaseModel):
|
||||
"""카카오 계정 정보 (내부 사용)"""
|
||||
|
||||
email: Optional[str] = None
|
||||
profile: Optional[KakaoProfile] = None
|
||||
|
||||
|
||||
class KakaoUserInfo(BaseModel):
|
||||
"""카카오 사용자 정보 (내부 사용)"""
|
||||
|
||||
id: int
|
||||
kakao_account: Optional[KakaoAccount] = None
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import apikey_settings
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger("chatgpt")
|
||||
|
||||
# fmt: off
|
||||
LYRICS_PROMPT_TEMPLATE_ORI = """
|
||||
|
|
@ -298,13 +298,12 @@ class ChatgptService:
|
|||
if prompt is None:
|
||||
prompt = self.build_lyrics_prompt()
|
||||
|
||||
print(f"[ChatgptService] Generated Prompt (length: {len(prompt)})")
|
||||
logger.debug(f"[ChatgptService] Generated Prompt (length: {len(prompt)})")
|
||||
logger.info(f"[ChatgptService] Starting GPT request with model: {self.model}")
|
||||
|
||||
# GPT API 호출
|
||||
response = await self._call_gpt_api(prompt)
|
||||
|
||||
print(f"[ChatgptService] SUCCESS - Response length: {len(response)}")
|
||||
logger.info(f"[ChatgptService] SUCCESS - Response length: {len(response)}")
|
||||
return response
|
||||
|
||||
|
|
@ -332,7 +331,7 @@ class ChatgptService:
|
|||
[OUTPUT REQUIREMENTS]
|
||||
- 5개 항목으로 구분: 타겟 고객, 핵심 차별점, 지역 특성, 시즌별 포인트
|
||||
- 각 항목은 줄바꿈으로 구분
|
||||
- 총 500자 이내로 요약
|
||||
- 총 800자 이내로 요약
|
||||
- 내용의 누락이 있어서는 안된다
|
||||
- 문장이 자연스러워야 한다
|
||||
- 핵심 정보만 간결하게 포함
|
||||
|
|
|
|||
|
|
@ -30,16 +30,16 @@ response = await creatomate.make_creatomate_call(template_id, modifications)
|
|||
"""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
import httpx
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import apikey_settings, creatomate_settings
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger("creatomate")
|
||||
|
||||
|
||||
# Orientation 타입 정의
|
||||
|
|
@ -76,14 +76,14 @@ async def close_shared_client() -> None:
|
|||
if _shared_client is not None and not _shared_client.is_closed:
|
||||
await _shared_client.aclose()
|
||||
_shared_client = None
|
||||
print("[CreatomateService] Shared HTTP client closed")
|
||||
logger.info("[CreatomateService] Shared HTTP client closed")
|
||||
|
||||
|
||||
def clear_template_cache() -> None:
|
||||
"""템플릿 캐시를 전체 삭제합니다."""
|
||||
global _template_cache
|
||||
_template_cache.clear()
|
||||
print("[CreatomateService] Template cache cleared")
|
||||
logger.info("[CreatomateService] Template cache cleared")
|
||||
|
||||
|
||||
def _is_cache_valid(cached_at: float) -> bool:
|
||||
|
|
@ -164,7 +164,6 @@ class CreatomateService:
|
|||
httpx.HTTPError: 요청 실패 시
|
||||
"""
|
||||
logger.info(f"[Creatomate] {method} {url}")
|
||||
print(f"[Creatomate] {method} {url}")
|
||||
|
||||
client = await get_shared_client()
|
||||
|
||||
|
|
@ -180,7 +179,6 @@ class CreatomateService:
|
|||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
|
||||
logger.info(f"[Creatomate] Response - Status: {response.status_code}")
|
||||
print(f"[Creatomate] Response - Status: {response.status_code}")
|
||||
return response
|
||||
|
||||
async def get_all_templates_data(self) -> dict:
|
||||
|
|
@ -210,12 +208,12 @@ class CreatomateService:
|
|||
if use_cache and template_id in _template_cache:
|
||||
cached = _template_cache[template_id]
|
||||
if _is_cache_valid(cached["cached_at"]):
|
||||
print(f"[CreatomateService] Cache HIT - {template_id}")
|
||||
logger.debug(f"[CreatomateService] Cache HIT - {template_id}")
|
||||
return copy.deepcopy(cached["data"])
|
||||
else:
|
||||
# 만료된 캐시 삭제
|
||||
del _template_cache[template_id]
|
||||
print(f"[CreatomateService] Cache EXPIRED - {template_id}")
|
||||
logger.debug(f"[CreatomateService] Cache EXPIRED - {template_id}")
|
||||
|
||||
# API 호출
|
||||
url = f"{self.BASE_URL}/v1/templates/{template_id}"
|
||||
|
|
@ -228,7 +226,7 @@ class CreatomateService:
|
|||
"data": data,
|
||||
"cached_at": time.time(),
|
||||
}
|
||||
print(f"[CreatomateService] Cache MISS - {template_id} (cached)")
|
||||
logger.debug(f"[CreatomateService] Cache MISS - {template_id} (cached)")
|
||||
|
||||
return copy.deepcopy(data)
|
||||
|
||||
|
|
@ -444,12 +442,13 @@ class CreatomateService:
|
|||
if animation["transition"]:
|
||||
total_template_duration -= animation["duration"]
|
||||
except Exception as e:
|
||||
print(f"[calc_scene_duration] Error processing element: {elem}, {e}")
|
||||
logger.error(f"[calc_scene_duration] Error processing element: {elem}, {e}")
|
||||
|
||||
return total_template_duration
|
||||
|
||||
def extend_template_duration(self, template: dict, target_duration: float) -> dict:
|
||||
"""템플릿의 duration을 target_duration으로 확장합니다."""
|
||||
target_duration += 0.1 # 수동으로 직접 변경 및 테스트 필요 : 파란박스 생기는것
|
||||
template["duration"] = target_duration
|
||||
total_template_duration = self.calc_scene_duration(template)
|
||||
extend_rate = target_duration / total_template_duration
|
||||
|
|
@ -466,7 +465,7 @@ class CreatomateService:
|
|||
assert animation["time"] == 0 # 0이 아닌 경우 확인 필요
|
||||
animation["duration"] = animation["duration"] * extend_rate
|
||||
except Exception as e:
|
||||
print(
|
||||
logger.error(
|
||||
f"[extend_template_duration] Error processing element: {elem}, {e}"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,337 @@
|
|||
"""
|
||||
FastAPI용 로깅 모듈
|
||||
|
||||
Django 로거 구조를 참고하여 FastAPI에 최적화된 로깅 시스템.
|
||||
|
||||
사용 예시:
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("song")
|
||||
logger.info("노래 생성 완료")
|
||||
logger.error("오류 발생", exc_info=True)
|
||||
|
||||
로그 레벨:
|
||||
1. DEBUG: 디버깅 목적
|
||||
2. INFO: 일반 정보
|
||||
3. WARNING: 경고 정보 (작은 문제)
|
||||
4. ERROR: 오류 정보 (큰 문제)
|
||||
5. CRITICAL: 아주 심각한 문제
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Literal
|
||||
|
||||
from config import log_settings
|
||||
|
||||
# 로그 디렉토리 설정 (config.py의 LogSettings에서 관리)
|
||||
LOG_DIR = log_settings.get_log_dir()
|
||||
|
||||
# 로그 레벨 타입
|
||||
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
|
||||
|
||||
class LoggerConfig:
|
||||
"""로거 설정 클래스 (config.py의 LogSettings 참조)"""
|
||||
|
||||
# 출력 대상 설정 (LogSettings에서 가져옴)
|
||||
CONSOLE_ENABLED: bool = log_settings.LOG_CONSOLE_ENABLED
|
||||
FILE_ENABLED: bool = log_settings.LOG_FILE_ENABLED
|
||||
|
||||
# 기본 설정 (LogSettings에서 가져옴)
|
||||
DEFAULT_LEVEL: str = log_settings.LOG_LEVEL
|
||||
CONSOLE_LEVEL: str = log_settings.LOG_CONSOLE_LEVEL
|
||||
FILE_LEVEL: str = log_settings.LOG_FILE_LEVEL
|
||||
MAX_BYTES: int = log_settings.LOG_MAX_SIZE_MB * 1024 * 1024
|
||||
BACKUP_COUNT: int = log_settings.LOG_BACKUP_COUNT
|
||||
ENCODING: str = "utf-8"
|
||||
|
||||
# 포맷 설정 (LogSettings에서 가져옴)
|
||||
CONSOLE_FORMAT: str = log_settings.LOG_CONSOLE_FORMAT
|
||||
FILE_FORMAT: str = log_settings.LOG_FILE_FORMAT
|
||||
DATE_FORMAT: str = log_settings.LOG_DATE_FORMAT
|
||||
|
||||
|
||||
def _create_console_handler() -> logging.StreamHandler:
|
||||
"""콘솔 핸들러 생성"""
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(getattr(logging, LoggerConfig.CONSOLE_LEVEL))
|
||||
formatter = logging.Formatter(
|
||||
fmt=LoggerConfig.CONSOLE_FORMAT,
|
||||
datefmt=LoggerConfig.DATE_FORMAT,
|
||||
style="{",
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
return handler
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 공유 파일 핸들러 (싱글톤)
|
||||
# 모든 로거가 동일한 파일 핸들러를 공유하여 하나의 로그 파일에 기록
|
||||
# =============================================================================
|
||||
_shared_file_handler: RotatingFileHandler | None = None
|
||||
_shared_error_handler: RotatingFileHandler | None = None
|
||||
|
||||
|
||||
def _get_shared_file_handler() -> RotatingFileHandler:
|
||||
"""
|
||||
공유 파일 핸들러 반환 (싱글톤)
|
||||
|
||||
모든 로거가 하나의 기본 로그 파일(app.log)에 기록합니다.
|
||||
파일명: logs/{날짜}_app.log
|
||||
"""
|
||||
global _shared_file_handler
|
||||
|
||||
if _shared_file_handler is None:
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
log_file = LOG_DIR / f"{today}_app.log"
|
||||
|
||||
_shared_file_handler = RotatingFileHandler(
|
||||
filename=log_file,
|
||||
maxBytes=LoggerConfig.MAX_BYTES,
|
||||
backupCount=LoggerConfig.BACKUP_COUNT,
|
||||
encoding=LoggerConfig.ENCODING,
|
||||
)
|
||||
_shared_file_handler.setLevel(getattr(logging, LoggerConfig.FILE_LEVEL))
|
||||
formatter = logging.Formatter(
|
||||
fmt=LoggerConfig.FILE_FORMAT,
|
||||
datefmt=LoggerConfig.DATE_FORMAT,
|
||||
style="{",
|
||||
)
|
||||
_shared_file_handler.setFormatter(formatter)
|
||||
|
||||
return _shared_file_handler
|
||||
|
||||
|
||||
def _get_shared_error_handler() -> RotatingFileHandler:
|
||||
"""
|
||||
공유 에러 파일 핸들러 반환 (싱글톤)
|
||||
|
||||
모든 로거의 ERROR 이상 로그가 하나의 에러 로그 파일(error.log)에 기록됩니다.
|
||||
파일명: logs/{날짜}_error.log
|
||||
"""
|
||||
global _shared_error_handler
|
||||
|
||||
if _shared_error_handler is None:
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
log_file = LOG_DIR / f"{today}_error.log"
|
||||
|
||||
_shared_error_handler = RotatingFileHandler(
|
||||
filename=log_file,
|
||||
maxBytes=LoggerConfig.MAX_BYTES,
|
||||
backupCount=LoggerConfig.BACKUP_COUNT,
|
||||
encoding=LoggerConfig.ENCODING,
|
||||
)
|
||||
_shared_error_handler.setLevel(logging.ERROR)
|
||||
formatter = logging.Formatter(
|
||||
fmt=LoggerConfig.FILE_FORMAT,
|
||||
datefmt=LoggerConfig.DATE_FORMAT,
|
||||
style="{",
|
||||
)
|
||||
_shared_error_handler.setFormatter(formatter)
|
||||
|
||||
return _shared_error_handler
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_logger(name: str = "app") -> logging.Logger:
|
||||
"""
|
||||
로거 인스턴스 반환 (캐싱 적용)
|
||||
|
||||
Args:
|
||||
name: 로거 이름 (모듈명 권장: "song", "lyric", "video" 등)
|
||||
|
||||
Returns:
|
||||
설정된 로거 인스턴스
|
||||
|
||||
Example:
|
||||
logger = get_logger("song")
|
||||
logger.info("노래 처리 시작")
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
# 이미 핸들러가 설정된 경우 반환
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# 로그 레벨 설정
|
||||
logger.setLevel(getattr(logging, LoggerConfig.DEFAULT_LEVEL))
|
||||
|
||||
# 핸들러 추가 (설정에 따라 선택적으로 추가)
|
||||
if LoggerConfig.CONSOLE_ENABLED:
|
||||
logger.addHandler(_create_console_handler())
|
||||
|
||||
if LoggerConfig.FILE_ENABLED:
|
||||
logger.addHandler(_get_shared_file_handler())
|
||||
logger.addHandler(_get_shared_error_handler())
|
||||
|
||||
# 상위 로거로 전파 방지 (중복 출력 방지)
|
||||
logger.propagate = False
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def setup_uvicorn_logging() -> dict:
|
||||
"""
|
||||
Uvicorn 서버의 로깅 설정을 반환합니다.
|
||||
|
||||
============================================================
|
||||
언제 사용하는가?
|
||||
============================================================
|
||||
Uvicorn 서버를 Python 코드로 직접 실행할 때 사용합니다.
|
||||
CLI 명령어(uvicorn main:app --reload)로 실행할 때는 적용되지 않습니다.
|
||||
|
||||
============================================================
|
||||
사용 방법
|
||||
============================================================
|
||||
1. Python 코드에서 uvicorn.run() 호출 시:
|
||||
|
||||
# run.py 또는 main.py 하단
|
||||
import uvicorn
|
||||
from app.utils.logger import setup_uvicorn_logging
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
log_config=setup_uvicorn_logging(), # 여기서 적용
|
||||
)
|
||||
|
||||
2. 실행:
|
||||
python run.py
|
||||
또는
|
||||
python main.py
|
||||
|
||||
============================================================
|
||||
어떤 동작을 하는가?
|
||||
============================================================
|
||||
Uvicorn의 기본 로깅 형식을 애플리케이션의 LogSettings와 일치시킵니다.
|
||||
|
||||
- formatters: 로그 출력 형식 정의
|
||||
- default: 일반 로그용 (서버 시작/종료, 에러 등)
|
||||
- access: HTTP 요청 로그용 (클라이언트 IP, 요청 경로, 상태 코드)
|
||||
|
||||
- handlers: 로그 출력 대상 설정
|
||||
- stdout으로 콘솔에 출력
|
||||
|
||||
- loggers: Uvicorn 내부 로거 설정
|
||||
- uvicorn: 메인 로거
|
||||
- uvicorn.error: 에러/시작/종료 로그
|
||||
- uvicorn.access: HTTP 요청 로그
|
||||
|
||||
============================================================
|
||||
출력 예시
|
||||
============================================================
|
||||
적용 전 (Uvicorn 기본):
|
||||
INFO: 127.0.0.1:52341 - "GET /docs HTTP/1.1" 200 OK
|
||||
INFO: Uvicorn running on http://0.0.0.0:8000
|
||||
|
||||
적용 후:
|
||||
[2026-01-14 15:30:00] INFO [uvicorn.access] 127.0.0.1 - "GET /docs HTTP/1.1" 200
|
||||
[2026-01-14 15:30:00] INFO [uvicorn:startup:45] Uvicorn running on http://0.0.0.0:8000
|
||||
|
||||
============================================================
|
||||
반환값 구조 (Python logging.config.dictConfig 형식)
|
||||
============================================================
|
||||
{
|
||||
"version": 1, # dictConfig 버전 (항상 1)
|
||||
"disable_existing_loggers": False, # 기존 로거 유지
|
||||
"formatters": { ... }, # 포맷터 정의
|
||||
"handlers": { ... }, # 핸들러 정의
|
||||
"loggers": { ... }, # 로거 정의
|
||||
}
|
||||
|
||||
Returns:
|
||||
dict: Uvicorn log_config 파라미터에 전달할 설정 딕셔너리
|
||||
"""
|
||||
return {
|
||||
# --------------------------------------------------------
|
||||
# dictConfig 버전 (필수, 항상 1)
|
||||
# --------------------------------------------------------
|
||||
"version": 1,
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 기존 로거 비활성화 여부
|
||||
# False: 기존 로거 유지 (권장)
|
||||
# True: 기존 로거 모두 비활성화
|
||||
# --------------------------------------------------------
|
||||
"disable_existing_loggers": False,
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 포맷터 정의
|
||||
# 로그 메시지의 출력 형식을 지정합니다.
|
||||
# --------------------------------------------------------
|
||||
"formatters": {
|
||||
# 일반 로그용 포맷터 (서버 시작/종료, 에러 등)
|
||||
"default": {
|
||||
"format": LoggerConfig.CONSOLE_FORMAT,
|
||||
"datefmt": LoggerConfig.DATE_FORMAT,
|
||||
"style": "{", # {변수명} 스타일 사용
|
||||
},
|
||||
# HTTP 요청 로그용 포맷터
|
||||
# 사용 가능한 변수: client_addr, request_line, status_code
|
||||
"access": {
|
||||
"format": "[{asctime}] {levelname:8} [{name}] {client_addr} - \"{request_line}\" {status_code}",
|
||||
"datefmt": LoggerConfig.DATE_FORMAT,
|
||||
"style": "{",
|
||||
},
|
||||
},
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 핸들러 정의
|
||||
# 로그를 어디에 출력할지 지정합니다.
|
||||
# --------------------------------------------------------
|
||||
"handlers": {
|
||||
# 일반 로그 핸들러 (stdout 출력)
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
# HTTP 요청 로그 핸들러 (stdout 출력)
|
||||
"access": {
|
||||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
},
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 로거 정의
|
||||
# Uvicorn 내부에서 사용하는 로거들을 설정합니다.
|
||||
# --------------------------------------------------------
|
||||
"loggers": {
|
||||
# Uvicorn 메인 로거
|
||||
"uvicorn": {
|
||||
"handlers": ["default"],
|
||||
"level": "INFO",
|
||||
"propagate": False, # 상위 로거로 전파 방지
|
||||
},
|
||||
# 에러/시작/종료 로그
|
||||
"uvicorn.error": {
|
||||
"handlers": ["default"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
# HTTP 요청 로그 (GET /path HTTP/1.1 200 등)
|
||||
"uvicorn.access": {
|
||||
"handlers": ["access"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# 편의를 위한 사전 정의된 로거 이름 상수
|
||||
HOME_LOGGER = "home"
|
||||
LYRIC_LOGGER = "lyric"
|
||||
SONG_LOGGER = "song"
|
||||
VIDEO_LOGGER = "video"
|
||||
CELERY_LOGGER = "celery"
|
||||
APP_LOGGER = "app"
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
import bs4
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import crawler_settings
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger("scraper")
|
||||
|
||||
|
||||
class GraphQLException(Exception):
|
||||
|
|
@ -109,7 +109,7 @@ query getAccommodation($id: String!, $deviceType: String) {
|
|||
self.scrap_type = "GraphQL"
|
||||
|
||||
except GraphQLException:
|
||||
print("fallback")
|
||||
logger.debug("GraphQL failed, fallback to Playwright")
|
||||
self.scrap_type = "Playwright"
|
||||
pass # 나중에 pw 이용한 crawling으로 fallback 추가
|
||||
|
||||
|
|
@ -138,7 +138,6 @@ query getAccommodation($id: String!, $deviceType: String) {
|
|||
|
||||
try:
|
||||
logger.info(f"[NvMapScraper] Requesting place_id: {place_id}")
|
||||
print(f"[NvMapScraper] Requesting place_id: {place_id}")
|
||||
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.post(
|
||||
|
|
@ -148,24 +147,20 @@ query getAccommodation($id: String!, $deviceType: String) {
|
|||
) as response:
|
||||
if response.status == 200:
|
||||
logger.info(f"[NvMapScraper] SUCCESS - place_id: {place_id}")
|
||||
print(f"[NvMapScraper] SUCCESS - place_id: {place_id}")
|
||||
return await response.json()
|
||||
|
||||
# 실패 상태 코드
|
||||
logger.error(f"[NvMapScraper] Failed with status {response.status} - place_id: {place_id}")
|
||||
print(f"[NvMapScraper] 실패 상태 코드: {response.status}")
|
||||
raise GraphQLException(
|
||||
f"Request failed with status {response.status}"
|
||||
)
|
||||
|
||||
except (TimeoutError, asyncio.TimeoutError):
|
||||
logger.error(f"[NvMapScraper] Timeout - place_id: {place_id}")
|
||||
print(f"[NvMapScraper] Timeout - place_id: {place_id}")
|
||||
raise CrawlingTimeoutException(f"Request timed out after {self.REQUEST_TIMEOUT}s")
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"[NvMapScraper] Client error: {e}")
|
||||
print(f"[NvMapScraper] Client error: {e}")
|
||||
raise GraphQLException(f"Client error: {e}")
|
||||
|
||||
async def _get_facility_string(self, place_id: str) -> str | None:
|
||||
|
|
|
|||
|
|
@ -32,17 +32,17 @@ URL 경로 형식:
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
import httpx
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import azure_blob_settings
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger("blob")
|
||||
|
||||
# =============================================================================
|
||||
# 모듈 레벨 공유 HTTP 클라이언트 (싱글톤 패턴)
|
||||
|
|
@ -56,12 +56,12 @@ async def get_shared_blob_client() -> httpx.AsyncClient:
|
|||
"""공유 HTTP 클라이언트를 반환합니다. 없으면 생성합니다."""
|
||||
global _shared_blob_client
|
||||
if _shared_blob_client is None or _shared_blob_client.is_closed:
|
||||
print("[AzureBlobUploader] Creating shared HTTP client...")
|
||||
logger.info("[AzureBlobUploader] Creating shared HTTP client...")
|
||||
_shared_blob_client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(180.0, connect=10.0),
|
||||
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
|
||||
)
|
||||
print("[AzureBlobUploader] Shared HTTP client created - "
|
||||
logger.info("[AzureBlobUploader] Shared HTTP client created - "
|
||||
"max_connections: 20, max_keepalive: 10")
|
||||
return _shared_blob_client
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ async def close_shared_blob_client() -> None:
|
|||
if _shared_blob_client is not None and not _shared_blob_client.is_closed:
|
||||
await _shared_blob_client.aclose()
|
||||
_shared_blob_client = None
|
||||
print("[AzureBlobUploader] Shared HTTP client closed")
|
||||
logger.info("[AzureBlobUploader] Shared HTTP client closed")
|
||||
|
||||
|
||||
class AzureBlobUploader:
|
||||
|
|
@ -158,14 +158,14 @@ class AzureBlobUploader:
|
|||
|
||||
try:
|
||||
logger.info(f"[{log_prefix}] Starting upload")
|
||||
print(f"[{log_prefix}] Getting shared client...")
|
||||
logger.debug(f"[{log_prefix}] Getting shared client...")
|
||||
|
||||
client = await get_shared_blob_client()
|
||||
client_time = time.perf_counter()
|
||||
elapsed_ms = (client_time - start_time) * 1000
|
||||
print(f"[{log_prefix}] Client acquired in {elapsed_ms:.1f}ms")
|
||||
logger.debug(f"[{log_prefix}] Client acquired in {elapsed_ms:.1f}ms")
|
||||
|
||||
print(f"[{log_prefix}] Starting upload... "
|
||||
logger.debug(f"[{log_prefix}] Starting upload... "
|
||||
f"(size: {size} bytes, timeout: {timeout}s)")
|
||||
|
||||
response = await asyncio.wait_for(
|
||||
|
|
@ -176,43 +176,37 @@ class AzureBlobUploader:
|
|||
duration_ms = (upload_time - start_time) * 1000
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
logger.info(f"[{log_prefix}] SUCCESS - Status: {response.status_code}")
|
||||
print(f"[{log_prefix}] SUCCESS - Status: {response.status_code}, "
|
||||
logger.info(f"[{log_prefix}] SUCCESS - Status: {response.status_code}, "
|
||||
f"Duration: {duration_ms:.1f}ms")
|
||||
print(f"[{log_prefix}] Public URL: {self._last_public_url}")
|
||||
logger.debug(f"[{log_prefix}] Public URL: {self._last_public_url}")
|
||||
return True
|
||||
|
||||
# 업로드 실패
|
||||
logger.error(f"[{log_prefix}] FAILED - Status: {response.status_code}")
|
||||
print(f"[{log_prefix}] FAILED - Status: {response.status_code}, "
|
||||
logger.error(f"[{log_prefix}] FAILED - Status: {response.status_code}, "
|
||||
f"Duration: {duration_ms:.1f}ms")
|
||||
print(f"[{log_prefix}] Response: {response.text[:500]}")
|
||||
logger.error(f"[{log_prefix}] Response: {response.text[:500]}")
|
||||
return False
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
elapsed = time.perf_counter() - start_time
|
||||
logger.error(f"[{log_prefix}] TIMEOUT after {elapsed:.1f}s")
|
||||
print(f"[{log_prefix}] TIMEOUT after {elapsed:.1f}s")
|
||||
return False
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
elapsed = time.perf_counter() - start_time
|
||||
logger.error(f"[{log_prefix}] CONNECT_ERROR: {e}")
|
||||
print(f"[{log_prefix}] CONNECT_ERROR after {elapsed:.1f}s - "
|
||||
logger.error(f"[{log_prefix}] CONNECT_ERROR after {elapsed:.1f}s - "
|
||||
f"{type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
except httpx.ReadError as e:
|
||||
elapsed = time.perf_counter() - start_time
|
||||
logger.error(f"[{log_prefix}] READ_ERROR: {e}")
|
||||
print(f"[{log_prefix}] READ_ERROR after {elapsed:.1f}s - "
|
||||
logger.error(f"[{log_prefix}] READ_ERROR after {elapsed:.1f}s - "
|
||||
f"{type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
elapsed = time.perf_counter() - start_time
|
||||
logger.error(f"[{log_prefix}] ERROR: {type(e).__name__}: {e}")
|
||||
print(f"[{log_prefix}] ERROR after {elapsed:.1f}s - "
|
||||
logger.error(f"[{log_prefix}] ERROR after {elapsed:.1f}s - "
|
||||
f"{type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
|
|
@ -241,7 +235,7 @@ class AzureBlobUploader:
|
|||
|
||||
upload_url = self._build_upload_url(category, file_name)
|
||||
self._last_public_url = self._build_public_url(category, file_name)
|
||||
print(f"[{log_prefix}] URL (without SAS): {self._last_public_url}")
|
||||
logger.debug(f"[{log_prefix}] URL (without SAS): {self._last_public_url}")
|
||||
|
||||
headers = {"Content-Type": content_type, "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
|
|
@ -306,7 +300,7 @@ class AzureBlobUploader:
|
|||
upload_url = self._build_upload_url("song", file_name)
|
||||
self._last_public_url = self._build_public_url("song", file_name)
|
||||
log_prefix = "upload_music_bytes"
|
||||
print(f"[{log_prefix}] URL (without SAS): {self._last_public_url}")
|
||||
logger.debug(f"[{log_prefix}] URL (without SAS): {self._last_public_url}")
|
||||
|
||||
headers = {"Content-Type": "audio/mpeg", "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
|
|
@ -368,7 +362,7 @@ class AzureBlobUploader:
|
|||
upload_url = self._build_upload_url("video", file_name)
|
||||
self._last_public_url = self._build_public_url("video", file_name)
|
||||
log_prefix = "upload_video_bytes"
|
||||
print(f"[{log_prefix}] URL (without SAS): {self._last_public_url}")
|
||||
logger.debug(f"[{log_prefix}] URL (without SAS): {self._last_public_url}")
|
||||
|
||||
headers = {"Content-Type": "video/mp4", "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
|
|
@ -434,7 +428,7 @@ class AzureBlobUploader:
|
|||
upload_url = self._build_upload_url("image", file_name)
|
||||
self._last_public_url = self._build_public_url("image", file_name)
|
||||
log_prefix = "upload_image_bytes"
|
||||
print(f"[{log_prefix}] URL (without SAS): {self._last_public_url}")
|
||||
logger.debug(f"[{log_prefix}] URL (without SAS): {self._last_public_url}")
|
||||
|
||||
headers = {"Content-Type": content_type, "x-ms-blob-type": "BlockBlob"}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ from app.video.schemas.video_schema import (
|
|||
from app.video.worker.video_task import download_and_upload_video_to_blob
|
||||
from app.utils.creatomate import CreatomateService
|
||||
from app.utils.pagination import PaginatedResponse
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("video")
|
||||
|
||||
router = APIRouter(prefix="/video", tags=["video"])
|
||||
|
||||
|
|
@ -115,7 +117,7 @@ async def generate_video(
|
|||
from app.database.session import AsyncSessionLocal
|
||||
|
||||
request_start = time.perf_counter()
|
||||
print(f"[generate_video] START - task_id: {task_id}, orientation: {orientation}")
|
||||
logger.info(f"[generate_video] START - task_id: {task_id}, orientation: {orientation}")
|
||||
|
||||
# ==========================================================================
|
||||
# 1단계: DB 조회 및 초기 데이터 저장 (세션을 명시적으로 열고 닫음)
|
||||
|
|
@ -168,13 +170,13 @@ async def generate_video(
|
|||
)
|
||||
|
||||
query_time = time.perf_counter()
|
||||
print(f"[generate_video] Queries completed - task_id: {task_id}, "
|
||||
logger.debug(f"[generate_video] Queries completed - task_id: {task_id}, "
|
||||
f"elapsed: {(query_time - request_start)*1000:.1f}ms")
|
||||
|
||||
# ===== 결과 처리: Project =====
|
||||
project = project_result.scalar_one_or_none()
|
||||
if not project:
|
||||
print(f"[generate_video] Project NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[generate_video] Project NOT FOUND - task_id: {task_id}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.",
|
||||
|
|
@ -184,7 +186,7 @@ async def generate_video(
|
|||
# ===== 결과 처리: Lyric =====
|
||||
lyric = lyric_result.scalar_one_or_none()
|
||||
if not lyric:
|
||||
print(f"[generate_video] Lyric NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[generate_video] Lyric NOT FOUND - task_id: {task_id}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 Lyric을 찾을 수 없습니다.",
|
||||
|
|
@ -194,7 +196,7 @@ async def generate_video(
|
|||
# ===== 결과 처리: Song =====
|
||||
song = song_result.scalar_one_or_none()
|
||||
if not song:
|
||||
print(f"[generate_video] Song NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[generate_video] Song NOT FOUND - task_id: {task_id}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 Song을 찾을 수 없습니다.",
|
||||
|
|
@ -220,14 +222,14 @@ async def generate_video(
|
|||
# ===== 결과 처리: Image =====
|
||||
images = image_result.scalars().all()
|
||||
if not images:
|
||||
print(f"[generate_video] Image NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[generate_video] Image NOT FOUND - task_id: {task_id}")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 이미지를 찾을 수 없습니다.",
|
||||
)
|
||||
image_urls = [img.img_url for img in images]
|
||||
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_video] Data loaded - task_id: {task_id}, "
|
||||
f"project_id: {project_id}, lyric_id: {lyric_id}, "
|
||||
f"song_id: {song_id}, images: {len(image_urls)}"
|
||||
|
|
@ -246,14 +248,14 @@ async def generate_video(
|
|||
await session.commit()
|
||||
video_id = video.id
|
||||
stage1_time = time.perf_counter()
|
||||
print(f"[generate_video] Video saved - task_id: {task_id}, id: {video_id}, "
|
||||
logger.info(f"[generate_video] Video saved - task_id: {task_id}, id: {video_id}, "
|
||||
f"stage1_elapsed: {(stage1_time - request_start)*1000:.1f}ms")
|
||||
# 세션이 여기서 자동으로 닫힘 (async with 블록 종료)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[generate_video] DB EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
logger.error(f"[generate_video] DB EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
return GenerateVideoResponse(
|
||||
success=False,
|
||||
task_id=task_id,
|
||||
|
|
@ -267,16 +269,16 @@ async def generate_video(
|
|||
# ==========================================================================
|
||||
stage2_start = time.perf_counter()
|
||||
try:
|
||||
print(f"[generate_video] Stage 2 START - Creatomate API - task_id: {task_id}")
|
||||
logger.info(f"[generate_video] Stage 2 START - Creatomate API - task_id: {task_id}")
|
||||
creatomate_service = CreatomateService(
|
||||
orientation=orientation,
|
||||
target_duration=song_duration,
|
||||
)
|
||||
print(f"[generate_video] Using template_id: {creatomate_service.template_id}, duration: {creatomate_service.target_duration} (song duration: {song_duration})")
|
||||
logger.debug(f"[generate_video] Using template_id: {creatomate_service.template_id}, duration: {creatomate_service.target_duration} (song duration: {song_duration})")
|
||||
|
||||
# 6-1. 템플릿 조회 (비동기)
|
||||
template = await creatomate_service.get_one_template_data_async(creatomate_service.template_id)
|
||||
print(f"[generate_video] Template fetched - task_id: {task_id}")
|
||||
logger.debug(f"[generate_video] Template fetched - task_id: {task_id}")
|
||||
|
||||
# 6-2. elements에서 리소스 매핑 생성
|
||||
modifications = creatomate_service.elements_connect_resource_blackbox(
|
||||
|
|
@ -285,7 +287,7 @@ async def generate_video(
|
|||
lyric=lyrics,
|
||||
music_url=music_url,
|
||||
)
|
||||
print(f"[generate_video] Modifications created - task_id: {task_id}")
|
||||
logger.debug(f"[generate_video] Modifications created - task_id: {task_id}")
|
||||
|
||||
# 6-3. elements 수정
|
||||
new_elements = creatomate_service.modify_element(
|
||||
|
|
@ -293,20 +295,20 @@ async def generate_video(
|
|||
modifications,
|
||||
)
|
||||
template["source"]["elements"] = new_elements
|
||||
print(f"[generate_video] Elements modified - task_id: {task_id}")
|
||||
logger.debug(f"[generate_video] Elements modified - task_id: {task_id}")
|
||||
|
||||
# 6-4. duration 확장
|
||||
final_template = creatomate_service.extend_template_duration(
|
||||
template,
|
||||
creatomate_service.target_duration,
|
||||
)
|
||||
print(f"[generate_video] Duration extended to {creatomate_service.target_duration}s - task_id: {task_id}")
|
||||
logger.debug(f"[generate_video] Duration extended to {creatomate_service.target_duration}s - task_id: {task_id}")
|
||||
|
||||
# 6-5. 커스텀 렌더링 요청 (비동기)
|
||||
render_response = await creatomate_service.make_creatomate_custom_call_async(
|
||||
final_template["source"],
|
||||
)
|
||||
print(f"[generate_video] Creatomate API response - task_id: {task_id}, response: {render_response}")
|
||||
logger.debug(f"[generate_video] Creatomate API response - task_id: {task_id}, response: {render_response}")
|
||||
|
||||
# 렌더 ID 추출
|
||||
if isinstance(render_response, list) and len(render_response) > 0:
|
||||
|
|
@ -317,14 +319,14 @@ async def generate_video(
|
|||
creatomate_render_id = None
|
||||
|
||||
stage2_time = time.perf_counter()
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_video] Stage 2 DONE - task_id: {task_id}, "
|
||||
f"render_id: {creatomate_render_id}, "
|
||||
f"stage2_elapsed: {(stage2_time - stage2_start)*1000:.1f}ms"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[generate_video] Creatomate API EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
logger.error(f"[generate_video] Creatomate API EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
# 외부 API 실패 시 Video 상태를 failed로 업데이트
|
||||
from app.database.session import AsyncSessionLocal
|
||||
async with AsyncSessionLocal() as update_session:
|
||||
|
|
@ -347,7 +349,7 @@ async def generate_video(
|
|||
# 3단계: creatomate_render_id 업데이트 (새 세션으로 빠르게 처리)
|
||||
# ==========================================================================
|
||||
stage3_start = time.perf_counter()
|
||||
print(f"[generate_video] Stage 3 START - DB update - task_id: {task_id}")
|
||||
logger.info(f"[generate_video] Stage 3 START - DB update - task_id: {task_id}")
|
||||
try:
|
||||
from app.database.session import AsyncSessionLocal
|
||||
async with AsyncSessionLocal() as update_session:
|
||||
|
|
@ -361,11 +363,11 @@ async def generate_video(
|
|||
|
||||
stage3_time = time.perf_counter()
|
||||
total_time = stage3_time - request_start
|
||||
print(
|
||||
logger.debug(
|
||||
f"[generate_video] Stage 3 DONE - task_id: {task_id}, "
|
||||
f"stage3_elapsed: {(stage3_time - stage3_start)*1000:.1f}ms"
|
||||
)
|
||||
print(
|
||||
logger.info(
|
||||
f"[generate_video] SUCCESS - task_id: {task_id}, "
|
||||
f"render_id: {creatomate_render_id}, "
|
||||
f"total_time: {total_time*1000:.1f}ms"
|
||||
|
|
@ -380,7 +382,7 @@ async def generate_video(
|
|||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[generate_video] Update EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
logger.error(f"[generate_video] Update EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
return GenerateVideoResponse(
|
||||
success=False,
|
||||
task_id=task_id,
|
||||
|
|
@ -439,11 +441,11 @@ async def get_video_status(
|
|||
succeeded 상태인 경우 백그라운드에서 MP4 파일을 다운로드하고
|
||||
Video 테이블의 status를 completed로, result_movie_url을 업데이트합니다.
|
||||
"""
|
||||
print(f"[get_video_status] START - creatomate_render_id: {creatomate_render_id}")
|
||||
logger.info(f"[get_video_status] START - creatomate_render_id: {creatomate_render_id}")
|
||||
try:
|
||||
creatomate_service = CreatomateService()
|
||||
result = await creatomate_service.get_render_status_async(creatomate_render_id)
|
||||
print(f"[get_video_status] Creatomate API response - creatomate_render_id: {creatomate_render_id}, status: {result.get('status')}")
|
||||
logger.debug(f"[get_video_status] Creatomate API response - creatomate_render_id: {creatomate_render_id}, status: {result.get('status')}")
|
||||
|
||||
status = result.get("status", "unknown")
|
||||
video_url = result.get("url")
|
||||
|
|
@ -481,7 +483,7 @@ async def get_video_status(
|
|||
store_name = project.store_name if project else "video"
|
||||
|
||||
# 백그라운드 태스크로 MP4 다운로드 → Blob 업로드 → DB 업데이트 → 임시 파일 삭제
|
||||
print(f"[get_video_status] Background task args - task_id: {video.task_id}, video_url: {video_url}, store_name: {store_name}")
|
||||
logger.info(f"[get_video_status] Background task args - task_id: {video.task_id}, video_url: {video_url}, store_name: {store_name}")
|
||||
background_tasks.add_task(
|
||||
download_and_upload_video_to_blob,
|
||||
task_id=video.task_id,
|
||||
|
|
@ -489,7 +491,7 @@ async def get_video_status(
|
|||
store_name=store_name,
|
||||
)
|
||||
elif video and video.status == "completed":
|
||||
print(f"[get_video_status] SKIPPED - Video already completed, creatomate_render_id: {creatomate_render_id}")
|
||||
logger.debug(f"[get_video_status] SKIPPED - Video already completed, creatomate_render_id: {creatomate_render_id}")
|
||||
|
||||
render_data = VideoRenderData(
|
||||
id=result.get("id"),
|
||||
|
|
@ -498,7 +500,7 @@ async def get_video_status(
|
|||
snapshot_url=result.get("snapshot_url"),
|
||||
)
|
||||
|
||||
print(f"[get_video_status] SUCCESS - creatomate_render_id: {creatomate_render_id}")
|
||||
logger.info(f"[get_video_status] SUCCESS - creatomate_render_id: {creatomate_render_id}")
|
||||
return PollingVideoResponse(
|
||||
success=True,
|
||||
status=status,
|
||||
|
|
@ -511,7 +513,7 @@ async def get_video_status(
|
|||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
print(f"[get_video_status] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}")
|
||||
logger.error(f"[get_video_status] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}")
|
||||
return PollingVideoResponse(
|
||||
success=False,
|
||||
status="error",
|
||||
|
|
@ -563,7 +565,7 @@ async def download_video(
|
|||
session: AsyncSession = Depends(get_session),
|
||||
) -> DownloadVideoResponse:
|
||||
"""task_id로 Video 상태를 polling하고 completed 시 Project 정보와 영상 URL을 반환합니다."""
|
||||
print(f"[download_video] START - task_id: {task_id}")
|
||||
logger.info(f"[download_video] START - task_id: {task_id}")
|
||||
try:
|
||||
# task_id로 Video 조회 (여러 개 있을 경우 가장 최근 것 선택)
|
||||
video_result = await session.execute(
|
||||
|
|
@ -575,7 +577,7 @@ async def download_video(
|
|||
video = video_result.scalar_one_or_none()
|
||||
|
||||
if not video:
|
||||
print(f"[download_video] Video NOT FOUND - task_id: {task_id}")
|
||||
logger.warning(f"[download_video] Video NOT FOUND - task_id: {task_id}")
|
||||
return DownloadVideoResponse(
|
||||
success=False,
|
||||
status="not_found",
|
||||
|
|
@ -583,11 +585,11 @@ async def download_video(
|
|||
error_message="Video not found",
|
||||
)
|
||||
|
||||
print(f"[download_video] Video found - task_id: {task_id}, status: {video.status}")
|
||||
logger.debug(f"[download_video] Video found - task_id: {task_id}, status: {video.status}")
|
||||
|
||||
# processing 상태인 경우
|
||||
if video.status == "processing":
|
||||
print(f"[download_video] PROCESSING - task_id: {task_id}")
|
||||
logger.debug(f"[download_video] PROCESSING - task_id: {task_id}")
|
||||
return DownloadVideoResponse(
|
||||
success=True,
|
||||
status="processing",
|
||||
|
|
@ -597,7 +599,7 @@ async def download_video(
|
|||
|
||||
# failed 상태인 경우
|
||||
if video.status == "failed":
|
||||
print(f"[download_video] FAILED - task_id: {task_id}")
|
||||
logger.error(f"[download_video] FAILED - task_id: {task_id}")
|
||||
return DownloadVideoResponse(
|
||||
success=False,
|
||||
status="failed",
|
||||
|
|
@ -612,7 +614,7 @@ async def download_video(
|
|||
)
|
||||
project = project_result.scalar_one_or_none()
|
||||
|
||||
print(f"[download_video] COMPLETED - task_id: {task_id}, result_movie_url: {video.result_movie_url}")
|
||||
logger.info(f"[download_video] COMPLETED - task_id: {task_id}, result_movie_url: {video.result_movie_url}")
|
||||
return DownloadVideoResponse(
|
||||
success=True,
|
||||
status="completed",
|
||||
|
|
@ -625,7 +627,7 @@ async def download_video(
|
|||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[download_video] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
logger.error(f"[download_video] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
return DownloadVideoResponse(
|
||||
success=False,
|
||||
status="error",
|
||||
|
|
@ -674,7 +676,7 @@ async def get_videos(
|
|||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
) -> PaginatedResponse[VideoListItem]:
|
||||
"""완료된 영상 목록을 페이지네이션하여 반환합니다."""
|
||||
print(f"[get_videos] START - page: {pagination.page}, page_size: {pagination.page_size}")
|
||||
logger.info(f"[get_videos] START - page: {pagination.page}, page_size: {pagination.page_size}")
|
||||
try:
|
||||
offset = (pagination.page - 1) * pagination.page_size
|
||||
|
||||
|
|
@ -732,14 +734,14 @@ async def get_videos(
|
|||
page_size=pagination.page_size,
|
||||
)
|
||||
|
||||
print(
|
||||
logger.info(
|
||||
f"[get_videos] SUCCESS - total: {total}, page: {pagination.page}, "
|
||||
f"page_size: {pagination.page_size}, items_count: {len(items)}"
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
print(f"[get_videos] EXCEPTION - error: {e}")
|
||||
logger.error(f"[get_videos] EXCEPTION - error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"영상 목록 조회에 실패했습니다: {str(e)}",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ from app.lyrics.schemas.lyrics_schema import (
|
|||
StoreData,
|
||||
)
|
||||
from app.utils.chatgpt_prompt import chatgpt_api
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("video")
|
||||
|
||||
|
||||
async def get_store_info(conn: Connection) -> List[StoreData]:
|
||||
|
|
@ -38,13 +41,13 @@ async def get_store_info(conn: Connection) -> List[StoreData]:
|
|||
result.close()
|
||||
return all_store_info
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemy error in get_store_info: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_store_info: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -69,13 +72,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
|
|||
result.close()
|
||||
return all_attribute
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemy error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -100,13 +103,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
|
|||
result.close()
|
||||
return all_attribute
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemy error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_attribute: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -132,13 +135,13 @@ async def get_sample_song(conn: Connection) -> List[SongSampleData]:
|
|||
result.close()
|
||||
return all_sample_song
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemy error in get_sample_song: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_sample_song: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -162,13 +165,13 @@ async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
|
|||
result.close()
|
||||
return all_prompt_template
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemy error in get_prompt_template: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_prompt_template: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -192,13 +195,13 @@ async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
|
|||
result.close()
|
||||
return all_prompt_template
|
||||
except SQLAlchemyError as e:
|
||||
print(e)
|
||||
logger.error(f"SQLAlchemy error in get_song_result: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(f"Unexpected error in get_song_result: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||
|
|
@ -210,11 +213,11 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
# 1. Form 데이터 파싱
|
||||
form_data = await SongFormData.from_form(request)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Store ID: {form_data.store_id}")
|
||||
print(f"Lyrics IDs: {form_data.lyrics_ids}")
|
||||
print(f"Prompt IDs: {form_data.prompts}")
|
||||
print(f"{'=' * 60}\n")
|
||||
logger.info(f"{'=' * 60}")
|
||||
logger.info(f"Store ID: {form_data.store_id}")
|
||||
logger.info(f"Lyrics IDs: {form_data.lyrics_ids}")
|
||||
logger.info(f"Prompt IDs: {form_data.prompts}")
|
||||
logger.info(f"{'=' * 60}")
|
||||
|
||||
# 2. Store 정보 조회
|
||||
store_query = """
|
||||
|
|
@ -243,7 +246,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
store_info = all_store_info[0]
|
||||
print(f"Store: {store_info.store_name}")
|
||||
logger.info(f"Store: {store_info.store_name}")
|
||||
|
||||
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||
|
||||
|
|
@ -251,7 +254,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
combined_sample_song = None
|
||||
|
||||
if form_data.lyrics_ids:
|
||||
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
|
||||
lyrics_query = """
|
||||
SELECT sample_song FROM song_sample
|
||||
|
|
@ -270,11 +273,11 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("선택된 lyrics가 없습니다")
|
||||
logger.info("선택된 lyrics가 없습니다")
|
||||
|
||||
# 5. 템플릿 가져오기
|
||||
if not form_data.prompts:
|
||||
|
|
@ -283,7 +286,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
detail="프롬프트 ID가 필요합니다",
|
||||
)
|
||||
|
||||
print("템플릿 가져오기")
|
||||
logger.info("템플릿 가져오기")
|
||||
|
||||
prompts_query = """
|
||||
SELECT * FROM prompt_template WHERE id=:id;
|
||||
|
|
@ -310,7 +313,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
prompt = prompts_info[0]
|
||||
print(f"Prompt Template: {prompt.prompt}")
|
||||
logger.debug(f"Prompt Template: {prompt.prompt}")
|
||||
|
||||
# ✅ 6. 프롬프트 조합
|
||||
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format(
|
||||
|
|
@ -329,7 +332,7 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
{combined_sample_song}
|
||||
"""
|
||||
|
||||
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
|
||||
logger.debug(f"[업데이트된 프롬프트]\n{updated_prompt}")
|
||||
|
||||
# 7. 모델에게 요청
|
||||
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
|
||||
|
|
@ -348,13 +351,12 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
||||
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
||||
|
||||
print("=" * 40)
|
||||
print("[translate:form_data.attributes_str:] ", form_data.attributes_str)
|
||||
print("[translate:total_chars_with_space:] ", total_chars_with_space)
|
||||
print("[translate:total_chars_without_space:] ", total_chars_without_space)
|
||||
print("[translate:final_lyrics:]")
|
||||
print(final_lyrics)
|
||||
print("=" * 40)
|
||||
logger.debug("=" * 40)
|
||||
logger.debug(f"[translate:form_data.attributes_str:] {form_data.attributes_str}")
|
||||
logger.debug(f"[translate:total_chars_with_space:] {total_chars_with_space}")
|
||||
logger.debug(f"[translate:total_chars_without_space:] {total_chars_without_space}")
|
||||
logger.debug(f"[translate:final_lyrics:]\n{final_lyrics}")
|
||||
logger.debug("=" * 40)
|
||||
|
||||
# 8. DB 저장
|
||||
insert_query = """
|
||||
|
|
@ -396,9 +398,9 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
await conn.execute(text(insert_query), insert_params)
|
||||
await conn.commit()
|
||||
|
||||
print("결과 저장 완료")
|
||||
logger.info("결과 저장 완료")
|
||||
|
||||
print("\n전체 결과 조회 중...")
|
||||
logger.info("전체 결과 조회 중...")
|
||||
|
||||
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||
select_query = """
|
||||
|
|
@ -430,26 +432,20 @@ async def make_song_result(request: Request, conn: Connection):
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
@ -490,25 +486,19 @@ async def get_song_result(conn: Connection): # 반환 타입 수정
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
except HTTPException: # HTTPException은 그대로 raise
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
@ -520,9 +510,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
# 1. Form 데이터 파싱
|
||||
form_data = await SongFormData.from_form(request)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Store ID: {form_data.store_id}")
|
||||
print(f"{'=' * 60}\n")
|
||||
logger.info(f"{'=' * 60}")
|
||||
logger.info(f"Store ID: {form_data.store_id}")
|
||||
logger.info(f"{'=' * 60}")
|
||||
|
||||
# 2. Store 정보 조회
|
||||
store_query = """
|
||||
|
|
@ -551,7 +541,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
)
|
||||
|
||||
store_info = all_store_info[0]
|
||||
print(f"Store: {store_info.store_name}")
|
||||
logger.info(f"Store: {store_info.store_name}")
|
||||
|
||||
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||
attribute_query = """
|
||||
|
|
@ -596,13 +586,13 @@ async def make_automation(request: Request, conn: Connection):
|
|||
# 최종 문자열 생성
|
||||
formatted_attributes = "\n".join(formatted_pairs)
|
||||
|
||||
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n")
|
||||
logger.debug(f"[포맷팅된 문자열 속성 정보]\n{formatted_attributes}")
|
||||
else:
|
||||
print("속성 데이터가 없습니다")
|
||||
logger.info("속성 데이터가 없습니다")
|
||||
formatted_attributes = ""
|
||||
|
||||
# 4. 템플릿 가져오기
|
||||
print("템플릿 가져오기 (ID=1)")
|
||||
logger.info("템플릿 가져오기 (ID=1)")
|
||||
|
||||
prompts_query = """
|
||||
SELECT * FROM prompt_template WHERE id=1;
|
||||
|
|
@ -624,7 +614,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
prompt=row[2],
|
||||
)
|
||||
|
||||
print(f"Prompt Template: {prompt.prompt}")
|
||||
logger.debug(f"Prompt Template: {prompt.prompt}")
|
||||
|
||||
# 5. 템플릿 조합
|
||||
|
||||
|
|
@ -635,17 +625,17 @@ async def make_automation(request: Request, conn: Connection):
|
|||
description=store_info.store_info or "",
|
||||
)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("업데이트된 프롬프트")
|
||||
print("=" * 80)
|
||||
print(updated_prompt)
|
||||
print("=" * 80 + "\n")
|
||||
logger.debug("=" * 80)
|
||||
logger.debug("업데이트된 프롬프트")
|
||||
logger.debug("=" * 80)
|
||||
logger.debug(updated_prompt)
|
||||
logger.debug("=" * 80)
|
||||
|
||||
# 4. Sample Song 조회 및 결합
|
||||
combined_sample_song = None
|
||||
|
||||
if form_data.lyrics_ids:
|
||||
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||
|
||||
lyrics_query = """
|
||||
SELECT sample_song FROM song_sample
|
||||
|
|
@ -664,14 +654,14 @@ async def make_automation(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("선택된 lyrics가 없습니다")
|
||||
logger.info("선택된 lyrics가 없습니다")
|
||||
|
||||
# 1. song_sample 테이블의 모든 ID 조회
|
||||
print("\n[샘플 가사 랜덤 선택]")
|
||||
logger.info("[샘플 가사 랜덤 선택]")
|
||||
|
||||
all_ids_query = """
|
||||
SELECT id FROM song_sample;
|
||||
|
|
@ -679,7 +669,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
ids_result = await conn.execute(text(all_ids_query))
|
||||
all_ids = [row.id for row in ids_result.fetchall()]
|
||||
|
||||
print(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
||||
logger.info(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
||||
|
||||
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
|
||||
combined_sample_song = None
|
||||
|
|
@ -689,7 +679,7 @@ async def make_automation(request: Request, conn: Connection):
|
|||
sample_count = min(3, len(all_ids))
|
||||
selected_ids = random.sample(all_ids, sample_count)
|
||||
|
||||
print(f"랜덤 선택된 ID: {selected_ids}")
|
||||
logger.info(f"랜덤 선택된 ID: {selected_ids}")
|
||||
|
||||
# 3. 선택된 ID로 샘플 가사 조회
|
||||
lyrics_query = """
|
||||
|
|
@ -710,11 +700,11 @@ async def make_automation(request: Request, conn: Connection):
|
|||
combined_sample_song = "\n\n".join(
|
||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||
)
|
||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||
else:
|
||||
print("샘플 가사가 비어있습니다")
|
||||
logger.info("샘플 가사가 비어있습니다")
|
||||
else:
|
||||
print("song_sample 테이블에 데이터가 없습니다")
|
||||
logger.info("song_sample 테이블에 데이터가 없습니다")
|
||||
|
||||
# 5. 프롬프트에 샘플 가사 추가
|
||||
if combined_sample_song:
|
||||
|
|
@ -726,11 +716,11 @@ async def make_automation(request: Request, conn: Connection):
|
|||
|
||||
{combined_sample_song}
|
||||
"""
|
||||
print("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
||||
logger.info("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
||||
else:
|
||||
print("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
||||
logger.info("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
||||
|
||||
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
|
||||
logger.debug(f"[최종 프롬프트 길이: {len(updated_prompt)} 자]")
|
||||
|
||||
# 7. 모델에게 요청
|
||||
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
|
||||
|
|
@ -763,10 +753,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
:sample_song, :result_song, NOW()
|
||||
);
|
||||
"""
|
||||
print("\n[insert_params 선택된 속성 확인]")
|
||||
print(f"Categories: {selected_categories}")
|
||||
print(f"Values: {selected_values}")
|
||||
print()
|
||||
logger.debug("[insert_params 선택된 속성 확인]")
|
||||
logger.debug(f"Categories: {selected_categories}")
|
||||
logger.debug(f"Values: {selected_values}")
|
||||
|
||||
# attr_category, attr_value
|
||||
insert_params = {
|
||||
|
|
@ -792,9 +781,9 @@ async def make_automation(request: Request, conn: Connection):
|
|||
await conn.execute(text(insert_query), insert_params)
|
||||
await conn.commit()
|
||||
|
||||
print("결과 저장 완료")
|
||||
logger.info("결과 저장 완료")
|
||||
|
||||
print("\n전체 결과 조회 중...")
|
||||
logger.info("전체 결과 조회 중...")
|
||||
|
||||
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||
select_query = """
|
||||
|
|
@ -826,26 +815,20 @@ async def make_automation(request: Request, conn: Connection):
|
|||
for row in all_results.fetchall()
|
||||
]
|
||||
|
||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
|
||||
|
||||
return results_list
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
print(f"Database Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Database Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Unexpected Error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.error(f"Unexpected Error: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ Video Background Tasks
|
|||
영상 생성 관련 백그라운드 태스크를 정의합니다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -16,9 +15,10 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||
from app.database.session import BackgroundSessionLocal
|
||||
from app.video.models import Video
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
# 로거 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger("video")
|
||||
|
||||
# HTTP 요청 설정
|
||||
REQUEST_TIMEOUT = 300.0 # 초 (영상은 용량이 크므로 5분)
|
||||
|
|
@ -66,20 +66,16 @@ async def _update_video_status(
|
|||
video.result_movie_url = video_url
|
||||
await session.commit()
|
||||
logger.info(f"[Video] Status updated - task_id: {task_id}, status: {status}")
|
||||
print(f"[Video] Status updated - task_id: {task_id}, status: {status}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"[Video] NOT FOUND in DB - task_id: {task_id}")
|
||||
print(f"[Video] NOT FOUND in DB - task_id: {task_id}")
|
||||
return False
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[Video] DB Error while updating status - task_id: {task_id}, error: {e}")
|
||||
print(f"[Video] DB Error while updating status - task_id: {task_id}, error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"[Video] Unexpected error while updating status - task_id: {task_id}, error: {e}")
|
||||
print(f"[Video] Unexpected error while updating status - task_id: {task_id}, error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -97,14 +93,12 @@ async def _download_video(url: str, task_id: str) -> bytes:
|
|||
httpx.HTTPError: 다운로드 실패 시
|
||||
"""
|
||||
logger.info(f"[VideoDownload] Downloading - task_id: {task_id}")
|
||||
print(f"[VideoDownload] Downloading - task_id: {task_id}")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, timeout=REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
|
||||
logger.info(f"[VideoDownload] SUCCESS - task_id: {task_id}, size: {len(response.content)} bytes")
|
||||
print(f"[VideoDownload] SUCCESS - task_id: {task_id}, size: {len(response.content)} bytes")
|
||||
return response.content
|
||||
|
||||
|
||||
|
|
@ -121,7 +115,6 @@ async def download_and_upload_video_to_blob(
|
|||
store_name: 저장할 파일명에 사용할 업체명
|
||||
"""
|
||||
logger.info(f"[download_and_upload_video_to_blob] START - task_id: {task_id}, store_name: {store_name}")
|
||||
print(f"[download_and_upload_video_to_blob] START - task_id: {task_id}, store_name: {store_name}")
|
||||
temp_file_path: Path | None = None
|
||||
|
||||
try:
|
||||
|
|
@ -136,12 +129,10 @@ async def download_and_upload_video_to_blob(
|
|||
temp_dir = Path("media") / "temp" / task_id
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_file_path = temp_dir / file_name
|
||||
logger.info(f"[download_and_upload_video_to_blob] Temp directory created - path: {temp_file_path}")
|
||||
print(f"[download_and_upload_video_to_blob] Temp directory created - path: {temp_file_path}")
|
||||
logger.debug(f"[download_and_upload_video_to_blob] Temp directory created - path: {temp_file_path}")
|
||||
|
||||
# 영상 파일 다운로드
|
||||
logger.info(f"[download_and_upload_video_to_blob] Downloading video - task_id: {task_id}, url: {video_url}")
|
||||
print(f"[download_and_upload_video_to_blob] Downloading video - task_id: {task_id}, url: {video_url}")
|
||||
|
||||
content = await _download_video(video_url, task_id)
|
||||
|
||||
|
|
@ -149,7 +140,6 @@ async def download_and_upload_video_to_blob(
|
|||
await f.write(content)
|
||||
|
||||
logger.info(f"[download_and_upload_video_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}")
|
||||
print(f"[download_and_upload_video_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}")
|
||||
|
||||
# Azure Blob Storage에 업로드
|
||||
uploader = AzureBlobUploader(task_id=task_id)
|
||||
|
|
@ -161,29 +151,21 @@ async def download_and_upload_video_to_blob(
|
|||
# SAS 토큰이 제외된 public_url 사용
|
||||
blob_url = uploader.public_url
|
||||
logger.info(f"[download_and_upload_video_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
|
||||
print(f"[download_and_upload_video_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
|
||||
|
||||
# Video 테이블 업데이트
|
||||
await _update_video_status(task_id, "completed", blob_url)
|
||||
logger.info(f"[download_and_upload_video_to_blob] SUCCESS - task_id: {task_id}")
|
||||
print(f"[download_and_upload_video_to_blob] SUCCESS - task_id: {task_id}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"[download_and_upload_video_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_upload_video_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_video_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_video_status(task_id, "failed")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[download_and_upload_video_to_blob] DB ERROR - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_upload_video_to_blob] DB ERROR - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_video_to_blob] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_video_status(task_id, "failed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[download_and_upload_video_to_blob] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
print(f"[download_and_upload_video_to_blob] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_video_to_blob] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
|
||||
await _update_video_status(task_id, "failed")
|
||||
|
||||
finally:
|
||||
|
|
@ -191,11 +173,9 @@ async def download_and_upload_video_to_blob(
|
|||
if temp_file_path and temp_file_path.exists():
|
||||
try:
|
||||
temp_file_path.unlink()
|
||||
logger.info(f"[download_and_upload_video_to_blob] Temp file deleted - path: {temp_file_path}")
|
||||
print(f"[download_and_upload_video_to_blob] Temp file deleted - path: {temp_file_path}")
|
||||
logger.debug(f"[download_and_upload_video_to_blob] Temp file deleted - path: {temp_file_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[download_and_upload_video_to_blob] Failed to delete temp file: {e}")
|
||||
print(f"[download_and_upload_video_to_blob] Failed to delete temp file: {e}")
|
||||
|
||||
# 임시 디렉토리 삭제 시도
|
||||
temp_dir = Path("media") / "temp" / task_id
|
||||
|
|
@ -219,7 +199,6 @@ async def download_and_upload_video_by_creatomate_render_id(
|
|||
store_name: 저장할 파일명에 사용할 업체명
|
||||
"""
|
||||
logger.info(f"[download_and_upload_video_by_creatomate_render_id] START - creatomate_render_id: {creatomate_render_id}, store_name: {store_name}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] START - creatomate_render_id: {creatomate_render_id}, store_name: {store_name}")
|
||||
temp_file_path: Path | None = None
|
||||
task_id: str | None = None
|
||||
|
||||
|
|
@ -236,12 +215,10 @@ async def download_and_upload_video_by_creatomate_render_id(
|
|||
|
||||
if not video:
|
||||
logger.warning(f"[download_and_upload_video_by_creatomate_render_id] Video NOT FOUND - creatomate_render_id: {creatomate_render_id}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] Video NOT FOUND - creatomate_render_id: {creatomate_render_id}")
|
||||
return
|
||||
|
||||
task_id = video.task_id
|
||||
logger.info(f"[download_and_upload_video_by_creatomate_render_id] Video found - creatomate_render_id: {creatomate_render_id}, task_id: {task_id}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] Video found - creatomate_render_id: {creatomate_render_id}, task_id: {task_id}")
|
||||
|
||||
# 파일명에 사용할 수 없는 문자 제거
|
||||
safe_store_name = "".join(
|
||||
|
|
@ -254,12 +231,10 @@ async def download_and_upload_video_by_creatomate_render_id(
|
|||
temp_dir = Path("media") / "temp" / task_id
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
temp_file_path = temp_dir / file_name
|
||||
logger.info(f"[download_and_upload_video_by_creatomate_render_id] Temp directory created - path: {temp_file_path}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] Temp directory created - path: {temp_file_path}")
|
||||
logger.debug(f"[download_and_upload_video_by_creatomate_render_id] Temp directory created - path: {temp_file_path}")
|
||||
|
||||
# 영상 파일 다운로드
|
||||
logger.info(f"[download_and_upload_video_by_creatomate_render_id] Downloading video - creatomate_render_id: {creatomate_render_id}, url: {video_url}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] Downloading video - creatomate_render_id: {creatomate_render_id}, url: {video_url}")
|
||||
|
||||
content = await _download_video(video_url, task_id)
|
||||
|
||||
|
|
@ -267,7 +242,6 @@ async def download_and_upload_video_by_creatomate_render_id(
|
|||
await f.write(content)
|
||||
|
||||
logger.info(f"[download_and_upload_video_by_creatomate_render_id] File downloaded - creatomate_render_id: {creatomate_render_id}, path: {temp_file_path}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] File downloaded - creatomate_render_id: {creatomate_render_id}, path: {temp_file_path}")
|
||||
|
||||
# Azure Blob Storage에 업로드
|
||||
uploader = AzureBlobUploader(task_id=task_id)
|
||||
|
|
@ -279,7 +253,6 @@ async def download_and_upload_video_by_creatomate_render_id(
|
|||
# SAS 토큰이 제외된 public_url 사용
|
||||
blob_url = uploader.public_url
|
||||
logger.info(f"[download_and_upload_video_by_creatomate_render_id] Uploaded to Blob - creatomate_render_id: {creatomate_render_id}, url: {blob_url}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] Uploaded to Blob - creatomate_render_id: {creatomate_render_id}, url: {blob_url}")
|
||||
|
||||
# Video 테이블 업데이트
|
||||
await _update_video_status(
|
||||
|
|
@ -289,26 +262,19 @@ async def download_and_upload_video_by_creatomate_render_id(
|
|||
creatomate_render_id=creatomate_render_id,
|
||||
)
|
||||
logger.info(f"[download_and_upload_video_by_creatomate_render_id] SUCCESS - creatomate_render_id: {creatomate_render_id}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] SUCCESS - creatomate_render_id: {creatomate_render_id}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"[download_and_upload_video_by_creatomate_render_id] DOWNLOAD ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] DOWNLOAD ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_video_by_creatomate_render_id] DOWNLOAD ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
|
||||
if task_id:
|
||||
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"[download_and_upload_video_by_creatomate_render_id] DB ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] DB ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_video_by_creatomate_render_id] DB ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
|
||||
if task_id:
|
||||
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[download_and_upload_video_by_creatomate_render_id] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}")
|
||||
traceback.print_exc()
|
||||
logger.error(f"[download_and_upload_video_by_creatomate_render_id] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
|
||||
if task_id:
|
||||
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
|
||||
|
||||
|
|
@ -317,11 +283,9 @@ async def download_and_upload_video_by_creatomate_render_id(
|
|||
if temp_file_path and temp_file_path.exists():
|
||||
try:
|
||||
temp_file_path.unlink()
|
||||
logger.info(f"[download_and_upload_video_by_creatomate_render_id] Temp file deleted - path: {temp_file_path}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] Temp file deleted - path: {temp_file_path}")
|
||||
logger.debug(f"[download_and_upload_video_by_creatomate_render_id] Temp file deleted - path: {temp_file_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[download_and_upload_video_by_creatomate_render_id] Failed to delete temp file: {e}")
|
||||
print(f"[download_and_upload_video_by_creatomate_render_id] Failed to delete temp file: {e}")
|
||||
|
||||
# 임시 디렉토리 삭제 시도
|
||||
if task_id:
|
||||
|
|
|
|||
223
config.py
223
config.py
|
|
@ -5,6 +5,10 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||
|
||||
PROJECT_DIR = Path(__file__).resolve().parent
|
||||
|
||||
# 미디어 파일 저장 디렉토리
|
||||
MEDIA_ROOT = PROJECT_DIR / "media"
|
||||
MEDIA_ROOT.mkdir(exist_ok=True)
|
||||
|
||||
_base_config = SettingsConfigDict(
|
||||
env_file=PROJECT_DIR / ".env",
|
||||
env_ignore_empty=True,
|
||||
|
|
@ -95,32 +99,6 @@ class DatabaseSettings(BaseSettings):
|
|||
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{db}"
|
||||
|
||||
|
||||
class SecuritySettings(BaseSettings):
|
||||
JWT_SECRET: str = "your-jwt-secret-key" # 기본값 추가 (필수 필드 안전)
|
||||
JWT_ALGORITHM: str = "HS256" # 기본값 추가 (필수 필드 안전)
|
||||
|
||||
model_config = _base_config
|
||||
|
||||
|
||||
class NotificationSettings(BaseSettings):
|
||||
MAIL_USERNAME: str = "your-email@example.com" # 기본값 추가
|
||||
MAIL_PASSWORD: str = "your-email-password" # 기본값 추가
|
||||
MAIL_FROM: str = "your-email@example.com" # 기본값 추가
|
||||
MAIL_PORT: int = 587 # 기본값 추가
|
||||
MAIL_SERVER: str = "smtp.gmail.com" # 기본값 추가
|
||||
MAIL_FROM_NAME: str = "FastPOC App" # 기본값 추가
|
||||
MAIL_STARTTLS: bool = True
|
||||
MAIL_SSL_TLS: bool = False
|
||||
USE_CREDENTIALS: bool = True
|
||||
VALIDATE_CERTS: bool = True
|
||||
|
||||
TWILIO_SID: str = "your-twilio-sid" # 기본값 추가
|
||||
TWILIO_AUTH_TOKEN: str = "your-twilio-token" # 기본값 추가
|
||||
TWILIO_NUMBER: str = "+1234567890" # 기본값 추가
|
||||
|
||||
model_config = _base_config
|
||||
|
||||
|
||||
class CrawlerSettings(BaseSettings):
|
||||
NAVER_COOKIES: str = Field(default="")
|
||||
|
||||
|
|
@ -168,12 +146,205 @@ class CreatomateSettings(BaseSettings):
|
|||
model_config = _base_config
|
||||
|
||||
|
||||
class LogSettings(BaseSettings):
|
||||
"""
|
||||
로깅 설정 클래스
|
||||
|
||||
애플리케이션의 로깅 동작을 제어하는 설정들을 관리합니다.
|
||||
모든 설정은 .env 파일 또는 환경변수로 오버라이드 가능합니다.
|
||||
|
||||
사용 예시 (.env 파일):
|
||||
LOG_LEVEL=INFO
|
||||
LOG_CONSOLE_LEVEL=WARNING
|
||||
LOG_FILE_LEVEL=DEBUG
|
||||
LOG_DIR=/var/log/myapp
|
||||
"""
|
||||
|
||||
# ============================================================
|
||||
# 로그 디렉토리 설정
|
||||
# ============================================================
|
||||
# 로그 파일이 저장될 디렉토리 경로입니다.
|
||||
# - 기본값: 프로젝트 루트의 logs 폴더
|
||||
# - 운영 환경에서는 /www/log/uvicorn 또는 /var/log/app 등으로 설정 권장
|
||||
# - 디렉토리가 존재하지 않으면 자동으로 생성됩니다.
|
||||
# - .env 파일에서 LOG_DIR 환경변수로 오버라이드 가능
|
||||
LOG_DIR: str = Field(
|
||||
default="logs",
|
||||
description="로그 파일 저장 디렉토리 (절대 경로 또는 상대 경로)",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 로그 출력 대상 설정
|
||||
# ============================================================
|
||||
# 콘솔 출력 활성화 여부
|
||||
# - True: 터미널/콘솔에 로그 출력
|
||||
# - False: 콘솔 출력 비활성화 (파일에만 기록)
|
||||
LOG_CONSOLE_ENABLED: bool = Field(
|
||||
default=True,
|
||||
description="콘솔 로그 출력 활성화 여부",
|
||||
)
|
||||
|
||||
# 파일 출력 활성화 여부
|
||||
# - True: 로그 파일에 기록 (app.log, error.log)
|
||||
# - False: 파일 출력 비활성화 (콘솔에만 출력)
|
||||
LOG_FILE_ENABLED: bool = Field(
|
||||
default=True,
|
||||
description="파일 로그 출력 활성화 여부",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 로그 레벨 설정
|
||||
# ============================================================
|
||||
# 로그 레벨 우선순위 (낮음 → 높음):
|
||||
# DEBUG < INFO < WARNING < ERROR < CRITICAL
|
||||
#
|
||||
# 설정된 레벨 이상의 로그만 출력됩니다.
|
||||
# 예: INFO로 설정 시 DEBUG는 무시되고, INFO, WARNING, ERROR, CRITICAL만 출력
|
||||
# ============================================================
|
||||
|
||||
# 기본 로그 레벨
|
||||
# - 로거 자체의 최소 로그 레벨을 설정합니다.
|
||||
# - 이 레벨보다 낮은 로그는 핸들러(콘솔/파일)로 전달되지 않습니다.
|
||||
# - 가능한 값: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# - DEBUG: 개발 시 상세 디버깅 정보 (변수 값, 흐름 추적 등)
|
||||
# - INFO: 일반적인 작업 진행 상황 (요청 시작/완료 등)
|
||||
# - WARNING: 잠재적 문제 또는 주의가 필요한 상황
|
||||
# - ERROR: 오류 발생, 하지만 애플리케이션은 계속 실행
|
||||
# - CRITICAL: 심각한 오류, 애플리케이션 중단 가능성
|
||||
LOG_LEVEL: str = Field(
|
||||
default="DEBUG",
|
||||
description="기본 로그 레벨",
|
||||
)
|
||||
|
||||
# 콘솔 출력 로그 레벨
|
||||
# - 터미널/콘솔에 출력되는 로그의 최소 레벨을 설정합니다.
|
||||
# - 개발 환경: DEBUG 권장 (모든 로그 확인)
|
||||
# - 운영 환경: INFO 또는 WARNING 권장 (중요한 정보만 출력)
|
||||
# - LOG_LEVEL보다 낮게 설정해도 LOG_LEVEL이 우선 적용됩니다.
|
||||
LOG_CONSOLE_LEVEL: str = Field(
|
||||
default="DEBUG",
|
||||
description="콘솔 출력 로그 레벨",
|
||||
)
|
||||
|
||||
# 파일 출력 로그 레벨
|
||||
# - 로그 파일에 기록되는 로그의 최소 레벨을 설정합니다.
|
||||
# - 파일에는 더 상세한 로그를 남기고 싶을 때 DEBUG로 설정
|
||||
# - 파일 저장 위치: logs/{날짜}_{모듈명}.log
|
||||
# - 에러 로그는 별도로 logs/{날짜}_error.log에도 기록됩니다.
|
||||
LOG_FILE_LEVEL: str = Field(
|
||||
default="DEBUG",
|
||||
description="파일 출력 로그 레벨",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 로그 파일 관리 설정
|
||||
# ============================================================
|
||||
|
||||
# 로그 파일 최대 크기 (MB)
|
||||
# - 파일이 이 크기를 초과하면 자동으로 새 파일로 롤오버됩니다.
|
||||
# - 기존 파일은 .1, .2 등의 접미사가 붙어 백업됩니다.
|
||||
# - 예: 15MB 설정 시, 파일이 15MB를 넘으면 새 파일 생성
|
||||
LOG_MAX_SIZE_MB: int = Field(
|
||||
default=15,
|
||||
description="로그 파일 최대 크기 (MB)",
|
||||
)
|
||||
|
||||
# 로그 파일 백업 개수
|
||||
# - 롤오버 시 보관할 백업 파일의 최대 개수입니다.
|
||||
# - 이 개수를 초과하면 가장 오래된 백업 파일이 삭제됩니다.
|
||||
# - 예: 30 설정 시, 최대 30개의 백업 파일 유지
|
||||
# - 디스크 용량 관리를 위해 적절한 값 설정 권장
|
||||
LOG_BACKUP_COUNT: int = Field(
|
||||
default=30,
|
||||
description="로그 파일 백업 개수",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 로그 포맷 설정
|
||||
# ============================================================
|
||||
# 사용 가능한 포맷 변수:
|
||||
# {asctime} - 로그 발생 시간 (LOG_DATE_FORMAT 형식)
|
||||
# {levelname} - 로그 레벨 (DEBUG, INFO 등)
|
||||
# {name} - 로거 이름 (home, song 등)
|
||||
# {filename} - 소스 파일명
|
||||
# {funcName} - 함수명
|
||||
# {lineno} - 라인 번호
|
||||
# {message} - 로그 메시지
|
||||
# {module} - 모듈명
|
||||
# {pathname} - 파일 전체 경로
|
||||
#
|
||||
# 포맷 예시:
|
||||
# "[{asctime}] {levelname} {message}"
|
||||
# 출력: [2024-01-14 15:30:00] INFO 서버 시작
|
||||
# ============================================================
|
||||
|
||||
# 콘솔 로그 포맷
|
||||
# - 터미널에 출력되는 로그의 형식을 지정합니다.
|
||||
# - [{levelname}]은 로그 레벨을 대괄호로 감싸서 출력합니다.
|
||||
LOG_CONSOLE_FORMAT: str = Field(
|
||||
default="[{asctime}] [{levelname}] [{name}:{funcName}:{lineno}] {message}",
|
||||
description="콘솔 로그 포맷",
|
||||
)
|
||||
|
||||
# 파일 로그 포맷
|
||||
# - 파일에 기록되는 로그의 형식을 지정합니다.
|
||||
# - 파일에는 더 상세한 정보(filename 등)를 포함할 수 있습니다.
|
||||
LOG_FILE_FORMAT: str = Field(
|
||||
default="[{asctime}] [{levelname}] [{filename}:{name} -> {funcName}():{lineno}] {message}",
|
||||
description="파일 로그 포맷",
|
||||
)
|
||||
|
||||
# 날짜 포맷
|
||||
# - {asctime}에 표시되는 시간의 형식을 지정합니다.
|
||||
# - Python strftime 형식을 따릅니다.
|
||||
# - 예시:
|
||||
# "%Y-%m-%d %H:%M:%S" -> 2024-01-14 15:30:00
|
||||
# "%Y/%m/%d %H:%M:%S.%f" -> 2024/01/14 15:30:00.123456
|
||||
# "%d-%b-%Y %H:%M:%S" -> 14-Jan-2024 15:30:00
|
||||
LOG_DATE_FORMAT: str = Field(
|
||||
default="%Y-%m-%d %H:%M:%S",
|
||||
description="로그 날짜 포맷",
|
||||
)
|
||||
|
||||
model_config = _base_config
|
||||
|
||||
def get_log_dir(self) -> Path:
|
||||
"""
|
||||
로그 디렉토리 경로를 반환합니다.
|
||||
|
||||
우선순위:
|
||||
1. .env의 LOG_DIR 설정값 (절대 경로인 경우)
|
||||
2. /www/log/uvicorn 폴더가 존재하면 사용 (운영 서버)
|
||||
3. 프로젝트 루트의 logs 폴더 (개발 환경 기본값)
|
||||
|
||||
Returns:
|
||||
Path: 로그 디렉토리 경로 (존재하지 않으면 자동 생성)
|
||||
"""
|
||||
# 1. .env에서 설정한 경로가 절대 경로인 경우 우선 사용
|
||||
log_dir_path = Path(self.LOG_DIR)
|
||||
if log_dir_path.is_absolute():
|
||||
log_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
return log_dir_path
|
||||
|
||||
# 2. 운영 서버 경로 확인 (/www/log/uvicorn)
|
||||
production_log_dir = Path("/www/log/uvicorn")
|
||||
if production_log_dir.exists():
|
||||
return production_log_dir
|
||||
|
||||
# 3. 기본값: 프로젝트 루트의 logs 폴더
|
||||
default_log_dir = PROJECT_DIR / self.LOG_DIR
|
||||
default_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
return default_log_dir
|
||||
|
||||
|
||||
prj_settings = ProjectSettings()
|
||||
apikey_settings = APIKeySettings()
|
||||
db_settings = DatabaseSettings()
|
||||
security_settings = SecuritySettings()
|
||||
kakao_settings = KakaoSettings()
|
||||
notification_settings = NotificationSettings()
|
||||
cors_settings = CORSSettings()
|
||||
crawler_settings = CrawlerSettings()
|
||||
azure_blob_settings = AzureBlobSettings()
|
||||
creatomate_settings = CreatomateSettings()
|
||||
log_settings = LogSettings()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,331 @@
|
|||
# 카카오 소셜 로그인 구현 가이드
|
||||
|
||||
## 목차
|
||||
|
||||
1. [개요](#1-개요)
|
||||
2. [인증 흐름](#2-인증-흐름)
|
||||
3. [User 모델 구조](#3-user-모델-구조)
|
||||
4. [API 엔드포인트](#4-api-엔드포인트)
|
||||
5. [환경 설정](#5-환경-설정)
|
||||
6. [에러 처리](#6-에러-처리)
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
CastAD는 카카오 소셜 로그인만 지원하며, 인증 후 자체 JWT 토큰을 발급합니다.
|
||||
|
||||
### 인증 방식
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| 소셜 로그인 | 카카오 OAuth 2.0 |
|
||||
| 자체 인증 | JWT (Access Token + Refresh Token) |
|
||||
| 카카오 토큰 저장 | X (1회 검증 후 폐기) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 인증 흐름
|
||||
|
||||
### 2.1 전체 흐름도
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Client │ │ Backend │ │ Kakao │ │ DB │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │ │
|
||||
│ 1. 로그인 요청 │ │ │
|
||||
│──────────────>│ │ │
|
||||
│ │ │ │
|
||||
│ 2. 카카오 로그인 URL 반환 │ │
|
||||
│<──────────────│ │ │
|
||||
│ │ │ │
|
||||
│ 3. 카카오 로그인 페이지로 이동 │ │
|
||||
│──────────────────────────────>│ │
|
||||
│ │ │ │
|
||||
│ 4. 사용자 인증 후 code 반환 │ │
|
||||
│<──────────────────────────────│ │
|
||||
│ │ │ │
|
||||
│ 5. code 전달 │ │ │
|
||||
│──────────────>│ │ │
|
||||
│ │ │ │
|
||||
│ │ 6. code로 토큰 요청 │
|
||||
│ │──────────────>│ │
|
||||
│ │ │ │
|
||||
│ │ 7. access_token 반환 │
|
||||
│ │<──────────────│ │
|
||||
│ │ │ │
|
||||
│ │ 8. 사용자 정보 조회 │
|
||||
│ │──────────────>│ │
|
||||
│ │ │ │
|
||||
│ │ 9. 사용자 정보 반환 │
|
||||
│ │<──────────────│ │
|
||||
│ │ │ │
|
||||
│ │ 10. kakao_id로 회원 조회 │
|
||||
│ │──────────────────────────────>│
|
||||
│ │ │ │
|
||||
│ │ 11. 회원 정보 반환 (없으면 생성) │
|
||||
│ │<──────────────────────────────│
|
||||
│ │ │ │
|
||||
│ 12. 자체 JWT 발급 및 반환 │ │
|
||||
│<──────────────│ │ │
|
||||
│ │ │ │
|
||||
```
|
||||
|
||||
### 2.2 단계별 설명
|
||||
|
||||
| 단계 | 설명 | 관련 API |
|
||||
|------|------|----------|
|
||||
| 1-2 | 클라이언트가 로그인 요청, 백엔드가 카카오 인증 URL 생성 | `GET /api/v1/auth/kakao/login` |
|
||||
| 3-4 | 사용자가 카카오에서 로그인, 인가 코드(code) 발급 | 카카오 OAuth |
|
||||
| 5-9 | 백엔드가 code로 카카오 토큰/사용자정보 획득 | 카카오 API |
|
||||
| 10-11 | DB에서 회원 조회, 없으면 신규 가입 | 내부 처리 |
|
||||
| 12 | 자체 JWT 토큰 발급 후 클라이언트에 반환 | `POST /api/v1/auth/kakao/callback` |
|
||||
|
||||
---
|
||||
|
||||
## 3. User 모델 구조
|
||||
|
||||
### 3.1 테이블 스키마
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ user │
|
||||
├─────────────────────┬───────────────┬───────────────────────┤
|
||||
│ Column │ Type │ Description │
|
||||
├─────────────────────┼───────────────┼───────────────────────┤
|
||||
│ id │ BIGINT (PK) │ 고유 식별자 (자동증가) │
|
||||
│ kakao_id │ BIGINT (UQ) │ 카카오 회원번호 │
|
||||
│ email │ VARCHAR(255) │ 이메일 (선택) │
|
||||
│ nickname │ VARCHAR(100) │ 닉네임 (선택) │
|
||||
│ profile_image_url │ VARCHAR(2048) │ 프로필 이미지 URL │
|
||||
│ thumbnail_image_url │ VARCHAR(2048) │ 썸네일 이미지 URL │
|
||||
│ is_active │ BOOLEAN │ 계정 활성화 상태 │
|
||||
│ is_admin │ BOOLEAN │ 관리자 권한 │
|
||||
│ last_login_at │ DATETIME │ 마지막 로그인 일시 │
|
||||
│ created_at │ DATETIME │ 생성 일시 │
|
||||
│ updated_at │ DATETIME │ 수정 일시 │
|
||||
└─────────────────────┴───────────────┴───────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 카카오 API 응답 매핑
|
||||
|
||||
```json
|
||||
// 카카오 API 응답 예시
|
||||
{
|
||||
"id": 1234567890,
|
||||
"kakao_account": {
|
||||
"email": "user@kakao.com",
|
||||
"profile": {
|
||||
"nickname": "홍길동",
|
||||
"profile_image_url": "https://k.kakaocdn.net/.../profile.jpg",
|
||||
"thumbnail_image_url": "https://k.kakaocdn.net/.../thumb.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| User 필드 | 카카오 응답 경로 |
|
||||
|-----------|-----------------|
|
||||
| `kakao_id` | `id` |
|
||||
| `email` | `kakao_account.email` |
|
||||
| `nickname` | `kakao_account.profile.nickname` |
|
||||
| `profile_image_url` | `kakao_account.profile.profile_image_url` |
|
||||
| `thumbnail_image_url` | `kakao_account.profile.thumbnail_image_url` |
|
||||
|
||||
---
|
||||
|
||||
## 4. API 엔드포인트
|
||||
|
||||
### 4.1 카카오 로그인 URL 요청
|
||||
|
||||
```
|
||||
GET /api/v1/auth/kakao/login
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"auth_url": "https://kauth.kakao.com/oauth/authorize?client_id=...&redirect_uri=...&response_type=code"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 카카오 콜백 (로그인/가입 처리)
|
||||
|
||||
```
|
||||
POST /api/v1/auth/kakao/callback
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"code": "인가코드"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (성공):**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"nickname": "홍길동",
|
||||
"email": "user@kakao.com",
|
||||
"profile_image_url": "https://...",
|
||||
"is_new_user": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 토큰 갱신
|
||||
|
||||
```
|
||||
POST /api/v1/auth/refresh
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 로그아웃
|
||||
|
||||
```
|
||||
POST /api/v1/auth/logout
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
### 4.5 내 정보 조회
|
||||
|
||||
```
|
||||
GET /api/v1/users/me
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"kakao_id": 1234567890,
|
||||
"nickname": "홍길동",
|
||||
"email": "user@kakao.com",
|
||||
"profile_image_url": "https://...",
|
||||
"is_admin": false,
|
||||
"created_at": "2026-01-14T16:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 환경 설정
|
||||
|
||||
### 5.1 카카오 개발자 설정
|
||||
|
||||
1. [카카오 개발자 콘솔](https://developers.kakao.com) 접속
|
||||
2. 애플리케이션 생성
|
||||
3. 플랫폼 > Web 사이트 도메인 등록
|
||||
4. 카카오 로그인 > Redirect URI 등록
|
||||
5. 동의항목 > 필요한 정보 설정
|
||||
|
||||
### 5.2 .env 설정
|
||||
|
||||
```env
|
||||
# 카카오 OAuth
|
||||
KAKAO_CLIENT_ID=your_rest_api_key
|
||||
KAKAO_CLIENT_SECRET=your_client_secret # 선택
|
||||
KAKAO_REDIRECT_URI=https://your-domain.com/api/v1/auth/kakao/callback
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-super-secret-key-min-32-characters
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||
```
|
||||
|
||||
### 5.3 config.py 설정
|
||||
|
||||
```python
|
||||
class KakaoSettings(BaseSettings):
|
||||
KAKAO_CLIENT_ID: str = Field(...)
|
||||
KAKAO_CLIENT_SECRET: str = Field(default="")
|
||||
KAKAO_REDIRECT_URI: str = Field(...)
|
||||
|
||||
model_config = _base_config
|
||||
|
||||
|
||||
class JWTSettings(BaseSettings):
|
||||
JWT_SECRET: str = Field(...)
|
||||
JWT_ALGORITHM: str = Field(default="HS256")
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=60)
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7)
|
||||
|
||||
model_config = _base_config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 에러 처리
|
||||
|
||||
### 6.1 에러 코드 정의
|
||||
|
||||
| HTTP Status | Error Code | 설명 |
|
||||
|-------------|------------|------|
|
||||
| 400 | `INVALID_CODE` | 유효하지 않은 인가 코드 |
|
||||
| 400 | `KAKAO_AUTH_FAILED` | 카카오 인증 실패 |
|
||||
| 401 | `TOKEN_EXPIRED` | 토큰 만료 |
|
||||
| 401 | `INVALID_TOKEN` | 유효하지 않은 토큰 |
|
||||
| 401 | `TOKEN_REVOKED` | 취소된 토큰 |
|
||||
| 403 | `USER_INACTIVE` | 비활성화된 계정 |
|
||||
| 403 | `ADMIN_REQUIRED` | 관리자 권한 필요 |
|
||||
| 404 | `USER_NOT_FOUND` | 사용자 없음 |
|
||||
| 500 | `KAKAO_API_ERROR` | 카카오 API 오류 |
|
||||
|
||||
### 6.2 에러 응답 형식
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": {
|
||||
"code": "TOKEN_EXPIRED",
|
||||
"message": "토큰이 만료되었습니다. 다시 로그인해주세요."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 부록: 파일 구조
|
||||
|
||||
```
|
||||
app/user/
|
||||
├── __init__.py
|
||||
├── models.py # User 모델
|
||||
├── schemas/
|
||||
│ ├── __init__.py
|
||||
│ └── user_schema.py # Pydantic 스키마
|
||||
├── services/
|
||||
│ ├── __init__.py
|
||||
│ ├── auth.py # 인증 서비스
|
||||
│ ├── jwt.py # JWT 서비스
|
||||
│ └── kakao.py # 카카오 OAuth 서비스
|
||||
├── api/
|
||||
│ ├── __init__.py
|
||||
│ └── routers/
|
||||
│ └── v1/
|
||||
│ ├── __init__.py
|
||||
│ └── auth.py # 인증 API 라우터
|
||||
└── exceptions.py # 사용자 정의 예외
|
||||
```
|
||||
Loading…
Reference in New Issue