diff --git a/src/dependencies.py b/src/dependencies.py index 8a814f66..98349578 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -1,7 +1,8 @@ from datetime import date from fastapi import Depends, Header, HTTPException, status -from jose import jwt +from jose import ExpiredSignatureError, JWTError, jwt +from sqlalchemy import select from sqlalchemy.orm import Session from config import Settings @@ -23,14 +24,26 @@ async def get_current_user(token=Header(None), db: Session = Depends(get_db)): detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}, ) - payload = jwt.decode(token, key=Settings().JWT_SECRET_KEY, algorithms=Settings().JWT_ALGORITHM) - user_id: int = int(payload.get("sub")) - if user_id is None: - raise credentials_exception - user = db.query(User).filter(User.id == user_id).first() - if user is None: - raise credentials_exception - return user + try: + payload = jwt.decode(token, key=Settings().JWT_SECRET_KEY, algorithms=Settings().JWT_ALGORITHM) + user_id: int = int(payload.get("sub")) + if user_id is None: + raise credentials_exception + stmt = select(User).where(User.id == user_id) + user = db.execute(stmt).scalar_one() + if user is None: + raise credentials_exception + return user + except ExpiredSignatureError as err: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token has expired", + ) from err + except JWTError as err: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid token", + ) from err def get_current_active_user(user: User = Depends(get_current_user)): diff --git a/src/domain/schemas/book_schemas.py b/src/domain/schemas/book_schemas.py index f1355bbe..9f266171 100644 --- a/src/domain/schemas/book_schemas.py +++ b/src/domain/schemas/book_schemas.py @@ -21,7 +21,7 @@ class DomainResGetBook(BaseModel): version: str | None = Field(title="version", description="판본", example="10e") major: bool = Field(title="major", description="전공책 여부", example=True) language: str = Field(title="language", description="언어", example="영문") - donor_name: str| None = Field(title="donor_name", description="책 기증자 성함", example="김철수") + donor_name: str | None = Field(title="donor_name", description="책 기증자 성함", example="김철수") book_status: bool = Field(title="book_stauts", description="책 상태", example=True) created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) @@ -105,3 +105,23 @@ class DomainResAdminPutBook(BaseModel): class DomainReqAdminDelBook(BaseModel): book_id: int = Field(title="book_id", description="책 ID", gt=0) + + +class DomainAdminGetBookItem(BaseModel): + book_id: int = Field(title="book_id", description="책 ID", example=1, gt=0) + book_title: str = Field(title="book_title", description="책 제목", example="FastAPI Tutorial") + code: str = Field(title="code", description="책 코드", example="A3") + category_name: str = Field(title="category_name", description="카테고리 이름", example="웹") + subtitle: str | None = Field(title="subtitle", description="부제목", example="for beginner") + author: str = Field(title="author", description="저자", example="minjae") + publisher: str = Field(title="publisher", description="출판사", example="KUCC") + publication_year: int = Field(title="publication_year", description="출판년도", example=2022, gt=0) + image_url: str | None = Field(title="image_url", description="책 이미지 링크", example="https://example.com/img") + version: str | None = Field(title="version", description="판본", example="10e") + major: bool | None = Field(title="major", description="전공책 여부", example=False, default=False) + language: str | None = Field(title="language", description="언어", example="영문", default="KOREAN") + donor_name: str | None = Field(title="donor_name", description="책 기증자 성함", example="김철수") + book_status: bool = Field(title="book_stauts", description="책 상태", example=True) + created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) + updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) + loan_status: bool | None = Field(title="loan_status", description="대출 상태", example=False) diff --git a/src/domain/schemas/loan_schemas.py b/src/domain/schemas/loan_schemas.py index 29b19f0e..4d80600c 100644 --- a/src/domain/schemas/loan_schemas.py +++ b/src/domain/schemas/loan_schemas.py @@ -1,6 +1,5 @@ # ruff: noqa: E501 -from datetime import date, timedelta -from datetime import datetime as _datetime +from datetime import date, datetime, timedelta from pydantic import BaseModel, Field @@ -9,10 +8,10 @@ class Loan(BaseModel): id: int = Field(title="loan_id", description="대출 정보 id", example=1, gt=0) book_id: int = Field(title="book_id", description="대출한 책 ID", example=1, gt=0) user_id: int = Field(title="user_id", description="대출한 사용자 ID", example=1, gt=0) - created_at: _datetime = Field(title="create_at", description="생성일시", example=_datetime.now()) - updated_at: _datetime = Field(title="update_at", description="수정일시", example=_datetime.now()) - loan_date: date = Field(title="loan_date", description="대출 날짜", example=_datetime.today().date()) - due_date: date = Field(title="due_date", description="반납 기한", example=(_datetime.today() + timedelta(days=14)).date()) + created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) + updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) + loan_date: date = Field(title="loan_date", description="대출 날짜", example=datetime.today().date()) + due_date: date = Field(title="due_date", description="반납 기한", example=(datetime.today() + timedelta(days=14)).date()) extend_status: bool = Field(title="extend_status", description="연장 상태", example=True) overdue_days: int = Field(title="overdue_days", description="연체 일자", example=1) return_status: bool = Field(title="return_status", description="반납 상태", example=False) @@ -23,10 +22,10 @@ class DomainResGetLoanItem(BaseModel): loan_id: int = Field(title="loan_id", description="대출 정보 id", example=1, gt=0) book_id: int = Field(title="book_id", description="대출한 책 ID", example=1, gt=0) user_id: int = Field(title="user_id", description="대출한 사용자 ID", example=1, gt=0) - created_at: _datetime = Field(title="create_at", description="생성일시", example=_datetime.now()) - updated_at: _datetime = Field(title="update_at", description="수정일시", example=_datetime.now()) - loan_date: date = Field(title="loan_date", description="대출 날짜", example=_datetime.today().date()) - due_date: date = Field(title="due_date", description="반납 기한", example=(_datetime.today() + timedelta(days=14)).date()) + created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) + updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) + loan_date: date = Field(title="loan_date", description="대출 날짜", example=datetime.today().date()) + due_date: date = Field(title="due_date", description="반납 기한", example=(datetime.today() + timedelta(days=14)).date()) extend_status: bool = Field(title="extend_status", description="연장 상태", example=True) overdue_days: int = Field(title="overdue_days", description="연체 일자", example=1) return_status: bool = Field(title="return_status", description="반납 상태", example=False) @@ -60,17 +59,32 @@ class DomainReqPostLoan(BaseModel): class LoanCreate(BaseModel): user_id: int = Field(title="user_id", description="대출한 사용자 ID", example=1, gt=0) book_id: int = Field(title="book_id", description="대출한 책 ID", example=1, gt=0) - created_at: _datetime = Field(title="create_at", description="생성일시", example=_datetime.now()) - updated_at: _datetime = Field(title="update_at", description="수정일시", example=_datetime.now()) + created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) + updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) class DomainResGetLoan(BaseModel): loan_id: int = Field(title="loan_id", description="대출 정보 id", example=1, gt=0) book_id: int = Field(title="book_id", description="대출한 책 ID", example=1, gt=0) user_id: int = Field(title="user_id", description="대출한 사용자 ID", example=1, gt=0) - loan_date: date = Field(title="loan_date", description="대출 날짜", example=_datetime.today().date()) - due_date: date = Field(title="due_date", description="반납 기한", example=(_datetime.today() + timedelta(days=14)).date()) + loan_date: date = Field(title="loan_date", description="대출 날짜", example=datetime.today().date()) + due_date: date = Field(title="due_date", description="반납 기한", example=(datetime.today() + timedelta(days=14)).date()) extend_status: bool = Field(title="extend_status", description="연장 상태", example=True) overdue_days: int = Field(title="overdue_days", description="연체 일자", example=1) return_status: bool = Field(title="return_status", description="반납 상태", example=False) return_date: date | None = Field(title="return_date", description="반납 날짜", example=None) + +class DomainAdminGetLoanItem(BaseModel): + loan_id: int = Field(title="loan_id", description="대출 id", example=1, gt=0) + book_id: int = Field(title="book_id", description="대출한 책 ID", example=1, gt=0) + user_id: int = Field(title="user_id", description="대출한 사용자 ID", example=1, gt=0) + user_name: str = Field(title="user_name", description="리뷰한 사용자 이름", example="test") + code: str = Field(title="code", description="책 코드", example="A3") + book_title: str = Field(title="book_title", description="구매 요청한 책 제목", example="book1") + loan_date: date = Field(title="loan_date", description="대출 날짜", example=datetime.today().date()) + due_date: date = Field(title="due_date", description="반납 기한", example=(datetime.today() + timedelta(days=14)).date()) + extend_status: bool = Field(title="extend_status", description="연장 상태", example=True) + return_status: bool = Field(title="return_status", description="반납 상태", example=False) + return_date: date | None = Field(title="return_date", description="반납 날짜", example=None) + created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) + updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) diff --git a/src/domain/schemas/notice_schemas.py b/src/domain/schemas/notice_schemas.py index 07debbd9..8eb29b92 100644 --- a/src/domain/schemas/notice_schemas.py +++ b/src/domain/schemas/notice_schemas.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, Field + class DomainResAdminGetNotice(BaseModel): notice_id: int = Field(title="notice_id", description="공지사항 ID", example=1, gt=0) admin_id: int = Field(title="admin_id", description="관리자 ID", example=1, gt=0) @@ -17,3 +18,29 @@ class DomainResGetNotice(BaseModel): title: str = Field(title="title", description="공지사항 제목", example="공지사항 제목") notice_content: str = Field(title="notice content", description="공지사항 내용", example="공지사항 내용") created_at: date = Field(title="created_at", description="공지사항 생성일", example=date.today()) + +class DomainReqAdminPostNotice(BaseModel): + user_id: int = Field(title="user_id", description="사용자 ID", example=1, gt=0) + admin_id: int = Field(title="user_id", description="관리자 ID", example=1, gt=0) + title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1"]) + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) + +class DomainResAdminPostNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) + admin_name: str = Field(title="admin_name", description="관리자 성명", examples=["관리자 성명1"]) + title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1", "공지사항 제목2"]) + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) + created_at: date = Field(title="created_at", description="공지사항 생성일", examples=[date.today()]) + +class DomainReqAdminPutNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) + admin_id: int = Field(title="admin_id", description="관리자 ID", example=1, gt=0) + title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1"]) + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) + +class DomainResAdminPutNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) + admin_name: str = Field(title="admin_name", description="관리자 성명", examples=["관리자1", "관리자2"]) + title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1", "공지사항 제목2"]) + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) + created_at: date = Field(title="created_at", description="공지사항 생성일", examples=[date.today()]) diff --git a/src/domain/schemas/user_schemas.py b/src/domain/schemas/user_schemas.py index 9c8c495e..2029404f 100644 --- a/src/domain/schemas/user_schemas.py +++ b/src/domain/schemas/user_schemas.py @@ -1,3 +1,5 @@ +from datetime import datetime + from pydantic import BaseModel, Field @@ -25,3 +27,14 @@ class DomainResPutUser(BaseModel): github: str | None = Field(None, title="github", description="깃허브 주소", example="https://github.com/kucc") instagram: str | None = Field(None, title="instagram", description="인스타그램 주소", example="https://www.instagram.com/") +class DomainAdminGetUserItem(BaseModel): + user_id: int = Field(title="user_id", description="대출한 사용자 ID", example=1, gt=0) + auth_id: str = Field(title="auth_id", description="인증 ID", max_length=255) + auth_type: str = Field(default="FIREBASE", description="인증 타입", max_length=20) + email: str = Field(title="email", description="이메일", max_length=100) + user_name: str = Field(title="user_name", description="사용자 이름", max_length=45) + github_id: str | None = Field(default=None, title="github_id", description="깃허브 ID", max_length=100) + instagram_id: str | None = Field(default=None, title="instagram_id", description="인스타그램 ID", max_length=100) + is_active: bool = Field(title="is_active", description="활동 상태") + created_at: datetime = Field(title="create_at", description="생성일시") + updated_at: datetime = Field(title="update_at", description="수정일시") diff --git a/src/domain/services/admin/__init__.py b/src/domain/services/admin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/domain/services/admin/book_service.py b/src/domain/services/admin/book_service.py index 9ad965f0..c129dccd 100644 --- a/src/domain/services/admin/book_service.py +++ b/src/domain/services/admin/book_service.py @@ -1,11 +1,13 @@ +# ruff: noqa: C901 from datetime import datetime from fastapi import HTTPException, status -from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy import select, text +from sqlalchemy.orm import Session, selectinload from domain.enums.book_category import BookCategoryStatus from domain.schemas.book_schemas import ( + DomainAdminGetBookItem, DomainReqAdminDelBook, DomainReqAdminPostBook, DomainReqAdminPutBook, @@ -16,6 +18,86 @@ from utils.crud_utils import delete_item +async def service_admin_search_books( + book_title: str | None, + category_name: str | None, + author: str | None, + publisher: str | None, + return_status: bool | None, + db: Session +) -> list[DomainAdminGetBookItem]: + stmt = (select(Book).options(selectinload(Book.loans)).where(Book.is_deleted == False,)) + + if book_title: + stmt = ( + stmt.where(text("MATCH(book_title) AGAINST(:book_title IN BOOLEAN MODE)")) + .params(book_title=f"{book_title}*") + ) + if category_name: + stmt = ( + stmt.where(text("MATCH(category_name) AGAINST(:category_name IN BOOLEAN MODE)")) + .params(category_name=f"{category_name}*") + ) + if author: + stmt = ( + stmt.where(text("MATCH(author) AGAINST(:author IN BOOLEAN MODE)")) + .params(author=f"{author}*") + ) + if publisher: + stmt = ( + stmt.where(text("MATCH(publisher) AGAINST(:publisher IN BOOLEAN MODE)")) + .params(publisher=f"{publisher}*") + ) + + try: + books = db.execute(stmt.order_by(Book.updated_at.desc())).scalars().all() # 최신 업데이트 순으로 정렬 + + if not books: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Books not found") + + search_books = [] + for book in books: + loan_status = None + if book.loans: + latest_load = max(book.loans, key=lambda loan: loan.updated_at, default=None) + loan_status = latest_load.return_status if latest_load else None + + if return_status is not None and loan_status != return_status: + continue + + search_books.append( + DomainAdminGetBookItem( + book_id=book.id, + book_title=book.book_title, + code=book.code, + category_name=book.category_name, + subtitle=book.subtitle, + author=book.author, + publisher=book.publisher, + publication_year=book.publication_year, + image_url=book.image_url, + version=book.version, + major=book.major, + language=book.language, + donor_name=book.donor_name, + book_status=book.book_status, + created_at=book.created_at, + updated_at=book.updated_at, + loan_status=loan_status + ) + ) + + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error occurred during retrieve: {str(e)}", + ) from e + + return search_books + + async def service_admin_create_book(request: DomainReqAdminPostBook, db: Session): # check if the book already exists in database stmt = select(Book).where(Book.book_title == request.book_title) @@ -142,3 +224,52 @@ async def service_admin_update_book(request: DomainReqAdminPutBook, db: Session) async def service_admin_delete_book(request: DomainReqAdminDelBook, db: Session): delete_item(Book, request.book_id, db) return + + +async def service_admin_read_books(db: Session) -> list[DomainAdminGetBookItem]: + stmt = (select(Book).options(selectinload(Book.loans)).where(Book.is_deleted == False,)) + + try: + books = db.execute(stmt.order_by(Book.updated_at.desc())).scalars().all() + + if not books: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Books not found") + + search_books = [] + for book in books: + loan_status = None + if book.loans: + latest_load = max(book.loans, key=lambda loan: loan.updated_at, default=None) + loan_status = latest_load.return_status if latest_load else None + + search_books.append( + DomainAdminGetBookItem( + book_id=book.id, + book_title=book.book_title, + code=book.code, + category_name=book.category_name, + subtitle=book.subtitle, + author=book.author, + publisher=book.publisher, + publication_year=book.publication_year, + image_url=book.image_url, + version=book.version, + major=book.major, + language=book.language, + donor_name=book.donor_name, + book_status=book.book_status, + created_at=book.created_at, + updated_at=book.updated_at, + loan_status=loan_status + ) + ) + + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error occurred during retrieve: {str(e)}", + ) from e + + return search_books diff --git a/src/domain/services/admin/loan_service.py b/src/domain/services/admin/loan_service.py new file mode 100644 index 00000000..14dc3a12 --- /dev/null +++ b/src/domain/services/admin/loan_service.py @@ -0,0 +1,125 @@ +from fastapi import HTTPException, status +from sqlalchemy import select, text +from sqlalchemy.orm import Session, joinedload, selectinload + +from domain.schemas.loan_schemas import DomainAdminGetLoanItem +from repositories.models import Loan + + +async def service_admin_search_loans( + user_name: str | None, + book_title: str | None, + category_name: str | None, + return_status: str | None, + db: Session +) -> list[DomainAdminGetLoanItem]: + stmt = ( + select(Loan) + .options(joinedload(Loan.user), joinedload(Loan.book)) + .join(Loan.user) + .join(Loan.book) + .where( + Loan.is_deleted == False + ) + ) + + if book_title: + stmt = ( + stmt.where(text("MATCH(book.book_title) AGAINST(:book_title IN BOOLEAN MODE)")) + .params(book_title=f"{book_title}*") + ) + if user_name: + stmt = ( + stmt.where(text("MATCH(user.user_name) AGAINST(:user_name IN BOOLEAN MODE)")) + .params(user_name=f"{user_name}*") + ) + if category_name: + stmt = ( + stmt.where(text("MATCH(category_name) AGAINST(:category_name IN BOOLEAN MODE)")) + .params(category_name=f"{category_name}*") + ) + if return_status is not None: + stmt = stmt.where(Loan.return_status == return_status) + + try: + loans = db.execute(stmt.order_by(Loan.updated_at.desc())).scalars().all() + + if not loans: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Loans not found") + + search_loans = [ + DomainAdminGetLoanItem( + loan_id=loan.id, + book_id=loan.book_id, + user_id=loan.user_id, + user_name=loan.user.user_name, + code=loan.book.code, + book_title=loan.book.book_title, + loan_date=loan.loan_date, + due_date=loan.due_date, + extend_status=loan.extend_status, + return_status=loan.return_status, + return_date=loan.return_date, + created_at=loan.created_at, + updated_at=loan.updated_at, + ) + for loan in loans + ] + + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error occurred during retrieve: {str(e)}", + ) from e + + return search_loans + + +async def service_admin_read_loans(db: Session) -> list[DomainAdminGetLoanItem]: + stmt = ( + select(Loan) + .options( + selectinload(Loan.user), + selectinload(Loan.book) + ) + .where( + Loan.is_deleted == False + ) + ) + + try: + loans = db.execute(stmt.order_by(Loan.updated_at.desc())).scalars().all() + + if not loans: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Books not found") + + search_loans = [ + DomainAdminGetLoanItem( + loan_id=loan.id, + book_id=loan.book_id, + user_id=loan.user_id, + user_name=loan.user.user_name, + code=loan.book.code, + book_title=loan.book.book_title, + loan_date=loan.loan_date, + due_date=loan.due_date, + extend_status=loan.extend_status, + return_status=loan.return_status, + return_date=loan.return_date, + created_at=loan.created_at, + updated_at=loan.updated_at, + ) + for loan in loans + ] + + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error occurred during retrieve: {str(e)}", + ) from e + + return search_loans diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index 4003ed7c..b3560b6b 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -1,56 +1,71 @@ +from math import ceil + from fastapi import HTTPException, status -from sqlalchemy import select +from sqlalchemy import and_, func, select from sqlalchemy.orm.session import Session -from repositories.models import Notice, User -from domain.schemas.notice_schemas import DomainResAdminGetNotice +from domain.schemas.notice_schemas import ( + DomainReqAdminPostNotice, + DomainReqAdminPutNotice, + DomainResAdminGetNotice, + DomainResAdminPostNotice, + DomainResAdminPutNotice, +) +from repositories.models import Notice +from utils.crud_utils import delete_item + async def service_admin_read_notices(page: int, limit: int, db: Session): - + offset=(page-1)*limit - stmt =(select(Notice) - .order_by(Notice.created_at.desc()) - .limit(limit) - .offset(offset) - ) + count_stmt=select(Notice).where(Notice.is_deleted == False) + total_stmt=count_stmt.with_only_columns(func.count(Notice.id)) + stmt = count_stmt.order_by(Notice.created_at.desc()).limit(limit).offset(offset) try: + + total=db.execute(total_stmt).scalar() + if ceil(total/limit) list[DomainAdminGetUserItem]: + stmt = ( + select(User) + .options(selectinload(User.admin)) + .where( + User.is_deleted == False, + ) + ) + + if user_name: + stmt = ( + stmt.where(text("MATCH(user_name) AGAINST(:user_name IN BOOLEAN MODE)")) + .params(user_name=f"{user_name}*") + ) + if authority is not None: + stmt = stmt.where(User.admin[0].admin_status == authority) + if active is not None: + stmt = stmt.where(User.is_active == active) + + try: + users = db.execute(stmt.order_by(User.updated_at.desc())).scalars().all() + + if not users: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Users not found") + + search_users = [ + DomainAdminGetUserItem( + user_id=user.id, + auth_id=user.auth_id, + auth_type=user.auth_type, + email=user.email, + user_name=user.user_name, + github_id=user.github_id, + instagram_id=user.instagram_id, + is_active=user.is_active, + created_at=user.created_at, + updated_at=user.updated_at, + ) + for user in users + ] + + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error occurred during retrieve: {str(e)}", + ) from e + + return search_users + + +async def service_admin_read_users(db: Session) -> list[DomainAdminGetUserItem]: + stmt = ( + select(User) + .options( + selectinload(User.admin) + ) + .where( + User.is_deleted == False + ) + ) + + try: + users = db.execute(stmt.order_by(User.updated_at.desc())).scalars().all() + + if not users: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Users not found") + + search_users = [ + DomainAdminGetUserItem( + user_id=user.id, + auth_id=user.auth_id, + auth_type=user.auth_type, + email=user.email, + user_name=user.user_name, + github_id=user.github_id, + instagram_id=user.instagram_id, + is_active=user.is_active, + created_at=user.created_at, + updated_at=user.updated_at, + ) + for user in users + ] + + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error occurred during retrieve: {str(e)}", + ) from e + + return search_users diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index eb0eb7c5..ca67fb0e 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -1,9 +1,10 @@ from fastapi import HTTPException, status +from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from config import Settings from domain.schemas.auth_schemas import LoginRequest, RegisterRequest -from domain.services.token_service import create_user_tokens +from domain.services.token_service import create_user_tokens, refresh_user_tokens from externals.firebase import sign_in_with_email_and_password from repositories.models import User @@ -30,16 +31,16 @@ async def register(request: RegisterRequest, db: Session): # Create JWT tokens token_response = create_user_tokens(user.id) - - return { - "token": token_response, - "user": { - "id": user.id, - "user_name": user.user_name, - "is_active": user.is_active, - "email": user.email - } - } + response = JSONResponse(content={ + "id": user.id, + "user_name": user.user_name, + "is_active": user.is_active, + "email": user.email + }, status_code=status.HTTP_201_CREATED) + response.headers["Authorization"] = token_response["access_token"] + response.set_cookie(key="refresh_token", value=token_response["refresh_token"], + httponly = True, secure=False, samesite="Lax") + return response # firebase를 사용한 로그인 @@ -104,13 +105,19 @@ async def login_with_username( # Create JWT tokens token_response = create_user_tokens(user.id) - - return { - "token": token_response, - "user": { - "id": user.id, - "user_name": user.user_name, - "is_active": user.is_active, - "email": user.email - } - } + response = JSONResponse(content={ + "id": user.id, + "user_name": user.user_name, + "is_active": user.is_active, + "email": user.email + }, status_code=status.HTTP_200_OK) + response.headers["Authorization"] = token_response["access_token"] + response.set_cookie(key="refresh_token", value=token_response["refresh_token"]) + return response + +async def service_refresh_token(access_token: str, refresh_token: str): + token_response = refresh_user_tokens(access_token, refresh_token) + response = JSONResponse(content=None, status_code=status.HTTP_202_ACCEPTED) + response.headers["Authorization"]= token_response["access_token"] + response.set_cookie(key="refresh_token", value=token_response["refresh_token"]) + return response diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index c6d127c9..8776ab3e 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -1,56 +1,63 @@ +from math import ceil + from fastapi import HTTPException, status -from sqlalchemy import select +from sqlalchemy import and_, func, select from sqlalchemy.orm.session import Session -from repositories.models import Notice, User from domain.schemas.notice_schemas import DomainResGetNotice +from repositories.models import Notice + async def service_read_notices(page: int, limit: int, db: Session): - + offset=(page-1)*limit - stmt =(select(Notice) - .order_by(Notice.created_at.desc()) - .limit(limit) - .offset(offset) - ) + count_stmt=select(Notice).where(Notice.is_deleted == False) + total_stmt=count_stmt.with_only_columns(func.count(Notice.id)) + stmt = count_stmt.order_by(Notice.created_at.desc()).limit(limit).offset(offset) try: + total=db.execute(total_stmt).scalar() + if ceil(total/limit) dict: "refresh_token": refresh_token, "token_type": "bearer" } + +def refresh_user_tokens(access_token: str, refresh_token: str) -> dict: + try: + # access token 유효성 검사 + payload_access = jwt.decode(access_token, key=Settings().JWT_SECRET_KEY, algorithms=Settings().JWT_ALGORITHM) + if datetime.fromtimestamp(payload_access.get("exp")) >= datetime.now(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Access token has not expired", + ) + # refresh token 유효성, 만료 여부 검사 + payload = jwt.decode(refresh_token, key=Settings().JWT_SECRET_KEY, algorithms=Settings().JWT_ALGORITHM) + user_id = payload.get("sub") + refresh_token = create_user_tokens(user_id=user_id) + return refresh_token + + except ExpiredSignatureError as err: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Refresh Token has expired", + ) from err + except JWTError as err: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid token", + ) from err diff --git a/src/main.py b/src/main.py index 815165d6..28aada0b 100644 --- a/src/main.py +++ b/src/main.py @@ -5,7 +5,10 @@ from config import Settings from routes.admin.admin_books_route import router as admin_books_router -from routes.admin.notice_route import router as admin_notice_router +from routes.admin.admin_notice_route import router as admin_notice_router +from routes.admin.book_route import router as admin_book_router +from routes.admin.loan_route import router as admin_loan_router +from routes.admin.user_route import router as admin_user_router from routes.authentication_route import router as auth_router from routes.book_review_route import router as review_router from routes.bookrequest_route import router as bookrequest_router @@ -45,7 +48,7 @@ allow_credentials=True, allow_methods=["*"], allow_headers=["*"], - expose_headers=["Access-Control-Allow-Origin"] + expose_headers=["Access-Control-Allow-Origin", "Authorization"] ) app.include_router(auth_router) @@ -54,9 +57,13 @@ app.include_router(user_router) app.include_router(loan_router) app.include_router(review_router) +app.include_router(notice_router) + app.include_router(bookrequest_router) +app.include_router(admin_loan_router) +app.include_router(admin_user_router) +app.include_router(admin_book_router) app.include_router(admin_notice_router) -app.include_router(notice_router) @app.exception_handler(StarletteHTTPException) @@ -85,4 +92,4 @@ async def http_exception_handler(request:Request, exc:StarletteHTTPException): @app.get("/") async def root(): - return {"message": "쿠책책 API 서버입니다!"} \ No newline at end of file + return {"message": "쿠책책 API 서버입니다!"} diff --git a/src/routes/admin/__init__.py b/src/routes/admin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/routes/admin/admin_notice_route.py b/src/routes/admin/admin_notice_route.py new file mode 100644 index 00000000..487d3ee6 --- /dev/null +++ b/src/routes/admin/admin_notice_route.py @@ -0,0 +1,144 @@ +from fastapi import APIRouter, Depends, Query, status +from sqlalchemy.orm import Session + +from dependencies import get_current_admin, get_db +from domain.schemas.notice_schemas import DomainReqAdminPostNotice, DomainReqAdminPutNotice +from domain.services.admin.notice_service import ( + service_admin_create_notice, + service_admin_delete_notice, + service_admin_read_notice, + service_admin_read_notices, + service_admin_update_notice, +) +from routes.admin.request.notice_request import RouteReqAdminPostNotice, RouteReqAdminPutNotice +from routes.admin.response.notice_response import ( + RouteResAdminGetNotice, + RouteResAdminGetNoticeList, + RouteResAdminPostNotice, + RouteResAdminPutNotice, +) + +router=APIRouter( + prefix="/admin/notice", + tags=["admin/notice"], + dependencies=[Depends(get_current_admin)] +) + +@router.get( + "", + response_model=RouteResAdminGetNoticeList, + status_code=status.HTTP_200_OK, + summary="공지사항 리스트 전체 조회", + ) + +async def get_all_notices( + page: int = Query(1, ge=1), + limit: int = Query(7, le=50), + db: Session=Depends(get_db), + current_user=Depends(get_current_admin) +): + domain_res, total = await service_admin_read_notices(page, limit, db) + response = RouteResAdminGetNoticeList( + data=domain_res, + total=total, + count=len(domain_res) + ) + + return response + + +@router.get( + "/{notice_id}", + response_model=RouteResAdminGetNotice, + status_code=status.HTTP_200_OK, + summary="공지사항 상세 조회", + ) + +async def get_notice( + notice_id: int, + db: Session=Depends(get_db), + current_user=Depends(get_current_admin) +): + domain_res = await service_admin_read_notice(notice_id, db) + response = RouteResAdminGetNotice( + notice_id=domain_res.notice_id, + admin_id=domain_res.admin_id, + admin_name=domain_res.admin_name, + title=domain_res.title, + notice_content=domain_res.notice_content, + created_at=domain_res.created_at + ) + + return response + +@router.post( + "", + response_model=RouteResAdminPostNotice, + status_code=status.HTTP_201_CREATED, + summary="공지사항 등록", +) +async def create_notice( + notice_create: RouteReqAdminPostNotice, + db: Session=Depends(get_db), + current_user=Depends(get_current_admin) +): + domain_req = DomainReqAdminPostNotice( + user_id=current_user.id, + admin_id=current_user.admin[0].id, + title=notice_create.title, + notice_content=notice_create.notice_content + ) + + domain_res = await service_admin_create_notice(domain_req, db) + response = RouteResAdminPostNotice( + notice_id=domain_res.notice_id, + admin_name=domain_res.admin_name, + title=domain_res.title, + notice_content=domain_res.notice_content, + created_at=domain_res.created_at + ) + + return response + +@router.put( + "/{notice_id}", + response_model=RouteResAdminPutNotice, + status_code=status.HTTP_200_OK, + summary="공지사항 수정", +) +async def update_notice( + notice_id: int, + notice_update: RouteReqAdminPutNotice, + db: Session=Depends(get_db), + current_user=Depends(get_current_admin) +): + domain_req = DomainReqAdminPutNotice( + notice_id=notice_id, + admin_id=current_user.admin[0].id, + title=notice_update.title, + notice_content=notice_update.notice_content + ) + + domain_res = await service_admin_update_notice(notice_id, domain_req, db) + response = RouteResAdminPutNotice( + notice_id=domain_res.notice_id, + admin_name=domain_res.admin_name, + title=domain_res.title, + notice_content=domain_res.notice_content, + created_at=domain_res.created_at + ) + + return response + +@router.delete( + "/{notice_id}", + status_code=status.HTTP_204_NO_CONTENT, + summary="공지사항 삭제", +) +async def delete_notice( + notice_id: int, + db: Session=Depends(get_db), + current_user=Depends(get_current_admin) +): + await service_admin_delete_notice(notice_id, db) + return diff --git a/src/routes/admin/book_route.py b/src/routes/admin/book_route.py new file mode 100644 index 00000000..4433ffa7 --- /dev/null +++ b/src/routes/admin/book_route.py @@ -0,0 +1,79 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, Query, status +from sqlalchemy.orm import Session + +from dependencies import get_current_admin, get_db +from domain.services.admin.book_service import service_admin_read_books, service_admin_search_books +from routes.admin.response.book_response import RouteResAdminGetBookList + +router = APIRouter( + prefix="/admin/books", + tags=["admin/books"], + dependencies=[Depends(get_current_admin)] +) + + +@router.get( + "/search", + response_model=RouteResAdminGetBookList, + status_code=status.HTTP_200_OK, + summary="전체 도서 목록 검색", +) +async def search_books( + db: Session = Depends(get_db), + book_title: Annotated[ + str | None, Query(description="도서 제목", example="book", min_length=2, max_length=50) + ] = None, + category_name: Annotated[ + str | None, Query(description="카테고리 이름", example="category", min_length=2, max_length=50) + ] = None, + author: Annotated[ + str, Query(description="저자", example="author", min_length=2, max_length=50) + ] = None, + publisher: Annotated[ + str, Query(description="출판사", example="publisher", min_length=2, max_length=50) + ] = None, + return_status: Annotated[ + bool, Query(description="반납 여부", example=False) + ] = None, + current_user=Depends(get_current_admin) +): + + response = await service_admin_search_books( + book_title=book_title, + category_name=category_name, + author=author, + publisher=publisher, + return_status=return_status, + db=db + ) + + result = RouteResAdminGetBookList( + data=response, + count=len(response) + ) + + return result + + +@router.get( + "/", + response_model=RouteResAdminGetBookList, + status_code=status.HTTP_200_OK, + summary="전체 도서 목록 조회", +) +async def get_all_books( + db: Session = Depends(get_db), + current_user=Depends(get_current_admin) +): + response = await service_admin_read_books( + db=db + ) + + result = RouteResAdminGetBookList( + data=response, + count=len(response) + ) + + return result diff --git a/src/routes/admin/loan_route.py b/src/routes/admin/loan_route.py new file mode 100644 index 00000000..63165dc0 --- /dev/null +++ b/src/routes/admin/loan_route.py @@ -0,0 +1,74 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, Query, status +from sqlalchemy.orm import Session + +from dependencies import get_current_admin, get_db +from domain.services.admin.loan_service import service_admin_read_loans, service_admin_search_loans +from routes.admin.response.loan_response import RouteResAdminGetLoanList + +router = APIRouter( + prefix="/admin/loans", + tags=["admin/loans"], + dependencies=[Depends(get_current_admin)] +) + +@router.get( + "/search", + response_model=RouteResAdminGetLoanList, + status_code=status.HTTP_200_OK, + summary="전체 대출 목록 검색", +) +async def search_loans( + book_title: Annotated[ + str, Query(description="도서 제목", example="book", min_length=2, max_length=50) + ] = None, + user_name: Annotated[ + str | None, Query(description="사용자 이름", example="test", min_length=2, max_length=45) + ] = None, + category_name: Annotated[ + str, Query(description="카테고리 이름", example="category", min_length=2, max_length=50) + ] = None, + return_status: Annotated[ + bool, Query(description="반납 여부", example=False) + ] = None, + db: Session = Depends(get_db), + current_user=Depends(get_current_admin) +): + response = await service_admin_search_loans( + user_name = user_name, + book_title = book_title, + category_name = category_name, + return_status = return_status, + db = db + ) + + result = RouteResAdminGetLoanList( + data=response, + count=len(response) + ) + + return result + + + +@router.get( + "/", + response_model=RouteResAdminGetLoanList, + status_code=status.HTTP_200_OK, + summary="전체 대출 목록 조회", +) +async def get_all_loans( + db: Session = Depends(get_db), + current_user=Depends(get_current_admin) +): + response = await service_admin_read_loans( + db = db + ) + + result = RouteResAdminGetLoanList( + data=response, + count=len(response) + ) + + return result diff --git a/src/routes/admin/notice_route.py b/src/routes/admin/notice_route.py deleted file mode 100644 index 7d82b8a0..00000000 --- a/src/routes/admin/notice_route.py +++ /dev/null @@ -1,59 +0,0 @@ -from fastapi import APIRouter, Depends, status, Query -from sqlalchemy.orm import Session - -from dependencies import get_current_admin, get_db -from domain.services.admin.notice_service import service_admin_read_notices, service_admin_read_notice -from routes.admin.response.notice_response import RouteResAdminGetNotice, RouteResAdminGetNoticeList - -router=APIRouter( - prefix="/admin/notice", - tags=["admin/notice"], - dependencies=[Depends(get_current_admin)] -) - -@router.get( - "", - response_model=RouteResAdminGetNoticeList, - status_code=status.HTTP_200_OK, - summary="공지사항 리스트 전체 조회", - ) - -async def get_all_notices( - page: int = Query(7, ge=1), - limit: int = Query(10, le=50), - db: Session=Depends(get_db), - current_user=Depends(get_current_admin) -): - domain_res = await service_admin_read_notices(page, limit, db) - response = RouteResAdminGetNoticeList( - data=domain_res, - count=len(domain_res) - ) - - return response - - -@router.get( - "/{notice_id}", - response_model=RouteResAdminGetNotice, - status_code=status.HTTP_200_OK, - summary="공지사항 상세 조회", - ) - -async def get_notice( - notice_id: int, - db: Session=Depends(get_db), - current_user=Depends(get_current_admin) -): - domain_res = await service_admin_read_notice(notice_id, db) - response = RouteResAdminGetNotice( - notice_id=domain_res.notice_id, - admin_id=domain_res.admin_id, - admin_name=domain_res.admin_name, - title=domain_res.title, - notice_content=domain_res.notice_content, - created_at=domain_res.created_at - ) - - return response - diff --git a/src/routes/admin/request/notice_request.py b/src/routes/admin/request/notice_request.py new file mode 100644 index 00000000..4f8aaaf2 --- /dev/null +++ b/src/routes/admin/request/notice_request.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field + + +class RouteReqAdminPostNotice(BaseModel): + title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1"]) + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) + +class RouteReqAdminPutNotice(BaseModel): + title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1"]) + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) diff --git a/src/routes/admin/response/book_response.py b/src/routes/admin/response/book_response.py index 6839ce89..883bace7 100644 --- a/src/routes/admin/response/book_response.py +++ b/src/routes/admin/response/book_response.py @@ -3,6 +3,13 @@ from pydantic import BaseModel, Field +from domain.schemas.book_schemas import DomainAdminGetBookItem + + +class RouteResAdminGetBookList(BaseModel): + data: list[DomainAdminGetBookItem] + count: int + class RouteResAdminPostBook(BaseModel): book_id: int = Field(title="book_id", description="책 ID", gt=0) @@ -39,3 +46,4 @@ class RouteResAdminPutBook(BaseModel): donor_name: Optional[str] = Field(title="donor_name", description="기부자명", default=None) created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) + diff --git a/src/routes/admin/response/loan_response.py b/src/routes/admin/response/loan_response.py new file mode 100644 index 00000000..969890b3 --- /dev/null +++ b/src/routes/admin/response/loan_response.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + +from domain.schemas.loan_schemas import DomainAdminGetLoanItem + + +class RouteResAdminGetLoanList(BaseModel): + data: list[DomainAdminGetLoanItem] + count: int + diff --git a/src/routes/admin/response/notice_response.py b/src/routes/admin/response/notice_response.py index 2e24bba3..a4c999ef 100644 --- a/src/routes/admin/response/notice_response.py +++ b/src/routes/admin/response/notice_response.py @@ -4,6 +4,7 @@ from domain.schemas.notice_schemas import DomainResAdminGetNotice + class RouteResAdminGetNotice(BaseModel): notice_id: int = Field(title="notice_id", description="공지사항 ID", example=1, gt=0) admin_id: int = Field(title="admin_id", description="관리자 ID", example=1, gt=0) @@ -15,4 +16,18 @@ class RouteResAdminGetNotice(BaseModel): class RouteResAdminGetNoticeList(BaseModel): data: list[DomainResAdminGetNotice] count: int + total: int + +class RouteResAdminPostNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) + admin_name: str = Field(title="admin_name", description="관리자 성명", examples=["관리자1"]) + title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1", "공지사항 제목2"]) + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) + created_at: date = Field(title="created_at", description="공지사항 생성일", examples=[date.today()]) +class RouteResAdminPutNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) + admin_name: str = Field(title="admin_name", description="관리자 성명", examples=["관리자1"]) + title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1", "공지사항 제목2"]) + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) + created_at: date = Field(title="created_at", description="공지사항 생성일", examples=[date.today()]) diff --git a/src/routes/admin/response/user_response.py b/src/routes/admin/response/user_response.py new file mode 100644 index 00000000..cb659fd9 --- /dev/null +++ b/src/routes/admin/response/user_response.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + +from domain.schemas.user_schemas import DomainAdminGetUserItem + + +class RouteResAdminGetUserList(BaseModel): + data: list[DomainAdminGetUserItem] + count: int diff --git a/src/routes/admin/user_route.py b/src/routes/admin/user_route.py new file mode 100644 index 00000000..b0d8fc3a --- /dev/null +++ b/src/routes/admin/user_route.py @@ -0,0 +1,70 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, Query, status +from sqlalchemy.orm import Session + +from dependencies import get_current_admin, get_db +from domain.services.admin.user_service import service_admin_read_users, service_admin_search_users +from routes.admin.response.user_response import RouteResAdminGetUserList + +router = APIRouter( + prefix="/admin/users", + tags=["admin/users"], + dependencies=[Depends(get_current_admin)] +) + + +@router.get( + "/search", + response_model=RouteResAdminGetUserList, + status_code=status.HTTP_200_OK, + summary="전체 사용자 목록 검색", +) +async def search_users( + db: Session = Depends(get_db), + user_name: Annotated[ + str, Query(description="사용자 이름", example="test") + ] = None, + authority: Annotated[ + bool, Query(description="권한 여부", example=False) + ] = None, + active: Annotated[ + bool, Query(description="관리자 활성 여부", example=False) + ] = None, + current_user: Annotated = Depends(get_current_admin) +): + response = await service_admin_search_users( + user_name=user_name, + authority=authority, + active=active, + db=db + ) + + result = RouteResAdminGetUserList( + data=response, + count=len(response) + ) + + return result + + +@router.get( + "/", + response_model=RouteResAdminGetUserList, + status_code=status.HTTP_200_OK, + summary="전체 사용자 목록 조회", +) +async def get_all_users( + db: Session = Depends(get_db), + current_user=Depends(get_current_admin) +): + response = await service_admin_read_users( + db=db + ) + + result = RouteResAdminGetUserList( + data=response, + count=len(response) + ) + + return result diff --git a/src/routes/authentication_route.py b/src/routes/authentication_route.py index 4d410218..7e8ce1c5 100644 --- a/src/routes/authentication_route.py +++ b/src/routes/authentication_route.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, status +from fastapi import APIRouter, Depends, Header, status from sqlalchemy.orm import Session import domain.schemas.auth_schemas as auth_schemas @@ -63,3 +63,15 @@ async def login( return await auth_service.login_with_username(request, db) # elif settings.ENVIRONMENT == "production": # return await auth_service.login(request, db) + +@router.post( + "/refresh-token", + status_code=status.HTTP_201_CREATED, + summary="토큰 재발급", + description="refresh_token 만료 이전에만 토큰 재발급 가능" +) +async def refresh_token( + access_token: str = Header(), + refresh_token: str = Header(), +): + return await auth_service.service_refresh_token(access_token = access_token, refresh_token = refresh_token) diff --git a/src/routes/book_review_route.py b/src/routes/book_review_route.py index b5b6afbf..5efc5d55 100644 --- a/src/routes/book_review_route.py +++ b/src/routes/book_review_route.py @@ -67,7 +67,7 @@ async def get_all_user_reviews( @router.post( "", response_model=DomainResPostReview, - status_code=status.HTTP_200_OK, + status_code=status.HTTP_201_CREATED, summary="리뷰 작성" ) async def create_review( diff --git a/src/routes/bookrequest_route.py b/src/routes/bookrequest_route.py index 9fd4ceb7..80c3c29c 100644 --- a/src/routes/bookrequest_route.py +++ b/src/routes/bookrequest_route.py @@ -26,7 +26,7 @@ @router.post( "", response_model=RouteResPostBookRequest, - status_code=status.HTTP_200_OK, + status_code=status.HTTP_201_CREATED, summary="구매 요청" ) async def create_book_request( diff --git a/src/routes/notice_route.py b/src/routes/notice_route.py index 8c67f460..6f4a9dd3 100644 --- a/src/routes/notice_route.py +++ b/src/routes/notice_route.py @@ -1,8 +1,8 @@ -from fastapi import APIRouter, Depends, status, Query +from fastapi import APIRouter, Depends, Query, status from sqlalchemy.orm import Session from dependencies import get_current_active_user, get_db -from domain.services.notice_service import service_read_notices, service_read_notice +from domain.services.notice_service import service_read_notice, service_read_notices from routes.response.notice_response import RouteResGetNotice, RouteResGetNoticeList router=APIRouter( @@ -19,14 +19,15 @@ ) async def get_all_notices( - page: int = Query(7, ge=1), - limit: int = Query(10, le=50), + page: int = Query(1, ge=1), + limit: int = Query(7, le=50), db: Session=Depends(get_db), current_user=Depends(get_current_active_user) ): - domain_res = await service_read_notices(page, limit, db) + domain_res, total = await service_read_notices(page, limit, db) response = RouteResGetNoticeList( data=domain_res, + total=total, count=len(domain_res) ) diff --git a/src/routes/response/book_response.py b/src/routes/response/book_response.py index 2e6def1a..6e2a89d0 100644 --- a/src/routes/response/book_response.py +++ b/src/routes/response/book_response.py @@ -1,5 +1,3 @@ -from datetime import datetime - from pydantic import BaseModel, Field from domain.schemas.book_schemas import DomainResGetBookList @@ -25,5 +23,4 @@ class RouteResGetBook(BaseModel): language: str = Field(title="language", description="언어", example="영문") donor_name: str | None = Field(title="donor_name", description="책 기증자 성함", example="김철수") book_status: bool = Field(title="book_stauts", description="책 상태", example=True) - created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) - updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) + diff --git a/src/routes/response/notice_response.py b/src/routes/response/notice_response.py index f573d673..9fef4df8 100644 --- a/src/routes/response/notice_response.py +++ b/src/routes/response/notice_response.py @@ -4,6 +4,7 @@ from domain.schemas.notice_schemas import DomainResGetNotice + class RouteResGetNotice(BaseModel): notice_id: int = Field(title="notice_id", description="공지사항 ID", example=1, gt=0) admin_id: int = Field(title="admin_id", description="관리자 ID", example=1, gt=0) @@ -15,4 +16,5 @@ class RouteResGetNotice(BaseModel): class RouteResGetNoticeList(BaseModel): data: list[DomainResGetNotice] count: int + total: int