add logger
parent
1acd8846ab
commit
bf7b53c8e8
|
|
@ -27,3 +27,10 @@ build/
|
||||||
*.mp3
|
*.mp3
|
||||||
*.mp4
|
*.mp4
|
||||||
media/
|
media/
|
||||||
|
|
||||||
|
# Static files
|
||||||
|
static/
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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,7 +657,7 @@ 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")
|
||||||
|
|
||||||
|
|
@ -692,7 +678,7 @@ 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에 직접 업로드
|
||||||
|
|
@ -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,13 +755,13 @@ 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="이미지 저장 중 데이터베이스 오류가 발생했습니다.",
|
||||||
|
|
@ -783,7 +769,7 @@ async def upload_images_blob(
|
||||||
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,7 +779,7 @@ 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(
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
"""업로드 파일을 지정된 경로에 저장"""
|
"""업로드 파일을 지정된 경로에 저장"""
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
|
|
||||||
|
|
@ -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)}")
|
||||||
|
|
|
||||||
|
|
@ -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)}",
|
||||||
|
|
|
||||||
|
|
@ -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="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
|
|
||||||
|
|
@ -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,0 +1,141 @@
|
||||||
|
"""
|
||||||
|
User 모듈 커스텀 예외 정의
|
||||||
|
|
||||||
|
인증 및 사용자 관련 에러를 처리하기 위한 예외 클래스들입니다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
|
||||||
|
class AuthException(HTTPException):
|
||||||
|
"""인증 관련 기본 예외"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
status_code: int,
|
||||||
|
code: str,
|
||||||
|
message: str,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status_code,
|
||||||
|
detail={"code": code, "message": message},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 카카오 OAuth 관련 예외
|
||||||
|
# =============================================================================
|
||||||
|
class InvalidAuthCodeError(AuthException):
|
||||||
|
"""유효하지 않은 인가 코드"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "유효하지 않은 인가 코드입니다."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
code="INVALID_CODE",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class KakaoAuthFailedError(AuthException):
|
||||||
|
"""카카오 인증 실패"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "카카오 인증에 실패했습니다."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
code="KAKAO_AUTH_FAILED",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class KakaoAPIError(AuthException):
|
||||||
|
"""카카오 API 호출 오류"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "카카오 API 호출 중 오류가 발생했습니다."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
code="KAKAO_API_ERROR",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# JWT 토큰 관련 예외
|
||||||
|
# =============================================================================
|
||||||
|
class TokenExpiredError(AuthException):
|
||||||
|
"""토큰 만료"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "토큰이 만료되었습니다. 다시 로그인해주세요."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
code="TOKEN_EXPIRED",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidTokenError(AuthException):
|
||||||
|
"""유효하지 않은 토큰"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "유효하지 않은 토큰입니다."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
code="INVALID_TOKEN",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenRevokedError(AuthException):
|
||||||
|
"""취소된 토큰"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "취소된 토큰입니다. 다시 로그인해주세요."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
code="TOKEN_REVOKED",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MissingTokenError(AuthException):
|
||||||
|
"""토큰 누락"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "인증 토큰이 필요합니다."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
code="MISSING_TOKEN",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 사용자 관련 예외
|
||||||
|
# =============================================================================
|
||||||
|
class UserNotFoundError(AuthException):
|
||||||
|
"""사용자 없음"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "사용자를 찾을 수 없습니다."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
code="USER_NOT_FOUND",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserInactiveError(AuthException):
|
||||||
|
"""비활성화된 계정"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "비활성화된 계정입니다. 관리자에게 문의하세요."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
code="USER_INACTIVE",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRequiredError(AuthException):
|
||||||
|
"""관리자 권한 필요"""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "관리자 권한이 필요합니다."):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
code="ADMIN_REQUIRED",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
"""
|
||||||
|
User 모듈 SQLAlchemy 모델 정의
|
||||||
|
|
||||||
|
카카오 소셜 로그인 기반 사용자 관리 모델입니다.
|
||||||
|
|
||||||
|
주의: 이 모델은 현재 개발 중이므로 create_db_tables()에서 import하지 않습니다.
|
||||||
|
테이블 생성이 필요할 때 app/database/session.py의 create_db_tables()에
|
||||||
|
아래 import를 추가하세요:
|
||||||
|
|
||||||
|
from app.user.models import User # noqa: F401
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, Boolean, DateTime, Index, String, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.database.session import Base
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
"""
|
||||||
|
사용자 테이블 (카카오 소셜 로그인)
|
||||||
|
|
||||||
|
카카오 로그인을 통해 인증된 사용자 정보를 저장합니다.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: 고유 식별자 (자동 증가)
|
||||||
|
kakao_id: 카카오 고유 ID (필수, 유니크)
|
||||||
|
email: 이메일 주소 (선택, 카카오에서 제공 시)
|
||||||
|
nickname: 카카오 닉네임 (선택)
|
||||||
|
profile_image_url: 카카오 프로필 이미지 URL (선택)
|
||||||
|
thumbnail_image_url: 카카오 썸네일 이미지 URL (선택)
|
||||||
|
is_active: 계정 활성화 상태 (기본 True)
|
||||||
|
is_admin: 관리자 여부 (기본 False)
|
||||||
|
last_login_at: 마지막 로그인 일시
|
||||||
|
created_at: 계정 생성 일시
|
||||||
|
updated_at: 계정 정보 수정 일시
|
||||||
|
|
||||||
|
카카오 API 응답 필드 매핑:
|
||||||
|
- kakao_id: id (카카오 회원번호)
|
||||||
|
- email: kakao_account.email
|
||||||
|
- nickname: kakao_account.profile.nickname 또는 properties.nickname
|
||||||
|
- profile_image_url: kakao_account.profile.profile_image_url
|
||||||
|
- thumbnail_image_url: kakao_account.profile.thumbnail_image_url
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "user"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_user_kakao_id", "kakao_id", unique=True),
|
||||||
|
Index("idx_user_email", "email"),
|
||||||
|
Index("idx_user_is_active", "is_active"),
|
||||||
|
Index("idx_user_created_at", "created_at"),
|
||||||
|
{
|
||||||
|
"mysql_engine": "InnoDB",
|
||||||
|
"mysql_charset": "utf8mb4",
|
||||||
|
"mysql_collate": "utf8mb4_unicode_ci",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# 기본 식별자
|
||||||
|
# ==========================================================================
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
BigInteger,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment="고유 식별자",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# 카카오 소셜 로그인 필수 정보
|
||||||
|
# ==========================================================================
|
||||||
|
kakao_id: Mapped[int] = mapped_column(
|
||||||
|
BigInteger,
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="카카오 고유 ID (회원번호)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# 카카오에서 제공하는 사용자 정보 (선택적)
|
||||||
|
# ==========================================================================
|
||||||
|
email: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
nullable=True,
|
||||||
|
comment="이메일 주소 (카카오 계정 이메일, 동의 시 제공)",
|
||||||
|
)
|
||||||
|
|
||||||
|
nickname: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(100),
|
||||||
|
nullable=True,
|
||||||
|
comment="카카오 닉네임",
|
||||||
|
)
|
||||||
|
|
||||||
|
profile_image_url: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(2048),
|
||||||
|
nullable=True,
|
||||||
|
comment="카카오 프로필 이미지 URL",
|
||||||
|
)
|
||||||
|
|
||||||
|
thumbnail_image_url: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(2048),
|
||||||
|
nullable=True,
|
||||||
|
comment="카카오 썸네일 이미지 URL",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# 계정 상태 관리
|
||||||
|
# ==========================================================================
|
||||||
|
is_active: Mapped[bool] = mapped_column(
|
||||||
|
Boolean,
|
||||||
|
nullable=False,
|
||||||
|
default=True,
|
||||||
|
comment="계정 활성화 상태 (비활성화 시 로그인 차단)",
|
||||||
|
)
|
||||||
|
|
||||||
|
is_admin: Mapped[bool] = mapped_column(
|
||||||
|
Boolean,
|
||||||
|
nullable=False,
|
||||||
|
default=False,
|
||||||
|
comment="관리자 권한 여부",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# 시간 정보
|
||||||
|
# ==========================================================================
|
||||||
|
last_login_at: Mapped[Optional[datetime]] = mapped_column(
|
||||||
|
DateTime,
|
||||||
|
nullable=True,
|
||||||
|
comment="마지막 로그인 일시",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime,
|
||||||
|
nullable=False,
|
||||||
|
server_default=func.now(),
|
||||||
|
comment="계정 생성 일시",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime,
|
||||||
|
nullable=False,
|
||||||
|
server_default=func.now(),
|
||||||
|
onupdate=func.now(),
|
||||||
|
comment="계정 정보 수정 일시",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<User("
|
||||||
|
f"id={self.id}, "
|
||||||
|
f"kakao_id={self.kakao_id}, "
|
||||||
|
f"nickname='{self.nickname}', "
|
||||||
|
f"is_active={self.is_active}"
|
||||||
|
f")>"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
from app.user.schemas.user_schema import (
|
||||||
|
AccessTokenResponse,
|
||||||
|
KakaoCallbackRequest,
|
||||||
|
KakaoLoginResponse,
|
||||||
|
KakaoTokenResponse,
|
||||||
|
KakaoUserInfo,
|
||||||
|
LoginResponse,
|
||||||
|
RefreshTokenRequest,
|
||||||
|
TokenResponse,
|
||||||
|
UserBriefResponse,
|
||||||
|
UserResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AccessTokenResponse",
|
||||||
|
"KakaoCallbackRequest",
|
||||||
|
"KakaoLoginResponse",
|
||||||
|
"KakaoTokenResponse",
|
||||||
|
"KakaoUserInfo",
|
||||||
|
"LoginResponse",
|
||||||
|
"RefreshTokenRequest",
|
||||||
|
"TokenResponse",
|
||||||
|
"UserBriefResponse",
|
||||||
|
"UserResponse",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
"""
|
||||||
|
User 모듈 Pydantic 스키마 정의
|
||||||
|
|
||||||
|
API 요청/응답 검증을 위한 스키마들입니다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 카카오 OAuth 스키마
|
||||||
|
# =============================================================================
|
||||||
|
class KakaoLoginResponse(BaseModel):
|
||||||
|
"""카카오 로그인 URL 응답"""
|
||||||
|
|
||||||
|
auth_url: str = Field(..., description="카카오 인증 페이지 URL")
|
||||||
|
|
||||||
|
|
||||||
|
class KakaoCallbackRequest(BaseModel):
|
||||||
|
"""카카오 콜백 요청 (인가 코드)"""
|
||||||
|
|
||||||
|
code: str = Field(..., min_length=1, description="카카오 인가 코드")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# JWT 토큰 스키마
|
||||||
|
# =============================================================================
|
||||||
|
class TokenResponse(BaseModel):
|
||||||
|
"""토큰 발급 응답"""
|
||||||
|
|
||||||
|
access_token: str = Field(..., description="액세스 토큰")
|
||||||
|
refresh_token: str = Field(..., description="리프레시 토큰")
|
||||||
|
token_type: str = Field(default="Bearer", description="토큰 타입")
|
||||||
|
expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)")
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenResponse(BaseModel):
|
||||||
|
"""액세스 토큰 갱신 응답"""
|
||||||
|
|
||||||
|
access_token: str = Field(..., description="액세스 토큰")
|
||||||
|
token_type: str = Field(default="Bearer", description="토큰 타입")
|
||||||
|
expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)")
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshTokenRequest(BaseModel):
|
||||||
|
"""토큰 갱신 요청"""
|
||||||
|
|
||||||
|
refresh_token: str = Field(..., min_length=1, description="리프레시 토큰")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 사용자 정보 스키마
|
||||||
|
# =============================================================================
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
"""사용자 정보 응답"""
|
||||||
|
|
||||||
|
id: int = Field(..., description="사용자 ID")
|
||||||
|
kakao_id: int = Field(..., description="카카오 회원번호")
|
||||||
|
email: Optional[str] = Field(None, description="이메일")
|
||||||
|
nickname: Optional[str] = Field(None, description="닉네임")
|
||||||
|
profile_image_url: Optional[str] = Field(None, description="프로필 이미지 URL")
|
||||||
|
thumbnail_image_url: Optional[str] = Field(None, description="썸네일 이미지 URL")
|
||||||
|
is_active: bool = Field(..., description="계정 활성화 상태")
|
||||||
|
is_admin: bool = Field(..., description="관리자 여부")
|
||||||
|
last_login_at: Optional[datetime] = Field(None, description="마지막 로그인 일시")
|
||||||
|
created_at: datetime = Field(..., description="가입 일시")
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class UserBriefResponse(BaseModel):
|
||||||
|
"""사용자 간략 정보 (토큰 응답에 포함)"""
|
||||||
|
|
||||||
|
id: int = Field(..., description="사용자 ID")
|
||||||
|
nickname: Optional[str] = Field(None, description="닉네임")
|
||||||
|
email: Optional[str] = Field(None, description="이메일")
|
||||||
|
profile_image_url: Optional[str] = Field(None, description="프로필 이미지 URL")
|
||||||
|
is_new_user: bool = Field(..., description="신규 가입 여부")
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class LoginResponse(BaseModel):
|
||||||
|
"""로그인 응답 (토큰 + 사용자 정보)"""
|
||||||
|
|
||||||
|
access_token: str = Field(..., description="액세스 토큰")
|
||||||
|
refresh_token: str = Field(..., description="리프레시 토큰")
|
||||||
|
token_type: str = Field(default="Bearer", description="토큰 타입")
|
||||||
|
expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)")
|
||||||
|
user: UserBriefResponse = Field(..., description="사용자 정보")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 내부 사용 스키마 (카카오 API 응답 파싱)
|
||||||
|
# =============================================================================
|
||||||
|
class KakaoTokenResponse(BaseModel):
|
||||||
|
"""카카오 토큰 응답 (내부 사용)"""
|
||||||
|
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
refresh_token: Optional[str] = None
|
||||||
|
expires_in: int
|
||||||
|
scope: Optional[str] = None
|
||||||
|
refresh_token_expires_in: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class KakaoProfile(BaseModel):
|
||||||
|
"""카카오 프로필 정보 (내부 사용)"""
|
||||||
|
|
||||||
|
nickname: Optional[str] = None
|
||||||
|
profile_image_url: Optional[str] = None
|
||||||
|
thumbnail_image_url: Optional[str] = None
|
||||||
|
is_default_image: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class KakaoAccount(BaseModel):
|
||||||
|
"""카카오 계정 정보 (내부 사용)"""
|
||||||
|
|
||||||
|
email: Optional[str] = None
|
||||||
|
profile: Optional[KakaoProfile] = None
|
||||||
|
|
||||||
|
|
||||||
|
class KakaoUserInfo(BaseModel):
|
||||||
|
"""카카오 사용자 정보 (내부 사용)"""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
kakao_account: Optional[KakaoAccount] = None
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import json
|
import 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자 이내로 요약
|
||||||
- 내용의 누락이 있어서는 안된다
|
- 내용의 누락이 있어서는 안된다
|
||||||
- 문장이 자연스러워야 한다
|
- 문장이 자연스러워야 한다
|
||||||
- 핵심 정보만 간결하게 포함
|
- 핵심 정보만 간결하게 포함
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,337 @@
|
||||||
|
"""
|
||||||
|
FastAPI용 로깅 모듈
|
||||||
|
|
||||||
|
Django 로거 구조를 참고하여 FastAPI에 최적화된 로깅 시스템.
|
||||||
|
|
||||||
|
사용 예시:
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("song")
|
||||||
|
logger.info("노래 생성 완료")
|
||||||
|
logger.error("오류 발생", exc_info=True)
|
||||||
|
|
||||||
|
로그 레벨:
|
||||||
|
1. DEBUG: 디버깅 목적
|
||||||
|
2. INFO: 일반 정보
|
||||||
|
3. WARNING: 경고 정보 (작은 문제)
|
||||||
|
4. ERROR: 오류 정보 (큰 문제)
|
||||||
|
5. CRITICAL: 아주 심각한 문제
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import lru_cache
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from config import log_settings
|
||||||
|
|
||||||
|
# 로그 디렉토리 설정 (config.py의 LogSettings에서 관리)
|
||||||
|
LOG_DIR = log_settings.get_log_dir()
|
||||||
|
|
||||||
|
# 로그 레벨 타입
|
||||||
|
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerConfig:
|
||||||
|
"""로거 설정 클래스 (config.py의 LogSettings 참조)"""
|
||||||
|
|
||||||
|
# 출력 대상 설정 (LogSettings에서 가져옴)
|
||||||
|
CONSOLE_ENABLED: bool = log_settings.LOG_CONSOLE_ENABLED
|
||||||
|
FILE_ENABLED: bool = log_settings.LOG_FILE_ENABLED
|
||||||
|
|
||||||
|
# 기본 설정 (LogSettings에서 가져옴)
|
||||||
|
DEFAULT_LEVEL: str = log_settings.LOG_LEVEL
|
||||||
|
CONSOLE_LEVEL: str = log_settings.LOG_CONSOLE_LEVEL
|
||||||
|
FILE_LEVEL: str = log_settings.LOG_FILE_LEVEL
|
||||||
|
MAX_BYTES: int = log_settings.LOG_MAX_SIZE_MB * 1024 * 1024
|
||||||
|
BACKUP_COUNT: int = log_settings.LOG_BACKUP_COUNT
|
||||||
|
ENCODING: str = "utf-8"
|
||||||
|
|
||||||
|
# 포맷 설정 (LogSettings에서 가져옴)
|
||||||
|
CONSOLE_FORMAT: str = log_settings.LOG_CONSOLE_FORMAT
|
||||||
|
FILE_FORMAT: str = log_settings.LOG_FILE_FORMAT
|
||||||
|
DATE_FORMAT: str = log_settings.LOG_DATE_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
def _create_console_handler() -> logging.StreamHandler:
|
||||||
|
"""콘솔 핸들러 생성"""
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
handler.setLevel(getattr(logging, LoggerConfig.CONSOLE_LEVEL))
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
fmt=LoggerConfig.CONSOLE_FORMAT,
|
||||||
|
datefmt=LoggerConfig.DATE_FORMAT,
|
||||||
|
style="{",
|
||||||
|
)
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 공유 파일 핸들러 (싱글톤)
|
||||||
|
# 모든 로거가 동일한 파일 핸들러를 공유하여 하나의 로그 파일에 기록
|
||||||
|
# =============================================================================
|
||||||
|
_shared_file_handler: RotatingFileHandler | None = None
|
||||||
|
_shared_error_handler: RotatingFileHandler | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_shared_file_handler() -> RotatingFileHandler:
|
||||||
|
"""
|
||||||
|
공유 파일 핸들러 반환 (싱글톤)
|
||||||
|
|
||||||
|
모든 로거가 하나의 기본 로그 파일(app.log)에 기록합니다.
|
||||||
|
파일명: logs/{날짜}_app.log
|
||||||
|
"""
|
||||||
|
global _shared_file_handler
|
||||||
|
|
||||||
|
if _shared_file_handler is None:
|
||||||
|
today = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
log_file = LOG_DIR / f"{today}_app.log"
|
||||||
|
|
||||||
|
_shared_file_handler = RotatingFileHandler(
|
||||||
|
filename=log_file,
|
||||||
|
maxBytes=LoggerConfig.MAX_BYTES,
|
||||||
|
backupCount=LoggerConfig.BACKUP_COUNT,
|
||||||
|
encoding=LoggerConfig.ENCODING,
|
||||||
|
)
|
||||||
|
_shared_file_handler.setLevel(getattr(logging, LoggerConfig.FILE_LEVEL))
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
fmt=LoggerConfig.FILE_FORMAT,
|
||||||
|
datefmt=LoggerConfig.DATE_FORMAT,
|
||||||
|
style="{",
|
||||||
|
)
|
||||||
|
_shared_file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
return _shared_file_handler
|
||||||
|
|
||||||
|
|
||||||
|
def _get_shared_error_handler() -> RotatingFileHandler:
|
||||||
|
"""
|
||||||
|
공유 에러 파일 핸들러 반환 (싱글톤)
|
||||||
|
|
||||||
|
모든 로거의 ERROR 이상 로그가 하나의 에러 로그 파일(error.log)에 기록됩니다.
|
||||||
|
파일명: logs/{날짜}_error.log
|
||||||
|
"""
|
||||||
|
global _shared_error_handler
|
||||||
|
|
||||||
|
if _shared_error_handler is None:
|
||||||
|
today = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
log_file = LOG_DIR / f"{today}_error.log"
|
||||||
|
|
||||||
|
_shared_error_handler = RotatingFileHandler(
|
||||||
|
filename=log_file,
|
||||||
|
maxBytes=LoggerConfig.MAX_BYTES,
|
||||||
|
backupCount=LoggerConfig.BACKUP_COUNT,
|
||||||
|
encoding=LoggerConfig.ENCODING,
|
||||||
|
)
|
||||||
|
_shared_error_handler.setLevel(logging.ERROR)
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
fmt=LoggerConfig.FILE_FORMAT,
|
||||||
|
datefmt=LoggerConfig.DATE_FORMAT,
|
||||||
|
style="{",
|
||||||
|
)
|
||||||
|
_shared_error_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
return _shared_error_handler
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=32)
|
||||||
|
def get_logger(name: str = "app") -> logging.Logger:
|
||||||
|
"""
|
||||||
|
로거 인스턴스 반환 (캐싱 적용)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 로거 이름 (모듈명 권장: "song", "lyric", "video" 등)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
설정된 로거 인스턴스
|
||||||
|
|
||||||
|
Example:
|
||||||
|
logger = get_logger("song")
|
||||||
|
logger.info("노래 처리 시작")
|
||||||
|
"""
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
|
||||||
|
# 이미 핸들러가 설정된 경우 반환
|
||||||
|
if logger.handlers:
|
||||||
|
return logger
|
||||||
|
|
||||||
|
# 로그 레벨 설정
|
||||||
|
logger.setLevel(getattr(logging, LoggerConfig.DEFAULT_LEVEL))
|
||||||
|
|
||||||
|
# 핸들러 추가 (설정에 따라 선택적으로 추가)
|
||||||
|
if LoggerConfig.CONSOLE_ENABLED:
|
||||||
|
logger.addHandler(_create_console_handler())
|
||||||
|
|
||||||
|
if LoggerConfig.FILE_ENABLED:
|
||||||
|
logger.addHandler(_get_shared_file_handler())
|
||||||
|
logger.addHandler(_get_shared_error_handler())
|
||||||
|
|
||||||
|
# 상위 로거로 전파 방지 (중복 출력 방지)
|
||||||
|
logger.propagate = False
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def setup_uvicorn_logging() -> dict:
|
||||||
|
"""
|
||||||
|
Uvicorn 서버의 로깅 설정을 반환합니다.
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
언제 사용하는가?
|
||||||
|
============================================================
|
||||||
|
Uvicorn 서버를 Python 코드로 직접 실행할 때 사용합니다.
|
||||||
|
CLI 명령어(uvicorn main:app --reload)로 실행할 때는 적용되지 않습니다.
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
사용 방법
|
||||||
|
============================================================
|
||||||
|
1. Python 코드에서 uvicorn.run() 호출 시:
|
||||||
|
|
||||||
|
# run.py 또는 main.py 하단
|
||||||
|
import uvicorn
|
||||||
|
from app.utils.logger import setup_uvicorn_logging
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8000,
|
||||||
|
reload=True,
|
||||||
|
log_config=setup_uvicorn_logging(), # 여기서 적용
|
||||||
|
)
|
||||||
|
|
||||||
|
2. 실행:
|
||||||
|
python run.py
|
||||||
|
또는
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
어떤 동작을 하는가?
|
||||||
|
============================================================
|
||||||
|
Uvicorn의 기본 로깅 형식을 애플리케이션의 LogSettings와 일치시킵니다.
|
||||||
|
|
||||||
|
- formatters: 로그 출력 형식 정의
|
||||||
|
- default: 일반 로그용 (서버 시작/종료, 에러 등)
|
||||||
|
- access: HTTP 요청 로그용 (클라이언트 IP, 요청 경로, 상태 코드)
|
||||||
|
|
||||||
|
- handlers: 로그 출력 대상 설정
|
||||||
|
- stdout으로 콘솔에 출력
|
||||||
|
|
||||||
|
- loggers: Uvicorn 내부 로거 설정
|
||||||
|
- uvicorn: 메인 로거
|
||||||
|
- uvicorn.error: 에러/시작/종료 로그
|
||||||
|
- uvicorn.access: HTTP 요청 로그
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
출력 예시
|
||||||
|
============================================================
|
||||||
|
적용 전 (Uvicorn 기본):
|
||||||
|
INFO: 127.0.0.1:52341 - "GET /docs HTTP/1.1" 200 OK
|
||||||
|
INFO: Uvicorn running on http://0.0.0.0:8000
|
||||||
|
|
||||||
|
적용 후:
|
||||||
|
[2026-01-14 15:30:00] INFO [uvicorn.access] 127.0.0.1 - "GET /docs HTTP/1.1" 200
|
||||||
|
[2026-01-14 15:30:00] INFO [uvicorn:startup:45] Uvicorn running on http://0.0.0.0:8000
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
반환값 구조 (Python logging.config.dictConfig 형식)
|
||||||
|
============================================================
|
||||||
|
{
|
||||||
|
"version": 1, # dictConfig 버전 (항상 1)
|
||||||
|
"disable_existing_loggers": False, # 기존 로거 유지
|
||||||
|
"formatters": { ... }, # 포맷터 정의
|
||||||
|
"handlers": { ... }, # 핸들러 정의
|
||||||
|
"loggers": { ... }, # 로거 정의
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Uvicorn log_config 파라미터에 전달할 설정 딕셔너리
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# dictConfig 버전 (필수, 항상 1)
|
||||||
|
# --------------------------------------------------------
|
||||||
|
"version": 1,
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 기존 로거 비활성화 여부
|
||||||
|
# False: 기존 로거 유지 (권장)
|
||||||
|
# True: 기존 로거 모두 비활성화
|
||||||
|
# --------------------------------------------------------
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 포맷터 정의
|
||||||
|
# 로그 메시지의 출력 형식을 지정합니다.
|
||||||
|
# --------------------------------------------------------
|
||||||
|
"formatters": {
|
||||||
|
# 일반 로그용 포맷터 (서버 시작/종료, 에러 등)
|
||||||
|
"default": {
|
||||||
|
"format": LoggerConfig.CONSOLE_FORMAT,
|
||||||
|
"datefmt": LoggerConfig.DATE_FORMAT,
|
||||||
|
"style": "{", # {변수명} 스타일 사용
|
||||||
|
},
|
||||||
|
# HTTP 요청 로그용 포맷터
|
||||||
|
# 사용 가능한 변수: client_addr, request_line, status_code
|
||||||
|
"access": {
|
||||||
|
"format": "[{asctime}] {levelname:8} [{name}] {client_addr} - \"{request_line}\" {status_code}",
|
||||||
|
"datefmt": LoggerConfig.DATE_FORMAT,
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 핸들러 정의
|
||||||
|
# 로그를 어디에 출력할지 지정합니다.
|
||||||
|
# --------------------------------------------------------
|
||||||
|
"handlers": {
|
||||||
|
# 일반 로그 핸들러 (stdout 출력)
|
||||||
|
"default": {
|
||||||
|
"formatter": "default",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"stream": "ext://sys.stdout",
|
||||||
|
},
|
||||||
|
# HTTP 요청 로그 핸들러 (stdout 출력)
|
||||||
|
"access": {
|
||||||
|
"formatter": "access",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"stream": "ext://sys.stdout",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 로거 정의
|
||||||
|
# Uvicorn 내부에서 사용하는 로거들을 설정합니다.
|
||||||
|
# --------------------------------------------------------
|
||||||
|
"loggers": {
|
||||||
|
# Uvicorn 메인 로거
|
||||||
|
"uvicorn": {
|
||||||
|
"handlers": ["default"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": False, # 상위 로거로 전파 방지
|
||||||
|
},
|
||||||
|
# 에러/시작/종료 로그
|
||||||
|
"uvicorn.error": {
|
||||||
|
"handlers": ["default"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
# HTTP 요청 로그 (GET /path HTTP/1.1 200 등)
|
||||||
|
"uvicorn.access": {
|
||||||
|
"handlers": ["access"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 편의를 위한 사전 정의된 로거 이름 상수
|
||||||
|
HOME_LOGGER = "home"
|
||||||
|
LYRIC_LOGGER = "lyric"
|
||||||
|
SONG_LOGGER = "song"
|
||||||
|
VIDEO_LOGGER = "video"
|
||||||
|
CELERY_LOGGER = "celery"
|
||||||
|
APP_LOGGER = "app"
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import asyncio
|
import 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:
|
||||||
|
|
|
||||||
|
|
@ -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,12 +56,12 @@ 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,14 +158,14 @@ 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(
|
||||||
|
|
@ -176,43 +176,37 @@ 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")
|
||||||
print(f"[{log_prefix}] Public URL: {self._last_public_url}")
|
logger.debug(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")
|
||||||
print(f"[{log_prefix}] Response: {response.text[:500]}")
|
logger.error(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
|
||||||
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)}",
|
||||||
|
|
|
||||||
|
|
@ -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="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
|
|
||||||
|
|
@ -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
223
config.py
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,331 @@
|
||||||
|
# 카카오 소셜 로그인 구현 가이드
|
||||||
|
|
||||||
|
## 목차
|
||||||
|
|
||||||
|
1. [개요](#1-개요)
|
||||||
|
2. [인증 흐름](#2-인증-흐름)
|
||||||
|
3. [User 모델 구조](#3-user-모델-구조)
|
||||||
|
4. [API 엔드포인트](#4-api-엔드포인트)
|
||||||
|
5. [환경 설정](#5-환경-설정)
|
||||||
|
6. [에러 처리](#6-에러-처리)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
CastAD는 카카오 소셜 로그인만 지원하며, 인증 후 자체 JWT 토큰을 발급합니다.
|
||||||
|
|
||||||
|
### 인증 방식
|
||||||
|
|
||||||
|
| 항목 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 소셜 로그인 | 카카오 OAuth 2.0 |
|
||||||
|
| 자체 인증 | JWT (Access Token + Refresh Token) |
|
||||||
|
| 카카오 토큰 저장 | X (1회 검증 후 폐기) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 인증 흐름
|
||||||
|
|
||||||
|
### 2.1 전체 흐름도
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||||
|
│ Client │ │ Backend │ │ Kakao │ │ DB │
|
||||||
|
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
|
||||||
|
│ │ │ │
|
||||||
|
│ 1. 로그인 요청 │ │ │
|
||||||
|
│──────────────>│ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ 2. 카카오 로그인 URL 반환 │ │
|
||||||
|
│<──────────────│ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ 3. 카카오 로그인 페이지로 이동 │ │
|
||||||
|
│──────────────────────────────>│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ 4. 사용자 인증 후 code 반환 │ │
|
||||||
|
│<──────────────────────────────│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ 5. code 전달 │ │ │
|
||||||
|
│──────────────>│ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 6. code로 토큰 요청 │
|
||||||
|
│ │──────────────>│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 7. access_token 반환 │
|
||||||
|
│ │<──────────────│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 8. 사용자 정보 조회 │
|
||||||
|
│ │──────────────>│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 9. 사용자 정보 반환 │
|
||||||
|
│ │<──────────────│ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 10. kakao_id로 회원 조회 │
|
||||||
|
│ │──────────────────────────────>│
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 11. 회원 정보 반환 (없으면 생성) │
|
||||||
|
│ │<──────────────────────────────│
|
||||||
|
│ │ │ │
|
||||||
|
│ 12. 자체 JWT 발급 및 반환 │ │
|
||||||
|
│<──────────────│ │ │
|
||||||
|
│ │ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 단계별 설명
|
||||||
|
|
||||||
|
| 단계 | 설명 | 관련 API |
|
||||||
|
|------|------|----------|
|
||||||
|
| 1-2 | 클라이언트가 로그인 요청, 백엔드가 카카오 인증 URL 생성 | `GET /api/v1/auth/kakao/login` |
|
||||||
|
| 3-4 | 사용자가 카카오에서 로그인, 인가 코드(code) 발급 | 카카오 OAuth |
|
||||||
|
| 5-9 | 백엔드가 code로 카카오 토큰/사용자정보 획득 | 카카오 API |
|
||||||
|
| 10-11 | DB에서 회원 조회, 없으면 신규 가입 | 내부 처리 |
|
||||||
|
| 12 | 자체 JWT 토큰 발급 후 클라이언트에 반환 | `POST /api/v1/auth/kakao/callback` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. User 모델 구조
|
||||||
|
|
||||||
|
### 3.1 테이블 스키마
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ user │
|
||||||
|
├─────────────────────┬───────────────┬───────────────────────┤
|
||||||
|
│ Column │ Type │ Description │
|
||||||
|
├─────────────────────┼───────────────┼───────────────────────┤
|
||||||
|
│ id │ BIGINT (PK) │ 고유 식별자 (자동증가) │
|
||||||
|
│ kakao_id │ BIGINT (UQ) │ 카카오 회원번호 │
|
||||||
|
│ email │ VARCHAR(255) │ 이메일 (선택) │
|
||||||
|
│ nickname │ VARCHAR(100) │ 닉네임 (선택) │
|
||||||
|
│ profile_image_url │ VARCHAR(2048) │ 프로필 이미지 URL │
|
||||||
|
│ thumbnail_image_url │ VARCHAR(2048) │ 썸네일 이미지 URL │
|
||||||
|
│ is_active │ BOOLEAN │ 계정 활성화 상태 │
|
||||||
|
│ is_admin │ BOOLEAN │ 관리자 권한 │
|
||||||
|
│ last_login_at │ DATETIME │ 마지막 로그인 일시 │
|
||||||
|
│ created_at │ DATETIME │ 생성 일시 │
|
||||||
|
│ updated_at │ DATETIME │ 수정 일시 │
|
||||||
|
└─────────────────────┴───────────────┴───────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 카카오 API 응답 매핑
|
||||||
|
|
||||||
|
```json
|
||||||
|
// 카카오 API 응답 예시
|
||||||
|
{
|
||||||
|
"id": 1234567890,
|
||||||
|
"kakao_account": {
|
||||||
|
"email": "user@kakao.com",
|
||||||
|
"profile": {
|
||||||
|
"nickname": "홍길동",
|
||||||
|
"profile_image_url": "https://k.kakaocdn.net/.../profile.jpg",
|
||||||
|
"thumbnail_image_url": "https://k.kakaocdn.net/.../thumb.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| User 필드 | 카카오 응답 경로 |
|
||||||
|
|-----------|-----------------|
|
||||||
|
| `kakao_id` | `id` |
|
||||||
|
| `email` | `kakao_account.email` |
|
||||||
|
| `nickname` | `kakao_account.profile.nickname` |
|
||||||
|
| `profile_image_url` | `kakao_account.profile.profile_image_url` |
|
||||||
|
| `thumbnail_image_url` | `kakao_account.profile.thumbnail_image_url` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. API 엔드포인트
|
||||||
|
|
||||||
|
### 4.1 카카오 로그인 URL 요청
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/auth/kakao/login
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"auth_url": "https://kauth.kakao.com/oauth/authorize?client_id=...&redirect_uri=...&response_type=code"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 카카오 콜백 (로그인/가입 처리)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/auth/kakao/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "인가코드"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (성공):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"nickname": "홍길동",
|
||||||
|
"email": "user@kakao.com",
|
||||||
|
"profile_image_url": "https://...",
|
||||||
|
"is_new_user": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 토큰 갱신
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/auth/refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 로그아웃
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/auth/logout
|
||||||
|
Authorization: Bearer {access_token}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 내 정보 조회
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/users/me
|
||||||
|
Authorization: Bearer {access_token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"kakao_id": 1234567890,
|
||||||
|
"nickname": "홍길동",
|
||||||
|
"email": "user@kakao.com",
|
||||||
|
"profile_image_url": "https://...",
|
||||||
|
"is_admin": false,
|
||||||
|
"created_at": "2026-01-14T16:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 환경 설정
|
||||||
|
|
||||||
|
### 5.1 카카오 개발자 설정
|
||||||
|
|
||||||
|
1. [카카오 개발자 콘솔](https://developers.kakao.com) 접속
|
||||||
|
2. 애플리케이션 생성
|
||||||
|
3. 플랫폼 > Web 사이트 도메인 등록
|
||||||
|
4. 카카오 로그인 > Redirect URI 등록
|
||||||
|
5. 동의항목 > 필요한 정보 설정
|
||||||
|
|
||||||
|
### 5.2 .env 설정
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 카카오 OAuth
|
||||||
|
KAKAO_CLIENT_ID=your_rest_api_key
|
||||||
|
KAKAO_CLIENT_SECRET=your_client_secret # 선택
|
||||||
|
KAKAO_REDIRECT_URI=https://your-domain.com/api/v1/auth/kakao/callback
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=your-super-secret-key-min-32-characters
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
|
JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 config.py 설정
|
||||||
|
|
||||||
|
```python
|
||||||
|
class KakaoSettings(BaseSettings):
|
||||||
|
KAKAO_CLIENT_ID: str = Field(...)
|
||||||
|
KAKAO_CLIENT_SECRET: str = Field(default="")
|
||||||
|
KAKAO_REDIRECT_URI: str = Field(...)
|
||||||
|
|
||||||
|
model_config = _base_config
|
||||||
|
|
||||||
|
|
||||||
|
class JWTSettings(BaseSettings):
|
||||||
|
JWT_SECRET: str = Field(...)
|
||||||
|
JWT_ALGORITHM: str = Field(default="HS256")
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=60)
|
||||||
|
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7)
|
||||||
|
|
||||||
|
model_config = _base_config
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 에러 처리
|
||||||
|
|
||||||
|
### 6.1 에러 코드 정의
|
||||||
|
|
||||||
|
| HTTP Status | Error Code | 설명 |
|
||||||
|
|-------------|------------|------|
|
||||||
|
| 400 | `INVALID_CODE` | 유효하지 않은 인가 코드 |
|
||||||
|
| 400 | `KAKAO_AUTH_FAILED` | 카카오 인증 실패 |
|
||||||
|
| 401 | `TOKEN_EXPIRED` | 토큰 만료 |
|
||||||
|
| 401 | `INVALID_TOKEN` | 유효하지 않은 토큰 |
|
||||||
|
| 401 | `TOKEN_REVOKED` | 취소된 토큰 |
|
||||||
|
| 403 | `USER_INACTIVE` | 비활성화된 계정 |
|
||||||
|
| 403 | `ADMIN_REQUIRED` | 관리자 권한 필요 |
|
||||||
|
| 404 | `USER_NOT_FOUND` | 사용자 없음 |
|
||||||
|
| 500 | `KAKAO_API_ERROR` | 카카오 API 오류 |
|
||||||
|
|
||||||
|
### 6.2 에러 응답 형식
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": {
|
||||||
|
"code": "TOKEN_EXPIRED",
|
||||||
|
"message": "토큰이 만료되었습니다. 다시 로그인해주세요."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 부록: 파일 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
app/user/
|
||||||
|
├── __init__.py
|
||||||
|
├── models.py # User 모델
|
||||||
|
├── schemas/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── user_schema.py # Pydantic 스키마
|
||||||
|
├── services/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── auth.py # 인증 서비스
|
||||||
|
│ ├── jwt.py # JWT 서비스
|
||||||
|
│ └── kakao.py # 카카오 OAuth 서비스
|
||||||
|
├── api/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── routers/
|
||||||
|
│ └── v1/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── auth.py # 인증 API 라우터
|
||||||
|
└── exceptions.py # 사용자 정의 예외
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue