크레딧 기능 추가

feature-credit
김성경 2026-04-28 14:34:19 +09:00
parent 7202376123
commit 6492d23bc1
8 changed files with 103 additions and 1 deletions

View File

@ -240,6 +240,21 @@ 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
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(f"[generate_lyric] ========== START ==========")
logger.info( logger.info(

View File

@ -103,6 +103,22 @@ 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()
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( 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}"

View File

@ -23,6 +23,7 @@ logger = logging.getLogger(__name__)
from app.user.dependencies import get_current_user from app.user.dependencies import get_current_user
from app.user.models import RefreshToken, User from app.user.models import RefreshToken, User
from app.user.schemas.user_schema import ( from app.user.schemas.user_schema import (
CreditResponse,
KakaoCodeRequest, KakaoCodeRequest,
KakaoLoginResponse, KakaoLoginResponse,
LoginResponse, LoginResponse,
@ -353,6 +354,22 @@ async def get_me(
return UserResponse.model_validate(current_user) 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에서 라우터가 등록됨) # 테스트용 엔드포인트 (DEBUG 모드에서만 main.py에서 라우터가 등록됨)
# ============================================================================= # =============================================================================

View File

@ -216,6 +216,14 @@ class User(Base):
comment="마지막 로그인 일시", comment="마지막 로그인 일시",
) )
credits: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=3,
server_default="3",
comment="잔여 영상 생성 크레딧",
)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
nullable=False, nullable=False,

View File

@ -160,6 +160,22 @@ class LoginResponse(BaseModel):
} }
} }
# =============================================================================
# 크레딧 스키마
# =============================================================================
class CreditResponse(BaseModel):
"""잔여 크레딧 응답"""
credits: int = Field(..., description="영상 생성 크레딧")
model_config = {
"json_schema_extra": {
"example": {
"credits": 3
}
}
}
# ============================================================================= # =============================================================================
# 내부 사용 스키마 (카카오 API 응답 파싱) # 내부 사용 스키마 (카카오 API 응답 파싱)

View File

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

View File

@ -13,6 +13,7 @@ from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from app.database.session import BackgroundSessionLocal from app.database.session import BackgroundSessionLocal
from app.user.services.credit import consume_credit
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 from app.utils.logger import get_logger
@ -154,6 +155,12 @@ async def download_and_upload_video_to_blob(
# Video 테이블 업데이트 (creatomate_render_id로 특정 Video 식별) # Video 테이블 업데이트 (creatomate_render_id로 특정 Video 식별)
await _update_video_status(task_id, "completed", blob_url, creatomate_render_id) 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}") 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: except httpx.HTTPError as e:

View File

@ -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 '영상 생성 크레딧';