add logger

insta
Dohyun Lim 2026-01-14 17:46:45 +09:00
parent 1acd8846ab
commit bf7b53c8e8
41 changed files with 1832 additions and 694 deletions

7
.gitignore vendored
View File

@ -27,3 +27,10 @@ build/
*.mp3 *.mp3
*.mp4 *.mp4
media/ media/
# Static files
static/
# Log files
*.log
logs/

View File

@ -4,12 +4,16 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from app.utils.logger import get_logger
logger = get_logger("core")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""FastAPI 애플리케이션 생명주기 관리""" """FastAPI 애플리케이션 생명주기 관리"""
# Startup - 애플리케이션 시작 시 # Startup - 애플리케이션 시작 시
print("Starting up...") logger.info("Starting up...")
try: try:
from config import prj_settings from config import prj_settings
@ -19,20 +23,20 @@ async def lifespan(app: FastAPI):
from app.database.session import create_db_tables from app.database.session import create_db_tables
await create_db_tables() await create_db_tables()
print("Database tables created (DEBUG mode)") logger.info("Database tables created (DEBUG mode)")
except asyncio.TimeoutError: except asyncio.TimeoutError:
print("Database initialization timed out") logger.error("Database initialization timed out")
# 타임아웃 시 앱 시작 중단하려면 raise, 계속하려면 pass # 타임아웃 시 앱 시작 중단하려면 raise, 계속하려면 pass
raise raise
except Exception as e: except Exception as e:
print(f"Database initialization failed: {e}") logger.error(f"Database initialization failed: {e}")
# 에러 시 앱 시작 중단하려면 raise, 계속하려면 pass # 에러 시 앱 시작 중단하려면 raise, 계속하려면 pass
raise raise
yield # 애플리케이션 실행 중 yield # 애플리케이션 실행 중
# Shutdown - 애플리케이션 종료 시 # Shutdown - 애플리케이션 종료 시
print("Shutting down...") logger.info("Shutting down...")
# 공유 HTTP 클라이언트 종료 # 공유 HTTP 클라이언트 종료
from app.utils.creatomate import close_shared_client from app.utils.creatomate import close_shared_client

View File

@ -1,4 +1,3 @@
import logging
import traceback import traceback
from functools import wraps from functools import wraps
from typing import Any, Callable, TypeVar from typing import Any, Callable, TypeVar
@ -7,8 +6,10 @@ from fastapi import FastAPI, HTTPException, Request, Response, status
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from app.utils.logger import get_logger
# 로거 설정 # 로거 설정
logger = logging.getLogger(__name__) logger = get_logger("core")
T = TypeVar("T") T = TypeVar("T")
@ -159,16 +160,14 @@ def handle_db_exceptions(
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[DB Error] {func.__name__}: {e}") logger.error(f"[DB Error] {func.__name__}: {e}")
logger.error(traceback.format_exc()) logger.debug(traceback.format_exc())
print(f"[DB Error] {func.__name__}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=error_message, detail=error_message,
) )
except Exception as e: except Exception as e:
logger.error(f"[Unexpected Error] {func.__name__}: {e}") logger.error(f"[Unexpected Error] {func.__name__}: {e}")
logger.error(traceback.format_exc()) logger.debug(traceback.format_exc())
print(f"[Unexpected Error] {func.__name__}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 예기치 않은 오류가 발생했습니다.", detail="서비스 처리 중 예기치 않은 오류가 발생했습니다.",
@ -205,8 +204,7 @@ def handle_external_service_exceptions(
except Exception as e: except Exception as e:
msg = error_message or f"{service_name} 서비스 호출 중 오류가 발생했습니다." msg = error_message or f"{service_name} 서비스 호출 중 오류가 발생했습니다."
logger.error(f"[{service_name} Error] {func.__name__}: {e}") logger.error(f"[{service_name} Error] {func.__name__}: {e}")
logger.error(traceback.format_exc()) logger.debug(traceback.format_exc())
print(f"[{service_name} Error] {func.__name__}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
detail=msg, detail=msg,
@ -240,16 +238,14 @@ def handle_api_exceptions(
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[API DB Error] {func.__name__}: {e}") logger.error(f"[API DB Error] {func.__name__}: {e}")
logger.error(traceback.format_exc()) logger.debug(traceback.format_exc())
print(f"[API DB Error] {func.__name__}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
logger.error(f"[API Error] {func.__name__}: {e}") logger.error(f"[API Error] {func.__name__}: {e}")
logger.error(traceback.format_exc()) logger.debug(traceback.format_exc())
print(f"[API Error] {func.__name__}: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_message, detail=error_message,
@ -263,16 +259,7 @@ def handle_api_exceptions(
def _get_handler(status: int, detail: str): def _get_handler(status: int, detail: str):
# Define # Define
def handler(request: Request, exception: Exception) -> Response: def handler(request: Request, exception: Exception) -> Response:
# DEBUG PRINT STATEMENT 👇 logger.debug(f"Handled Exception: {exception.__class__.__name__}")
from rich import print, panel
print(
panel.Panel(
exception.__class__.__name__,
title="Handled Exception",
border_style="red",
),
)
# DEBUG PRINT STATEMENT 👆
# Raise HTTPException with given status and detail # Raise HTTPException with given status and detail
# can return JSONResponse as well # can return JSONResponse as well

View File

@ -9,8 +9,11 @@ from sqlalchemy.ext.asyncio import (
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.pool import AsyncQueuePool # 비동기 풀 클래스 from sqlalchemy.pool import AsyncQueuePool # 비동기 풀 클래스
from app.utils.logger import get_logger
from config import db_settings from config import db_settings
logger = get_logger("database")
# Base 클래스 정의 # Base 클래스 정의
class Base(DeclarativeBase): class Base(DeclarativeBase):
@ -61,7 +64,7 @@ async def create_db_tables() -> None:
async with engine.begin() as conn: async with engine.begin() as conn:
# from app.database.models import Shipment, Seller # noqa: F401 # from app.database.models import Shipment, Seller # noqa: F401
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
print("MySQL tables created successfully") logger.info("MySQL tables created successfully")
# 세션 제너레이터 (FastAPI Depends에 사용) # 세션 제너레이터 (FastAPI Depends에 사용)
@ -80,13 +83,13 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
# FastAPI 요청 완료 시 자동 commit (예외 발생 시 rollback) # FastAPI 요청 완료 시 자동 commit (예외 발생 시 rollback)
except Exception as e: except Exception as e:
await session.rollback() # 명시적 롤백 (선택적) await session.rollback() # 명시적 롤백 (선택적)
print(f"Session rollback due to: {e}") # 로깅 logger.error(f"Session rollback due to: {e}")
raise raise
finally: finally:
# 명시적 세션 종료 (Connection Pool에 반환) # 명시적 세션 종료 (Connection Pool에 반환)
# context manager가 자동 처리하지만, 명시적으로 유지 # context manager가 자동 처리하지만, 명시적으로 유지
await session.close() await session.close()
print("session closed successfully") logger.debug("session closed successfully")
# 또는 session.aclose() - Python 3.10+ # 또는 session.aclose() - Python 3.10+
@ -94,4 +97,4 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
async def dispose_engine() -> None: async def dispose_engine() -> None:
"""애플리케이션 종료 시 모든 연결 해제""" """애플리케이션 종료 시 모든 연결 해제"""
await engine.dispose() await engine.dispose()
print("Database engine disposed") logger.info("Database engine disposed")

View File

@ -4,8 +4,11 @@ from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from app.utils.logger import get_logger
from config import db_settings from config import db_settings
logger = get_logger("database")
class Base(DeclarativeBase): class Base(DeclarativeBase):
pass pass
@ -74,7 +77,7 @@ async def create_db_tables():
from app.song.models import Song # noqa: F401 from app.song.models import Song # noqa: F401
from app.video.models import Video # 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 asyncio.timeout(10):
async with engine.begin() as connection: async with engine.begin() as connection:
@ -87,7 +90,7 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
pool = engine.pool pool = engine.pool
# 커넥션 풀 상태 로깅 (디버깅용) # 커넥션 풀 상태 로깅 (디버깅용)
print( logger.debug(
f"[get_session] ACQUIRE - pool_size: {pool.size()}, " f"[get_session] ACQUIRE - pool_size: {pool.size()}, "
f"in: {pool.checkedin()}, out: {pool.checkedout()}, " f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
f"overflow: {pool.overflow()}" f"overflow: {pool.overflow()}"
@ -95,7 +98,7 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
acquire_time = time.perf_counter() acquire_time = time.perf_counter()
print( logger.debug(
f"[get_session] Session acquired in " f"[get_session] Session acquired in "
f"{(acquire_time - start_time)*1000:.1f}ms" f"{(acquire_time - start_time)*1000:.1f}ms"
) )
@ -103,14 +106,14 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
yield session yield session
except Exception as e: except Exception as e:
await session.rollback() await session.rollback()
print( logger.error(
f"[get_session] ROLLBACK - error: {type(e).__name__}: {e}, " f"[get_session] ROLLBACK - error: {type(e).__name__}: {e}, "
f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms" f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms"
) )
raise e raise e
finally: finally:
total_time = time.perf_counter() - start_time total_time = time.perf_counter() - start_time
print( logger.debug(
f"[get_session] RELEASE - duration: {total_time*1000:.1f}ms, " f"[get_session] RELEASE - duration: {total_time*1000:.1f}ms, "
f"pool_out: {pool.checkedout()}" f"pool_out: {pool.checkedout()}"
) )
@ -121,7 +124,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
start_time = time.perf_counter() start_time = time.perf_counter()
pool = background_engine.pool pool = background_engine.pool
print( logger.debug(
f"[get_background_session] ACQUIRE - pool_size: {pool.size()}, " f"[get_background_session] ACQUIRE - pool_size: {pool.size()}, "
f"in: {pool.checkedin()}, out: {pool.checkedout()}, " f"in: {pool.checkedin()}, out: {pool.checkedout()}, "
f"overflow: {pool.overflow()}" f"overflow: {pool.overflow()}"
@ -129,7 +132,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
async with BackgroundSessionLocal() as session: async with BackgroundSessionLocal() as session:
acquire_time = time.perf_counter() acquire_time = time.perf_counter()
print( logger.debug(
f"[get_background_session] Session acquired in " f"[get_background_session] Session acquired in "
f"{(acquire_time - start_time)*1000:.1f}ms" f"{(acquire_time - start_time)*1000:.1f}ms"
) )
@ -137,7 +140,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
yield session yield session
except Exception as e: except Exception as e:
await session.rollback() await session.rollback()
print( logger.error(
f"[get_background_session] ROLLBACK - " f"[get_background_session] ROLLBACK - "
f"error: {type(e).__name__}: {e}, " f"error: {type(e).__name__}: {e}, "
f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms" f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms"
@ -145,7 +148,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
raise e raise e
finally: finally:
total_time = time.perf_counter() - start_time total_time = time.perf_counter() - start_time
print( logger.debug(
f"[get_background_session] RELEASE - " f"[get_background_session] RELEASE - "
f"duration: {total_time*1000:.1f}ms, " f"duration: {total_time*1000:.1f}ms, "
f"pool_out: {pool.checkedout()}" f"pool_out: {pool.checkedout()}"
@ -154,8 +157,8 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
# 앱 종료 시 엔진 리소스 정리 함수 # 앱 종료 시 엔진 리소스 정리 함수
async def dispose_engine() -> None: async def dispose_engine() -> None:
print("[dispose_engine] Disposing database engines...") logger.info("[dispose_engine] Disposing database engines...")
await engine.dispose() await engine.dispose()
print("[dispose_engine] Main engine disposed") logger.info("[dispose_engine] Main engine disposed")
await background_engine.dispose() await background_engine.dispose()
print("[dispose_engine] Background engine disposed - ALL DONE") logger.info("[dispose_engine] Background engine disposed - ALL DONE")

View File

@ -1,6 +1,5 @@
import json import json
import logging import time
import traceback
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
from typing import Optional 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.upload_blob_as_request import AzureBlobUploader
from app.utils.chatgpt_prompt import ChatgptService from app.utils.chatgpt_prompt import ChatgptService
from app.utils.common import generate_task_id from app.utils.common import generate_task_id
from app.utils.logger import get_logger
from app.utils.nvMapScraper import NvMapScraper, GraphQLException from app.utils.nvMapScraper import NvMapScraper, GraphQLException
from config import MEDIA_ROOT
# 로거 설정 # 로거 설정
logger = logging.getLogger(__name__) logger = get_logger("home")
MEDIA_ROOT = Path("media")
# 전국 시 이름 목록 (roadAddress에서 region 추출용) # 전국 시 이름 목록 (roadAddress에서 region 추출용)
# fmt: off # fmt: off
@ -106,16 +105,13 @@ def _extract_region_from_address(road_address: str | None) -> str:
) )
async def crawling(request_body: CrawlingRequest): async def crawling(request_body: CrawlingRequest):
"""네이버 지도 장소 크롤링""" """네이버 지도 장소 크롤링"""
import time
request_start = time.perf_counter() request_start = time.perf_counter()
logger.info(f"[crawling] START - url: {request_body.url[:80]}...") logger.info("[crawling] ========== START ==========")
print(f"[crawling] ========== START ==========") logger.info(f"[crawling] URL: {request_body.url[:80]}...")
print(f"[crawling] URL: {request_body.url[:80]}...")
# ========== Step 1: 네이버 지도 크롤링 ========== # ========== Step 1: 네이버 지도 크롤링 ==========
step1_start = time.perf_counter() step1_start = time.perf_counter()
print(f"[crawling] Step 1: 네이버 지도 크롤링 시작...") logger.info("[crawling] Step 1: 네이버 지도 크롤링 시작...")
try: try:
scraper = NvMapScraper(request_body.url) scraper = NvMapScraper(request_body.url)
@ -123,7 +119,6 @@ async def crawling(request_body: CrawlingRequest):
except GraphQLException as e: except GraphQLException as e:
step1_elapsed = (time.perf_counter() - step1_start) * 1000 step1_elapsed = (time.perf_counter() - step1_start) * 1000
logger.error(f"[crawling] Step 1 FAILED - GraphQL 크롤링 실패: {e} ({step1_elapsed:.1f}ms)") 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( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"네이버 지도 크롤링에 실패했습니다: {e}", detail=f"네이버 지도 크롤링에 실패했습니다: {e}",
@ -131,8 +126,7 @@ async def crawling(request_body: CrawlingRequest):
except Exception as e: except Exception as e:
step1_elapsed = (time.perf_counter() - step1_start) * 1000 step1_elapsed = (time.perf_counter() - step1_start) * 1000
logger.error(f"[crawling] Step 1 FAILED - 크롤링 중 예기치 않은 오류: {e} ({step1_elapsed:.1f}ms)") logger.error(f"[crawling] Step 1 FAILED - 크롤링 중 예기치 않은 오류: {e} ({step1_elapsed:.1f}ms)")
print(f"[crawling] Step 1 FAILED - {e} ({step1_elapsed:.1f}ms)") logger.exception("[crawling] Step 1 상세 오류:")
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
detail="네이버 지도 크롤링 중 오류가 발생했습니다.", detail="네이버 지도 크롤링 중 오류가 발생했습니다.",
@ -141,11 +135,10 @@ async def crawling(request_body: CrawlingRequest):
step1_elapsed = (time.perf_counter() - step1_start) * 1000 step1_elapsed = (time.perf_counter() - step1_start) * 1000
image_count = len(scraper.image_link_list) if scraper.image_link_list else 0 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)") 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: 정보 가공 ========== # ========== Step 2: 정보 가공 ==========
step2_start = time.perf_counter() step2_start = time.perf_counter()
print(f"[crawling] Step 2: 정보 가공 시작...") logger.info("[crawling] Step 2: 정보 가공 시작...")
processed_info = None processed_info = None
marketing_analysis = None marketing_analysis = None
@ -163,11 +156,10 @@ async def crawling(request_body: CrawlingRequest):
step2_elapsed = (time.perf_counter() - step2_start) * 1000 step2_elapsed = (time.perf_counter() - step2_start) * 1000
logger.info(f"[crawling] Step 2 완료 - {customer_name}, {region} ({step2_elapsed:.1f}ms)") 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 마케팅 분석 ========== # ========== Step 3: ChatGPT 마케팅 분석 ==========
step3_start = time.perf_counter() step3_start = time.perf_counter()
print(f"[crawling] Step 3: ChatGPT 마케팅 분석 시작...") logger.info("[crawling] Step 3: ChatGPT 마케팅 분석 시작...")
try: try:
# Step 3-1: ChatGPT 서비스 초기화 # Step 3-1: ChatGPT 서비스 초기화
@ -178,59 +170,54 @@ async def crawling(request_body: CrawlingRequest):
detail_region_info=road_address or "", detail_region_info=road_address or "",
) )
step3_1_elapsed = (time.perf_counter() - step3_1_start) * 1000 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: 프롬프트 생성 # Step 3-2: 프롬프트 생성
step3_2_start = time.perf_counter() step3_2_start = time.perf_counter()
prompt = chatgpt_service.build_market_analysis_prompt() prompt = chatgpt_service.build_market_analysis_prompt()
step3_2_elapsed = (time.perf_counter() - step3_2_start) * 1000 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 호출 # Step 3-3: GPT API 호출
step3_3_start = time.perf_counter() step3_3_start = time.perf_counter()
raw_response = await chatgpt_service.generate(prompt) raw_response = await chatgpt_service.generate(prompt)
step3_3_elapsed = (time.perf_counter() - step3_3_start) * 1000 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)") 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 전달) # Step 3-4: 응답 파싱 (크롤링에서 가져온 facility_info 전달)
step3_4_start = time.perf_counter() 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( parsed = await chatgpt_service.parse_marketing_analysis(
raw_response raw_response
) )
marketing_analysis = MarketingAnalysis(**parsed) marketing_analysis = MarketingAnalysis(**parsed)
step3_4_elapsed = (time.perf_counter() - step3_4_start) * 1000 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 step3_elapsed = (time.perf_counter() - step3_start) * 1000
logger.info(f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)") logger.info(f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)")
print(f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)")
except Exception as e: except Exception as e:
step3_elapsed = (time.perf_counter() - step3_start) * 1000 step3_elapsed = (time.perf_counter() - step3_start) * 1000
logger.error(f"[crawling] Step 3 FAILED - GPT 마케팅 분석 중 오류: {e} ({step3_elapsed:.1f}ms)") logger.error(f"[crawling] Step 3 FAILED - GPT 마케팅 분석 중 오류: {e} ({step3_elapsed:.1f}ms)")
print(f"[crawling] Step 3 FAILED - {e} ({step3_elapsed:.1f}ms)") logger.exception("[crawling] Step 3 상세 오류:")
traceback.print_exc()
# GPT 실패 시에도 크롤링 결과는 반환 # GPT 실패 시에도 크롤링 결과는 반환
marketing_analysis = None marketing_analysis = None
else: else:
step2_elapsed = (time.perf_counter() - step2_start) * 1000 step2_elapsed = (time.perf_counter() - step2_start) * 1000
logger.warning(f"[crawling] Step 2 - base_info 없음 ({step2_elapsed:.1f}ms)") logger.warning(f"[crawling] Step 2 - base_info 없음, 마케팅 분석 스킵 ({step2_elapsed:.1f}ms)")
print(f"[crawling] Step 2 - base_info 없음, 마케팅 분석 스킵 ({step2_elapsed:.1f}ms)")
# ========== 완료 ========== # ========== 완료 ==========
total_elapsed = (time.perf_counter() - request_start) * 1000 total_elapsed = (time.perf_counter() - request_start) * 1000
logger.info(f"[crawling] COMPLETE - 총 소요시간: {total_elapsed:.1f}ms") logger.info("[crawling] ========== COMPLETE ==========")
print(f"[crawling] ========== COMPLETE ==========") logger.info(f"[crawling] 총 소요시간: {total_elapsed:.1f}ms")
print(f"[crawling] 총 소요시간: {total_elapsed:.1f}ms") logger.info(f"[crawling] - Step 1 (크롤링): {step1_elapsed:.1f}ms")
print(f"[crawling] - Step 1 (크롤링): {step1_elapsed:.1f}ms")
if scraper.base_info: 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(): 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(): 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 { return {
"image_list": scraper.image_link_list, "image_list": scraper.image_link_list,
@ -612,12 +599,11 @@ async def upload_images_blob(
- Stage 2: Azure Blob 업로드 (세션 없음) - Stage 2: Azure Blob 업로드 (세션 없음)
- Stage 3: DB 저장 ( 세션으로 빠르게 처리) - Stage 3: DB 저장 ( 세션으로 빠르게 처리)
""" """
import time
request_start = time.perf_counter() request_start = time.perf_counter()
# task_id 생성 # task_id 생성
task_id = await generate_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: 입력 검증 및 파일 데이터 준비 (세션 없음) ========== # ========== Stage 1: 입력 검증 및 파일 데이터 준비 (세션 없음) ==========
has_images_json = images_json is not None and images_json.strip() != "" has_images_json = images_json is not None and images_json.strip() != ""
@ -671,9 +657,9 @@ async def upload_images_blob(
) )
stage1_time = time.perf_counter() 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"files: {len(valid_files_data)}, "
f"elapsed: {(stage1_time - request_start)*1000:.1f}ms") f"elapsed: {(stage1_time - request_start)*1000:.1f}ms")
# ========== Stage 2: Azure Blob 업로드 (세션 없음) ========== # ========== Stage 2: Azure Blob 업로드 (세션 없음) ==========
# 업로드 결과를 저장할 리스트 (나중에 DB에 저장) # 업로드 결과를 저장할 리스트 (나중에 DB에 저장)
@ -692,8 +678,8 @@ async def upload_images_blob(
) )
filename = f"{name_without_ext}_{img_order:03d}{ext}" 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)") f"{filename} ({len(file_content)} bytes)")
# Azure Blob Storage에 직접 업로드 # Azure Blob Storage에 직접 업로드
upload_success = await uploader.upload_image_bytes(file_content, filename) upload_success = await uploader.upload_image_bytes(file_content, filename)
@ -702,18 +688,18 @@ async def upload_images_blob(
blob_url = uploader.public_url blob_url = uploader.public_url
blob_upload_results.append((original_name, blob_url)) blob_upload_results.append((original_name, blob_url))
img_order += 1 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: else:
skipped_files.append(filename) 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() 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"{len(blob_upload_results)}, skipped: {len(skipped_files)}, "
f"elapsed: {(stage2_time - stage1_time)*1000:.1f}ms") f"elapsed: {(stage2_time - stage1_time)*1000:.1f}ms")
# ========== Stage 3: DB 저장 (새 세션으로 빠르게 처리) ========== # ========== 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] = [] result_images: list[ImageUploadResultItem] = []
img_order = 0 img_order = 0
@ -769,21 +755,21 @@ async def upload_images_blob(
await session.commit() await session.commit()
stage3_time = time.perf_counter() 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"saved: {len(result_images)}, "
f"elapsed: {(stage3_time - stage2_time)*1000:.1f}ms") f"elapsed: {(stage3_time - stage2_time)*1000:.1f}ms")
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[upload_images_blob] DB Error - task_id: {task_id}, error: {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( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="이미지 저장 중 데이터베이스 오류가 발생했습니다.", detail="이미지 저장 중 데이터베이스 오류가 발생했습니다.",
) )
except Exception as e: except Exception as e:
logger.error(f"[upload_images_blob] Stage 3 EXCEPTION - " logger.error(f"[upload_images_blob] Stage 3 EXCEPTION - "
f"task_id: {task_id}, error: {type(e).__name__}: {e}") f"task_id: {task_id}, error: {type(e).__name__}: {e}")
traceback.print_exc() logger.exception("[upload_images_blob] Stage 3 상세 오류:")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="이미지 업로드 중 오류가 발생했습니다.", detail="이미지 업로드 중 오류가 발생했습니다.",
@ -793,8 +779,8 @@ async def upload_images_blob(
image_urls = [img.img_url for img in result_images] image_urls = [img.img_url for img in result_images]
total_time = time.perf_counter() - request_start 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") f"total: {saved_count}, total_time: {total_time*1000:.1f}ms")
return ImageUploadResponse( return ImageUploadResponse(
task_id=task_id, task_id=task_id,

View File

@ -2,6 +2,10 @@ import pytest
from sqlalchemy import text from sqlalchemy import text
from app.database.session import AsyncSessionLocal, engine from app.database.session import AsyncSessionLocal, engine
from app.utils.logger import get_logger
# 로거 설정
logger = get_logger("test_db")
@pytest.mark.asyncio @pytest.mark.asyncio
@ -27,4 +31,4 @@ async def test_database_version():
result = await session.execute(text("SELECT VERSION()")) result = await session.execute(text("SELECT VERSION()"))
version = result.scalar() version = result.scalar()
assert version is not None assert version is not None
print(f"MySQL Version: {version}") logger.info(f"MySQL Version: {version}")

View File

@ -11,8 +11,6 @@ from fastapi import UploadFile
from app.utils.upload_blob_as_request import AzureBlobUploader from app.utils.upload_blob_as_request import AzureBlobUploader
MEDIA_ROOT = Path("media")
async def save_upload_file(file: UploadFile, save_path: Path) -> None: async def save_upload_file(file: UploadFile, save_path: Path) -> None:
"""업로드 파일을 지정된 경로에 저장""" """업로드 파일을 지정된 경로에 저장"""

View File

@ -41,8 +41,12 @@ from app.lyric.schemas.lyric import (
) )
from app.lyric.worker.lyric_task import generate_lyric_background from app.lyric.worker.lyric_task import generate_lyric_background
from app.utils.chatgpt_prompt import ChatgptService from app.utils.chatgpt_prompt import ChatgptService
from app.utils.logger import get_logger
from app.utils.pagination import PaginatedResponse, get_paginated from app.utils.pagination import PaginatedResponse, get_paginated
# 로거 설정
logger = get_logger("lyric")
router = APIRouter(prefix="/lyric", tags=["lyric"]) router = APIRouter(prefix="/lyric", tags=["lyric"])
@ -74,7 +78,7 @@ async def get_lyric_status_by_task_id(
if status_info.status == "completed": 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( result = await session.execute(
select(Lyric) select(Lyric)
.where(Lyric.task_id == task_id) .where(Lyric.task_id == task_id)
@ -84,7 +88,7 @@ async def get_lyric_status_by_task_id(
lyric = result.scalar_one_or_none() lyric = result.scalar_one_or_none()
if not lyric: 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( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.", detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.",
@ -96,7 +100,7 @@ async def get_lyric_status_by_task_id(
"failed": "가사 생성에 실패했습니다.", "failed": "가사 생성에 실패했습니다.",
} }
print( logger.info(
f"[get_lyric_status_by_task_id] SUCCESS - task_id: {task_id}, status: {lyric.status}" f"[get_lyric_status_by_task_id] SUCCESS - task_id: {task_id}, status: {lyric.status}"
) )
return LyricStatusResponse( return LyricStatusResponse(
@ -127,7 +131,7 @@ async def get_lyric_by_task_id(
lyric = await get_lyric_by_task_id(session, 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( result = await session.execute(
select(Lyric) select(Lyric)
.where(Lyric.task_id == task_id) .where(Lyric.task_id == task_id)
@ -137,13 +141,13 @@ async def get_lyric_by_task_id(
lyric = result.scalar_one_or_none() lyric = result.scalar_one_or_none()
if not lyric: 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( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.", 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( return LyricDetailResponse(
id=lyric.id, id=lyric.id,
task_id=lyric.task_id, task_id=lyric.task_id,
@ -224,8 +228,8 @@ async def generate_lyric(
request_start = time.perf_counter() request_start = time.perf_counter()
task_id = request_body.task_id task_id = request_body.task_id
print(f"[generate_lyric] ========== START ==========") logger.info(f"[generate_lyric] ========== START ==========")
print( logger.info(
f"[generate_lyric] task_id: {task_id}, " f"[generate_lyric] task_id: {task_id}, "
f"customer_name: {request_body.customer_name}, " f"customer_name: {request_body.customer_name}, "
f"region: {request_body.region}" f"region: {request_body.region}"
@ -234,7 +238,7 @@ async def generate_lyric(
try: try:
# ========== Step 1: ChatGPT 서비스 초기화 및 프롬프트 생성 ========== # ========== Step 1: ChatGPT 서비스 초기화 및 프롬프트 생성 ==========
step1_start = time.perf_counter() step1_start = time.perf_counter()
print(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...") logger.debug(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...")
service = ChatgptService( service = ChatgptService(
customer_name=request_body.customer_name, customer_name=request_body.customer_name,
@ -245,11 +249,11 @@ async def generate_lyric(
prompt = service.build_lyrics_prompt() prompt = service.build_lyrics_prompt()
step1_elapsed = (time.perf_counter() - step1_start) * 1000 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 테이블에 데이터 저장 ========== # ========== Step 2: Project 테이블에 데이터 저장 ==========
step2_start = time.perf_counter() step2_start = time.perf_counter()
print(f"[generate_lyric] Step 2: Project 저장...") logger.debug(f"[generate_lyric] Step 2: Project 저장...")
project = Project( project = Project(
store_name=request_body.customer_name, store_name=request_body.customer_name,
@ -263,11 +267,11 @@ async def generate_lyric(
await session.refresh(project) await session.refresh(project)
step2_elapsed = (time.perf_counter() - step2_start) * 1000 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 테이블에 데이터 저장 ========== # ========== Step 3: Lyric 테이블에 데이터 저장 ==========
step3_start = time.perf_counter() step3_start = time.perf_counter()
print(f"[generate_lyric] Step 3: Lyric 저장 (processing)...") logger.debug(f"[generate_lyric] Step 3: Lyric 저장 (processing)...")
lyric = Lyric( lyric = Lyric(
project_id=project.id, project_id=project.id,
@ -282,11 +286,11 @@ async def generate_lyric(
await session.refresh(lyric) await session.refresh(lyric)
step3_elapsed = (time.perf_counter() - step3_start) * 1000 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: 백그라운드 태스크 스케줄링 ========== # ========== Step 4: 백그라운드 태스크 스케줄링 ==========
step4_start = time.perf_counter() step4_start = time.perf_counter()
print(f"[generate_lyric] Step 4: 백그라운드 태스크 스케줄링...") logger.debug(f"[generate_lyric] Step 4: 백그라운드 태스크 스케줄링...")
background_tasks.add_task( background_tasks.add_task(
generate_lyric_background, generate_lyric_background,
@ -296,17 +300,17 @@ async def generate_lyric(
) )
step4_elapsed = (time.perf_counter() - step4_start) * 1000 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 total_elapsed = (time.perf_counter() - request_start) * 1000
print(f"[generate_lyric] ========== COMPLETE ==========") logger.info(f"[generate_lyric] ========== COMPLETE ==========")
print(f"[generate_lyric] API 응답 소요시간: {total_elapsed:.1f}ms") logger.info(f"[generate_lyric] API 응답 소요시간: {total_elapsed:.1f}ms")
print(f"[generate_lyric] - Step 1 (프롬프트 생성): {step1_elapsed:.1f}ms") logger.debug(f"[generate_lyric] - Step 1 (프롬프트 생성): {step1_elapsed:.1f}ms")
print(f"[generate_lyric] - Step 2 (Project 저장): {step2_elapsed:.1f}ms") logger.debug(f"[generate_lyric] - Step 2 (Project 저장): {step2_elapsed:.1f}ms")
print(f"[generate_lyric] - Step 3 (Lyric 저장): {step3_elapsed:.1f}ms") logger.debug(f"[generate_lyric] - Step 3 (Lyric 저장): {step3_elapsed:.1f}ms")
print(f"[generate_lyric] - Step 4 (태스크 스케줄링): {step4_elapsed:.1f}ms") logger.debug(f"[generate_lyric] - Step 4 (태스크 스케줄링): {step4_elapsed:.1f}ms")
print(f"[generate_lyric] (GPT API 호출은 백그라운드에서 별도 진행)") logger.debug(f"[generate_lyric] (GPT API 호출은 백그라운드에서 별도 진행)")
# 5. 즉시 응답 반환 # 5. 즉시 응답 반환
return GenerateLyricResponse( return GenerateLyricResponse(
@ -319,7 +323,7 @@ async def generate_lyric(
except Exception as e: except Exception as e:
elapsed = (time.perf_counter() - request_start) * 1000 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() await session.rollback()
return GenerateLyricResponse( return GenerateLyricResponse(
success=False, success=False,

View File

@ -14,6 +14,10 @@ from app.lyric.schemas.lyrics_schema import (
StoreData, StoreData,
) )
from app.utils.chatgpt_prompt import chatgpt_api 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]: async def get_store_info(conn: Connection) -> List[StoreData]:
@ -38,13 +42,13 @@ async def get_store_info(conn: Connection) -> List[StoreData]:
result.close() result.close()
return all_store_info return all_store_info
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"Database error in get_store_info: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_store_info: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -69,13 +73,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
result.close() result.close()
return all_attribute return all_attribute
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"Database error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -100,13 +104,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
result.close() result.close()
return all_attribute return all_attribute
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"Database error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -132,13 +136,13 @@ async def get_sample_song(conn: Connection) -> List[SongSampleData]:
result.close() result.close()
return all_sample_song return all_sample_song
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"Database error in get_sample_song: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_sample_song: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -162,13 +166,13 @@ async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
result.close() result.close()
return all_prompt_template return all_prompt_template
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"Database error in get_prompt_template: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_prompt_template: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -192,13 +196,13 @@ async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
result.close() result.close()
return all_prompt_template return all_prompt_template
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"Database error in get_song_result: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_song_result: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -210,11 +214,11 @@ async def make_song_result(request: Request, conn: Connection):
# 1. Form 데이터 파싱 # 1. Form 데이터 파싱
form_data = await SongFormData.from_form(request) form_data = await SongFormData.from_form(request)
print(f"\n{'=' * 60}") logger.info(f"{'=' * 60}")
print(f"Store ID: {form_data.store_id}") logger.info(f"Store ID: {form_data.store_id}")
print(f"Lyrics IDs: {form_data.lyrics_ids}") logger.info(f"Lyrics IDs: {form_data.lyrics_ids}")
print(f"Prompt IDs: {form_data.prompts}") logger.info(f"Prompt IDs: {form_data.prompts}")
print(f"{'=' * 60}\n") logger.info(f"{'=' * 60}")
# 2. Store 정보 조회 # 2. Store 정보 조회
store_query = """ store_query = """
@ -243,7 +247,7 @@ async def make_song_result(request: Request, conn: Connection):
) )
store_info = all_store_info[0] store_info = all_store_info[0]
print(f"Store: {store_info.store_name}") logger.info(f"Store: {store_info.store_name}")
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음 # 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
@ -251,7 +255,7 @@ async def make_song_result(request: Request, conn: Connection):
combined_sample_song = None combined_sample_song = None
if form_data.lyrics_ids: if form_data.lyrics_ids:
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}") logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}")
lyrics_query = """ lyrics_query = """
SELECT sample_song FROM song_sample 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( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("선택된 lyrics가 없습니다") logger.info("선택된 lyrics가 없습니다")
# 5. 템플릿 가져오기 # 5. 템플릿 가져오기
if not form_data.prompts: if not form_data.prompts:
@ -283,7 +287,7 @@ async def make_song_result(request: Request, conn: Connection):
detail="프롬프트 ID가 필요합니다", detail="프롬프트 ID가 필요합니다",
) )
print("템플릿 가져오기") logger.info("템플릿 가져오기")
prompts_query = """ prompts_query = """
SELECT * FROM prompt_template WHERE id=:id; SELECT * FROM prompt_template WHERE id=:id;
@ -310,7 +314,7 @@ async def make_song_result(request: Request, conn: Connection):
) )
prompt = prompts_info[0] prompt = prompts_info[0]
print(f"Prompt Template: {prompt.prompt}") logger.debug(f"Prompt Template: {prompt.prompt}")
# ✅ 6. 프롬프트 조합 # ✅ 6. 프롬프트 조합
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format( 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} {combined_sample_song}
""" """
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") logger.debug(f"[업데이트된 프롬프트]\n{updated_prompt}")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt) 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_with_space}
전체 글자 (공백 제외): {total_chars_without_space}\r\n\r\n{generated_lyrics}""" 전체 글자 (공백 제외): {total_chars_without_space}\r\n\r\n{generated_lyrics}"""
print("=" * 40) logger.debug("=" * 40)
print("[translate:form_data.attributes_str:] ", form_data.attributes_str) logger.debug(f"[translate:form_data.attributes_str:] {form_data.attributes_str}")
print("[translate:total_chars_with_space:] ", total_chars_with_space) logger.debug(f"[translate:total_chars_with_space:] {total_chars_with_space}")
print("[translate:total_chars_without_space:] ", total_chars_without_space) logger.debug(f"[translate:total_chars_without_space:] {total_chars_without_space}")
print("[translate:final_lyrics:]") logger.debug(f"[translate:final_lyrics:]\n{final_lyrics}")
print(final_lyrics) logger.debug("=" * 40)
print("=" * 40)
# 8. DB 저장 # 8. DB 저장
insert_query = """ 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.execute(text(insert_query), insert_params)
await conn.commit() await conn.commit()
print("결과 저장 완료") logger.info("결과 저장 완료")
print("\n전체 결과 조회 중...") logger.info("전체 결과 조회 중...")
# 9. 생성 결과 가져오기 (created_at 역순) # 9. 생성 결과 가져오기 (created_at 역순)
select_query = """ select_query = """
@ -430,26 +433,20 @@ async def make_song_result(request: Request, conn: Connection):
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: except HTTPException:
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",
@ -490,25 +487,19 @@ async def get_song_result(conn: Connection): # 반환 타입 수정
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: # HTTPException은 그대로 raise except HTTPException: # HTTPException은 그대로 raise
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",
@ -520,9 +511,9 @@ async def make_automation(request: Request, conn: Connection):
# 1. Form 데이터 파싱 # 1. Form 데이터 파싱
form_data = await SongFormData.from_form(request) form_data = await SongFormData.from_form(request)
print(f"\n{'=' * 60}") logger.info(f"{'=' * 60}")
print(f"Store ID: {form_data.store_id}") logger.info(f"Store ID: {form_data.store_id}")
print(f"{'=' * 60}\n") logger.info(f"{'=' * 60}")
# 2. Store 정보 조회 # 2. Store 정보 조회
store_query = """ store_query = """
@ -551,7 +542,7 @@ async def make_automation(request: Request, conn: Connection):
) )
store_info = all_store_info[0] store_info = all_store_info[0]
print(f"Store: {store_info.store_name}") logger.info(f"Store: {store_info.store_name}")
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음 # 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
attribute_query = """ attribute_query = """
@ -596,13 +587,13 @@ async def make_automation(request: Request, conn: Connection):
# 최종 문자열 생성 # 최종 문자열 생성
formatted_attributes = "\n".join(formatted_pairs) formatted_attributes = "\n".join(formatted_pairs)
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n") logger.debug(f"[포맷팅된 문자열 속성 정보]\n{formatted_attributes}")
else: else:
print("속성 데이터가 없습니다") logger.info("속성 데이터가 없습니다")
formatted_attributes = "" formatted_attributes = ""
# 4. 템플릿 가져오기 # 4. 템플릿 가져오기
print("템플릿 가져오기 (ID=1)") logger.info("템플릿 가져오기 (ID=1)")
prompts_query = """ prompts_query = """
SELECT * FROM prompt_template WHERE id=1; SELECT * FROM prompt_template WHERE id=1;
@ -624,7 +615,7 @@ async def make_automation(request: Request, conn: Connection):
prompt=row[2], prompt=row[2],
) )
print(f"Prompt Template: {prompt.prompt}") logger.debug(f"Prompt Template: {prompt.prompt}")
# 5. 템플릿 조합 # 5. 템플릿 조합
@ -635,17 +626,17 @@ async def make_automation(request: Request, conn: Connection):
description=store_info.store_info or "", description=store_info.store_info or "",
) )
print("\n" + "=" * 80) logger.debug("=" * 80)
print("업데이트된 프롬프트") logger.debug("업데이트된 프롬프트")
print("=" * 80) logger.debug("=" * 80)
print(updated_prompt) logger.debug(updated_prompt)
print("=" * 80 + "\n") logger.debug("=" * 80)
# 4. Sample Song 조회 및 결합 # 4. Sample Song 조회 및 결합
combined_sample_song = None combined_sample_song = None
if form_data.lyrics_ids: if form_data.lyrics_ids:
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}") logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}")
lyrics_query = """ lyrics_query = """
SELECT sample_song FROM song_sample SELECT sample_song FROM song_sample
@ -664,14 +655,14 @@ async def make_automation(request: Request, conn: Connection):
combined_sample_song = "\n\n".join( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("선택된 lyrics가 없습니다") logger.info("선택된 lyrics가 없습니다")
# 1. song_sample 테이블의 모든 ID 조회 # 1. song_sample 테이블의 모든 ID 조회
print("\n[샘플 가사 랜덤 선택]") logger.info("[샘플 가사 랜덤 선택]")
all_ids_query = """ all_ids_query = """
SELECT id FROM song_sample; 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)) ids_result = await conn.execute(text(all_ids_query))
all_ids = [row.id for row in ids_result.fetchall()] all_ids = [row.id for row in ids_result.fetchall()]
print(f"전체 샘플 가사 개수: {len(all_ids)}") logger.info(f"전체 샘플 가사 개수: {len(all_ids)}")
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체) # 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
combined_sample_song = None combined_sample_song = None
@ -689,7 +680,7 @@ async def make_automation(request: Request, conn: Connection):
sample_count = min(3, len(all_ids)) sample_count = min(3, len(all_ids))
selected_ids = random.sample(all_ids, sample_count) selected_ids = random.sample(all_ids, sample_count)
print(f"랜덤 선택된 ID: {selected_ids}") logger.info(f"랜덤 선택된 ID: {selected_ids}")
# 3. 선택된 ID로 샘플 가사 조회 # 3. 선택된 ID로 샘플 가사 조회
lyrics_query = """ lyrics_query = """
@ -710,11 +701,11 @@ async def make_automation(request: Request, conn: Connection):
combined_sample_song = "\n\n".join( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("song_sample 테이블에 데이터가 없습니다") logger.info("song_sample 테이블에 데이터가 없습니다")
# 5. 프롬프트에 샘플 가사 추가 # 5. 프롬프트에 샘플 가사 추가
if combined_sample_song: if combined_sample_song:
@ -726,11 +717,11 @@ async def make_automation(request: Request, conn: Connection):
{combined_sample_song} {combined_sample_song}
""" """
print("샘플 가사 정보가 프롬프트에 추가되었습니다") logger.info("샘플 가사 정보가 프롬프트에 추가되었습니다")
else: else:
print("샘플 가사가 없어 기본 프롬프트만 사용합니다") logger.info("샘플 가사가 없어 기본 프롬프트만 사용합니다")
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") logger.info(f"[최종 프롬프트 길이: {len(updated_prompt)} 자]")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt) 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() :sample_song, :result_song, NOW()
); );
""" """
print("\n[insert_params 선택된 속성 확인]") logger.debug("[insert_params 선택된 속성 확인]")
print(f"Categories: {selected_categories}") logger.debug(f"Categories: {selected_categories}")
print(f"Values: {selected_values}") logger.debug(f"Values: {selected_values}")
print()
# attr_category, attr_value # attr_category, attr_value
insert_params = { insert_params = {
@ -792,9 +782,9 @@ async def make_automation(request: Request, conn: Connection):
await conn.execute(text(insert_query), insert_params) await conn.execute(text(insert_query), insert_params)
await conn.commit() await conn.commit()
print("결과 저장 완료") logger.info("결과 저장 완료")
print("\n전체 결과 조회 중...") logger.info("전체 결과 조회 중...")
# 9. 생성 결과 가져오기 (created_at 역순) # 9. 생성 결과 가져오기 (created_at 역순)
select_query = """ select_query = """
@ -826,26 +816,20 @@ async def make_automation(request: Request, conn: Connection):
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: except HTTPException:
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",

View File

@ -4,7 +4,6 @@ Lyric Background Tasks
가사 생성 관련 백그라운드 태스크를 정의합니다. 가사 생성 관련 백그라운드 태스크를 정의합니다.
""" """
import logging
import traceback import traceback
from sqlalchemy import select from sqlalchemy import select
@ -13,9 +12,10 @@ from sqlalchemy.exc import SQLAlchemyError
from app.database.session import BackgroundSessionLocal from app.database.session import BackgroundSessionLocal
from app.lyric.models import Lyric from app.lyric.models import Lyric
from app.utils.chatgpt_prompt import ChatgptService 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( async def _update_lyric_status(
@ -49,20 +49,16 @@ async def _update_lyric_status(
lyric.lyric_result = result lyric.lyric_result = result
await session.commit() await session.commit()
logger.info(f"[Lyric] Status updated - task_id: {task_id}, status: {status}") 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 return True
else: else:
logger.warning(f"[Lyric] NOT FOUND in DB - task_id: {task_id}") 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 return False
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[Lyric] DB Error while updating status - task_id: {task_id}, error: {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 return False
except Exception as e: except Exception as e:
logger.error(f"[Lyric] Unexpected error while updating status - task_id: {task_id}, error: {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 return False
@ -82,15 +78,15 @@ async def generate_lyric_background(
task_start = time.perf_counter() task_start = time.perf_counter()
logger.info(f"[generate_lyric_background] START - task_id: {task_id}") logger.info(f"[generate_lyric_background] START - task_id: {task_id}")
print(f"[generate_lyric_background] ========== START ==========") logger.debug(f"[generate_lyric_background] ========== START ==========")
print(f"[generate_lyric_background] task_id: {task_id}") logger.debug(f"[generate_lyric_background] task_id: {task_id}")
print(f"[generate_lyric_background] language: {language}") logger.debug(f"[generate_lyric_background] language: {language}")
print(f"[generate_lyric_background] prompt length: {len(prompt)}") logger.debug(f"[generate_lyric_background] prompt length: {len(prompt)}")
try: try:
# ========== Step 1: ChatGPT 서비스 초기화 ========== # ========== Step 1: ChatGPT 서비스 초기화 ==========
step1_start = time.perf_counter() step1_start = time.perf_counter()
print(f"[generate_lyric_background] Step 1: ChatGPT 서비스 초기화...") logger.debug(f"[generate_lyric_background] Step 1: ChatGPT 서비스 초기화...")
service = ChatgptService( service = ChatgptService(
customer_name="", # 프롬프트가 이미 생성되었으므로 빈 값 customer_name="", # 프롬프트가 이미 생성되었으므로 빈 값
@ -100,47 +96,42 @@ async def generate_lyric_background(
) )
step1_elapsed = (time.perf_counter() - step1_start) * 1000 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 호출 (가사 생성) ========== # ========== Step 2: ChatGPT API 호출 (가사 생성) ==========
step2_start = time.perf_counter() step2_start = time.perf_counter()
logger.info(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작 - task_id: {task_id}") 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) result = await service.generate(prompt=prompt)
step2_elapsed = (time.perf_counter() - step2_start) * 1000 step2_elapsed = (time.perf_counter() - step2_start) * 1000
logger.info(f"[generate_lyric_background] Step 2 완료 - 응답 {len(result)}자 ({step2_elapsed:.1f}ms)") 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 상태 업데이트 ========== # ========== Step 3: DB 상태 업데이트 ==========
step3_start = time.perf_counter() 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) await _update_lyric_status(task_id, "completed", result)
step3_elapsed = (time.perf_counter() - step3_start) * 1000 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 total_elapsed = (time.perf_counter() - task_start) * 1000
logger.info(f"[generate_lyric_background] SUCCESS - task_id: {task_id}, 총 소요시간: {total_elapsed:.1f}ms") logger.info(f"[generate_lyric_background] SUCCESS - task_id: {task_id}, 총 소요시간: {total_elapsed:.1f}ms")
print(f"[generate_lyric_background] ========== SUCCESS ==========") logger.debug(f"[generate_lyric_background] ========== SUCCESS ==========")
print(f"[generate_lyric_background] 총 소요시간: {total_elapsed:.1f}ms") logger.debug(f"[generate_lyric_background] 총 소요시간: {total_elapsed:.1f}ms")
print(f"[generate_lyric_background] - Step 1 (서비스 초기화): {step1_elapsed:.1f}ms") logger.debug(f"[generate_lyric_background] - Step 1 (서비스 초기화): {step1_elapsed:.1f}ms")
print(f"[generate_lyric_background] - Step 2 (GPT API 호출): {step2_elapsed:.1f}ms") logger.debug(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] - Step 3 (DB 업데이트): {step3_elapsed:.1f}ms")
except SQLAlchemyError as e: except SQLAlchemyError as e:
elapsed = (time.perf_counter() - task_start) * 1000 elapsed = (time.perf_counter() - task_start) * 1000
logger.error(f"[generate_lyric_background] DB ERROR - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)") logger.error(f"[generate_lyric_background] DB ERROR - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)", exc_info=True)
print(f"[generate_lyric_background] DB ERROR - {e} ({elapsed:.1f}ms)")
traceback.print_exc()
await _update_lyric_status(task_id, "failed", f"Database Error: {str(e)}") await _update_lyric_status(task_id, "failed", f"Database Error: {str(e)}")
except Exception as e: except Exception as e:
elapsed = (time.perf_counter() - task_start) * 1000 elapsed = (time.perf_counter() - task_start) * 1000
logger.error(f"[generate_lyric_background] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)") logger.error(f"[generate_lyric_background] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)", exc_info=True)
print(f"[generate_lyric_background] EXCEPTION - {e} ({elapsed:.1f}ms)")
traceback.print_exc()
await _update_lyric_status(task_id, "failed", f"Error: {str(e)}") await _update_lyric_status(task_id, "failed", f"Error: {str(e)}")

View File

@ -32,9 +32,11 @@ from app.song.schemas.song_schema import (
PollingSongResponse, PollingSongResponse,
SongListItem, SongListItem,
) )
from app.utils.logger import get_logger
from app.utils.pagination import PaginatedResponse from app.utils.pagination import PaginatedResponse
from app.utils.suno import SunoService from app.utils.suno import SunoService
logger = get_logger("song")
router = APIRouter(prefix="/song", tags=["song"]) router = APIRouter(prefix="/song", tags=["song"])
@ -99,7 +101,7 @@ async def generate_song(
from app.database.session import AsyncSessionLocal from app.database.session import AsyncSessionLocal
request_start = time.perf_counter() request_start = time.perf_counter()
print( logger.info(
f"[generate_song] START - task_id: {task_id}, " f"[generate_song] START - task_id: {task_id}, "
f"genre: {request_body.genre}, language: {request_body.language}" f"genre: {request_body.genre}, language: {request_body.language}"
) )
@ -124,7 +126,7 @@ async def generate_song(
project = project_result.scalar_one_or_none() project = project_result.scalar_one_or_none()
if not project: 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( raise HTTPException(
status_code=404, status_code=404,
detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.", detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.",
@ -141,7 +143,7 @@ async def generate_song(
lyric = lyric_result.scalar_one_or_none() lyric = lyric_result.scalar_one_or_none()
if not lyric: 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( raise HTTPException(
status_code=404, status_code=404,
detail=f"task_id '{task_id}'에 해당하는 Lyric을 찾을 수 없습니다.", detail=f"task_id '{task_id}'에 해당하는 Lyric을 찾을 수 없습니다.",
@ -149,7 +151,7 @@ async def generate_song(
lyric_id = lyric.id lyric_id = lyric.id
query_time = time.perf_counter() query_time = time.perf_counter()
print( logger.info(
f"[generate_song] Queries completed - task_id: {task_id}, " f"[generate_song] Queries completed - task_id: {task_id}, "
f"project_id: {project_id}, lyric_id: {lyric_id}, " f"project_id: {project_id}, lyric_id: {lyric_id}, "
f"elapsed: {(query_time - request_start)*1000:.1f}ms" f"elapsed: {(query_time - request_start)*1000:.1f}ms"
@ -174,7 +176,7 @@ async def generate_song(
song_id = song.id song_id = song.id
stage1_time = time.perf_counter() stage1_time = time.perf_counter()
print( logger.info(
f"[generate_song] Stage 1 DONE - Song saved - " f"[generate_song] Stage 1 DONE - Song saved - "
f"task_id: {task_id}, song_id: {song_id}, " f"task_id: {task_id}, song_id: {song_id}, "
f"elapsed: {(stage1_time - request_start)*1000:.1f}ms" f"elapsed: {(stage1_time - request_start)*1000:.1f}ms"
@ -184,7 +186,7 @@ async def generate_song(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
print( logger.error(
f"[generate_song] Stage 1 EXCEPTION - " f"[generate_song] Stage 1 EXCEPTION - "
f"task_id: {task_id}, error: {type(e).__name__}: {e}" f"task_id: {task_id}, error: {type(e).__name__}: {e}"
) )
@ -203,7 +205,7 @@ async def generate_song(
suno_task_id: str | None = None suno_task_id: str | None = None
try: 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_service = SunoService()
suno_task_id = await suno_service.generate( suno_task_id = await suno_service.generate(
prompt=request_body.lyrics, prompt=request_body.lyrics,
@ -211,14 +213,14 @@ async def generate_song(
) )
stage2_time = time.perf_counter() stage2_time = time.perf_counter()
print( logger.info(
f"[generate_song] Stage 2 DONE - task_id: {task_id}, " f"[generate_song] Stage 2 DONE - task_id: {task_id}, "
f"suno_task_id: {suno_task_id}, " f"suno_task_id: {suno_task_id}, "
f"elapsed: {(stage2_time - stage2_start)*1000:.1f}ms" f"elapsed: {(stage2_time - stage2_start)*1000:.1f}ms"
) )
except Exception as e: except Exception as e:
print( logger.error(
f"[generate_song] Stage 2 EXCEPTION - Suno API failed - " f"[generate_song] Stage 2 EXCEPTION - Suno API failed - "
f"task_id: {task_id}, error: {type(e).__name__}: {e}" f"task_id: {task_id}, error: {type(e).__name__}: {e}"
) )
@ -244,7 +246,7 @@ async def generate_song(
# 3단계: suno_task_id 업데이트 (새 세션으로 빠르게 처리) # 3단계: suno_task_id 업데이트 (새 세션으로 빠르게 처리)
# ========================================================================== # ==========================================================================
stage3_start = time.perf_counter() 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: try:
async with AsyncSessionLocal() as update_session: async with AsyncSessionLocal() as update_session:
@ -258,11 +260,11 @@ async def generate_song(
stage3_time = time.perf_counter() stage3_time = time.perf_counter()
total_time = stage3_time - request_start total_time = stage3_time - request_start
print( logger.info(
f"[generate_song] Stage 3 DONE - task_id: {task_id}, " f"[generate_song] Stage 3 DONE - task_id: {task_id}, "
f"elapsed: {(stage3_time - stage3_start)*1000:.1f}ms" f"elapsed: {(stage3_time - stage3_start)*1000:.1f}ms"
) )
print( logger.info(
f"[generate_song] SUCCESS - task_id: {task_id}, " f"[generate_song] SUCCESS - task_id: {task_id}, "
f"suno_task_id: {suno_task_id}, " f"suno_task_id: {suno_task_id}, "
f"total_time: {total_time*1000:.1f}ms" f"total_time: {total_time*1000:.1f}ms"
@ -277,7 +279,7 @@ async def generate_song(
) )
except Exception as e: except Exception as e:
print( logger.error(
f"[generate_song] Stage 3 EXCEPTION - " f"[generate_song] Stage 3 EXCEPTION - "
f"task_id: {task_id}, error: {type(e).__name__}: {e}" 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로, Azure Blob Storage에 업로드한 Song 테이블의 status를 completed로,
song_result_url을 Blob URL로 업데이트합니다. 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: try:
suno_service = SunoService() suno_service = SunoService()
result = await suno_service.get_task_status(suno_task_id) result = await suno_service.get_task_status(suno_task_id)
parsed_response = suno_service.parse_status_response(result) 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에 직접 저장 # SUCCESS 상태인 경우 첫 번째 클립 정보를 DB에 직접 저장
if parsed_response.status == "SUCCESS" and parsed_response.clips: if parsed_response.status == "SUCCESS" and parsed_response.clips:
@ -354,7 +356,7 @@ async def get_song_status(
first_clip = parsed_response.clips[0] first_clip = parsed_response.clips[0]
audio_url = first_clip.audio_url audio_url = first_clip.audio_url
clip_duration = first_clip.duration 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: if audio_url:
# suno_task_id로 Song 조회 # suno_task_id로 Song 조회
@ -373,17 +375,17 @@ async def get_song_status(
if clip_duration is not None: if clip_duration is not None:
song.duration = clip_duration song.duration = clip_duration
await session.commit() 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": 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 return parsed_response
except Exception as e: except Exception as e:
import traceback 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( return PollingSongResponse(
success=False, success=False,
status="error", status="error",
@ -438,7 +440,7 @@ async def download_song(
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> DownloadSongResponse: ) -> DownloadSongResponse:
"""task_id로 Song 상태를 polling하고 completed 시 Project 정보와 노래 URL을 반환합니다.""" """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: try:
# task_id로 Song 조회 (여러 개 있을 경우 가장 최근 것 선택) # task_id로 Song 조회 (여러 개 있을 경우 가장 최근 것 선택)
song_result = await session.execute( song_result = await session.execute(
@ -450,7 +452,7 @@ async def download_song(
song = song_result.scalar_one_or_none() song = song_result.scalar_one_or_none()
if not song: 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( return DownloadSongResponse(
success=False, success=False,
status="not_found", status="not_found",
@ -458,11 +460,11 @@ async def download_song(
error_message="Song not found", 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 상태인 경우 # processing 상태인 경우
if 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( return DownloadSongResponse(
success=True, success=True,
status="processing", status="processing",
@ -472,7 +474,7 @@ async def download_song(
# failed 상태인 경우 # failed 상태인 경우
if song.status == "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( return DownloadSongResponse(
success=False, success=False,
status="failed", status="failed",
@ -487,7 +489,7 @@ async def download_song(
) )
project = project_result.scalar_one_or_none() 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( return DownloadSongResponse(
success=True, success=True,
status="completed", status="completed",
@ -502,7 +504,7 @@ async def download_song(
) )
except Exception as e: 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( return DownloadSongResponse(
success=False, success=False,
status="error", status="error",
@ -550,7 +552,7 @@ async def get_songs(
pagination: PaginationParams = Depends(get_pagination_params), pagination: PaginationParams = Depends(get_pagination_params),
) -> PaginatedResponse[SongListItem]: ) -> 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: try:
offset = (pagination.page - 1) * pagination.page_size offset = (pagination.page - 1) * pagination.page_size
@ -622,14 +624,14 @@ async def get_songs(
page_size=pagination.page_size, page_size=pagination.page_size,
) )
print( logger.info(
f"[get_songs] SUCCESS - total: {total}, page: {pagination.page}, " f"[get_songs] SUCCESS - total: {total}, page: {pagination.page}, "
f"page_size: {pagination.page_size}, items_count: {len(items)}" f"page_size: {pagination.page_size}, items_count: {len(items)}"
) )
return response return response
except Exception as e: except Exception as e:
print(f"[get_songs] EXCEPTION - error: {e}") logger.error(f"[get_songs] EXCEPTION - error: {e}")
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail=f"노래 목록 조회에 실패했습니다: {str(e)}", detail=f"노래 목록 조회에 실패했습니다: {str(e)}",

View File

@ -6,6 +6,7 @@ from fastapi.exceptions import HTTPException
from sqlalchemy import Connection, text from sqlalchemy import Connection, text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from app.utils.logger import get_logger
from app.lyrics.schemas.lyrics_schema import ( from app.lyrics.schemas.lyrics_schema import (
AttributeData, AttributeData,
PromptTemplateData, PromptTemplateData,
@ -15,6 +16,8 @@ from app.lyrics.schemas.lyrics_schema import (
) )
from app.utils.chatgpt_prompt import chatgpt_api from app.utils.chatgpt_prompt import chatgpt_api
logger = get_logger("song")
async def get_store_info(conn: Connection) -> List[StoreData]: async def get_store_info(conn: Connection) -> List[StoreData]:
try: try:
@ -38,13 +41,13 @@ async def get_store_info(conn: Connection) -> List[StoreData]:
result.close() result.close()
return all_store_info return all_store_info
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemyError in get_store_info: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_store_info: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -69,13 +72,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
result.close() result.close()
return all_attribute return all_attribute
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemyError in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -100,13 +103,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
result.close() result.close()
return all_attribute return all_attribute
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemyError in get_attribute (duplicate): {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_attribute (duplicate): {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -132,13 +135,13 @@ async def get_sample_song(conn: Connection) -> List[SongSampleData]:
result.close() result.close()
return all_sample_song return all_sample_song
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemyError in get_sample_song: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_sample_song: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -162,13 +165,13 @@ async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
result.close() result.close()
return all_prompt_template return all_prompt_template
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemyError in get_prompt_template: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_prompt_template: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -192,13 +195,13 @@ async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
result.close() result.close()
return all_prompt_template return all_prompt_template
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemyError in get_song_result (prompt_template): {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_song_result (prompt_template): {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -210,11 +213,11 @@ async def make_song_result(request: Request, conn: Connection):
# 1. Form 데이터 파싱 # 1. Form 데이터 파싱
form_data = await SongFormData.from_form(request) form_data = await SongFormData.from_form(request)
print(f"\n{'=' * 60}") logger.info(f"{'=' * 60}")
print(f"Store ID: {form_data.store_id}") logger.info(f"Store ID: {form_data.store_id}")
print(f"Lyrics IDs: {form_data.lyrics_ids}") logger.info(f"Lyrics IDs: {form_data.lyrics_ids}")
print(f"Prompt IDs: {form_data.prompts}") logger.info(f"Prompt IDs: {form_data.prompts}")
print(f"{'=' * 60}\n") logger.info(f"{'=' * 60}")
# 2. Store 정보 조회 # 2. Store 정보 조회
store_query = """ store_query = """
@ -243,7 +246,7 @@ async def make_song_result(request: Request, conn: Connection):
) )
store_info = all_store_info[0] store_info = all_store_info[0]
print(f"Store: {store_info.store_name}") logger.info(f"Store: {store_info.store_name}")
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음 # 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
@ -251,7 +254,7 @@ async def make_song_result(request: Request, conn: Connection):
combined_sample_song = None combined_sample_song = None
if form_data.lyrics_ids: if form_data.lyrics_ids:
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}") logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}")
lyrics_query = """ lyrics_query = """
SELECT sample_song FROM song_sample 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( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("선택된 lyrics가 없습니다") logger.info("선택된 lyrics가 없습니다")
# 5. 템플릿 가져오기 # 5. 템플릿 가져오기
if not form_data.prompts: if not form_data.prompts:
@ -283,7 +286,7 @@ async def make_song_result(request: Request, conn: Connection):
detail="프롬프트 ID가 필요합니다", detail="프롬프트 ID가 필요합니다",
) )
print("템플릿 가져오기") logger.info("템플릿 가져오기")
prompts_query = """ prompts_query = """
SELECT * FROM prompt_template WHERE id=:id; SELECT * FROM prompt_template WHERE id=:id;
@ -310,7 +313,7 @@ async def make_song_result(request: Request, conn: Connection):
) )
prompt = prompts_info[0] prompt = prompts_info[0]
print(f"Prompt Template: {prompt.prompt}") logger.debug(f"Prompt Template: {prompt.prompt}")
# ✅ 6. 프롬프트 조합 # ✅ 6. 프롬프트 조합
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format( 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} {combined_sample_song}
""" """
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") logger.debug(f"[업데이트된 프롬프트]\n{updated_prompt}")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt) 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_with_space}
전체 글자 (공백 제외): {total_chars_without_space}\r\n\r\n{generated_lyrics}""" 전체 글자 (공백 제외): {total_chars_without_space}\r\n\r\n{generated_lyrics}"""
print("=" * 40) logger.debug("=" * 40)
print("[translate:form_data.attributes_str:] ", form_data.attributes_str) logger.debug(f"[translate:form_data.attributes_str:] {form_data.attributes_str}")
print("[translate:total_chars_with_space:] ", total_chars_with_space) logger.debug(f"[translate:total_chars_with_space:] {total_chars_with_space}")
print("[translate:total_chars_without_space:] ", total_chars_without_space) logger.debug(f"[translate:total_chars_without_space:] {total_chars_without_space}")
print("[translate:final_lyrics:]") logger.debug(f"[translate:final_lyrics:]\n{final_lyrics}")
print(final_lyrics) logger.debug("=" * 40)
print("=" * 40)
# 8. DB 저장 # 8. DB 저장
insert_query = """ 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.execute(text(insert_query), insert_params)
await conn.commit() await conn.commit()
print("결과 저장 완료") logger.info("make_song_result 결과 저장 완료")
print("\n전체 결과 조회 중...") logger.info("make_song_result 전체 결과 조회 중...")
# 9. 생성 결과 가져오기 (created_at 역순) # 9. 생성 결과 가져오기 (created_at 역순)
select_query = """ select_query = """
@ -430,26 +432,20 @@ async def make_song_result(request: Request, conn: Connection):
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"make_song_result 전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: except HTTPException:
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"make_song_result Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"make_song_result Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",
@ -490,25 +486,19 @@ async def get_song_result(conn: Connection): # 반환 타입 수정
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"get_song_result 전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: # HTTPException은 그대로 raise except HTTPException: # HTTPException은 그대로 raise
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"get_song_result Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"get_song_result Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",
@ -520,9 +510,9 @@ async def make_automation(request: Request, conn: Connection):
# 1. Form 데이터 파싱 # 1. Form 데이터 파싱
form_data = await SongFormData.from_form(request) form_data = await SongFormData.from_form(request)
print(f"\n{'=' * 60}") logger.info(f"{'=' * 60}")
print(f"Store ID: {form_data.store_id}") logger.info(f"make_automation Store ID: {form_data.store_id}")
print(f"{'=' * 60}\n") logger.info(f"{'=' * 60}")
# 2. Store 정보 조회 # 2. Store 정보 조회
store_query = """ store_query = """
@ -551,7 +541,7 @@ async def make_automation(request: Request, conn: Connection):
) )
store_info = all_store_info[0] store_info = all_store_info[0]
print(f"Store: {store_info.store_name}") logger.info(f"make_automation Store: {store_info.store_name}")
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음 # 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
attribute_query = """ attribute_query = """
@ -596,13 +586,13 @@ async def make_automation(request: Request, conn: Connection):
# 최종 문자열 생성 # 최종 문자열 생성
formatted_attributes = "\n".join(formatted_pairs) formatted_attributes = "\n".join(formatted_pairs)
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n") logger.debug(f"[포맷팅된 문자열 속성 정보]\n{formatted_attributes}")
else: else:
print("속성 데이터가 없습니다") logger.info("속성 데이터가 없습니다")
formatted_attributes = "" formatted_attributes = ""
# 4. 템플릿 가져오기 # 4. 템플릿 가져오기
print("템플릿 가져오기 (ID=1)") logger.info("템플릿 가져오기 (ID=1)")
prompts_query = """ prompts_query = """
SELECT * FROM prompt_template WHERE id=1; SELECT * FROM prompt_template WHERE id=1;
@ -624,7 +614,7 @@ async def make_automation(request: Request, conn: Connection):
prompt=row[2], prompt=row[2],
) )
print(f"Prompt Template: {prompt.prompt}") logger.debug(f"Prompt Template: {prompt.prompt}")
# 5. 템플릿 조합 # 5. 템플릿 조합
@ -635,17 +625,17 @@ async def make_automation(request: Request, conn: Connection):
description=store_info.store_info or "", description=store_info.store_info or "",
) )
print("\n" + "=" * 80) logger.debug("=" * 80)
print("업데이트된 프롬프트") logger.debug("업데이트된 프롬프트")
print("=" * 80) logger.debug("=" * 80)
print(updated_prompt) logger.debug(updated_prompt)
print("=" * 80 + "\n") logger.debug("=" * 80)
# 4. Sample Song 조회 및 결합 # 4. Sample Song 조회 및 결합
combined_sample_song = None combined_sample_song = None
if form_data.lyrics_ids: if form_data.lyrics_ids:
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}") logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}")
lyrics_query = """ lyrics_query = """
SELECT sample_song FROM song_sample SELECT sample_song FROM song_sample
@ -664,14 +654,14 @@ async def make_automation(request: Request, conn: Connection):
combined_sample_song = "\n\n".join( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("선택된 lyrics가 없습니다") logger.info("선택된 lyrics가 없습니다")
# 1. song_sample 테이블의 모든 ID 조회 # 1. song_sample 테이블의 모든 ID 조회
print("\n[샘플 가사 랜덤 선택]") logger.info("[샘플 가사 랜덤 선택]")
all_ids_query = """ all_ids_query = """
SELECT id FROM song_sample; 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)) ids_result = await conn.execute(text(all_ids_query))
all_ids = [row.id for row in ids_result.fetchall()] all_ids = [row.id for row in ids_result.fetchall()]
print(f"전체 샘플 가사 개수: {len(all_ids)}") logger.info(f"전체 샘플 가사 개수: {len(all_ids)}")
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체) # 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
combined_sample_song = None combined_sample_song = None
@ -689,7 +679,7 @@ async def make_automation(request: Request, conn: Connection):
sample_count = min(3, len(all_ids)) sample_count = min(3, len(all_ids))
selected_ids = random.sample(all_ids, sample_count) selected_ids = random.sample(all_ids, sample_count)
print(f"랜덤 선택된 ID: {selected_ids}") logger.debug(f"랜덤 선택된 ID: {selected_ids}")
# 3. 선택된 ID로 샘플 가사 조회 # 3. 선택된 ID로 샘플 가사 조회
lyrics_query = """ lyrics_query = """
@ -710,11 +700,11 @@ async def make_automation(request: Request, conn: Connection):
combined_sample_song = "\n\n".join( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("song_sample 테이블에 데이터가 없습니다") logger.info("song_sample 테이블에 데이터가 없습니다")
# 5. 프롬프트에 샘플 가사 추가 # 5. 프롬프트에 샘플 가사 추가
if combined_sample_song: if combined_sample_song:
@ -726,11 +716,11 @@ async def make_automation(request: Request, conn: Connection):
{combined_sample_song} {combined_sample_song}
""" """
print("샘플 가사 정보가 프롬프트에 추가되었습니다") logger.info("샘플 가사 정보가 프롬프트에 추가되었습니다")
else: else:
print("샘플 가사가 없어 기본 프롬프트만 사용합니다") logger.info("샘플 가사가 없어 기본 프롬프트만 사용합니다")
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") logger.info(f"[최종 프롬프트 길이: {len(updated_prompt)} 자]")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt) 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() :sample_song, :result_song, NOW()
); );
""" """
print("\n[insert_params 선택된 속성 확인]") logger.debug("[insert_params 선택된 속성 확인]")
print(f"Categories: {selected_categories}") logger.debug(f"Categories: {selected_categories}")
print(f"Values: {selected_values}") logger.debug(f"Values: {selected_values}")
print()
# attr_category, attr_value # attr_category, attr_value
insert_params = { insert_params = {
@ -792,9 +781,9 @@ async def make_automation(request: Request, conn: Connection):
await conn.execute(text(insert_query), insert_params) await conn.execute(text(insert_query), insert_params)
await conn.commit() await conn.commit()
print("결과 저장 완료") logger.info("make_automation 결과 저장 완료")
print("\n전체 결과 조회 중...") logger.info("make_automation 전체 결과 조회 중...")
# 9. 생성 결과 가져오기 (created_at 역순) # 9. 생성 결과 가져오기 (created_at 역순)
select_query = """ select_query = """
@ -826,26 +815,20 @@ async def make_automation(request: Request, conn: Connection):
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"make_automation 전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: except HTTPException:
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"make_automation Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"make_automation Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",

View File

@ -4,7 +4,6 @@ Song Background Tasks
노래 생성 관련 백그라운드 태스크를 정의합니다. 노래 생성 관련 백그라운드 태스크를 정의합니다.
""" """
import logging
import traceback import traceback
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
@ -17,11 +16,12 @@ from sqlalchemy.exc import SQLAlchemyError
from app.database.session import BackgroundSessionLocal from app.database.session import BackgroundSessionLocal
from app.song.models import Song from app.song.models import Song
from app.utils.common import generate_task_id 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 app.utils.upload_blob_as_request import AzureBlobUploader
from config import prj_settings from config import prj_settings
# 로거 설정 # 로거 설정
logger = logging.getLogger(__name__) logger = get_logger("song")
# HTTP 요청 설정 # HTTP 요청 설정
REQUEST_TIMEOUT = 120.0 # 초 REQUEST_TIMEOUT = 120.0 # 초
@ -73,20 +73,16 @@ async def _update_song_status(
song.duration = duration song.duration = duration
await session.commit() await session.commit()
logger.info(f"[Song] Status updated - task_id: {task_id}, status: {status}") 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 return True
else: else:
logger.warning(f"[Song] NOT FOUND in DB - task_id: {task_id}") 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 return False
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[Song] DB Error while updating status - task_id: {task_id}, error: {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 return False
except Exception as e: except Exception as e:
logger.error(f"[Song] Unexpected error while updating status - task_id: {task_id}, error: {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 return False
@ -104,14 +100,12 @@ async def _download_audio(url: str, task_id: str) -> bytes:
httpx.HTTPError: 다운로드 실패 httpx.HTTPError: 다운로드 실패
""" """
logger.info(f"[Download] Downloading - task_id: {task_id}") logger.info(f"[Download] Downloading - task_id: {task_id}")
print(f"[Download] Downloading - task_id: {task_id}")
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=REQUEST_TIMEOUT) response = await client.get(url, timeout=REQUEST_TIMEOUT)
response.raise_for_status() response.raise_for_status()
logger.info(f"[Download] SUCCESS - task_id: {task_id}, size: {len(response.content)} bytes") 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 return response.content
@ -128,7 +122,6 @@ async def download_and_save_song(
store_name: 저장할 파일명에 사용할 업체명 store_name: 저장할 파일명에 사용할 업체명
""" """
logger.info(f"[download_and_save_song] START - task_id: {task_id}, store_name: {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: try:
# 저장 경로 생성: media/song/{날짜}/{uuid7}/{store_name}.mp3 # 저장 경로 생성: media/song/{날짜}/{uuid7}/{store_name}.mp3
@ -146,11 +139,9 @@ async def download_and_save_song(
media_dir.mkdir(parents=True, exist_ok=True) media_dir.mkdir(parents=True, exist_ok=True)
file_path = media_dir / file_name file_path = media_dir / file_name
logger.info(f"[download_and_save_song] Directory created - path: {file_path}") 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}") 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) content = await _download_audio(audio_url, task_id)
@ -158,36 +149,27 @@ async def download_and_save_song(
await f.write(content) await f.write(content)
logger.info(f"[download_and_save_song] File saved - task_id: {task_id}, path: {file_path}") 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 생성 # 프론트엔드에서 접근 가능한 URL 생성
relative_path = f"/media/song/{today}/{unique_id}/{file_name}" relative_path = f"/media/song/{today}/{unique_id}/{file_name}"
base_url = f"http://{prj_settings.PROJECT_DOMAIN}" base_url = f"http://{prj_settings.PROJECT_DOMAIN}"
file_url = f"{base_url}{relative_path}" file_url = f"{base_url}{relative_path}"
logger.info(f"[download_and_save_song] URL generated - task_id: {task_id}, url: {file_url}") logger.info(f"[download_and_save_song] URL generated - task_id: {task_id}, url: {file_url}")
print(f"[download_and_save_song] URL generated - task_id: {task_id}, url: {file_url}")
# Song 테이블 업데이트 # Song 테이블 업데이트
await _update_song_status(task_id, "completed", file_url) await _update_song_status(task_id, "completed", file_url)
logger.info(f"[download_and_save_song] SUCCESS - task_id: {task_id}") 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: except httpx.HTTPError as e:
logger.error(f"[download_and_save_song] DOWNLOAD ERROR - task_id: {task_id}, error: {e}") logger.error(f"[download_and_save_song] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_save_song] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_song_status(task_id, "failed") await _update_song_status(task_id, "failed")
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[download_and_save_song] DB ERROR - task_id: {task_id}, error: {e}") logger.error(f"[download_and_save_song] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_save_song] DB ERROR - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_song_status(task_id, "failed") await _update_song_status(task_id, "failed")
except Exception as e: except Exception as e:
logger.error(f"[download_and_save_song] EXCEPTION - task_id: {task_id}, error: {e}") logger.error(f"[download_and_save_song] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_save_song] EXCEPTION - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_song_status(task_id, "failed") await _update_song_status(task_id, "failed")
@ -204,7 +186,6 @@ async def download_and_upload_song_to_blob(
store_name: 저장할 파일명에 사용할 업체명 store_name: 저장할 파일명에 사용할 업체명
""" """
logger.info(f"[download_and_upload_song_to_blob] START - task_id: {task_id}, store_name: {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 temp_file_path: Path | None = None
try: try:
@ -220,11 +201,9 @@ async def download_and_upload_song_to_blob(
temp_dir.mkdir(parents=True, exist_ok=True) temp_dir.mkdir(parents=True, exist_ok=True)
temp_file_path = temp_dir / file_name temp_file_path = temp_dir / file_name
logger.info(f"[download_and_upload_song_to_blob] Temp directory created - path: {temp_file_path}") 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}") 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) content = await _download_audio(audio_url, task_id)
@ -232,7 +211,6 @@ async def download_and_upload_song_to_blob(
await f.write(content) await f.write(content)
logger.info(f"[download_and_upload_song_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}") 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에 업로드 # Azure Blob Storage에 업로드
uploader = AzureBlobUploader(task_id=task_id) uploader = AzureBlobUploader(task_id=task_id)
@ -244,29 +222,21 @@ async def download_and_upload_song_to_blob(
# SAS 토큰이 제외된 public_url 사용 # SAS 토큰이 제외된 public_url 사용
blob_url = uploader.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}") 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 테이블 업데이트 # Song 테이블 업데이트
await _update_song_status(task_id, "completed", blob_url) await _update_song_status(task_id, "completed", blob_url)
logger.info(f"[download_and_upload_song_to_blob] SUCCESS - task_id: {task_id}") 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: except httpx.HTTPError as e:
logger.error(f"[download_and_upload_song_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}") logger.error(f"[download_and_upload_song_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_song_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_song_status(task_id, "failed") await _update_song_status(task_id, "failed")
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[download_and_upload_song_to_blob] DB ERROR - task_id: {task_id}, error: {e}") logger.error(f"[download_and_upload_song_to_blob] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_song_to_blob] DB ERROR - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_song_status(task_id, "failed") await _update_song_status(task_id, "failed")
except Exception as e: except Exception as e:
logger.error(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}") logger.error(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_song_status(task_id, "failed") await _update_song_status(task_id, "failed")
finally: finally:
@ -275,10 +245,8 @@ async def download_and_upload_song_to_blob(
try: try:
temp_file_path.unlink() temp_file_path.unlink()
logger.info(f"[download_and_upload_song_to_blob] Temp file deleted - path: {temp_file_path}") 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: except Exception as e:
logger.warning(f"[download_and_upload_song_to_blob] Failed to delete temp file: {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 temp_dir = Path("media") / "temp" / task_id
@ -304,7 +272,6 @@ async def download_and_upload_song_by_suno_task_id(
duration: 노래 재생 시간 () 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}") 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 temp_file_path: Path | None = None
task_id: str | None = None task_id: str | None = None
@ -321,12 +288,10 @@ async def download_and_upload_song_by_suno_task_id(
if not song: if not song:
logger.warning(f"[download_and_upload_song_by_suno_task_id] Song NOT FOUND - suno_task_id: {suno_task_id}") 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 return
task_id = song.task_id 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}") 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( 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_dir.mkdir(parents=True, exist_ok=True)
temp_file_path = temp_dir / file_name 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}") 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}") 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) 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) 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}") 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에 업로드 # Azure Blob Storage에 업로드
uploader = AzureBlobUploader(task_id=task_id) uploader = AzureBlobUploader(task_id=task_id)
@ -364,7 +326,6 @@ async def download_and_upload_song_by_suno_task_id(
# SAS 토큰이 제외된 public_url 사용 # SAS 토큰이 제외된 public_url 사용
blob_url = uploader.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}") 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 테이블 업데이트 # Song 테이블 업데이트
await _update_song_status( await _update_song_status(
@ -375,26 +336,19 @@ async def download_and_upload_song_by_suno_task_id(
duration=duration, duration=duration,
) )
logger.info(f"[download_and_upload_song_by_suno_task_id] SUCCESS - suno_task_id: {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: 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}") logger.error(f"[download_and_upload_song_by_suno_task_id] DOWNLOAD ERROR - suno_task_id: {suno_task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_song_by_suno_task_id] DOWNLOAD ERROR - suno_task_id: {suno_task_id}, error: {e}")
traceback.print_exc()
if task_id: if task_id:
await _update_song_status(task_id, "failed", suno_task_id=suno_task_id) await _update_song_status(task_id, "failed", suno_task_id=suno_task_id)
except SQLAlchemyError as e: 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}") logger.error(f"[download_and_upload_song_by_suno_task_id] DB ERROR - suno_task_id: {suno_task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_song_by_suno_task_id] DB ERROR - suno_task_id: {suno_task_id}, error: {e}")
traceback.print_exc()
if task_id: if task_id:
await _update_song_status(task_id, "failed", suno_task_id=suno_task_id) await _update_song_status(task_id, "failed", suno_task_id=suno_task_id)
except Exception as e: except Exception as e:
logger.error(f"[download_and_upload_song_by_suno_task_id] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}") logger.error(f"[download_and_upload_song_by_suno_task_id] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_song_by_suno_task_id] EXCEPTION - suno_task_id: {suno_task_id}, error: {e}")
traceback.print_exc()
if task_id: if task_id:
await _update_song_status(task_id, "failed", suno_task_id=suno_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: try:
temp_file_path.unlink() temp_file_path.unlink()
logger.info(f"[download_and_upload_song_by_suno_task_id] Temp file deleted - path: {temp_file_path}") 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: except Exception as e:
logger.warning(f"[download_and_upload_song_by_suno_task_id] Failed to delete temp file: {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: if task_id:

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

0
app/user/api/__init__.py Normal file
View File

View File

View File

View File

View File

0
app/user/dependency.py Normal file
View File

141
app/user/exceptions.py Normal file
View File

@ -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,
)

159
app/user/models.py Normal file
View File

@ -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")>"
)

View File

@ -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",
]

View File

@ -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

View File

View File

View File

View File

View File

View File

View File

@ -1,13 +1,13 @@
import json import json
import logging
import re import re
from openai import AsyncOpenAI from openai import AsyncOpenAI
from app.utils.logger import get_logger
from config import apikey_settings from config import apikey_settings
# 로거 설정 # 로거 설정
logger = logging.getLogger(__name__) logger = get_logger("chatgpt")
# fmt: off # fmt: off
LYRICS_PROMPT_TEMPLATE_ORI = """ LYRICS_PROMPT_TEMPLATE_ORI = """
@ -298,13 +298,12 @@ class ChatgptService:
if prompt is None: if prompt is None:
prompt = self.build_lyrics_prompt() 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}") logger.info(f"[ChatgptService] Starting GPT request with model: {self.model}")
# GPT API 호출 # GPT API 호출
response = await self._call_gpt_api(prompt) response = await self._call_gpt_api(prompt)
print(f"[ChatgptService] SUCCESS - Response length: {len(response)}")
logger.info(f"[ChatgptService] SUCCESS - Response length: {len(response)}") logger.info(f"[ChatgptService] SUCCESS - Response length: {len(response)}")
return response return response
@ -332,7 +331,7 @@ class ChatgptService:
[OUTPUT REQUIREMENTS] [OUTPUT REQUIREMENTS]
- 5 항목으로 구분: 타겟 고객, 핵심 차별점, 지역 특성, 시즌별 포인트 - 5 항목으로 구분: 타겟 고객, 핵심 차별점, 지역 특성, 시즌별 포인트
- 항목은 줄바꿈으로 구분 - 항목은 줄바꿈으로 구분
- 500 이내로 요약 - 800 이내로 요약
- 내용의 누락이 있어서는 안된다 - 내용의 누락이 있어서는 안된다
- 문장이 자연스러워야 한다 - 문장이 자연스러워야 한다
- 핵심 정보만 간결하게 포함 - 핵심 정보만 간결하게 포함

View File

@ -30,16 +30,16 @@ response = await creatomate.make_creatomate_call(template_id, modifications)
""" """
import copy import copy
import logging
import time import time
from typing import Literal from typing import Literal
import httpx import httpx
from app.utils.logger import get_logger
from config import apikey_settings, creatomate_settings from config import apikey_settings, creatomate_settings
# 로거 설정 # 로거 설정
logger = logging.getLogger(__name__) logger = get_logger("creatomate")
# Orientation 타입 정의 # Orientation 타입 정의
@ -76,14 +76,14 @@ async def close_shared_client() -> None:
if _shared_client is not None and not _shared_client.is_closed: if _shared_client is not None and not _shared_client.is_closed:
await _shared_client.aclose() await _shared_client.aclose()
_shared_client = None _shared_client = None
print("[CreatomateService] Shared HTTP client closed") logger.info("[CreatomateService] Shared HTTP client closed")
def clear_template_cache() -> None: def clear_template_cache() -> None:
"""템플릿 캐시를 전체 삭제합니다.""" """템플릿 캐시를 전체 삭제합니다."""
global _template_cache global _template_cache
_template_cache.clear() _template_cache.clear()
print("[CreatomateService] Template cache cleared") logger.info("[CreatomateService] Template cache cleared")
def _is_cache_valid(cached_at: float) -> bool: def _is_cache_valid(cached_at: float) -> bool:
@ -164,7 +164,6 @@ class CreatomateService:
httpx.HTTPError: 요청 실패 httpx.HTTPError: 요청 실패
""" """
logger.info(f"[Creatomate] {method} {url}") logger.info(f"[Creatomate] {method} {url}")
print(f"[Creatomate] {method} {url}")
client = await get_shared_client() client = await get_shared_client()
@ -180,7 +179,6 @@ class CreatomateService:
raise ValueError(f"Unsupported HTTP method: {method}") raise ValueError(f"Unsupported HTTP method: {method}")
logger.info(f"[Creatomate] Response - Status: {response.status_code}") logger.info(f"[Creatomate] Response - Status: {response.status_code}")
print(f"[Creatomate] Response - Status: {response.status_code}")
return response return response
async def get_all_templates_data(self) -> dict: async def get_all_templates_data(self) -> dict:
@ -210,12 +208,12 @@ class CreatomateService:
if use_cache and template_id in _template_cache: if use_cache and template_id in _template_cache:
cached = _template_cache[template_id] cached = _template_cache[template_id]
if _is_cache_valid(cached["cached_at"]): 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"]) return copy.deepcopy(cached["data"])
else: else:
# 만료된 캐시 삭제 # 만료된 캐시 삭제
del _template_cache[template_id] del _template_cache[template_id]
print(f"[CreatomateService] Cache EXPIRED - {template_id}") logger.debug(f"[CreatomateService] Cache EXPIRED - {template_id}")
# API 호출 # API 호출
url = f"{self.BASE_URL}/v1/templates/{template_id}" url = f"{self.BASE_URL}/v1/templates/{template_id}"
@ -228,7 +226,7 @@ class CreatomateService:
"data": data, "data": data,
"cached_at": time.time(), "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) return copy.deepcopy(data)
@ -444,12 +442,13 @@ class CreatomateService:
if animation["transition"]: if animation["transition"]:
total_template_duration -= animation["duration"] total_template_duration -= animation["duration"]
except Exception as e: 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 return total_template_duration
def extend_template_duration(self, template: dict, target_duration: float) -> dict: def extend_template_duration(self, template: dict, target_duration: float) -> dict:
"""템플릿의 duration을 target_duration으로 확장합니다.""" """템플릿의 duration을 target_duration으로 확장합니다."""
target_duration += 0.1 # 수동으로 직접 변경 및 테스트 필요 : 파란박스 생기는것
template["duration"] = target_duration template["duration"] = target_duration
total_template_duration = self.calc_scene_duration(template) total_template_duration = self.calc_scene_duration(template)
extend_rate = target_duration / total_template_duration extend_rate = target_duration / total_template_duration
@ -466,7 +465,7 @@ class CreatomateService:
assert animation["time"] == 0 # 0이 아닌 경우 확인 필요 assert animation["time"] == 0 # 0이 아닌 경우 확인 필요
animation["duration"] = animation["duration"] * extend_rate animation["duration"] = animation["duration"] * extend_rate
except Exception as e: except Exception as e:
print( logger.error(
f"[extend_template_duration] Error processing element: {elem}, {e}" f"[extend_template_duration] Error processing element: {elem}, {e}"
) )

337
app/utils/logger.py Normal file
View File

@ -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"

View File

@ -1,15 +1,15 @@
import asyncio import asyncio
import json import json
import logging
import re import re
import aiohttp import aiohttp
import bs4 import bs4
from app.utils.logger import get_logger
from config import crawler_settings from config import crawler_settings
# 로거 설정 # 로거 설정
logger = logging.getLogger(__name__) logger = get_logger("scraper")
class GraphQLException(Exception): class GraphQLException(Exception):
@ -109,7 +109,7 @@ query getAccommodation($id: String!, $deviceType: String) {
self.scrap_type = "GraphQL" self.scrap_type = "GraphQL"
except GraphQLException: except GraphQLException:
print("fallback") logger.debug("GraphQL failed, fallback to Playwright")
self.scrap_type = "Playwright" self.scrap_type = "Playwright"
pass # 나중에 pw 이용한 crawling으로 fallback 추가 pass # 나중에 pw 이용한 crawling으로 fallback 추가
@ -138,7 +138,6 @@ query getAccommodation($id: String!, $deviceType: String) {
try: try:
logger.info(f"[NvMapScraper] Requesting place_id: {place_id}") 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 aiohttp.ClientSession(timeout=timeout) as session:
async with session.post( async with session.post(
@ -148,24 +147,20 @@ query getAccommodation($id: String!, $deviceType: String) {
) as response: ) as response:
if response.status == 200: if response.status == 200:
logger.info(f"[NvMapScraper] SUCCESS - place_id: {place_id}") logger.info(f"[NvMapScraper] SUCCESS - place_id: {place_id}")
print(f"[NvMapScraper] SUCCESS - place_id: {place_id}")
return await response.json() return await response.json()
# 실패 상태 코드 # 실패 상태 코드
logger.error(f"[NvMapScraper] Failed with status {response.status} - place_id: {place_id}") logger.error(f"[NvMapScraper] Failed with status {response.status} - place_id: {place_id}")
print(f"[NvMapScraper] 실패 상태 코드: {response.status}")
raise GraphQLException( raise GraphQLException(
f"Request failed with status {response.status}" f"Request failed with status {response.status}"
) )
except (TimeoutError, asyncio.TimeoutError): except (TimeoutError, asyncio.TimeoutError):
logger.error(f"[NvMapScraper] Timeout - place_id: {place_id}") 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") raise CrawlingTimeoutException(f"Request timed out after {self.REQUEST_TIMEOUT}s")
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
logger.error(f"[NvMapScraper] Client error: {e}") logger.error(f"[NvMapScraper] Client error: {e}")
print(f"[NvMapScraper] Client error: {e}")
raise GraphQLException(f"Client error: {e}") raise GraphQLException(f"Client error: {e}")
async def _get_facility_string(self, place_id: str) -> str | None: async def _get_facility_string(self, place_id: str) -> str | None:

View File

@ -32,17 +32,17 @@ URL 경로 형식:
""" """
import asyncio import asyncio
import logging
import time import time
from pathlib import Path from pathlib import Path
import aiofiles import aiofiles
import httpx import httpx
from app.utils.logger import get_logger
from config import azure_blob_settings from config import azure_blob_settings
# 로거 설정 # 로거 설정
logger = logging.getLogger(__name__) logger = get_logger("blob")
# ============================================================================= # =============================================================================
# 모듈 레벨 공유 HTTP 클라이언트 (싱글톤 패턴) # 모듈 레벨 공유 HTTP 클라이언트 (싱글톤 패턴)
@ -56,13 +56,13 @@ async def get_shared_blob_client() -> httpx.AsyncClient:
"""공유 HTTP 클라이언트를 반환합니다. 없으면 생성합니다.""" """공유 HTTP 클라이언트를 반환합니다. 없으면 생성합니다."""
global _shared_blob_client global _shared_blob_client
if _shared_blob_client is None or _shared_blob_client.is_closed: 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( _shared_blob_client = httpx.AsyncClient(
timeout=httpx.Timeout(180.0, connect=10.0), timeout=httpx.Timeout(180.0, connect=10.0),
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20), 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") "max_connections: 20, max_keepalive: 10")
return _shared_blob_client 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: if _shared_blob_client is not None and not _shared_blob_client.is_closed:
await _shared_blob_client.aclose() await _shared_blob_client.aclose()
_shared_blob_client = None _shared_blob_client = None
print("[AzureBlobUploader] Shared HTTP client closed") logger.info("[AzureBlobUploader] Shared HTTP client closed")
class AzureBlobUploader: class AzureBlobUploader:
@ -158,15 +158,15 @@ class AzureBlobUploader:
try: try:
logger.info(f"[{log_prefix}] Starting upload") 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 = await get_shared_blob_client()
client_time = time.perf_counter() client_time = time.perf_counter()
elapsed_ms = (client_time - start_time) * 1000 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)") f"(size: {size} bytes, timeout: {timeout}s)")
response = await asyncio.wait_for( response = await asyncio.wait_for(
client.put(upload_url, content=file_content, headers=headers), client.put(upload_url, content=file_content, headers=headers),
@ -176,44 +176,38 @@ class AzureBlobUploader:
duration_ms = (upload_time - start_time) * 1000 duration_ms = (upload_time - start_time) * 1000
if response.status_code in [200, 201]: if response.status_code in [200, 201]:
logger.info(f"[{log_prefix}] SUCCESS - Status: {response.status_code}") logger.info(f"[{log_prefix}] SUCCESS - Status: {response.status_code}, "
print(f"[{log_prefix}] SUCCESS - Status: {response.status_code}, " f"Duration: {duration_ms:.1f}ms")
f"Duration: {duration_ms:.1f}ms") logger.debug(f"[{log_prefix}] Public URL: {self._last_public_url}")
print(f"[{log_prefix}] Public URL: {self._last_public_url}")
return True return True
# 업로드 실패 # 업로드 실패
logger.error(f"[{log_prefix}] FAILED - Status: {response.status_code}") logger.error(f"[{log_prefix}] FAILED - Status: {response.status_code}, "
print(f"[{log_prefix}] FAILED - Status: {response.status_code}, " f"Duration: {duration_ms:.1f}ms")
f"Duration: {duration_ms:.1f}ms") logger.error(f"[{log_prefix}] Response: {response.text[:500]}")
print(f"[{log_prefix}] Response: {response.text[:500]}")
return False return False
except asyncio.TimeoutError: except asyncio.TimeoutError:
elapsed = time.perf_counter() - start_time elapsed = time.perf_counter() - start_time
logger.error(f"[{log_prefix}] TIMEOUT after {elapsed:.1f}s") logger.error(f"[{log_prefix}] TIMEOUT after {elapsed:.1f}s")
print(f"[{log_prefix}] TIMEOUT after {elapsed:.1f}s")
return False return False
except httpx.ConnectError as e: except httpx.ConnectError as e:
elapsed = time.perf_counter() - start_time elapsed = time.perf_counter() - start_time
logger.error(f"[{log_prefix}] CONNECT_ERROR: {e}") logger.error(f"[{log_prefix}] CONNECT_ERROR after {elapsed:.1f}s - "
print(f"[{log_prefix}] CONNECT_ERROR after {elapsed:.1f}s - " f"{type(e).__name__}: {e}")
f"{type(e).__name__}: {e}")
return False return False
except httpx.ReadError as e: except httpx.ReadError as e:
elapsed = time.perf_counter() - start_time elapsed = time.perf_counter() - start_time
logger.error(f"[{log_prefix}] READ_ERROR: {e}") logger.error(f"[{log_prefix}] READ_ERROR after {elapsed:.1f}s - "
print(f"[{log_prefix}] READ_ERROR after {elapsed:.1f}s - " f"{type(e).__name__}: {e}")
f"{type(e).__name__}: {e}")
return False return False
except Exception as e: except Exception as e:
elapsed = time.perf_counter() - start_time elapsed = time.perf_counter() - start_time
logger.error(f"[{log_prefix}] ERROR: {type(e).__name__}: {e}") logger.error(f"[{log_prefix}] ERROR after {elapsed:.1f}s - "
print(f"[{log_prefix}] ERROR after {elapsed:.1f}s - " f"{type(e).__name__}: {e}")
f"{type(e).__name__}: {e}")
return False return False
async def _upload_file( async def _upload_file(
@ -241,7 +235,7 @@ class AzureBlobUploader:
upload_url = self._build_upload_url(category, file_name) upload_url = self._build_upload_url(category, file_name)
self._last_public_url = self._build_public_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"} 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) upload_url = self._build_upload_url("song", file_name)
self._last_public_url = self._build_public_url("song", file_name) self._last_public_url = self._build_public_url("song", file_name)
log_prefix = "upload_music_bytes" 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"} 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) upload_url = self._build_upload_url("video", file_name)
self._last_public_url = self._build_public_url("video", file_name) self._last_public_url = self._build_public_url("video", file_name)
log_prefix = "upload_video_bytes" 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"} 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) upload_url = self._build_upload_url("image", file_name)
self._last_public_url = self._build_public_url("image", file_name) self._last_public_url = self._build_public_url("image", file_name)
log_prefix = "upload_image_bytes" 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"} headers = {"Content-Type": content_type, "x-ms-blob-type": "BlockBlob"}

View File

@ -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.video.worker.video_task import download_and_upload_video_to_blob
from app.utils.creatomate import CreatomateService from app.utils.creatomate import CreatomateService
from app.utils.pagination import PaginatedResponse from app.utils.pagination import PaginatedResponse
from app.utils.logger import get_logger
logger = get_logger("video")
router = APIRouter(prefix="/video", tags=["video"]) router = APIRouter(prefix="/video", tags=["video"])
@ -115,7 +117,7 @@ async def generate_video(
from app.database.session import AsyncSessionLocal from app.database.session import AsyncSessionLocal
request_start = time.perf_counter() 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 조회 및 초기 데이터 저장 (세션을 명시적으로 열고 닫음) # 1단계: DB 조회 및 초기 데이터 저장 (세션을 명시적으로 열고 닫음)
@ -168,13 +170,13 @@ async def generate_video(
) )
query_time = time.perf_counter() 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") f"elapsed: {(query_time - request_start)*1000:.1f}ms")
# ===== 결과 처리: Project ===== # ===== 결과 처리: Project =====
project = project_result.scalar_one_or_none() project = project_result.scalar_one_or_none()
if not project: 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( raise HTTPException(
status_code=404, status_code=404,
detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.", detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.",
@ -184,7 +186,7 @@ async def generate_video(
# ===== 결과 처리: Lyric ===== # ===== 결과 처리: Lyric =====
lyric = lyric_result.scalar_one_or_none() lyric = lyric_result.scalar_one_or_none()
if not lyric: 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( raise HTTPException(
status_code=404, status_code=404,
detail=f"task_id '{task_id}'에 해당하는 Lyric을 찾을 수 없습니다.", detail=f"task_id '{task_id}'에 해당하는 Lyric을 찾을 수 없습니다.",
@ -194,7 +196,7 @@ async def generate_video(
# ===== 결과 처리: Song ===== # ===== 결과 처리: Song =====
song = song_result.scalar_one_or_none() song = song_result.scalar_one_or_none()
if not song: 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( raise HTTPException(
status_code=404, status_code=404,
detail=f"task_id '{task_id}'에 해당하는 Song을 찾을 수 없습니다.", detail=f"task_id '{task_id}'에 해당하는 Song을 찾을 수 없습니다.",
@ -220,14 +222,14 @@ async def generate_video(
# ===== 결과 처리: Image ===== # ===== 결과 처리: Image =====
images = image_result.scalars().all() images = image_result.scalars().all()
if not images: 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( raise HTTPException(
status_code=404, status_code=404,
detail=f"task_id '{task_id}'에 해당하는 이미지를 찾을 수 없습니다.", detail=f"task_id '{task_id}'에 해당하는 이미지를 찾을 수 없습니다.",
) )
image_urls = [img.img_url for img in images] image_urls = [img.img_url for img in images]
print( logger.info(
f"[generate_video] Data loaded - task_id: {task_id}, " f"[generate_video] Data loaded - task_id: {task_id}, "
f"project_id: {project_id}, lyric_id: {lyric_id}, " f"project_id: {project_id}, lyric_id: {lyric_id}, "
f"song_id: {song_id}, images: {len(image_urls)}" f"song_id: {song_id}, images: {len(image_urls)}"
@ -246,14 +248,14 @@ async def generate_video(
await session.commit() await session.commit()
video_id = video.id video_id = video.id
stage1_time = time.perf_counter() 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") f"stage1_elapsed: {(stage1_time - request_start)*1000:.1f}ms")
# 세션이 여기서 자동으로 닫힘 (async with 블록 종료) # 세션이 여기서 자동으로 닫힘 (async with 블록 종료)
except HTTPException: except HTTPException:
raise raise
except Exception as e: 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( return GenerateVideoResponse(
success=False, success=False,
task_id=task_id, task_id=task_id,
@ -267,16 +269,16 @@ async def generate_video(
# ========================================================================== # ==========================================================================
stage2_start = time.perf_counter() stage2_start = time.perf_counter()
try: 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( creatomate_service = CreatomateService(
orientation=orientation, orientation=orientation,
target_duration=song_duration, 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. 템플릿 조회 (비동기) # 6-1. 템플릿 조회 (비동기)
template = await creatomate_service.get_one_template_data_async(creatomate_service.template_id) 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에서 리소스 매핑 생성 # 6-2. elements에서 리소스 매핑 생성
modifications = creatomate_service.elements_connect_resource_blackbox( modifications = creatomate_service.elements_connect_resource_blackbox(
@ -285,7 +287,7 @@ async def generate_video(
lyric=lyrics, lyric=lyrics,
music_url=music_url, 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 수정 # 6-3. elements 수정
new_elements = creatomate_service.modify_element( new_elements = creatomate_service.modify_element(
@ -293,20 +295,20 @@ async def generate_video(
modifications, modifications,
) )
template["source"]["elements"] = new_elements 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 확장 # 6-4. duration 확장
final_template = creatomate_service.extend_template_duration( final_template = creatomate_service.extend_template_duration(
template, template,
creatomate_service.target_duration, 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. 커스텀 렌더링 요청 (비동기) # 6-5. 커스텀 렌더링 요청 (비동기)
render_response = await creatomate_service.make_creatomate_custom_call_async( render_response = await creatomate_service.make_creatomate_custom_call_async(
final_template["source"], 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 추출 # 렌더 ID 추출
if isinstance(render_response, list) and len(render_response) > 0: if isinstance(render_response, list) and len(render_response) > 0:
@ -317,14 +319,14 @@ async def generate_video(
creatomate_render_id = None creatomate_render_id = None
stage2_time = time.perf_counter() stage2_time = time.perf_counter()
print( logger.info(
f"[generate_video] Stage 2 DONE - task_id: {task_id}, " f"[generate_video] Stage 2 DONE - task_id: {task_id}, "
f"render_id: {creatomate_render_id}, " f"render_id: {creatomate_render_id}, "
f"stage2_elapsed: {(stage2_time - stage2_start)*1000:.1f}ms" f"stage2_elapsed: {(stage2_time - stage2_start)*1000:.1f}ms"
) )
except Exception as e: 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로 업데이트 # 외부 API 실패 시 Video 상태를 failed로 업데이트
from app.database.session import AsyncSessionLocal from app.database.session import AsyncSessionLocal
async with AsyncSessionLocal() as update_session: async with AsyncSessionLocal() as update_session:
@ -347,7 +349,7 @@ async def generate_video(
# 3단계: creatomate_render_id 업데이트 (새 세션으로 빠르게 처리) # 3단계: creatomate_render_id 업데이트 (새 세션으로 빠르게 처리)
# ========================================================================== # ==========================================================================
stage3_start = time.perf_counter() 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: try:
from app.database.session import AsyncSessionLocal from app.database.session import AsyncSessionLocal
async with AsyncSessionLocal() as update_session: async with AsyncSessionLocal() as update_session:
@ -361,11 +363,11 @@ async def generate_video(
stage3_time = time.perf_counter() stage3_time = time.perf_counter()
total_time = stage3_time - request_start total_time = stage3_time - request_start
print( logger.debug(
f"[generate_video] Stage 3 DONE - task_id: {task_id}, " f"[generate_video] Stage 3 DONE - task_id: {task_id}, "
f"stage3_elapsed: {(stage3_time - stage3_start)*1000:.1f}ms" f"stage3_elapsed: {(stage3_time - stage3_start)*1000:.1f}ms"
) )
print( logger.info(
f"[generate_video] SUCCESS - task_id: {task_id}, " f"[generate_video] SUCCESS - task_id: {task_id}, "
f"render_id: {creatomate_render_id}, " f"render_id: {creatomate_render_id}, "
f"total_time: {total_time*1000:.1f}ms" f"total_time: {total_time*1000:.1f}ms"
@ -380,7 +382,7 @@ async def generate_video(
) )
except Exception as e: 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( return GenerateVideoResponse(
success=False, success=False,
task_id=task_id, task_id=task_id,
@ -439,11 +441,11 @@ async def get_video_status(
succeeded 상태인 경우 백그라운드에서 MP4 파일을 다운로드하고 succeeded 상태인 경우 백그라운드에서 MP4 파일을 다운로드하고
Video 테이블의 status를 completed로, result_movie_url을 업데이트합니다. 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: try:
creatomate_service = CreatomateService() creatomate_service = CreatomateService()
result = await creatomate_service.get_render_status_async(creatomate_render_id) 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") status = result.get("status", "unknown")
video_url = result.get("url") video_url = result.get("url")
@ -481,7 +483,7 @@ async def get_video_status(
store_name = project.store_name if project else "video" store_name = project.store_name if project else "video"
# 백그라운드 태스크로 MP4 다운로드 → Blob 업로드 → DB 업데이트 → 임시 파일 삭제 # 백그라운드 태스크로 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( background_tasks.add_task(
download_and_upload_video_to_blob, download_and_upload_video_to_blob,
task_id=video.task_id, task_id=video.task_id,
@ -489,7 +491,7 @@ async def get_video_status(
store_name=store_name, store_name=store_name,
) )
elif video and video.status == "completed": 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( render_data = VideoRenderData(
id=result.get("id"), id=result.get("id"),
@ -498,7 +500,7 @@ async def get_video_status(
snapshot_url=result.get("snapshot_url"), 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( return PollingVideoResponse(
success=True, success=True,
status=status, status=status,
@ -511,7 +513,7 @@ async def get_video_status(
except Exception as e: except Exception as e:
import traceback 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( return PollingVideoResponse(
success=False, success=False,
status="error", status="error",
@ -563,7 +565,7 @@ async def download_video(
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> DownloadVideoResponse: ) -> DownloadVideoResponse:
"""task_id로 Video 상태를 polling하고 completed 시 Project 정보와 영상 URL을 반환합니다.""" """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: try:
# task_id로 Video 조회 (여러 개 있을 경우 가장 최근 것 선택) # task_id로 Video 조회 (여러 개 있을 경우 가장 최근 것 선택)
video_result = await session.execute( video_result = await session.execute(
@ -575,7 +577,7 @@ async def download_video(
video = video_result.scalar_one_or_none() video = video_result.scalar_one_or_none()
if not video: 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( return DownloadVideoResponse(
success=False, success=False,
status="not_found", status="not_found",
@ -583,11 +585,11 @@ async def download_video(
error_message="Video not found", 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 상태인 경우 # processing 상태인 경우
if 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( return DownloadVideoResponse(
success=True, success=True,
status="processing", status="processing",
@ -597,7 +599,7 @@ async def download_video(
# failed 상태인 경우 # failed 상태인 경우
if video.status == "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( return DownloadVideoResponse(
success=False, success=False,
status="failed", status="failed",
@ -612,7 +614,7 @@ async def download_video(
) )
project = project_result.scalar_one_or_none() 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( return DownloadVideoResponse(
success=True, success=True,
status="completed", status="completed",
@ -625,7 +627,7 @@ async def download_video(
) )
except Exception as e: 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( return DownloadVideoResponse(
success=False, success=False,
status="error", status="error",
@ -674,7 +676,7 @@ async def get_videos(
pagination: PaginationParams = Depends(get_pagination_params), pagination: PaginationParams = Depends(get_pagination_params),
) -> PaginatedResponse[VideoListItem]: ) -> 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: try:
offset = (pagination.page - 1) * pagination.page_size offset = (pagination.page - 1) * pagination.page_size
@ -732,14 +734,14 @@ async def get_videos(
page_size=pagination.page_size, page_size=pagination.page_size,
) )
print( logger.info(
f"[get_videos] SUCCESS - total: {total}, page: {pagination.page}, " f"[get_videos] SUCCESS - total: {total}, page: {pagination.page}, "
f"page_size: {pagination.page_size}, items_count: {len(items)}" f"page_size: {pagination.page_size}, items_count: {len(items)}"
) )
return response return response
except Exception as e: except Exception as e:
print(f"[get_videos] EXCEPTION - error: {e}") logger.error(f"[get_videos] EXCEPTION - error: {e}")
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail=f"영상 목록 조회에 실패했습니다: {str(e)}", detail=f"영상 목록 조회에 실패했습니다: {str(e)}",

View File

@ -14,6 +14,9 @@ from app.lyrics.schemas.lyrics_schema import (
StoreData, StoreData,
) )
from app.utils.chatgpt_prompt import chatgpt_api 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]: async def get_store_info(conn: Connection) -> List[StoreData]:
@ -38,13 +41,13 @@ async def get_store_info(conn: Connection) -> List[StoreData]:
result.close() result.close()
return all_store_info return all_store_info
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemy error in get_store_info: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_store_info: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -69,13 +72,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
result.close() result.close()
return all_attribute return all_attribute
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemy error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -100,13 +103,13 @@ async def get_attribute(conn: Connection) -> List[AttributeData]:
result.close() result.close()
return all_attribute return all_attribute
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemy error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_attribute: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -132,13 +135,13 @@ async def get_sample_song(conn: Connection) -> List[SongSampleData]:
result.close() result.close()
return all_sample_song return all_sample_song
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemy error in get_sample_song: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_sample_song: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -162,13 +165,13 @@ async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
result.close() result.close()
return all_prompt_template return all_prompt_template
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemy error in get_prompt_template: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_prompt_template: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -192,13 +195,13 @@ async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
result.close() result.close()
return all_prompt_template return all_prompt_template
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(e) logger.error(f"SQLAlchemy error in get_song_result: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.", detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
) )
except Exception as e: except Exception as e:
print(e) logger.error(f"Unexpected error in get_song_result: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="알수없는 이유로 서비스 오류가 발생하였습니다", detail="알수없는 이유로 서비스 오류가 발생하였습니다",
@ -210,11 +213,11 @@ async def make_song_result(request: Request, conn: Connection):
# 1. Form 데이터 파싱 # 1. Form 데이터 파싱
form_data = await SongFormData.from_form(request) form_data = await SongFormData.from_form(request)
print(f"\n{'=' * 60}") logger.info(f"{'=' * 60}")
print(f"Store ID: {form_data.store_id}") logger.info(f"Store ID: {form_data.store_id}")
print(f"Lyrics IDs: {form_data.lyrics_ids}") logger.info(f"Lyrics IDs: {form_data.lyrics_ids}")
print(f"Prompt IDs: {form_data.prompts}") logger.info(f"Prompt IDs: {form_data.prompts}")
print(f"{'=' * 60}\n") logger.info(f"{'=' * 60}")
# 2. Store 정보 조회 # 2. Store 정보 조회
store_query = """ store_query = """
@ -243,7 +246,7 @@ async def make_song_result(request: Request, conn: Connection):
) )
store_info = all_store_info[0] store_info = all_store_info[0]
print(f"Store: {store_info.store_name}") logger.info(f"Store: {store_info.store_name}")
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음 # 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
@ -251,7 +254,7 @@ async def make_song_result(request: Request, conn: Connection):
combined_sample_song = None combined_sample_song = None
if form_data.lyrics_ids: if form_data.lyrics_ids:
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}") logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}")
lyrics_query = """ lyrics_query = """
SELECT sample_song FROM song_sample 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( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("선택된 lyrics가 없습니다") logger.info("선택된 lyrics가 없습니다")
# 5. 템플릿 가져오기 # 5. 템플릿 가져오기
if not form_data.prompts: if not form_data.prompts:
@ -283,7 +286,7 @@ async def make_song_result(request: Request, conn: Connection):
detail="프롬프트 ID가 필요합니다", detail="프롬프트 ID가 필요합니다",
) )
print("템플릿 가져오기") logger.info("템플릿 가져오기")
prompts_query = """ prompts_query = """
SELECT * FROM prompt_template WHERE id=:id; SELECT * FROM prompt_template WHERE id=:id;
@ -310,7 +313,7 @@ async def make_song_result(request: Request, conn: Connection):
) )
prompt = prompts_info[0] prompt = prompts_info[0]
print(f"Prompt Template: {prompt.prompt}") logger.debug(f"Prompt Template: {prompt.prompt}")
# ✅ 6. 프롬프트 조합 # ✅ 6. 프롬프트 조합
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format( 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} {combined_sample_song}
""" """
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") logger.debug(f"[업데이트된 프롬프트]\n{updated_prompt}")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt) 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_with_space}
전체 글자 (공백 제외): {total_chars_without_space}\r\n\r\n{generated_lyrics}""" 전체 글자 (공백 제외): {total_chars_without_space}\r\n\r\n{generated_lyrics}"""
print("=" * 40) logger.debug("=" * 40)
print("[translate:form_data.attributes_str:] ", form_data.attributes_str) logger.debug(f"[translate:form_data.attributes_str:] {form_data.attributes_str}")
print("[translate:total_chars_with_space:] ", total_chars_with_space) logger.debug(f"[translate:total_chars_with_space:] {total_chars_with_space}")
print("[translate:total_chars_without_space:] ", total_chars_without_space) logger.debug(f"[translate:total_chars_without_space:] {total_chars_without_space}")
print("[translate:final_lyrics:]") logger.debug(f"[translate:final_lyrics:]\n{final_lyrics}")
print(final_lyrics) logger.debug("=" * 40)
print("=" * 40)
# 8. DB 저장 # 8. DB 저장
insert_query = """ 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.execute(text(insert_query), insert_params)
await conn.commit() await conn.commit()
print("결과 저장 완료") logger.info("결과 저장 완료")
print("\n전체 결과 조회 중...") logger.info("전체 결과 조회 중...")
# 9. 생성 결과 가져오기 (created_at 역순) # 9. 생성 결과 가져오기 (created_at 역순)
select_query = """ select_query = """
@ -430,26 +432,20 @@ async def make_song_result(request: Request, conn: Connection):
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: except HTTPException:
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",
@ -490,25 +486,19 @@ async def get_song_result(conn: Connection): # 반환 타입 수정
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: # HTTPException은 그대로 raise except HTTPException: # HTTPException은 그대로 raise
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",
@ -520,9 +510,9 @@ async def make_automation(request: Request, conn: Connection):
# 1. Form 데이터 파싱 # 1. Form 데이터 파싱
form_data = await SongFormData.from_form(request) form_data = await SongFormData.from_form(request)
print(f"\n{'=' * 60}") logger.info(f"{'=' * 60}")
print(f"Store ID: {form_data.store_id}") logger.info(f"Store ID: {form_data.store_id}")
print(f"{'=' * 60}\n") logger.info(f"{'=' * 60}")
# 2. Store 정보 조회 # 2. Store 정보 조회
store_query = """ store_query = """
@ -551,7 +541,7 @@ async def make_automation(request: Request, conn: Connection):
) )
store_info = all_store_info[0] store_info = all_store_info[0]
print(f"Store: {store_info.store_name}") logger.info(f"Store: {store_info.store_name}")
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음 # 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
attribute_query = """ attribute_query = """
@ -596,13 +586,13 @@ async def make_automation(request: Request, conn: Connection):
# 최종 문자열 생성 # 최종 문자열 생성
formatted_attributes = "\n".join(formatted_pairs) formatted_attributes = "\n".join(formatted_pairs)
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n") logger.debug(f"[포맷팅된 문자열 속성 정보]\n{formatted_attributes}")
else: else:
print("속성 데이터가 없습니다") logger.info("속성 데이터가 없습니다")
formatted_attributes = "" formatted_attributes = ""
# 4. 템플릿 가져오기 # 4. 템플릿 가져오기
print("템플릿 가져오기 (ID=1)") logger.info("템플릿 가져오기 (ID=1)")
prompts_query = """ prompts_query = """
SELECT * FROM prompt_template WHERE id=1; SELECT * FROM prompt_template WHERE id=1;
@ -624,7 +614,7 @@ async def make_automation(request: Request, conn: Connection):
prompt=row[2], prompt=row[2],
) )
print(f"Prompt Template: {prompt.prompt}") logger.debug(f"Prompt Template: {prompt.prompt}")
# 5. 템플릿 조합 # 5. 템플릿 조합
@ -635,17 +625,17 @@ async def make_automation(request: Request, conn: Connection):
description=store_info.store_info or "", description=store_info.store_info or "",
) )
print("\n" + "=" * 80) logger.debug("=" * 80)
print("업데이트된 프롬프트") logger.debug("업데이트된 프롬프트")
print("=" * 80) logger.debug("=" * 80)
print(updated_prompt) logger.debug(updated_prompt)
print("=" * 80 + "\n") logger.debug("=" * 80)
# 4. Sample Song 조회 및 결합 # 4. Sample Song 조회 및 결합
combined_sample_song = None combined_sample_song = None
if form_data.lyrics_ids: if form_data.lyrics_ids:
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}") logger.info(f"[샘플 가사 조회] - {len(form_data.lyrics_ids)}")
lyrics_query = """ lyrics_query = """
SELECT sample_song FROM song_sample SELECT sample_song FROM song_sample
@ -664,14 +654,14 @@ async def make_automation(request: Request, conn: Connection):
combined_sample_song = "\n\n".join( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("선택된 lyrics가 없습니다") logger.info("선택된 lyrics가 없습니다")
# 1. song_sample 테이블의 모든 ID 조회 # 1. song_sample 테이블의 모든 ID 조회
print("\n[샘플 가사 랜덤 선택]") logger.info("[샘플 가사 랜덤 선택]")
all_ids_query = """ all_ids_query = """
SELECT id FROM song_sample; 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)) ids_result = await conn.execute(text(all_ids_query))
all_ids = [row.id for row in ids_result.fetchall()] all_ids = [row.id for row in ids_result.fetchall()]
print(f"전체 샘플 가사 개수: {len(all_ids)}") logger.info(f"전체 샘플 가사 개수: {len(all_ids)}")
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체) # 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
combined_sample_song = None combined_sample_song = None
@ -689,7 +679,7 @@ async def make_automation(request: Request, conn: Connection):
sample_count = min(3, len(all_ids)) sample_count = min(3, len(all_ids))
selected_ids = random.sample(all_ids, sample_count) selected_ids = random.sample(all_ids, sample_count)
print(f"랜덤 선택된 ID: {selected_ids}") logger.info(f"랜덤 선택된 ID: {selected_ids}")
# 3. 선택된 ID로 샘플 가사 조회 # 3. 선택된 ID로 샘플 가사 조회
lyrics_query = """ lyrics_query = """
@ -710,11 +700,11 @@ async def make_automation(request: Request, conn: Connection):
combined_sample_song = "\n\n".join( combined_sample_song = "\n\n".join(
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)] [f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
) )
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료") logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else: else:
print("샘플 가사가 비어있습니다") logger.info("샘플 가사가 비어있습니다")
else: else:
print("song_sample 테이블에 데이터가 없습니다") logger.info("song_sample 테이블에 데이터가 없습니다")
# 5. 프롬프트에 샘플 가사 추가 # 5. 프롬프트에 샘플 가사 추가
if combined_sample_song: if combined_sample_song:
@ -726,11 +716,11 @@ async def make_automation(request: Request, conn: Connection):
{combined_sample_song} {combined_sample_song}
""" """
print("샘플 가사 정보가 프롬프트에 추가되었습니다") logger.info("샘플 가사 정보가 프롬프트에 추가되었습니다")
else: else:
print("샘플 가사가 없어 기본 프롬프트만 사용합니다") logger.info("샘플 가사가 없어 기본 프롬프트만 사용합니다")
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") logger.debug(f"[최종 프롬프트 길이: {len(updated_prompt)} 자]")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt) 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() :sample_song, :result_song, NOW()
); );
""" """
print("\n[insert_params 선택된 속성 확인]") logger.debug("[insert_params 선택된 속성 확인]")
print(f"Categories: {selected_categories}") logger.debug(f"Categories: {selected_categories}")
print(f"Values: {selected_values}") logger.debug(f"Values: {selected_values}")
print()
# attr_category, attr_value # attr_category, attr_value
insert_params = { insert_params = {
@ -792,9 +781,9 @@ async def make_automation(request: Request, conn: Connection):
await conn.execute(text(insert_query), insert_params) await conn.execute(text(insert_query), insert_params)
await conn.commit() await conn.commit()
print("결과 저장 완료") logger.info("결과 저장 완료")
print("\n전체 결과 조회 중...") logger.info("전체 결과 조회 중...")
# 9. 생성 결과 가져오기 (created_at 역순) # 9. 생성 결과 가져오기 (created_at 역순)
select_query = """ select_query = """
@ -826,26 +815,20 @@ async def make_automation(request: Request, conn: Connection):
for row in all_results.fetchall() for row in all_results.fetchall()
] ]
print(f"전체 {len(results_list)}개의 결과 조회 완료\n") logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list return results_list
except HTTPException: except HTTPException:
raise raise
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Database Error: {e}") logger.error(f"Database Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.", detail="데이터베이스 연결에 문제가 발생했습니다.",
) )
except Exception as e: except Exception as e:
print(f"Unexpected Error: {e}") logger.error(f"Unexpected Error: {e}", exc_info=True)
import traceback
traceback.print_exc()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.", detail="서비스 처리 중 오류가 발생했습니다.",

View File

@ -4,7 +4,6 @@ Video Background Tasks
영상 생성 관련 백그라운드 태스크를 정의합니다. 영상 생성 관련 백그라운드 태스크를 정의합니다.
""" """
import logging
import traceback import traceback
from pathlib import Path from pathlib import Path
@ -16,9 +15,10 @@ from sqlalchemy.exc import SQLAlchemyError
from app.database.session import BackgroundSessionLocal from app.database.session import BackgroundSessionLocal
from app.video.models import Video from app.video.models import Video
from app.utils.upload_blob_as_request import AzureBlobUploader 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 요청 설정 # HTTP 요청 설정
REQUEST_TIMEOUT = 300.0 # 초 (영상은 용량이 크므로 5분) REQUEST_TIMEOUT = 300.0 # 초 (영상은 용량이 크므로 5분)
@ -66,20 +66,16 @@ async def _update_video_status(
video.result_movie_url = video_url video.result_movie_url = video_url
await session.commit() await session.commit()
logger.info(f"[Video] Status updated - task_id: {task_id}, status: {status}") 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 return True
else: else:
logger.warning(f"[Video] NOT FOUND in DB - task_id: {task_id}") 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 return False
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[Video] DB Error while updating status - task_id: {task_id}, error: {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 return False
except Exception as e: except Exception as e:
logger.error(f"[Video] Unexpected error while updating status - task_id: {task_id}, error: {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 return False
@ -97,14 +93,12 @@ async def _download_video(url: str, task_id: str) -> bytes:
httpx.HTTPError: 다운로드 실패 httpx.HTTPError: 다운로드 실패
""" """
logger.info(f"[VideoDownload] Downloading - task_id: {task_id}") logger.info(f"[VideoDownload] Downloading - task_id: {task_id}")
print(f"[VideoDownload] Downloading - task_id: {task_id}")
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=REQUEST_TIMEOUT) response = await client.get(url, timeout=REQUEST_TIMEOUT)
response.raise_for_status() response.raise_for_status()
logger.info(f"[VideoDownload] SUCCESS - task_id: {task_id}, size: {len(response.content)} bytes") 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 return response.content
@ -121,7 +115,6 @@ async def download_and_upload_video_to_blob(
store_name: 저장할 파일명에 사용할 업체명 store_name: 저장할 파일명에 사용할 업체명
""" """
logger.info(f"[download_and_upload_video_to_blob] START - task_id: {task_id}, store_name: {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 temp_file_path: Path | None = None
try: try:
@ -136,12 +129,10 @@ async def download_and_upload_video_to_blob(
temp_dir = Path("media") / "temp" / task_id temp_dir = Path("media") / "temp" / task_id
temp_dir.mkdir(parents=True, exist_ok=True) temp_dir.mkdir(parents=True, exist_ok=True)
temp_file_path = temp_dir / file_name temp_file_path = temp_dir / file_name
logger.info(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}")
print(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}") 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) content = await _download_video(video_url, task_id)
@ -149,7 +140,6 @@ async def download_and_upload_video_to_blob(
await f.write(content) await f.write(content)
logger.info(f"[download_and_upload_video_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}") 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에 업로드 # Azure Blob Storage에 업로드
uploader = AzureBlobUploader(task_id=task_id) uploader = AzureBlobUploader(task_id=task_id)
@ -161,29 +151,21 @@ async def download_and_upload_video_to_blob(
# SAS 토큰이 제외된 public_url 사용 # SAS 토큰이 제외된 public_url 사용
blob_url = uploader.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}") 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 테이블 업데이트 # Video 테이블 업데이트
await _update_video_status(task_id, "completed", blob_url) await _update_video_status(task_id, "completed", blob_url)
logger.info(f"[download_and_upload_video_to_blob] SUCCESS - task_id: {task_id}") 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: except httpx.HTTPError as e:
logger.error(f"[download_and_upload_video_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}") logger.error(f"[download_and_upload_video_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_video_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_video_status(task_id, "failed") await _update_video_status(task_id, "failed")
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[download_and_upload_video_to_blob] DB ERROR - task_id: {task_id}, error: {e}") logger.error(f"[download_and_upload_video_to_blob] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_video_to_blob] DB ERROR - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_video_status(task_id, "failed") await _update_video_status(task_id, "failed")
except Exception as e: except Exception as e:
logger.error(f"[download_and_upload_video_to_blob] EXCEPTION - task_id: {task_id}, error: {e}") logger.error(f"[download_and_upload_video_to_blob] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_video_to_blob] EXCEPTION - task_id: {task_id}, error: {e}")
traceback.print_exc()
await _update_video_status(task_id, "failed") await _update_video_status(task_id, "failed")
finally: finally:
@ -191,11 +173,9 @@ async def download_and_upload_video_to_blob(
if temp_file_path and temp_file_path.exists(): if temp_file_path and temp_file_path.exists():
try: try:
temp_file_path.unlink() temp_file_path.unlink()
logger.info(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}")
print(f"[download_and_upload_video_to_blob] Temp file deleted - path: {temp_file_path}")
except Exception as e: except Exception as e:
logger.warning(f"[download_and_upload_video_to_blob] Failed to delete temp file: {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 temp_dir = Path("media") / "temp" / task_id
@ -219,7 +199,6 @@ async def download_and_upload_video_by_creatomate_render_id(
store_name: 저장할 파일명에 사용할 업체명 store_name: 저장할 파일명에 사용할 업체명
""" """
logger.info(f"[download_and_upload_video_by_creatomate_render_id] START - creatomate_render_id: {creatomate_render_id}, store_name: {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 temp_file_path: Path | None = None
task_id: str | None = None task_id: str | None = None
@ -236,12 +215,10 @@ async def download_and_upload_video_by_creatomate_render_id(
if not video: if not video:
logger.warning(f"[download_and_upload_video_by_creatomate_render_id] Video NOT FOUND - creatomate_render_id: {creatomate_render_id}") 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 return
task_id = video.task_id 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}") 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( 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 = Path("media") / "temp" / task_id
temp_dir.mkdir(parents=True, exist_ok=True) temp_dir.mkdir(parents=True, exist_ok=True)
temp_file_path = temp_dir / file_name 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}") logger.debug(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.info(f"[download_and_upload_video_by_creatomate_render_id] Downloading video - creatomate_render_id: {creatomate_render_id}, url: {video_url}") 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) 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) 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}") 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에 업로드 # Azure Blob Storage에 업로드
uploader = AzureBlobUploader(task_id=task_id) uploader = AzureBlobUploader(task_id=task_id)
@ -279,7 +253,6 @@ async def download_and_upload_video_by_creatomate_render_id(
# SAS 토큰이 제외된 public_url 사용 # SAS 토큰이 제외된 public_url 사용
blob_url = uploader.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}") 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 테이블 업데이트 # Video 테이블 업데이트
await _update_video_status( await _update_video_status(
@ -289,26 +262,19 @@ async def download_and_upload_video_by_creatomate_render_id(
creatomate_render_id=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}") 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: 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}") logger.error(f"[download_and_upload_video_by_creatomate_render_id] DOWNLOAD ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_video_by_creatomate_render_id] DOWNLOAD ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}")
traceback.print_exc()
if task_id: if task_id:
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id) await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
except SQLAlchemyError as e: 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}") logger.error(f"[download_and_upload_video_by_creatomate_render_id] DB ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_video_by_creatomate_render_id] DB ERROR - creatomate_render_id: {creatomate_render_id}, error: {e}")
traceback.print_exc()
if task_id: if task_id:
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id) await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_id)
except Exception as e: except Exception as e:
logger.error(f"[download_and_upload_video_by_creatomate_render_id] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}") logger.error(f"[download_and_upload_video_by_creatomate_render_id] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}", exc_info=True)
print(f"[download_and_upload_video_by_creatomate_render_id] EXCEPTION - creatomate_render_id: {creatomate_render_id}, error: {e}")
traceback.print_exc()
if task_id: if task_id:
await _update_video_status(task_id, "failed", creatomate_render_id=creatomate_render_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(): if temp_file_path and temp_file_path.exists():
try: try:
temp_file_path.unlink() temp_file_path.unlink()
logger.info(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}")
print(f"[download_and_upload_video_by_creatomate_render_id] Temp file deleted - path: {temp_file_path}")
except Exception as e: except Exception as e:
logger.warning(f"[download_and_upload_video_by_creatomate_render_id] Failed to delete temp file: {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: if task_id:

223
config.py
View File

@ -5,6 +5,10 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
PROJECT_DIR = Path(__file__).resolve().parent PROJECT_DIR = Path(__file__).resolve().parent
# 미디어 파일 저장 디렉토리
MEDIA_ROOT = PROJECT_DIR / "media"
MEDIA_ROOT.mkdir(exist_ok=True)
_base_config = SettingsConfigDict( _base_config = SettingsConfigDict(
env_file=PROJECT_DIR / ".env", env_file=PROJECT_DIR / ".env",
env_ignore_empty=True, env_ignore_empty=True,
@ -95,32 +99,6 @@ class DatabaseSettings(BaseSettings):
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{db}" 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): class CrawlerSettings(BaseSettings):
NAVER_COOKIES: str = Field(default="") NAVER_COOKIES: str = Field(default="")
@ -168,12 +146,205 @@ class CreatomateSettings(BaseSettings):
model_config = _base_config 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() prj_settings = ProjectSettings()
apikey_settings = APIKeySettings() apikey_settings = APIKeySettings()
db_settings = DatabaseSettings() db_settings = DatabaseSettings()
security_settings = SecuritySettings() security_settings = SecuritySettings()
kakao_settings = KakaoSettings()
notification_settings = NotificationSettings() notification_settings = NotificationSettings()
cors_settings = CORSSettings() cors_settings = CORSSettings()
crawler_settings = CrawlerSettings() crawler_settings = CrawlerSettings()
azure_blob_settings = AzureBlobSettings() azure_blob_settings = AzureBlobSettings()
creatomate_settings = CreatomateSettings() creatomate_settings = CreatomateSettings()
log_settings = LogSettings()

331
docs/user/kakao.md Normal file
View File

@ -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 # 사용자 정의 예외
```