""" 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") model_config = { "json_schema_extra": { "example": { "auth_url": "https://kauth.kakao.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:8000/api/v1/user/auth/kakao/callback&response_type=code" } } } class KakaoCodeRequest(BaseModel): """카카오 인가 코드 검증 요청 (프론트엔드에서 전달)""" code: str = Field(..., min_length=1, description="카카오 인가 코드") model_config = { "json_schema_extra": { "example": { "code": "AUTHORIZATION_CODE_FROM_KAKAO" } } } # ============================================================================= # 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="액세스 토큰 만료 시간 (초)") model_config = { "json_schema_extra": { "example": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzA1MzE1MjAwfQ.xxx", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3MDU4MzM2MDB9.yyy", "token_type": "Bearer", "expires_in": 3600 } } } class AccessTokenResponse(BaseModel): """액세스 토큰 갱신 응답""" access_token: str = Field(..., description="액세스 토큰") token_type: str = Field(default="Bearer", description="토큰 타입") expires_in: int = Field(..., description="액세스 토큰 만료 시간 (초)") model_config = { "json_schema_extra": { "example": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzA1MzE1MjAwfQ.new_token", "token_type": "Bearer", "expires_in": 3600 } } } class RefreshTokenRequest(BaseModel): """토큰 갱신 요청""" refresh_token: str = Field(..., min_length=1, description="리프레시 토큰") model_config = { "json_schema_extra": { "example": { "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3MDU4MzM2MDB9.yyy" } } } # ============================================================================= # 사용자 정보 스키마 # ============================================================================= 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, "json_schema_extra": { "example": { "id": 1, "kakao_id": 1234567890, "email": "user@kakao.com", "nickname": "홍길동", "profile_image_url": "https://k.kakaocdn.net/dn/.../profile.jpg", "thumbnail_image_url": "https://k.kakaocdn.net/dn/.../thumb.jpg", "is_active": True, "is_admin": False, "last_login_at": "2026-01-15T10:30:00", "created_at": "2026-01-01T09:00:00" } } } 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, "json_schema_extra": { "example": { "id": 1, "nickname": "홍길동", "email": "user@kakao.com", "profile_image_url": "https://k.kakaocdn.net/dn/.../profile.jpg", "is_new_user": False } } } 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="사용자 정보") redirect_url: str = Field(..., description="로그인 후 리다이렉트할 프론트엔드 URL") model_config = { "json_schema_extra": { "example": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzA1MzE1MjAwfQ.xxx", "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 }, "redirect_url": "http://localhost:3000" } } } # ============================================================================= # 내부 사용 스키마 (카카오 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