사용자의 uuid -> ksuid로 변경, 테이블 구조 변경
parent
dc7351d0f9
commit
aa8d9d7c14
|
|
@ -149,7 +149,7 @@ class Project(Base):
|
|||
id: 고유 식별자 (자동 증가)
|
||||
store_name: 고객명 (필수)
|
||||
region: 지역명 (필수, 예: 서울, 부산, 대구 등)
|
||||
task_id: 작업 고유 식별자 (UUID 형식, 36자)
|
||||
task_id: 작업 고유 식별자 (KSUID 형식, 27자)
|
||||
detail_region_info: 상세 지역 정보 (선택, JSON 또는 텍스트 형식)
|
||||
created_at: 생성 일시 (자동 설정)
|
||||
|
||||
|
|
@ -184,21 +184,20 @@ class Project(Base):
|
|||
store_name: Mapped[str] = mapped_column(
|
||||
String(255),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="가게명",
|
||||
)
|
||||
|
||||
region: Mapped[str] = mapped_column(
|
||||
String(100),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="지역명 (예: 군산)",
|
||||
)
|
||||
|
||||
task_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
String(27),
|
||||
nullable=False,
|
||||
comment="프로젝트 작업 고유 식별자 (UUID)",
|
||||
unique=True,
|
||||
comment="프로젝트 작업 고유 식별자 (KSUID)",
|
||||
)
|
||||
|
||||
detail_region_info: Mapped[Optional[str]] = mapped_column(
|
||||
|
|
@ -281,7 +280,7 @@ class Image(Base):
|
|||
|
||||
Attributes:
|
||||
id: 고유 식별자 (자동 증가)
|
||||
task_id: 이미지 업로드 작업 고유 식별자 (UUID)
|
||||
task_id: 이미지 업로드 작업 고유 식별자 (KSUID)
|
||||
img_name: 이미지명
|
||||
img_url: 이미지 URL (S3, CDN 등의 경로)
|
||||
created_at: 생성 일시 (자동 설정)
|
||||
|
|
@ -289,6 +288,7 @@ class Image(Base):
|
|||
|
||||
__tablename__ = "image"
|
||||
__table_args__ = (
|
||||
Index("idx_image_task_id", "task_id"),
|
||||
{
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_charset": "utf8mb4",
|
||||
|
|
@ -305,9 +305,10 @@ class Image(Base):
|
|||
)
|
||||
|
||||
task_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
String(27),
|
||||
nullable=False,
|
||||
comment="이미지 업로드 작업 고유 식별자 (UUID)",
|
||||
unique=True,
|
||||
comment="이미지 업로드 작업 고유 식별자 (KSUID)",
|
||||
)
|
||||
|
||||
img_name: Mapped[str] = mapped_column(
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class GenerateUrlsRequest(GenerateRequestInfo):
|
|||
class GenerateUploadResponse(BaseModel):
|
||||
"""파일 업로드 기반 생성 응답 스키마"""
|
||||
|
||||
task_id: str = Field(..., description="작업 고유 식별자 (UUID7)")
|
||||
task_id: str = Field(..., description="작업 고유 식별자 (KSUID)")
|
||||
status: Literal["processing", "completed", "failed"] = Field(
|
||||
..., description="작업 상태"
|
||||
)
|
||||
|
|
@ -102,7 +102,7 @@ class GenerateUploadResponse(BaseModel):
|
|||
class GenerateResponse(BaseModel):
|
||||
"""생성 응답 스키마"""
|
||||
|
||||
task_id: str = Field(..., description="작업 고유 식별자 (UUID7)")
|
||||
task_id: str = Field(..., description="작업 고유 식별자 (KSUID)")
|
||||
status: Literal["processing", "completed", "failed"] = Field(
|
||||
..., description="작업 상태"
|
||||
)
|
||||
|
|
@ -291,7 +291,7 @@ class ImageUploadResponse(BaseModel):
|
|||
}
|
||||
)
|
||||
|
||||
task_id: str = Field(..., description="작업 고유 식별자 (새로 생성된 UUID7)")
|
||||
task_id: str = Field(..., description="작업 고유 식별자 (새로 생성된 KSUID)")
|
||||
total_count: int = Field(..., description="총 업로드된 이미지 개수")
|
||||
url_count: int = Field(..., description="URL로 등록된 이미지 개수")
|
||||
file_count: int = Field(..., description="파일로 업로드된 이미지 개수")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, func
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ class Lyric(Base):
|
|||
Attributes:
|
||||
id: 고유 식별자 (자동 증가)
|
||||
project_id: 연결된 Project의 id (외래키)
|
||||
task_id: 가사 생성 작업의 고유 식별자 (UUID 형식)
|
||||
task_id: 가사 생성 작업의 고유 식별자 (KSUID 형식)
|
||||
status: 처리 상태 (pending, processing, completed, failed 등)
|
||||
lyric_prompt: 가사 생성에 사용된 프롬프트
|
||||
lyric_result: 생성된 가사 결과 (LONGTEXT로 긴 가사 지원)
|
||||
|
|
@ -37,6 +37,8 @@ class Lyric(Base):
|
|||
|
||||
__tablename__ = "lyric"
|
||||
__table_args__ = (
|
||||
Index("idx_lyric_task_id", "task_id"),
|
||||
Index("idx_lyric_project_id", "project_id"),
|
||||
{
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_charset": "utf8mb4",
|
||||
|
|
@ -56,14 +58,14 @@ class Lyric(Base):
|
|||
Integer,
|
||||
ForeignKey("project.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="연결된 Project의 id",
|
||||
)
|
||||
|
||||
task_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
String(27),
|
||||
nullable=False,
|
||||
comment="가사 생성 작업 고유 식별자 (UUID)",
|
||||
unique=True,
|
||||
comment="가사 생성 작업 고유 식별자 (KSUID)",
|
||||
)
|
||||
|
||||
status: Mapped[str] = mapped_column(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database.session import Base
|
||||
|
|
@ -23,7 +23,7 @@ class Song(Base):
|
|||
id: 고유 식별자 (자동 증가)
|
||||
project_id: 연결된 Project의 id (외래키)
|
||||
lyric_id: 연결된 Lyric의 id (외래키)
|
||||
task_id: 노래 생성 작업의 고유 식별자 (UUID 형식)
|
||||
task_id: 노래 생성 작업의 고유 식별자 (KSUID 형식)
|
||||
suno_task_id: Suno API 작업 고유 식별자 (선택)
|
||||
status: 처리 상태 (processing, uploading, completed, failed)
|
||||
song_prompt: 노래 생성에 사용된 프롬프트
|
||||
|
|
@ -39,6 +39,9 @@ class Song(Base):
|
|||
|
||||
__tablename__ = "song"
|
||||
__table_args__ = (
|
||||
Index("idx_song_task_id", "task_id"),
|
||||
Index("idx_song_project_id", "project_id"),
|
||||
Index("idx_song_lyric_id", "lyric_id"),
|
||||
{
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_charset": "utf8mb4",
|
||||
|
|
@ -58,7 +61,6 @@ class Song(Base):
|
|||
Integer,
|
||||
ForeignKey("project.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="연결된 Project의 id",
|
||||
)
|
||||
|
||||
|
|
@ -66,14 +68,14 @@ class Song(Base):
|
|||
Integer,
|
||||
ForeignKey("lyric.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="연결된 Lyric의 id",
|
||||
)
|
||||
|
||||
task_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
String(27),
|
||||
nullable=False,
|
||||
comment="노래 생성 작업 고유 식별자 (UUID)",
|
||||
unique=True,
|
||||
comment="노래 생성 작업 고유 식별자 (KSUID)",
|
||||
)
|
||||
|
||||
suno_task_id: Mapped[Optional[str]] = mapped_column(
|
||||
|
|
@ -177,6 +179,7 @@ class SongTimestamp(Base):
|
|||
|
||||
__tablename__ = "song_timestamp"
|
||||
__table_args__ = (
|
||||
Index("idx_song_timestamp_suno_audio_id", "suno_audio_id"),
|
||||
{
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_charset": "utf8mb4",
|
||||
|
|
@ -195,7 +198,6 @@ class SongTimestamp(Base):
|
|||
suno_audio_id: Mapped[str] = mapped_column(
|
||||
String(64),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="가사의 원본 오디오 ID",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,14 +58,14 @@ async def get_current_user(
|
|||
if payload.get("type") != "access":
|
||||
raise InvalidTokenError("액세스 토큰이 아닙니다.")
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if user_id is None:
|
||||
user_ksuid = payload.get("sub")
|
||||
if user_ksuid is None:
|
||||
raise InvalidTokenError()
|
||||
|
||||
# 사용자 조회
|
||||
result = await session.execute(
|
||||
select(User).where(
|
||||
User.id == int(user_id),
|
||||
User.user_ksuid == user_ksuid,
|
||||
User.is_deleted == False, # noqa: E712
|
||||
)
|
||||
)
|
||||
|
|
@ -106,13 +106,13 @@ async def get_current_user_optional(
|
|||
if payload.get("type") != "access":
|
||||
return None
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if user_id is None:
|
||||
user_ksuid = payload.get("sub")
|
||||
if user_ksuid is None:
|
||||
return None
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(
|
||||
User.id == int(user_id),
|
||||
User.user_ksuid == user_ksuid,
|
||||
User.is_deleted == False, # noqa: E712
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class User(Base):
|
|||
Attributes:
|
||||
id: 고유 식별자 (자동 증가)
|
||||
kakao_id: 카카오 고유 ID (필수, 유니크)
|
||||
user_ksuid: 사용자 식별을 위한 KSUID (필수, 유니크)
|
||||
email: 이메일 주소 (선택, 카카오에서 제공 시)
|
||||
nickname: 카카오 닉네임 (선택)
|
||||
profile_image_url: 카카오 프로필 이미지 URL (선택)
|
||||
|
|
@ -68,7 +69,8 @@ class User(Base):
|
|||
|
||||
__tablename__ = "user"
|
||||
__table_args__ = (
|
||||
Index("idx_user_kakao_id", "kakao_id", unique=True),
|
||||
Index("idx_user_kakao_id", "kakao_id"),
|
||||
Index("idx_user_ksuid", "user_ksuid"),
|
||||
Index("idx_user_email", "email"),
|
||||
Index("idx_user_phone", "phone"),
|
||||
Index("idx_user_is_active", "is_active"),
|
||||
|
|
@ -103,6 +105,13 @@ class User(Base):
|
|||
comment="카카오 고유 ID (회원번호)",
|
||||
)
|
||||
|
||||
user_ksuid: Mapped[str] = mapped_column(
|
||||
String(27),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
comment="사용자 식별을 위한 KSUID (K-Sortable Unique Identifier)",
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# 카카오에서 제공하는 사용자 정보 (선택적)
|
||||
# ==========================================================================
|
||||
|
|
@ -282,6 +291,7 @@ class RefreshToken(Base):
|
|||
Attributes:
|
||||
id: 고유 식별자 (자동 증가)
|
||||
user_id: 사용자 외래키 (User.id 참조)
|
||||
user_ksuid: 사용자 KSUID (User.user_ksuid 참조)
|
||||
token_hash: 리프레시 토큰의 SHA-256 해시값 (원본 저장 X)
|
||||
expires_at: 토큰 만료 일시
|
||||
is_revoked: 토큰 폐기 여부 (로그아웃 시 True)
|
||||
|
|
@ -299,6 +309,7 @@ class RefreshToken(Base):
|
|||
__tablename__ = "refresh_token"
|
||||
__table_args__ = (
|
||||
Index("idx_refresh_token_user_id", "user_id"),
|
||||
Index("idx_refresh_token_user_ksuid", "user_ksuid"),
|
||||
Index("idx_refresh_token_token_hash", "token_hash", unique=True),
|
||||
Index("idx_refresh_token_expires_at", "expires_at"),
|
||||
Index("idx_refresh_token_is_revoked", "is_revoked"),
|
||||
|
|
@ -324,6 +335,12 @@ class RefreshToken(Base):
|
|||
comment="사용자 외래키 (User.id 참조)",
|
||||
)
|
||||
|
||||
user_ksuid: Mapped[str] = mapped_column(
|
||||
String(27),
|
||||
nullable=False,
|
||||
comment="사용자 KSUID (User.user_ksuid 참조)",
|
||||
)
|
||||
|
||||
token_hash: Mapped[str] = mapped_column(
|
||||
String(64),
|
||||
nullable=False,
|
||||
|
|
|
|||
|
|
@ -156,13 +156,13 @@ class UserBriefResponse(BaseModel):
|
|||
|
||||
|
||||
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="사용자 정보")
|
||||
is_new_user: bool = Field(..., description="신규 가입 여부")
|
||||
redirect_url: str = Field(..., description="로그인 후 리다이렉트할 프론트엔드 URL")
|
||||
|
||||
model_config = {
|
||||
|
|
@ -172,13 +172,7 @@ class LoginResponse(BaseModel):
|
|||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3MDU4MzM2MDB9.yyy",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"nickname": "홍길동",
|
||||
"email": "user@kakao.com",
|
||||
"profile_image_url": "https://k.kakaocdn.net/dn/.../profile.jpg",
|
||||
"is_new_user": False
|
||||
},
|
||||
"is_new_user": False,
|
||||
"redirect_url": "http://localhost:3000"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ from app.user.exceptions import (
|
|||
UserNotFoundError,
|
||||
)
|
||||
from app.user.models import RefreshToken, User
|
||||
from app.utils.common import generate_ksuid
|
||||
from app.user.schemas.user_schema import (
|
||||
AccessTokenResponse,
|
||||
KakaoUserInfo,
|
||||
LoginResponse,
|
||||
UserBriefResponse,
|
||||
)
|
||||
from app.user.services.jwt import (
|
||||
create_access_token,
|
||||
|
|
@ -96,27 +96,28 @@ class AuthService:
|
|||
|
||||
# 5. JWT 토큰 생성
|
||||
logger.info("[AUTH] 5단계: JWT 토큰 생성 시작")
|
||||
access_token = create_access_token(user.id)
|
||||
refresh_token = create_refresh_token(user.id)
|
||||
logger.debug(f"[AUTH] JWT 토큰 생성 완료 - user_id: {user.id}")
|
||||
access_token = create_access_token(user.user_ksuid)
|
||||
refresh_token = create_refresh_token(user.user_ksuid)
|
||||
logger.debug(f"[AUTH] JWT 토큰 생성 완료 - user_ksuid: {user.user_ksuid}")
|
||||
|
||||
# 6. 리프레시 토큰 DB 저장
|
||||
logger.info("[AUTH] 6단계: 리프레시 토큰 저장 시작")
|
||||
await self._save_refresh_token(
|
||||
user_id=user.id,
|
||||
user_ksuid=user.user_ksuid,
|
||||
token=refresh_token,
|
||||
session=session,
|
||||
user_agent=user_agent,
|
||||
ip_address=ip_address,
|
||||
)
|
||||
logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}")
|
||||
logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}, user_ksuid: {user.user_ksuid}")
|
||||
|
||||
# 7. 마지막 로그인 시간 업데이트
|
||||
user.last_login_at = datetime.now(timezone.utc)
|
||||
await session.commit()
|
||||
|
||||
redirect_url = f"{prj_settings.PROJECT_DOMAIN}"
|
||||
logger.info(f"[AUTH] 카카오 로그인 완료 - user_id: {user.id}, redirect_url: {redirect_url}")
|
||||
logger.info(f"[AUTH] 카카오 로그인 완료 - user_id: {user.id}, user_ksuid: {user.user_ksuid}, redirect_url: {redirect_url}")
|
||||
logger.debug(f"[AUTH] 응답 토큰 정보 - access_token: {access_token[:30]}..., refresh_token: {refresh_token[:30]}...")
|
||||
|
||||
return LoginResponse(
|
||||
|
|
@ -124,13 +125,7 @@ class AuthService:
|
|||
refresh_token=refresh_token,
|
||||
token_type="Bearer",
|
||||
expires_in=get_access_token_expire_seconds(),
|
||||
user=UserBriefResponse(
|
||||
id=user.id,
|
||||
nickname=user.nickname,
|
||||
email=user.email,
|
||||
profile_image_url=user.profile_image_url,
|
||||
is_new_user=is_new_user,
|
||||
),
|
||||
is_new_user=is_new_user,
|
||||
redirect_url=redirect_url,
|
||||
)
|
||||
|
||||
|
|
@ -177,8 +172,8 @@ class AuthService:
|
|||
raise TokenExpiredError()
|
||||
|
||||
# 4. 사용자 확인
|
||||
user_id = int(payload.get("sub"))
|
||||
user = await self._get_user_by_id(user_id, session)
|
||||
user_ksuid = payload.get("sub")
|
||||
user = await self._get_user_by_ksuid(user_ksuid, session)
|
||||
|
||||
if user is None:
|
||||
raise UserNotFoundError()
|
||||
|
|
@ -187,7 +182,7 @@ class AuthService:
|
|||
raise UserInactiveError()
|
||||
|
||||
# 5. 새 액세스 토큰 발급
|
||||
new_access_token = create_access_token(user.id)
|
||||
new_access_token = create_access_token(user.user_ksuid)
|
||||
|
||||
return AccessTokenResponse(
|
||||
access_token=new_access_token,
|
||||
|
|
@ -269,8 +264,10 @@ class AuthService:
|
|||
|
||||
# 신규 사용자 생성
|
||||
logger.info(f"[AUTH] 신규 사용자 생성 시작 - kakao_id: {kakao_id}")
|
||||
ksuid = await generate_ksuid(session=session, table_name=User)
|
||||
new_user = User(
|
||||
kakao_id=kakao_id,
|
||||
user_ksuid=ksuid,
|
||||
email=kakao_account.email if kakao_account else None,
|
||||
nickname=profile.nickname if profile else None,
|
||||
profile_image_url=profile.profile_image_url if profile else None,
|
||||
|
|
@ -319,6 +316,7 @@ class AuthService:
|
|||
async def _save_refresh_token(
|
||||
self,
|
||||
user_id: int,
|
||||
user_ksuid: str,
|
||||
token: str,
|
||||
session: AsyncSession,
|
||||
user_agent: Optional[str] = None,
|
||||
|
|
@ -329,6 +327,7 @@ class AuthService:
|
|||
|
||||
Args:
|
||||
user_id: 사용자 ID
|
||||
user_ksuid: 사용자 KSUID
|
||||
token: 리프레시 토큰
|
||||
session: DB 세션
|
||||
user_agent: User-Agent
|
||||
|
|
@ -342,6 +341,7 @@ class AuthService:
|
|||
|
||||
refresh_token = RefreshToken(
|
||||
user_id=user_id,
|
||||
user_ksuid=user_ksuid,
|
||||
token_hash=token_hash,
|
||||
expires_at=expires_at,
|
||||
user_agent=user_agent,
|
||||
|
|
@ -391,6 +391,26 @@ class AuthService:
|
|||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _get_user_by_ksuid(
|
||||
self,
|
||||
user_ksuid: str,
|
||||
session: AsyncSession,
|
||||
) -> Optional[User]:
|
||||
"""
|
||||
KSUID로 사용자 조회
|
||||
|
||||
Args:
|
||||
user_ksuid: 사용자 KSUID
|
||||
session: DB 세션
|
||||
|
||||
Returns:
|
||||
User 객체 또는 None
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(User).where(User.user_ksuid == user_ksuid, User.is_deleted == False) # noqa: E712
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def _revoke_refresh_token_by_hash(
|
||||
self,
|
||||
token_hash: str,
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ from jose import JWTError, jwt
|
|||
from config import jwt_settings
|
||||
|
||||
|
||||
def create_access_token(user_id: int) -> str:
|
||||
def create_access_token(user_ksuid: str) -> str:
|
||||
"""
|
||||
JWT 액세스 토큰 생성
|
||||
|
||||
Args:
|
||||
user_id: 사용자 ID
|
||||
user_ksuid: 사용자 KSUID
|
||||
|
||||
Returns:
|
||||
JWT 액세스 토큰 문자열
|
||||
|
|
@ -27,7 +27,7 @@ def create_access_token(user_id: int) -> str:
|
|||
minutes=jwt_settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {
|
||||
"sub": str(user_id),
|
||||
"sub": user_ksuid,
|
||||
"exp": expire,
|
||||
"type": "access",
|
||||
}
|
||||
|
|
@ -38,12 +38,12 @@ def create_access_token(user_id: int) -> str:
|
|||
)
|
||||
|
||||
|
||||
def create_refresh_token(user_id: int) -> str:
|
||||
def create_refresh_token(user_ksuid: str) -> str:
|
||||
"""
|
||||
JWT 리프레시 토큰 생성
|
||||
|
||||
Args:
|
||||
user_id: 사용자 ID
|
||||
user_ksuid: 사용자 KSUID
|
||||
|
||||
Returns:
|
||||
JWT 리프레시 토큰 문자열
|
||||
|
|
@ -52,7 +52,7 @@ def create_refresh_token(user_id: int) -> str:
|
|||
days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
||||
)
|
||||
to_encode = {
|
||||
"sub": str(user_id),
|
||||
"sub": user_ksuid,
|
||||
"exp": expire,
|
||||
"type": "refresh",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ Common Utility Functions
|
|||
공통으로 사용되는 유틸리티 함수들을 정의합니다.
|
||||
|
||||
사용 예시:
|
||||
from app.utils.common import generate_task_id
|
||||
from app.utils.common import generate_task_id, generate_ksuid
|
||||
|
||||
# task_id 생성
|
||||
task_id = await generate_task_id(session=session, table_name=Project)
|
||||
|
||||
# ksuid 생성
|
||||
ksuid = await generate_ksuid(session=session, table_name=User)
|
||||
|
||||
Note:
|
||||
페이지네이션 기능은 app.utils.pagination 모듈을 사용하세요:
|
||||
from app.utils.pagination import PaginatedResponse, get_paginated
|
||||
|
|
@ -18,7 +21,7 @@ from typing import Any, Optional, Type
|
|||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid_extensions import uuid7
|
||||
from svix_ksuid import Ksuid
|
||||
|
||||
|
||||
async def generate_task_id(
|
||||
|
|
@ -32,16 +35,16 @@ async def generate_task_id(
|
|||
table_name: task_id 컬럼이 있는 SQLAlchemy 테이블 클래스 (optional)
|
||||
|
||||
Returns:
|
||||
str: 생성된 uuid7 문자열
|
||||
str: 생성된 ksuid 문자열
|
||||
|
||||
Usage:
|
||||
# 단순 uuid7 생성
|
||||
# 단순 ksuid 생성
|
||||
task_id = await generate_task_id()
|
||||
|
||||
# 테이블에서 중복 검사 후 생성
|
||||
task_id = await generate_task_id(session=session, table_name=Project)
|
||||
"""
|
||||
task_id = str(uuid7())
|
||||
task_id = str(Ksuid())
|
||||
|
||||
if session is None or table_name is None:
|
||||
return task_id
|
||||
|
|
@ -55,4 +58,41 @@ async def generate_task_id(
|
|||
if existing is None:
|
||||
return task_id
|
||||
|
||||
task_id = str(uuid7())
|
||||
task_id = str(Ksuid())
|
||||
|
||||
|
||||
async def generate_ksuid(
|
||||
session: Optional[AsyncSession] = None,
|
||||
table_name: Optional[Type[Any]] = None,
|
||||
) -> str:
|
||||
"""고유한 ksuid를 생성합니다.
|
||||
|
||||
Args:
|
||||
session: SQLAlchemy AsyncSession (optional)
|
||||
table_name: user_ksuid 컬럼이 있는 SQLAlchemy 테이블 클래스 (optional)
|
||||
|
||||
Returns:
|
||||
str: 생성된 ksuid 문자열
|
||||
|
||||
Usage:
|
||||
# 단순 ksuid 생성
|
||||
ksuid = await generate_ksuid()
|
||||
|
||||
# 테이블에서 중복 검사 후 생성
|
||||
ksuid = await generate_ksuid(session=session, table_name=User)
|
||||
"""
|
||||
ksuid = str(Ksuid())
|
||||
|
||||
if session is None or table_name is None:
|
||||
return ksuid
|
||||
|
||||
while True:
|
||||
result = await session.execute(
|
||||
select(table_name).where(table_name.user_ksuid == ksuid)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing is None:
|
||||
return ksuid
|
||||
|
||||
ksuid = str(Ksuid())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database.session import Base
|
||||
|
|
@ -24,7 +24,7 @@ class Video(Base):
|
|||
project_id: 연결된 Project의 id (외래키)
|
||||
lyric_id: 연결된 Lyric의 id (외래키)
|
||||
song_id: 연결된 Song의 id (외래키)
|
||||
task_id: 영상 생성 작업의 고유 식별자 (UUID 형식)
|
||||
task_id: 영상 생성 작업의 고유 식별자 (KSUID 형식)
|
||||
status: 처리 상태 (pending, processing, completed, failed 등)
|
||||
result_movie_url: 생성된 영상 URL (S3, CDN 경로)
|
||||
created_at: 생성 일시 (자동 설정)
|
||||
|
|
@ -37,6 +37,10 @@ class Video(Base):
|
|||
|
||||
__tablename__ = "video"
|
||||
__table_args__ = (
|
||||
Index("idx_video_task_id", "task_id"),
|
||||
Index("idx_video_project_id", "project_id"),
|
||||
Index("idx_video_lyric_id", "lyric_id"),
|
||||
Index("idx_video_song_id", "song_id"),
|
||||
{
|
||||
"mysql_engine": "InnoDB",
|
||||
"mysql_charset": "utf8mb4",
|
||||
|
|
@ -56,7 +60,6 @@ class Video(Base):
|
|||
Integer,
|
||||
ForeignKey("project.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="연결된 Project의 id",
|
||||
)
|
||||
|
||||
|
|
@ -64,7 +67,6 @@ class Video(Base):
|
|||
Integer,
|
||||
ForeignKey("lyric.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="연결된 Lyric의 id",
|
||||
)
|
||||
|
||||
|
|
@ -72,15 +74,14 @@ class Video(Base):
|
|||
Integer,
|
||||
ForeignKey("song.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="연결된 Song의 id",
|
||||
)
|
||||
|
||||
task_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
String(27),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="영상 생성 작업 고유 식별자 (UUID)",
|
||||
unique=True,
|
||||
comment="영상 생성 작업 고유 식별자 (KSUID)",
|
||||
)
|
||||
|
||||
creatomate_render_id: Mapped[Optional[str]] = mapped_column(
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ dependencies = [
|
|||
"scalar-fastapi>=1.6.1",
|
||||
"sqladmin[full]>=0.22.0",
|
||||
"sqlalchemy[asyncio]>=2.0.45",
|
||||
"svix-ksuid>=0.6.2",
|
||||
"uuid7>=0.1.0",
|
||||
]
|
||||
|
||||
|
|
|
|||
17
uv.lock
17
uv.lock
|
|
@ -725,6 +725,7 @@ dependencies = [
|
|||
{ name = "scalar-fastapi" },
|
||||
{ name = "sqladmin", extra = ["full"] },
|
||||
{ name = "sqlalchemy", extra = ["asyncio"] },
|
||||
{ name = "svix-ksuid" },
|
||||
{ name = "uuid7" },
|
||||
]
|
||||
|
||||
|
|
@ -753,6 +754,7 @@ requires-dist = [
|
|||
{ name = "scalar-fastapi", specifier = ">=1.6.1" },
|
||||
{ name = "sqladmin", extras = ["full"], specifier = ">=0.22.0" },
|
||||
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.45" },
|
||||
{ name = "svix-ksuid", specifier = ">=0.6.2" },
|
||||
{ name = "uuid7", specifier = ">=0.1.0" },
|
||||
]
|
||||
|
||||
|
|
@ -1019,6 +1021,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-baseconv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/d0/9297d7d8dd74767b4d5560d834b30b2fff17d39987c23ed8656f476e0d9b/python-baseconv-1.2.2.tar.gz", hash = "sha256:0539f8bd0464013b05ad62e0a1673f0ac9086c76b43ebf9f833053527cd9931b", size = 4929, upload-time = "2019-04-04T19:28:57.17Z" }
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
|
|
@ -1314,6 +1322,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svix-ksuid"
|
||||
version = "0.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-baseconv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/7a/0c98b77ca01d64f13143607b88273a13110d659780b93cb1333abebf8039/svix-ksuid-0.6.2.tar.gz", hash = "sha256:beb95bd6284bdbd526834e233846653d2bd26eb162b3233513d8f2c853c78964", size = 6957, upload-time = "2023-07-07T09:18:24.717Z" }
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue