diff --git a/app/home/api/routers/v1/home.py b/app/home/api/routers/v1/home.py index 6595df6..e7bfe4a 100644 --- a/app/home/api/routers/v1/home.py +++ b/app/home/api/routers/v1/home.py @@ -102,7 +102,7 @@ def _extract_region_from_address(road_address: str | None) -> str: "model": ErrorResponse, }, }, - tags=["crawling"], + tags=["Crawling"], ) async def crawling(request_body: CrawlingRequest): """네이버 지도 장소 크롤링""" @@ -379,7 +379,7 @@ print(response.json()) 200: {"description": "이미지 업로드 성공"}, 400: {"description": "이미지가 제공되지 않음", "model": ErrorResponse}, }, - tags=["Image"], + tags=["Image-Server"], ) async def upload_images( images_json: Optional[str] = Form( @@ -597,7 +597,7 @@ curl -X POST "http://localhost:8000/image/upload/blob" \\ 200: {"description": "이미지 업로드 성공"}, 400: {"description": "이미지가 제공되지 않음", "model": ErrorResponse}, }, - tags=["image"], + tags=["Image-Blob"], ) async def upload_images_blob( images_json: Optional[str] = Form( diff --git a/app/user/api/routers/v1/auth.py b/app/user/api/routers/v1/auth.py index 904a05a..2ba6768 100644 --- a/app/user/api/routers/v1/auth.py +++ b/app/user/api/routers/v1/auth.py @@ -15,6 +15,7 @@ from app.user.dependencies import get_current_user from app.user.models import User from app.user.schemas.user_schema import ( AccessTokenResponse, + KakaoCodeRequest, KakaoLoginResponse, LoginResponse, RefreshTokenRequest, @@ -78,6 +79,54 @@ async def kakao_callback( ) +@router.post( + "/kakao/verify", + response_model=LoginResponse, + summary="카카오 인가 코드 검증 및 토큰 발급", + description=""" +프론트엔드에서 카카오 로그인 후 받은 인가 코드를 검증하고 JWT 토큰을 발급합니다. + +## 사용 시나리오 +1. 프론트엔드가 카카오 로그인 완료 후 인가 코드(code)를 받음 +2. 프론트엔드가 이 엔드포인트에 code를 POST로 전달 +3. 서버가 카카오 서버에 code 검증 및 사용자 정보 조회 +4. JWT 토큰 발급 및 사용자 정보 반환 + +## 응답 +- 신규 사용자인 경우 `user.is_new_user`가 `true`로 반환됩니다. +- `redirect_url`은 로그인 후 이동할 프론트엔드 URL입니다. +""", +) +async def kakao_verify( + request: Request, + body: KakaoCodeRequest, + session: AsyncSession = Depends(get_session), + user_agent: Optional[str] = Header(None, alias="User-Agent"), +) -> LoginResponse: + """ + 카카오 인가 코드 검증 및 토큰 발급 + + 프론트엔드가 카카오 콜백에서 받은 인가 코드를 전달하면 + 카카오 서버에서 검증 후 JWT 토큰을 발급합니다. + + 신규 사용자인 경우 자동으로 회원가입이 처리됩니다. + """ + # 클라이언트 IP 추출 + ip_address = request.client.host if request.client else None + + # X-Forwarded-For 헤더 확인 (프록시/로드밸런서 뒤에 있는 경우) + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + ip_address = forwarded_for.split(",")[0].strip() + + return await auth_service.kakao_login( + code=body.code, + session=session, + user_agent=user_agent, + ip_address=ip_address, + ) + + @router.post( "/refresh", response_model=AccessTokenResponse, diff --git a/app/user/schemas/__init__.py b/app/user/schemas/__init__.py index 25dc83a..6841f87 100644 --- a/app/user/schemas/__init__.py +++ b/app/user/schemas/__init__.py @@ -1,6 +1,6 @@ from app.user.schemas.user_schema import ( AccessTokenResponse, - KakaoCallbackRequest, + KakaoCodeRequest, KakaoLoginResponse, KakaoTokenResponse, KakaoUserInfo, @@ -13,7 +13,7 @@ from app.user.schemas.user_schema import ( __all__ = [ "AccessTokenResponse", - "KakaoCallbackRequest", + "KakaoCodeRequest", "KakaoLoginResponse", "KakaoTokenResponse", "KakaoUserInfo", diff --git a/app/user/schemas/user_schema.py b/app/user/schemas/user_schema.py index f1b8c32..dee7fe6 100644 --- a/app/user/schemas/user_schema.py +++ b/app/user/schemas/user_schema.py @@ -27,8 +27,8 @@ class KakaoLoginResponse(BaseModel): } -class KakaoCallbackRequest(BaseModel): - """카카오 콜백 요청 (인가 코드)""" +class KakaoCodeRequest(BaseModel): + """카카오 인가 코드 검증 요청 (프론트엔드에서 전달)""" code: str = Field(..., min_length=1, description="카카오 인가 코드") @@ -163,6 +163,7 @@ class LoginResponse(BaseModel): 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": { @@ -177,7 +178,8 @@ class LoginResponse(BaseModel): "email": "user@kakao.com", "profile_image_url": "https://k.kakaocdn.net/dn/.../profile.jpg", "is_new_user": False - } + }, + "redirect_url": "http://localhost:3000" } } } diff --git a/app/user/services/auth.py b/app/user/services/auth.py index 011d2fd..fbdc9a5 100644 --- a/app/user/services/auth.py +++ b/app/user/services/auth.py @@ -111,6 +111,7 @@ class AuthService: profile_image_url=user.profile_image_url, is_new_user=is_new_user, ), + redirect_url="http://localhost:3000", ) async def refresh_tokens( diff --git a/main.py b/main.py index c158c05..810c8a2 100644 --- a/main.py +++ b/main.py @@ -43,12 +43,12 @@ tags_metadata = [ "description": "홈 화면 및 프로젝트 관리 API", }, { - "name": "crawling", + "name": "Crawling", "description": "네이버 지도 크롤링 API - 장소 정보 및 이미지 수집", }, { - "name": "image", - "description": "이미지 업로드 API - 로컬 서버 또는 Azure Blob Storage", + "name": "Image-Blob", + "description": "이미지 업로드 API - Azure Blob Storage", }, { "name": "Lyric",