From 7df96db8e520af8d36dfabc92bf09928ca8b0e5d Mon Sep 17 00:00:00 2001 From: ssarisong Date: Thu, 26 Mar 2026 16:22:18 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(member):=20=EB=8B=A8=EA=B1=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B2=BD=EB=A1=9C,=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=20ID=EA=B0=80=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/member/router.py | 2 +- app/modules/member/schemas.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/modules/member/router.py b/app/modules/member/router.py index 2b30030..8e6ad2d 100644 --- a/app/modules/member/router.py +++ b/app/modules/member/router.py @@ -57,7 +57,7 @@ async def list_members( #summary: Swagger에서 보여줄 간단한 API 설명 #description: Swagger에서 보여줄 상세한 API 설명 @router.get( - path="", + path="/{member_id}", response_model=ApiResponse[MemberOut], summary="멤버 단건 조회", description="member ID에 해당하는 멤버의 상세 정보를 조회합니다.", diff --git a/app/modules/member/schemas.py b/app/modules/member/schemas.py index 82200fe..c97cf0a 100644 --- a/app/modules/member/schemas.py +++ b/app/modules/member/schemas.py @@ -1,5 +1,6 @@ #dto from datetime import date +from uuid import UUID from pydantic import HttpUrl, ConfigDict, EmailStr from sqlmodel import SQLModel, Field @@ -81,6 +82,7 @@ class MemberUpdateIn(SQLModel): #응답 class MemberOut(SQLModel): + id: UUID = Field(description="회원 ID") email: EmailStr = Field(description="회원 이메일") name: str = Field(description="이름") birth: date = Field(description="생년월일") @@ -97,6 +99,7 @@ class MemberOut(SQLModel): from_attributes=True, json_schema_extra={ "example": { + "id": "3e1672cf-8d99-4b1c-9b5e-9c3ece11b089", "email": "test@example.com", "name": "홍길동", "birth": "1999-01-01", From 0cf5526da0e804c863b5641ff1e6d2ff0b5eb7cd Mon Sep 17 00:00:00 2001 From: ssarisong Date: Thu, 26 Mar 2026 17:08:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(member):=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=90=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=8F=84=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/member/dependencies.py | 2 +- app/modules/member/schemas.py | 38 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/modules/member/dependencies.py b/app/modules/member/dependencies.py index 796a7f9..8910657 100644 --- a/app/modules/member/dependencies.py +++ b/app/modules/member/dependencies.py @@ -38,7 +38,7 @@ async def get_current_member( if payload.get("type") != "access": raise AppError.unauthorized("엑세스 토큰이 아닙니다.") - member = await service.get(UUID(member_id)) + member = await service.get(UUID(member_id), include_deleted=True) if member is None: raise AppError.unauthorized("존재하지 않는 사용자입니다.") diff --git a/app/modules/member/schemas.py b/app/modules/member/schemas.py index c97cf0a..b9b0762 100644 --- a/app/modules/member/schemas.py +++ b/app/modules/member/schemas.py @@ -36,17 +36,17 @@ class MemberCreateIn(SQLModel): model_config = { "json_schema_extra": { "example": { - "email": "test@example.com", + "email": "test@naver.com", "password": "test1234!", - "name": "홍길동", - "birth": "1999-01-01", + "name": "송시월", + "birth": "2001-05-21", "gender": True, "phone_num": "01012345678", - "nickname": "길동이", - "organization": "한국대학교", - "dept": "산업디자인과", + "nickname": "쏴리쏭", + "organization": "한성대학교", + "dept": "컴퓨터공학과", "profile_url": "https://example.com/profile.jpg", - "detail": "안녕하세요~" + "detail": "안녕하세요!" } } } @@ -67,15 +67,15 @@ class MemberUpdateIn(SQLModel): "json_schema_extra": { "example": { "password": "test1234!", - "name": "홍길동", - "birth": "1999-01-01", + "name": "송시월", + "birth": "2001-05-21", "gender": True, "phone_num": "01012345678", - "nickname": "길동이", - "organization": "한국대학교", - "dept": "산업디자인과", + "nickname": "쏴리쏭", + "organization": "한성대학교", + "dept": "컴퓨터공학과", "profile_url": "https://example.com/profile.jpg", - "detail": "안녕하세요~" + "detail": "안녕하세요!" } } } @@ -101,14 +101,14 @@ class MemberOut(SQLModel): "example": { "id": "3e1672cf-8d99-4b1c-9b5e-9c3ece11b089", "email": "test@example.com", - "name": "홍길동", - "birth": "1999-01-01", + "name": "송시월", + "birth": "2001-05-21", "gender": True, - "nickname": "길동이", - "organization": "한국대학교", - "dept": "산업디자인과", + "nickname": "쏴리쏭", + "organization": "한성대학교", + "dept": "컴퓨터공학과", "profile_url": "https://example.com/profile.jpg", - "detail": "안녕하세요~" + "detail": "안녕하세요!" } } ) From 1370f9cf636be9e7b866879af7d74759d1bf43e4 Mon Sep 17 00:00:00 2001 From: ssarisong Date: Thu, 26 Mar 2026 17:43:45 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(member):=20refresh=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/member/router.py | 22 ++++++++++++++++++++-- app/modules/member/schemas.py | 14 +++++++++++++- app/modules/member/service.py | 26 +++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/app/modules/member/router.py b/app/modules/member/router.py index 8e6ad2d..7243397 100644 --- a/app/modules/member/router.py +++ b/app/modules/member/router.py @@ -6,7 +6,7 @@ from fastapi.security import OAuth2PasswordRequestForm from app.modules.member.dependencies import CurrentMemberDep, MemberServiceDep -from app.modules.member.schemas import MemberCreateIn, MemberOut, MemberUpdateIn, TokenOut +from app.modules.member.schemas import MemberCreateIn, MemberOut, MemberUpdateIn, TokenOut, RefreshTokenIn from app.shared.schemas import ApiResponse, PageOut @@ -185,11 +185,29 @@ async def restore_member( data=MemberOut.model_validate(restored) ) -@router.post("/login") +@router.post( + path="/login", + response_model=TokenOut, + summary="로그인", + description="이메일과 비밀번호를 통해 로그인 합니다." +) async def login_member( service: MemberServiceDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()], ): tokens = await service.login(form_data.username, form_data.password) + return TokenOut(**tokens) + +@router.post( + path="/reissue", + response_model=TokenOut, + summary="refresh 토큰 재발급", + description="refresh 토큰으로 새로운 access 토큰과 refresh 토큰을 재발급합니다." +) +async def reissue_refresh_token( + service: MemberServiceDep, + data: RefreshTokenIn, +): + tokens = await service.reissue(data.refresh_token) return TokenOut(**tokens) \ No newline at end of file diff --git a/app/modules/member/schemas.py b/app/modules/member/schemas.py index b9b0762..9d20d2d 100644 --- a/app/modules/member/schemas.py +++ b/app/modules/member/schemas.py @@ -128,4 +128,16 @@ class TokenOut(SQLModel): "token_type": "Bearer" } } - ) \ No newline at end of file + ) + +#refreshToken 요청용 dto +class RefreshTokenIn(SQLModel): + refresh_token: str = Field(description="리프레시 토큰 (JWT)") + + model_config = { + "json_schema_extra": { + "example": { + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + } + } \ No newline at end of file diff --git a/app/modules/member/service.py b/app/modules/member/service.py index 031514b..27a5e01 100644 --- a/app/modules/member/service.py +++ b/app/modules/member/service.py @@ -1,3 +1,4 @@ +from base64 import decode from typing import Any from uuid import UUID @@ -5,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import AppError -from app.core.security import password_hash, verify_password, create_access_token, create_refresh_token +from app.core.security import password_hash, verify_password, create_access_token, create_refresh_token, decode_token from app.modules.member.models import Member from app.modules.member.repository import MemberRepository from app.modules.member.schemas import MemberCreateIn, MemberUpdateIn @@ -261,3 +262,26 @@ async def login(self, email: str, password: str) -> dict[str, str]: "refresh_token": create_refresh_token(data=str(member.id)), } + #accessToken 재발급 함수 + async def reissue(self, refresh_token: str) -> dict[str, str]: + try: + payload = decode_token(refresh_token) + member_id: str | None = payload.get("sub") + token_type: str | None = payload.get("type") + + if member_id is None or token_type != "refresh": + raise AppError.unauthorized("유효하지 않은 refresh 토큰입니다.") + + member = await self.get(UUID(member_id)) + if not member or member.is_deleted: + raise AppError.unauthorized("존재하지 않거나 삭제된 사용자입니다.") + + return { + "access_token": create_access_token(data=str(member.id)), + "refresh_token": create_refresh_token(data=str(member.id)), + } + except ValueError as e: + raise AppError.unauthorized(str(e)) + except Exception as e: + raise AppError.unauthorized(f"토큰 재발급 과정에서 오류가 발생했습니다.: {str(e)}") +