크레딧 기능 추가
parent
7202376123
commit
6492d23bc1
|
|
@ -239,7 +239,22 @@ async def generate_lyric(
|
|||
|
||||
request_start = time.perf_counter()
|
||||
task_id = request_body.task_id
|
||||
|
||||
|
||||
user = (await session.execute(
|
||||
select(User).where(User.user_uuid == current_user.user_uuid)
|
||||
)).scalar_one()
|
||||
|
||||
if user.credits <= 0:
|
||||
logger.info(
|
||||
f"크레딧 부족, user_uuid: {current_user.user_uuid}, credits: {current_user.credits}"
|
||||
)
|
||||
return GenerateLyricResponse(
|
||||
success=False,
|
||||
task_id=task_id,
|
||||
lyric=None,
|
||||
language=request_body.language,
|
||||
error_message="No credits remaining.",
|
||||
)
|
||||
|
||||
logger.info(f"[generate_lyric] ========== START ==========")
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -103,6 +103,22 @@ async def generate_song(
|
|||
from app.database.session import AsyncSessionLocal
|
||||
|
||||
request_start = time.perf_counter()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
user = (await session.execute(
|
||||
select(User).where(User.user_uuid == current_user.user_uuid)
|
||||
)).scalar_one()
|
||||
|
||||
if user.credits <= 0:
|
||||
logger.info(f"크레딧 부족, user_uuid: {current_user.user_uuid}, credits: {user.credits}")
|
||||
return GenerateSongResponse(
|
||||
success=False,
|
||||
task_id=task_id,
|
||||
song_id=None,
|
||||
message="No credits remaining.",
|
||||
error_message="No credits remaining.",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[generate_song] START - task_id: {task_id}, "
|
||||
f"genre: {request_body.genre}, language: {request_body.language}"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ logger = logging.getLogger(__name__)
|
|||
from app.user.dependencies import get_current_user
|
||||
from app.user.models import RefreshToken, User
|
||||
from app.user.schemas.user_schema import (
|
||||
CreditResponse,
|
||||
KakaoCodeRequest,
|
||||
KakaoLoginResponse,
|
||||
LoginResponse,
|
||||
|
|
@ -353,6 +354,22 @@ async def get_me(
|
|||
return UserResponse.model_validate(current_user)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me/credits",
|
||||
response_model=CreditResponse,
|
||||
summary="잔여 크레딧 조회",
|
||||
description="현재 로그인한 사용자의 잔여 영상 생성 크레딧을 반환합니다.",
|
||||
responses={
|
||||
200: {"description": "조회 성공"},
|
||||
401: {"description": "인증 실패 (토큰 없음/만료)"},
|
||||
},
|
||||
)
|
||||
async def get_my_credits(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> CreditResponse:
|
||||
return CreditResponse(credits=current_user.credits)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 테스트용 엔드포인트 (DEBUG 모드에서만 main.py에서 라우터가 등록됨)
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -216,6 +216,14 @@ class User(Base):
|
|||
comment="마지막 로그인 일시",
|
||||
)
|
||||
|
||||
credits: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
default=3,
|
||||
server_default="3",
|
||||
comment="잔여 영상 생성 크레딧",
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
|
|
|
|||
|
|
@ -160,6 +160,22 @@ class LoginResponse(BaseModel):
|
|||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# 크레딧 스키마
|
||||
# =============================================================================
|
||||
class CreditResponse(BaseModel):
|
||||
"""잔여 크레딧 응답"""
|
||||
|
||||
credits: int = Field(..., description="영상 생성 크레딧")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"credits": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 내부 사용 스키마 (카카오 API 응답 파싱)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
from sqlalchemy import update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.user.models import User
|
||||
|
||||
|
||||
async def consume_credit(user_uuid, session: AsyncSession) -> bool:
|
||||
"""atomic UPDATE로 1크레딧 차감.
|
||||
|
||||
WHERE credits > 0 조건으로 음수 차감 방지 + PostgreSQL 행 락.
|
||||
차감 성공 여부 반환.
|
||||
"""
|
||||
result = await session.execute(
|
||||
update(User)
|
||||
.where(User.user_uuid == user_uuid, User.credits > 0)
|
||||
.values(credits=User.credits - 1)
|
||||
)
|
||||
return result.rowcount > 0
|
||||
|
|
@ -13,6 +13,7 @@ from sqlalchemy import select
|
|||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from app.database.session import BackgroundSessionLocal
|
||||
from app.user.services.credit import consume_credit
|
||||
from app.video.models import Video
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
from app.utils.logger import get_logger
|
||||
|
|
@ -154,6 +155,12 @@ async def download_and_upload_video_to_blob(
|
|||
|
||||
# Video 테이블 업데이트 (creatomate_render_id로 특정 Video 식별)
|
||||
await _update_video_status(task_id, "completed", blob_url, creatomate_render_id)
|
||||
|
||||
# 영상 생성 완료 시 크레딧 1 차감 (credits > 0 조건으로 음수 방지)
|
||||
async with BackgroundSessionLocal() as session:
|
||||
await consume_credit(user_uuid, session)
|
||||
await session.commit()
|
||||
|
||||
logger.info(f"[download_and_upload_video_to_blob] SUCCESS - task_id: {task_id}, creatomate_render_id: {creatomate_render_id}")
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
-- 2026-04-28: 사용자 크레딧 시스템 도입
|
||||
-- 실행 방법: psql -U <user> -d <dbname> -f 2026_04_28_add_user_credits.sql
|
||||
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN credits INT NOT NULL DEFAULT 3 COMMENT '영상 생성 크레딧';
|
||||
Loading…
Reference in New Issue