From d7376392aae625bd87b884d7f32fe724c40f7f59 Mon Sep 17 00:00:00 2001 From: Tiger-Muke Date: Fri, 29 Nov 2024 22:32:24 +0900 Subject: [PATCH 01/43] chore: replace root_validator with model_validator --- src/routes/request/user_request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/request/user_request.py b/src/routes/request/user_request.py index 47e49677..2cb8dcaa 100644 --- a/src/routes/request/user_request.py +++ b/src/routes/request/user_request.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field, root_validator +from pydantic import BaseModel, Field, model_validator from typing import Optional from fastapi import HTTPException, status class RouteReqPutUser(BaseModel): @@ -6,7 +6,7 @@ class RouteReqPutUser(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/") - @root_validator(pre=True) + @model_validator(mode="before") def check_at_least_one_field(cls, values): # If all three fields are None, raise an error if not any(values.get(field) for field in ['user_name', 'github', 'instagram']): From bbc18aba7a1d2573a53e9341949ce90968db3952 Mon Sep 17 00:00:00 2001 From: smreosms13 Date: Sat, 30 Nov 2024 22:42:35 +0900 Subject: [PATCH 02/43] feat: create table for settings --- sql/V4__create_settings_table.sql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 sql/V4__create_settings_table.sql diff --git a/sql/V4__create_settings_table.sql b/sql/V4__create_settings_table.sql new file mode 100644 index 00000000..43d3db08 --- /dev/null +++ b/sql/V4__create_settings_table.sql @@ -0,0 +1,14 @@ +CREATE TABLE `settings` ( + `id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `start_date` DATETIME NOT NULL, + `end_date` DATETIME NOT NULL, + `extend_days` TINYINT unsigned NOT NULL, + `extend_max_count` TINYINT unsigned NOT NULL, + `loan_days` TINYINT unsigned NOT NULL, + `loan_max_book` TINYINT unsigned NOT NULL, + `request_max_count` TINYINT unsigned NOT NULL, + `request_max_price` TINYINT unsigned NOT NULL, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_deleted` BOOLEAN NOT NULL DEFAULT FALSE +); \ No newline at end of file From 424c6bb6ddc2fcfce61bc12bcac8a78921718540 Mon Sep 17 00:00:00 2001 From: smreosms13 Date: Sat, 30 Nov 2024 22:57:02 +0900 Subject: [PATCH 03/43] feat: add settings in models --- src/repositories/models.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/repositories/models.py b/src/repositories/models.py index f4c7b654..6c46b5da 100644 --- a/src/repositories/models.py +++ b/src/repositories/models.py @@ -1,4 +1,5 @@ from sqlalchemy import TIMESTAMP, Boolean, Column, Date, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy.dialects.mysql import TINYINT from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.sql import func @@ -143,6 +144,24 @@ class Notice(Base): user = relationship("User", foreign_keys=[user_id]) +class Settings(Base): + __tablename__ = "settings" + + id = Column(Integer, primary_key=True, index=True) + start_date = Column(DateTime, nullable=False) + end_date = Column(DateTime, nullable=False) + extend_days = Column(TINYINT(unsigned=True)(unsigned=True), nullable=False) + extend_max_count = Column(TINYINT(unsigned=True), nullable=False) + loan_days = Column(TINYINT(unsigned=True), nullable=False) + loan_max_book = Column(TINYINT(unsigned=True), nullable=False) + request_max_count = Column(TINYINT(unsigned=True), nullable=False) + request_max_price = Column(TINYINT(unsigned=True), nullable=False) + created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) + updated_at = Column(TIMESTAMP, nullable=False, server_default=func.now(), onupdate=func.now()) + is_deleted = Column(Boolean, nullable=False, default=False) + + + # 여기서부터는 예시로 작성한 코드입니다. From cda84080e729a79a63fe5bff1717dbd421682b14 Mon Sep 17 00:00:00 2001 From: smreosms13 Date: Sat, 30 Nov 2024 22:57:17 +0900 Subject: [PATCH 04/43] fix: setting route schema --- src/routes/admin/response/setting_response.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/routes/admin/response/setting_response.py b/src/routes/admin/response/setting_response.py index 4328b941..644cf5ad 100644 --- a/src/routes/admin/response/setting_response.py +++ b/src/routes/admin/response/setting_response.py @@ -44,7 +44,7 @@ class ExtendSetting(BaseModel): class LoanSetting(BaseModel): loan_days : int = Field(..., title="대출 기간", description="1회 당 대출 기간", \ examples=[3, 7, 10], ge=1) - loan_max_count : int = Field(..., title="대출 가능 권수", description="1회당 대출 가능 권수", \ + loan_max_book : int = Field(..., title="대출 가능 권수", description="1회당 대출 가능 권수", \ examples=[3, 7, 10], ge=1) class BookRequestSetting(BaseModel): @@ -52,3 +52,12 @@ class BookRequestSetting(BaseModel): examples=[3, 7, 10], ge=1) request_max_price : int = Field(..., title="도서 구매 최대 가격", description="1인당 구매 가능한 최고 도서 가격", \ examples=[15000, 30000, 50000], ge=1) + +class RouteResAdminSetting(BaseModel): + setting_id : int = Field(title="setting_id", description="설정 ID", gt=0) + service_date : ServiceDate + loan : LoanSetting + extend : ExtendSetting + bookreqeust : BookRequestSetting + created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) + updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) From 4252a3359ea1c68e157d5b8359688d0f448be64a Mon Sep 17 00:00:00 2001 From: smreosms13 Date: Sat, 30 Nov 2024 23:06:32 +0900 Subject: [PATCH 05/43] fix: setting.extend_days TINYINT is not callable --- src/repositories/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/models.py b/src/repositories/models.py index 6c46b5da..7244786e 100644 --- a/src/repositories/models.py +++ b/src/repositories/models.py @@ -150,7 +150,7 @@ class Settings(Base): id = Column(Integer, primary_key=True, index=True) start_date = Column(DateTime, nullable=False) end_date = Column(DateTime, nullable=False) - extend_days = Column(TINYINT(unsigned=True)(unsigned=True), nullable=False) + extend_days = Column(TINYINT(unsigned=True), nullable=False) extend_max_count = Column(TINYINT(unsigned=True), nullable=False) loan_days = Column(TINYINT(unsigned=True), nullable=False) loan_max_book = Column(TINYINT(unsigned=True), nullable=False) From 25ee944abd69152ed6b758891c6103e2871f8c81 Mon Sep 17 00:00:00 2001 From: smreosms13 Date: Sat, 30 Nov 2024 23:11:07 +0900 Subject: [PATCH 06/43] fix: remove constraint of Field request_max_price --- src/routes/admin/response/setting_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/admin/response/setting_response.py b/src/routes/admin/response/setting_response.py index 644cf5ad..0bf691c8 100644 --- a/src/routes/admin/response/setting_response.py +++ b/src/routes/admin/response/setting_response.py @@ -51,7 +51,7 @@ class BookRequestSetting(BaseModel): request_max_count : int = Field(..., title="도서 구매 최대 권수", description="1인당 구매 가능한 최대 도서 권수", \ examples=[3, 7, 10], ge=1) request_max_price : int = Field(..., title="도서 구매 최대 가격", description="1인당 구매 가능한 최고 도서 가격", \ - examples=[15000, 30000, 50000], ge=1) + examples=[15000, 30000, 50000]) class RouteResAdminSetting(BaseModel): setting_id : int = Field(title="setting_id", description="설정 ID", gt=0) From 5dfc5af3b2744bb16c046599b68c2a0a0b15af77 Mon Sep 17 00:00:00 2001 From: Moonsu Kang Date: Sun, 1 Dec 2024 01:04:52 +0900 Subject: [PATCH 07/43] Feat/add fields review and loan (#107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add fields for review and loan entities: * chore: Ignore ruff E501 * refactor: Move `회원의 전체 리뷰 목록 조회` to `user_route` * feat: Add field business logic and exception handling * style: `회원`->`user` * chore: get_current_active_user -> get_current_user * style: Add int type annotation to parameters * refactor: Delete `회원의 전체 리뷰 목록 조회` in book_review_route.py --- src/domain/schemas/book_review_schemas.py | 1 + src/domain/schemas/loan_schemas.py | 3 ++ src/domain/services/book_review_service.py | 35 ++++++++----- src/domain/services/loan_service.py | 57 +++++++++++++++------- src/routes/book_review_route.py | 22 +-------- src/routes/loan_route.py | 1 - src/routes/response/loan_response.py | 1 + src/routes/user_route.py | 21 +++++++- 8 files changed, 88 insertions(+), 53 deletions(-) diff --git a/src/domain/schemas/book_review_schemas.py b/src/domain/schemas/book_review_schemas.py index c64fced0..480d1ea2 100644 --- a/src/domain/schemas/book_review_schemas.py +++ b/src/domain/schemas/book_review_schemas.py @@ -28,6 +28,7 @@ class DomainResGetReviewItem(BaseModel): review_content: str = Field(title="review_content", description="리뷰 내용") created_at: _datetime = Field(title="create_at", description="생성일시", example=_datetime.now()) updated_at: _datetime = Field(title="update_at", description="수정일시", example=_datetime.now()) + book_title: str = Field(title="book_title", description="책 제목", example="FastAPI Tutorial") class DomainReqPostReview(BaseModel): diff --git a/src/domain/schemas/loan_schemas.py b/src/domain/schemas/loan_schemas.py index c66e6942..712762e6 100644 --- a/src/domain/schemas/loan_schemas.py +++ b/src/domain/schemas/loan_schemas.py @@ -16,6 +16,9 @@ class DomainResGetLoan(BaseModel): 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) + book_title: str = Field(title="book_title", description="책 제목", example="FastAPI Tutorial") + code: str = Field(title="code", description="책 코드", example="A3") + version: str | None = Field(title="version", description="판본", example="10e") class DomainReqPutLoan(BaseModel): diff --git a/src/domain/services/book_review_service.py b/src/domain/services/book_review_service.py index 12ff8d38..ad9e3553 100644 --- a/src/domain/services/book_review_service.py +++ b/src/domain/services/book_review_service.py @@ -50,7 +50,10 @@ async def service_read_reviews_by_book_id(book_id, db: Session): return response -async def service_read_reviews_by_user_id(user_id, db: Session): +async def service_read_reviews_by_user_id( + user_id, + db: Session +) -> list[DomainResGetReviewItem]: stmt = ( select(BookReview) .where(and_(BookReview.user_id == user_id, BookReview.is_deleted == False)) @@ -71,17 +74,25 @@ async def service_read_reviews_by_user_id(user_id, db: Session): detail="Reviews not found" ) - result = [ - DomainResGetReviewItem( - review_id=review.id, - user_id=review.user_id, - book_id=review.book_id, - review_content=review.review_content, - created_at=review.created_at, - updated_at=review.updated_at, - ) - for review in reviews - ] + result = [] + for review in reviews: + if review.book is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Book with ID {review.book_id} not found for review ID {review.id}" + ) + else: + result.append( + DomainResGetReviewItem( + review_id=review.id, + user_id=review.user_id, + book_id=review.book_id, + review_content=review.review_content, + created_at=review.created_at, + updated_at=review.updated_at, + book_title=review.book.book_title, + ) + ) return result diff --git a/src/domain/services/loan_service.py b/src/domain/services/loan_service.py index 09372b4e..97809aa3 100644 --- a/src/domain/services/loan_service.py +++ b/src/domain/services/loan_service.py @@ -9,29 +9,50 @@ from utils.crud_utils import get_item -async def service_read_loans_by_user_id(user_id, db: Session): - stmt = select(Loan).where(and_(Loan.user_id == user_id, Loan.is_deleted == False)).order_by(Loan.updated_at) +async def service_read_loans_by_user_id( + user_id: int, + db: Session +) -> list[DomainResGetLoan]: + stmt = ( + select(Loan) + .where( + and_( + Loan.user_id == user_id, + Loan.is_deleted == False + ) + ).order_by(Loan.updated_at.desc())) try: loans = db.scalars(stmt).all() # loans를 리스트로 반환 if not loans: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Loans not found") from None - result = [ - DomainResGetLoan( - loan_id=loan.id, - book_id=loan.book_id, - user_id=loan.user_id, - created_at=loan.created_at, - updated_at=loan.updated_at, - loan_date=loan.loan_date, - due_date=loan.due_date, - extend_status=loan.extend_status, - overdue_days=loan.overdue_days, - return_status=loan.return_status, - return_date=loan.return_date, - ) - for loan in loans - ] + result = [] + for loan in loans: + if loan.book is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Book with ID {loan.book_id} not found for loan ID {loan.id}" + ) + else: + result.append( + DomainResGetLoan( + loan_id=loan.id, + book_id=loan.book_id, + user_id=loan.user_id, + created_at=loan.created_at, + updated_at=loan.updated_at, + loan_date=loan.loan_date, + due_date=loan.due_date, + extend_status=loan.extend_status, + overdue_days=loan.overdue_days, + return_status=loan.return_status, + return_date=loan.return_date, + book_title=loan.book.book_title, + code=loan.book.code, + version=loan.book.version, + ) + ) + except HTTPException as e: raise e from e except Exception as e: diff --git a/src/routes/book_review_route.py b/src/routes/book_review_route.py index 30adce4a..a8097ed5 100644 --- a/src/routes/book_review_route.py +++ b/src/routes/book_review_route.py @@ -12,11 +12,10 @@ service_create_review, service_delete_review, service_read_reviews_by_book_id, - service_read_reviews_by_user_id, service_update_review, ) from routes.request.book_review_request import RouteReqPostReview, RouteReqPutReview -from routes.response.book_review_response import RouteResGetReviewList, RouteResGetReviewListByInfoId +from routes.response.book_review_response import RouteResGetReviewListByInfoId router = APIRouter( prefix="/reviews", @@ -43,25 +42,6 @@ async def get_all_reviews_by_book_id( return result -@router.get( - "/list", - response_model=RouteResGetReviewList, - status_code=status.HTTP_200_OK, - summary="회원의 전체 리뷰 목록 조회", - dependencies=[Depends(get_current_user)] -) -async def get_all_user_reviews( - db: Session = Depends(get_db), - current_user=Depends(get_current_user) -): - domain_res = await service_read_reviews_by_user_id(current_user.id, db) - - result = RouteResGetReviewList( - data=domain_res, - count=len(domain_res) - ) - return result - @router.post( "", diff --git a/src/routes/loan_route.py b/src/routes/loan_route.py index b802294a..335120d4 100644 --- a/src/routes/loan_route.py +++ b/src/routes/loan_route.py @@ -2,7 +2,6 @@ from sqlalchemy.orm import Session from dependencies import get_current_active_user, get_db - from domain.schemas.loan_schemas import DomainReqPostLoan, DomainReqPutLoan, DomainResGetLoan from domain.services.loan_service import service_create_loan, service_extend_loan from routes.request.loan_request import RouteReqPostLoan diff --git a/src/routes/response/loan_response.py b/src/routes/response/loan_response.py index ad511d4a..ec804956 100644 --- a/src/routes/response/loan_response.py +++ b/src/routes/response/loan_response.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 from datetime import date, timedelta from datetime import datetime as _datetime diff --git a/src/routes/user_route.py b/src/routes/user_route.py index de42a3b0..8aaf3284 100644 --- a/src/routes/user_route.py +++ b/src/routes/user_route.py @@ -3,10 +3,12 @@ from dependencies import get_current_user, get_db from domain.schemas.bookrequest_schemas import DomainReqGetBookRequest +from domain.services.book_review_service import service_read_reviews_by_user_id from domain.services.bookrequest_service import service_read_bookrequest_list from domain.services.loan_service import service_read_loans_by_user_id from domain.services.user_service import service_read_user, service_update_user from routes.request.user_request import RouteReqPutUser +from routes.response.book_review_response import RouteResGetReviewList from routes.response.bookrequest_response import RouteResBookRequest, RouteResBookRequestList from routes.response.loan_response import RouteResGetLoanList from routes.response.user_response import RouteResGetUser, RouteResPutUser @@ -21,7 +23,7 @@ "/my-loans", response_model=RouteResGetLoanList, status_code=status.HTTP_200_OK, - summary="회원의 전체 대출 목록 조회", + summary="user의 전체 대출 목록 조회", ) async def get_all_user_loans( db: Session = Depends(get_db), @@ -111,3 +113,20 @@ async def put_user( ) return response +@router.get( + "/my-reviews", + response_model=RouteResGetReviewList, + status_code=status.HTTP_200_OK, + summary="user의 전체 리뷰 목록 조회", +) +async def get_all_user_reviews( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + domain_res = await service_read_reviews_by_user_id(current_user.id, db) + + result = RouteResGetReviewList( + data=domain_res, + count=len(domain_res) + ) + return result From 7e82556325ce287f96a76f21418b12e5d08f498a Mon Sep 17 00:00:00 2001 From: Moonsu Kang Date: Mon, 2 Dec 2024 10:57:49 +0900 Subject: [PATCH 08/43] Fix/exceptions admin search (#108) * feat: Add error handling for serialization processes * style: Enter in stmt * fix: Add join option in search_loans --- src/domain/services/admin/book_service.py | 6 +- src/domain/services/admin/loan_service.py | 100 ++++++++++++++-------- 2 files changed, 68 insertions(+), 38 deletions(-) diff --git a/src/domain/services/admin/book_service.py b/src/domain/services/admin/book_service.py index dde1821e..cd011ba8 100644 --- a/src/domain/services/admin/book_service.py +++ b/src/domain/services/admin/book_service.py @@ -26,7 +26,11 @@ async def service_admin_search_books( return_status: bool | None, db: Session ) -> list[DomainAdminGetBookItem]: - stmt = (select(Book).options(selectinload(Book.loans)).where(Book.is_deleted == False,)) + stmt = ( + select(Book) + .options(selectinload(Book.loans)) + .where(Book.is_deleted == False) + ) if book_title: stmt = ( diff --git a/src/domain/services/admin/loan_service.py b/src/domain/services/admin/loan_service.py index 65697c06..ac313d44 100644 --- a/src/domain/services/admin/loan_service.py +++ b/src/domain/services/admin/loan_service.py @@ -1,8 +1,9 @@ +# ruff: noqa: C901 from datetime import datetime from fastapi import HTTPException, status from sqlalchemy import select, text -from sqlalchemy.orm import Session, joinedload, selectinload +from sqlalchemy.orm import Session, selectinload from domain.schemas.loan_schemas import DomainAdminGetLoan, DomainResGetLoan from repositories.models import Loan @@ -62,9 +63,12 @@ async def service_admin_search_loans( ) -> list[DomainAdminGetLoan]: stmt = ( select(Loan) - .options(joinedload(Loan.user), joinedload(Loan.book)) - .join(Loan.user) .join(Loan.book) + .join(Loan.user) + .options( + selectinload(Loan.user), + selectinload(Loan.book) + ) .where( Loan.is_deleted == False ) @@ -94,24 +98,35 @@ async def service_admin_search_loans( if not loans: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Loans not found") - search_loans = [ - DomainAdminGetLoan( - 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, + search_loans = [] + for loan in loans: + if not loan.user: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"User with ID {loan.user_id} not found for loan ID {loan.id}" + ) + if not loan.book: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Book with ID {loan.book_id} not found for loan ID {loan.id}" + ) + search_loans.append( + DomainAdminGetLoan( + 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 @@ -142,24 +157,35 @@ async def service_admin_read_loans(db: Session) -> list[DomainAdminGetLoan]: if not loans: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Books not found") - search_loans = [ - DomainAdminGetLoan( - 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, + search_loans = [] + for loan in loans: + if not loan.user: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"User with ID {loan.user_id} not found for loan ID {loan.id}" + ) + if not loan.book: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Book with ID {loan.book_id} not found for loan ID {loan.id}" + ) + search_loans.append( + DomainAdminGetLoan( + 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 From d9a5a3e236d38fd5498356aacbc8d3053b87649a Mon Sep 17 00:00:00 2001 From: sounmu Date: Tue, 3 Dec 2024 13:52:37 +0900 Subject: [PATCH 09/43] feat: Add `category_name` to search(read) results --- src/domain/services/admin/loan_service.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/domain/services/admin/loan_service.py b/src/domain/services/admin/loan_service.py index ac313d44..e406c6dc 100644 --- a/src/domain/services/admin/loan_service.py +++ b/src/domain/services/admin/loan_service.py @@ -5,7 +5,7 @@ from sqlalchemy import select, text from sqlalchemy.orm import Session, selectinload -from domain.schemas.loan_schemas import DomainAdminGetLoan, DomainResGetLoan +from domain.schemas.loan_schemas import DomainResAdminGetLoan, DomainResGetLoan from repositories.models import Loan from utils.crud_utils import get_item @@ -60,7 +60,7 @@ async def service_admin_search_loans( category_name: str | None, return_status: str | None, db: Session -) -> list[DomainAdminGetLoan]: +) -> list[DomainResAdminGetLoan]: stmt = ( select(Loan) .join(Loan.book) @@ -86,7 +86,7 @@ async def service_admin_search_loans( ) if category_name: stmt = ( - stmt.where(text("MATCH(category_name) AGAINST(:category_name IN BOOLEAN MODE)")) + stmt.where(text("MATCH(book.category_name) AGAINST(:category_name IN BOOLEAN MODE)")) .params(category_name=f"{category_name}*") ) if return_status is not None: @@ -111,13 +111,14 @@ async def service_admin_search_loans( detail=f"Book with ID {loan.book_id} not found for loan ID {loan.id}" ) search_loans.append( - DomainAdminGetLoan( + DomainResAdminGetLoan( 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, + category_name=loan.book.category_name, loan_date=loan.loan_date, due_date=loan.due_date, extend_status=loan.extend_status, @@ -139,7 +140,7 @@ async def service_admin_search_loans( return search_loans -async def service_admin_read_loans(db: Session) -> list[DomainAdminGetLoan]: +async def service_admin_read_loans(db: Session) -> list[DomainResAdminGetLoan]: stmt = ( select(Loan) .options( @@ -170,13 +171,14 @@ async def service_admin_read_loans(db: Session) -> list[DomainAdminGetLoan]: detail=f"Book with ID {loan.book_id} not found for loan ID {loan.id}" ) search_loans.append( - DomainAdminGetLoan( + DomainResAdminGetLoan( 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, + category_name=loan.book.category_name, loan_date=loan.loan_date, due_date=loan.due_date, extend_status=loan.extend_status, From 35f2ba026e0d1b91717c247a7d5d1b11a0f4f7ee Mon Sep 17 00:00:00 2001 From: sounmu Date: Tue, 3 Dec 2024 13:53:17 +0900 Subject: [PATCH 10/43] style: DomainAdminGetLoan -> DomainResAdminGetLoan --- src/domain/schemas/loan_schemas.py | 3 ++- src/routes/admin/response/loan_response.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/domain/schemas/loan_schemas.py b/src/domain/schemas/loan_schemas.py index 712762e6..28ed7b93 100644 --- a/src/domain/schemas/loan_schemas.py +++ b/src/domain/schemas/loan_schemas.py @@ -45,13 +45,14 @@ class DomainReqPostLoan(BaseModel): book_id: int = Field(title="book_id", description="대출한 책 ID", example=1, gt=0) -class DomainAdminGetLoan(BaseModel): +class DomainResAdminGetLoan(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") + category_name: str = Field(title="category_name", description="카테고리명", example="웹") 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) diff --git a/src/routes/admin/response/loan_response.py b/src/routes/admin/response/loan_response.py index 90837835..418e078a 100644 --- a/src/routes/admin/response/loan_response.py +++ b/src/routes/admin/response/loan_response.py @@ -1,9 +1,9 @@ from pydantic import BaseModel -from domain.schemas.loan_schemas import DomainAdminGetLoan +from domain.schemas.loan_schemas import DomainResAdminGetLoan class RouteResAdminGetLoanList(BaseModel): - data: list[DomainAdminGetLoan] + data: list[DomainResAdminGetLoan] count: int From c8c78aef9edcdd3e95318abeee1274e0d8bc5884 Mon Sep 17 00:00:00 2001 From: sounmu Date: Wed, 4 Dec 2024 21:53:27 +0900 Subject: [PATCH 11/43] style: Reorder import statements in user_service.py --- src/domain/services/user_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/services/user_service.py b/src/domain/services/user_service.py index 72ede2e5..afcbd946 100644 --- a/src/domain/services/user_service.py +++ b/src/domain/services/user_service.py @@ -1,10 +1,11 @@ from fastapi import HTTPException, status from sqlalchemy.orm import Session -from domain.schemas.user_schemas import DomainResGetUser, DomainReqPutUser, DomainResPutUser +from domain.schemas.user_schemas import DomainReqPutUser, DomainResGetUser, DomainResPutUser from repositories.models import User from utils.crud_utils import get_item + async def service_read_user(user_id:int, db: Session): user = get_item(User, user_id, db) From a46a0cf2978a65534eb932e87f3d7046c53f3f40 Mon Sep 17 00:00:00 2001 From: sounmu Date: Wed, 4 Dec 2024 22:04:27 +0900 Subject: [PATCH 12/43] feat: Improve book search functionality using full-text-index search --- src/domain/services/book_service.py | 54 +++++++++++++++++++---------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/domain/services/book_service.py b/src/domain/services/book_service.py index c7642872..8207b1db 100644 --- a/src/domain/services/book_service.py +++ b/src/domain/services/book_service.py @@ -1,5 +1,5 @@ from fastapi import HTTPException, status -from sqlalchemy import and_, or_, select +from sqlalchemy import select, text from sqlalchemy.orm import Session from domain.schemas.book_schemas import DomainReqGetBook, DomainResGetBook, DomainResGetBookList @@ -7,36 +7,52 @@ from utils.crud_utils import get_item -async def service_search_books(searching_keyword: str, page: int, limit: int, db: Session): - keyword = f"%{searching_keyword}%" - +async def service_search_books( + searching_keyword: str, + page: int, + limit: int, + db: Session +) -> DomainResGetBookList: offset = (page - 1) * limit # Calculate offset based on the page numbe stmt = ( select(Book) - .where( - and_( - Book.is_deleted == False, - or_( - Book.book_title.ilike(keyword), - Book.author.ilike(keyword), - Book.publisher.ilike(keyword), - Book.category_name.ilike(keyword), - ), - ) - ) - .order_by(Book.updated_at.desc()) - .limit(limit) - .offset(offset) + .where(Book.is_deleted == False) ) + + search_columns = ['book_title', 'author', 'publisher', 'category_name'] + search_conditions = [] + params = {} + + for column in search_columns: + search_conditions.append(f"MATCH({column}) AGAINST(:{column} IN BOOLEAN MODE)") + params[column] = f"{searching_keyword}*" + + # Combine all search conditions with OR + combined_search = text(" OR ".join(search_conditions)) + stmt = stmt.where(combined_search).params(**params) + """.order_by(Book.updated_at.desc()) + .limit(limit) + .offset(offset)""" + try: - books = db.execute(stmt).scalars().all() + books = (db.execute( + stmt + .order_by(Book.updated_at.desc()) + .limit(limit) + .offset(offset) + ) + .scalars() + .all()) if not books: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Books not found" ) + except HTTPException as e: + raise e + except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, From fc3761b70081c089ac58a888e23cb92f83a7ce53 Mon Sep 17 00:00:00 2001 From: Tiger-Muke Date: Wed, 4 Dec 2024 22:13:38 +0900 Subject: [PATCH 13/43] =?UTF-8?q?chore=20:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/request/user_request.py | 11 ++++++----- src/routes/user_route.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/routes/request/user_request.py b/src/routes/request/user_request.py index 2cb8dcaa..a35d8c19 100644 --- a/src/routes/request/user_request.py +++ b/src/routes/request/user_request.py @@ -1,17 +1,18 @@ -from pydantic import BaseModel, Field, model_validator -from typing import Optional from fastapi import HTTPException, status +from pydantic import BaseModel, Field, model_validator + + class RouteReqPutUser(BaseModel): user_name: str | None = Field(title="user_name", description="사용자 이름", example="홍길동") 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/") + instagram: str | None = Field(None,title="instagram", description="인스타그램 주소", example="https://www.instagram.com/") @model_validator(mode="before") def check_at_least_one_field(cls, values): - # If all three fields are None, raise an error + # If all three fields are FALSY VALUE, raise an error if not any(values.get(field) for field in ['user_name', 'github', 'instagram']): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="At lease 'user_name', 'github', or 'instagram' must be provided." + detail="At least 'user_name', 'github', or 'instagram' must be provided." ) return values diff --git a/src/routes/user_route.py b/src/routes/user_route.py index d1578cee..cdc655f7 100644 --- a/src/routes/user_route.py +++ b/src/routes/user_route.py @@ -51,7 +51,7 @@ async def get_user( user_id=result.user_id, auth_id=result.auth_id, email=result.email, - user_name=result.email, + user_name=result.user_name, is_active=result.is_active, github=result.github, instagram=result.instagram @@ -104,7 +104,7 @@ async def put_user( user_id=result.user_id, auth_id=result.auth_id, email=result.email, - user_name=result.email, + user_name=result.user_name, is_active=result.is_active, github=result.github, instagram=result.instagram From 296b885d6fb6c21070ee9506758b13015218e3a8 Mon Sep 17 00:00:00 2001 From: smreosms13 Date: Wed, 4 Dec 2024 22:15:13 +0900 Subject: [PATCH 14/43] fix: change setting.request_max_price type as int --- sql/V4__create_settings_table.sql | 2 +- src/repositories/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/V4__create_settings_table.sql b/sql/V4__create_settings_table.sql index 43d3db08..363dc346 100644 --- a/sql/V4__create_settings_table.sql +++ b/sql/V4__create_settings_table.sql @@ -7,7 +7,7 @@ CREATE TABLE `settings` ( `loan_days` TINYINT unsigned NOT NULL, `loan_max_book` TINYINT unsigned NOT NULL, `request_max_count` TINYINT unsigned NOT NULL, - `request_max_price` TINYINT unsigned NOT NULL, + `request_max_price` INT unsigned NOT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `is_deleted` BOOLEAN NOT NULL DEFAULT FALSE diff --git a/src/repositories/models.py b/src/repositories/models.py index 7244786e..c8931447 100644 --- a/src/repositories/models.py +++ b/src/repositories/models.py @@ -155,7 +155,7 @@ class Settings(Base): loan_days = Column(TINYINT(unsigned=True), nullable=False) loan_max_book = Column(TINYINT(unsigned=True), nullable=False) request_max_count = Column(TINYINT(unsigned=True), nullable=False) - request_max_price = Column(TINYINT(unsigned=True), nullable=False) + request_max_price = Column(Integer, nullable=False) created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) updated_at = Column(TIMESTAMP, nullable=False, server_default=func.now(), onupdate=func.now()) is_deleted = Column(Boolean, nullable=False, default=False) From c5ddc15e43b356a3e81a177cf1c36dde235b7775 Mon Sep 17 00:00:00 2001 From: Tiger-Muke Date: Wed, 4 Dec 2024 22:33:29 +0900 Subject: [PATCH 15/43] =?UTF-8?q?chore:=20=EA=B3=B5=EB=B0=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/schemas/user_schemas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/schemas/user_schemas.py b/src/domain/schemas/user_schemas.py index a5c5b64c..194be0b6 100644 --- a/src/domain/schemas/user_schemas.py +++ b/src/domain/schemas/user_schemas.py @@ -41,9 +41,9 @@ class DomainAdminGetUserItem(BaseModel): class DomainReqAdminPutUser(BaseModel): user_id: int = Field(title="user_id", description="관리자의 회원 ID", gt=0) - user_status: bool | None = Field(None, title="is_active", description="회원 상태(대출 가능 여부)", examples=[True]) + user_status: bool | None = Field(None, title="is_active", description="회원 상태(대출 가능 여부",examples=[True]) admin_status: bool | None = Field(None, title="admin_status", description="관리자 권한 상태") - expiration_date : date | None = Field(None, title="expiration_date", description="관리자 권한 만료일, \ + expiration_date : date | None = Field(None, title="expiration_date", description="관리자 권한 만료일,\ 기본적으로 권한 부여일로부터 1년", examples=["2025-07-02"]) class DomainResAdminPutUser(BaseModel): user_id: int = Field(title="user_id", description="유저 고유 ID", example=1111, gt=0) From 6a5c0bfed44e430fbefa7706eeb6e9b0cae15b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Thu, 5 Dec 2024 11:40:17 +0900 Subject: [PATCH 16/43] fix: remove delete logic in canceling bookreqeust by user --- src/domain/services/bookrequest_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/domain/services/bookrequest_service.py b/src/domain/services/bookrequest_service.py index 950553fd..91a294ce 100644 --- a/src/domain/services/bookrequest_service.py +++ b/src/domain/services/bookrequest_service.py @@ -159,7 +159,6 @@ async def service_delete_bookrequest(request_data: DomainReqDelBookRequest, db: try: requested_book.processing_status = 2 requested_book.processed_date = date.today() - requested_book.is_deleted = True db.add(requested_book) db.flush() except Exception as e: From 5f504d04ca0d96a172b342c24f60017d1bba1894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Thu, 5 Dec 2024 11:41:16 +0900 Subject: [PATCH 17/43] chore: change summary in user bookrequest router --- src/routes/bookrequest_route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/bookrequest_route.py b/src/routes/bookrequest_route.py index d3e19860..58295883 100644 --- a/src/routes/bookrequest_route.py +++ b/src/routes/bookrequest_route.py @@ -95,7 +95,7 @@ async def update_user_bookrequest( @router.delete( "/{request_id}", - summary="도서 구매 요청 삭제 (요청자 취소)", + summary="도서 구매 요청 취소", status_code=status.HTTP_204_NO_CONTENT, ) async def delete_user_bookrequest( From 348b05a3aa8da5fa0c4163fda172b3b3863eec23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Thu, 5 Dec 2024 11:42:55 +0900 Subject: [PATCH 18/43] fix: remove validation of duplicate request book --- src/domain/services/bookrequest_service.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/domain/services/bookrequest_service.py b/src/domain/services/bookrequest_service.py index 91a294ce..bc16fb8e 100644 --- a/src/domain/services/bookrequest_service.py +++ b/src/domain/services/bookrequest_service.py @@ -17,14 +17,6 @@ async def service_create_bookrequest(request: DomainReqPostBookRequest, db: Session): - # check if the book already exists in database - stmt = select(RequestedBook).where(RequestedBook.book_title == request.book_title) - valid_request = db.execute(stmt).scalar_one_or_none() - - if valid_request: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, - detail="Already requested book") - # check if the user exists in database stmt = select(User).where(User.id == request.user_id) valid_user_id = db.execute(stmt).scalar_one_or_none() From 35865b9318681e035278e0b7cf800c0ccfbc039e Mon Sep 17 00:00:00 2001 From: Tiger-Muke Date: Thu, 5 Dec 2024 19:42:19 +0900 Subject: [PATCH 19/43] fix : clear user_name put response error --- src/domain/schemas/user_schemas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/schemas/user_schemas.py b/src/domain/schemas/user_schemas.py index 194be0b6..05fcfb9d 100644 --- a/src/domain/schemas/user_schemas.py +++ b/src/domain/schemas/user_schemas.py @@ -14,7 +14,7 @@ class DomainResGetUser(BaseModel): class DomainReqPutUser(BaseModel): user_id: int = Field(title="user_id", description="유저 고유 ID", example=1111, gt=0) - user_name: str = Field(title="user_name", description="사용자 이름", example="홍길동") + user_name: str | None = Field(title="user_name", description="사용자 이름", example="홍길동") 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/") @@ -22,7 +22,7 @@ class DomainResPutUser(BaseModel): user_id: int = Field(title="user_id", description="유저 고유 ID", example=1111, gt=0) auth_id: str = Field(title="auth_id", description="로그인 ID", example="gildong1") email: str = Field(title="email", description="이메일 주소", example="KUCC@korea.ac.kr") - user_name: str = Field(title="user_name", description="사용자 이름", example="홍길동") + user_name: str | None = Field(title="user_name", description="사용자 이름", example="홍길동") is_active: bool = Field(title="is_active", description="활동 상태", example=1) 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/") From 68348d2c550f2cfa41a7fdf781856fa351fc5868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Thu, 5 Dec 2024 23:25:50 +0900 Subject: [PATCH 20/43] feat: login/resgister schema update --- src/domain/schemas/auth_schemas.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/domain/schemas/auth_schemas.py b/src/domain/schemas/auth_schemas.py index d83742eb..23b331bb 100644 --- a/src/domain/schemas/auth_schemas.py +++ b/src/domain/schemas/auth_schemas.py @@ -1,3 +1,4 @@ +from typing import Optional from pydantic import BaseModel, Field @@ -20,7 +21,8 @@ class FirebaseLoginRequest(BaseModel): class LoginRequest(BaseModel): - auth_id: str = Field(..., example="테스트 이름") + email: str = Field(..., example="test@test.com") + password: str = Field(..., example="asdf1234") class LoginResponse(BaseModel): @@ -30,7 +32,11 @@ class LoginResponse(BaseModel): class RegisterRequest(BaseModel): + email: str = Field(..., example="test@test.com") + password: str = Field(..., example="asdf1234") user_name: str = Field(..., example="테스트 이름") + github: Optional[str] = Field(default=None) + instagram: Optional[str] = Field(default=None) is_active: bool = Field(..., example=True) From dc62a0cf15cb869c4fe0804edbcc58f6065e83db Mon Sep 17 00:00:00 2001 From: sounmu Date: Fri, 6 Dec 2024 15:49:37 +0900 Subject: [PATCH 21/43] feat: Enhance book search functionality with OR conditions and improved query parameters --- src/domain/services/book_service.py | 41 ++++++++++++++++------------- src/routes/books_route.py | 15 ++++++++--- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/domain/services/book_service.py b/src/domain/services/book_service.py index 8207b1db..010d25ea 100644 --- a/src/domain/services/book_service.py +++ b/src/domain/services/book_service.py @@ -1,5 +1,5 @@ from fastapi import HTTPException, status -from sqlalchemy import select, text +from sqlalchemy import or_, select, text from sqlalchemy.orm import Session from domain.schemas.book_schemas import DomainReqGetBook, DomainResGetBook, DomainResGetBookList @@ -21,29 +21,32 @@ async def service_search_books( ) search_columns = ['book_title', 'author', 'publisher', 'category_name'] - search_conditions = [] - params = {} - for column in search_columns: - search_conditions.append(f"MATCH({column}) AGAINST(:{column} IN BOOLEAN MODE)") - params[column] = f"{searching_keyword}*" + # OR 조건을 위한 조건 리스트 생성 + conditions = [ + text(f"MATCH({column}) AGAINST(:{column} IN BOOLEAN MODE)") + for column in search_columns + ] - # Combine all search conditions with OR - combined_search = text(" OR ".join(search_conditions)) - stmt = stmt.where(combined_search).params(**params) - """.order_by(Book.updated_at.desc()) - .limit(limit) - .offset(offset)""" + # 모든 조건을 OR로 결합 + stmt = stmt.where(or_(*conditions)) + # 각 열에 대해 검색 키워드 파라미터 설정 + search_params = {column: f"{searching_keyword}*" for column in search_columns} + stmt = stmt.params(**search_params) + + print(stmt) try: - books = (db.execute( - stmt - .order_by(Book.updated_at.desc()) - .limit(limit) - .offset(offset) + books = ( + db.execute( + stmt + .order_by(Book.updated_at.desc()) + .limit(limit) + .offset(offset) + ) + .scalars() + .all() ) - .scalars() - .all()) if not books: raise HTTPException( diff --git a/src/routes/books_route.py b/src/routes/books_route.py index cc3a4443..ad10b65a 100644 --- a/src/routes/books_route.py +++ b/src/routes/books_route.py @@ -1,3 +1,5 @@ +from typing import Annotated + from fastapi import APIRouter, Depends, Query, status from sqlalchemy.orm import Session @@ -53,9 +55,15 @@ async def get_book_by_book_id( status_code=status.HTTP_200_OK ) async def search_books( - searching_keyword: str = Query(alias="search"), - page: int = Query(1, gt=0), - limit: int = Query(10, gt=0), # 차후 기본 값은 적당히 변경할 예정 + searching_keyword: Annotated[ + str, Query(description="Search Query", min_length=2, max_length=50) + ], + page: Annotated[ + int, Query(description="페이지", example=1, gt=0) + ] = 1, + limit: Annotated[ + int, Query(description="페이지 당 조회 개수", example=10, gt=0) + ] = 10, # 차후 기본 값은 적당히 변경할 예정 db: Session = Depends(get_db) ): domain_res = await service_search_books(searching_keyword, page, limit, db) @@ -66,6 +74,7 @@ async def search_books( return result + @router.get( "", summary="전체 도서 목록 조회", From 6a1f86f024efa599eb907413d9465eee47b95056 Mon Sep 17 00:00:00 2001 From: sounmu Date: Fri, 6 Dec 2024 15:50:15 +0900 Subject: [PATCH 22/43] feat: Add loan status field to DomainResGetBookList schema --- src/domain/schemas/book_schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/schemas/book_schemas.py b/src/domain/schemas/book_schemas.py index 9f266171..d57e0b28 100644 --- a/src/domain/schemas/book_schemas.py +++ b/src/domain/schemas/book_schemas.py @@ -35,6 +35,7 @@ class DomainResGetBookList(BaseModel): 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) class DomainReqAdminPostBook(BaseModel): book_title: str = Field(title="book_title", description="책 제목", example="FastAPI Tutorial") From 55f514ba14cbef2c272a296a5dad22bd53706f0c Mon Sep 17 00:00:00 2001 From: sounmu Date: Fri, 6 Dec 2024 16:14:40 +0900 Subject: [PATCH 23/43] feat: Add loan status to book search results and optimize performance --- src/domain/services/book_service.py | 48 ++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/domain/services/book_service.py b/src/domain/services/book_service.py index 010d25ea..d4e86146 100644 --- a/src/domain/services/book_service.py +++ b/src/domain/services/book_service.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import Session from domain.schemas.book_schemas import DomainReqGetBook, DomainResGetBook, DomainResGetBookList -from repositories.models import Book +from repositories.models import Book, Loan from utils.crud_utils import get_item @@ -15,8 +15,23 @@ async def service_search_books( ) -> DomainResGetBookList: offset = (page - 1) * limit # Calculate offset based on the page numbe + latest_loan_subq = ( + select(Loan.return_status) + .where(Loan.book_id == Book.id) + .order_by(Loan.updated_at.desc()) + .limit(1) + ) stmt = ( - select(Book) + select( + Book.id, + Book.book_title, + Book.category_name, + Book.image_url, + Book.book_status, + Book.created_at, + Book.updated_at, + latest_loan_subq.scalar_subquery().label("loan_status") + ) .where(Book.is_deleted == False) ) @@ -44,7 +59,6 @@ async def service_search_books( .limit(limit) .offset(offset) ) - .scalars() .all() ) @@ -62,20 +76,24 @@ async def service_search_books( detail=f"Unexpected error occurred during retrieve: {str(e)}", ) from e - response = [ - DomainResGetBookList( - book_id=book.id, - book_title=book.book_title, - category_name=book.category_name, - image_url=book.image_url, - book_status=book.book_status, - created_at=book.created_at, - updated_at=book.updated_at + search_books = [] + for book in books: + (book_id, book_title, category_name, image_url, book_status, created_at, updated_at, loan_status) = book + + search_books.append( + DomainResGetBookList( + book_id=book_id, + book_title=book_title, + category_name=category_name, + image_url=image_url, + book_status=book_status, + created_at=created_at, + updated_at=updated_at, + loan_status=loan_status + ) ) - for book in books - ] - return response + return search_books async def service_read_book(request_data: DomainReqGetBook, db: Session): From e3cdf1ee5bb6c7296fbf29b42f0d054dd31f5d7e Mon Sep 17 00:00:00 2001 From: sounmu Date: Fri, 13 Dec 2024 21:55:51 +0900 Subject: [PATCH 24/43] feat: Update book search query to include active book status filter --- src/domain/services/book_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/services/book_service.py b/src/domain/services/book_service.py index d4e86146..aed6f015 100644 --- a/src/domain/services/book_service.py +++ b/src/domain/services/book_service.py @@ -32,7 +32,7 @@ async def service_search_books( Book.updated_at, latest_loan_subq.scalar_subquery().label("loan_status") ) - .where(Book.is_deleted == False) + .where(Book.is_deleted == False and Book.book_status == True) ) search_columns = ['book_title', 'author', 'publisher', 'category_name'] @@ -50,7 +50,7 @@ async def service_search_books( search_params = {column: f"{searching_keyword}*" for column in search_columns} stmt = stmt.params(**search_params) - print(stmt) + # print(stmt) # 디버깅용 try: books = ( db.execute( From f4d24734644fee1d638c10ac46d8a5128d7945d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sun, 22 Dec 2024 20:59:02 +0900 Subject: [PATCH 25/43] style: add whitespace --- src/domain/schemas/auth_schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/schemas/auth_schemas.py b/src/domain/schemas/auth_schemas.py index 23b331bb..a4a5d97a 100644 --- a/src/domain/schemas/auth_schemas.py +++ b/src/domain/schemas/auth_schemas.py @@ -1,4 +1,5 @@ from typing import Optional + from pydantic import BaseModel, Field From 09fc5322a6f755a72606f40317d00b90d0f0bfd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sun, 22 Dec 2024 20:59:47 +0900 Subject: [PATCH 26/43] feat: add 'email, github, instagram' into user attribute --- src/domain/services/auth_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index d11f617c..29e26f2e 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -12,14 +12,16 @@ async def register(request: RegisterRequest, db: Session): # Check if user information exists in the DB - user = db.query(User).filter(User.user_name == request.user_name).first() + user = db.query(User).filter((User.user_name == request.user_name) | (User.email == request.email)).first() # If user information does not exist in the DB, create a new user if user is None: user = User( auth_id=request.user_name, auth_type='EXP', - email="none", + email=request.email, + github_id=request.github, + instagram_id=request.instagram, user_name=request.user_name, is_active=True ) From 8cd128f93471c2d81f5e3b409acf34388874c5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sun, 22 Dec 2024 21:03:02 +0900 Subject: [PATCH 27/43] fix: change user_name -> email in login logic --- src/domain/services/auth_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index 29e26f2e..1acaf48d 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -95,7 +95,7 @@ async def login_with_username( db: Session): # Authenticate user # Check if user information exists in the DB - user = db.query(User).filter(User.auth_id == request.auth_id).first() + user = db.query(User).filter(User.email == request.email).first() # If user information does not exist in the DB, return error if user is None: From 920a90bd617f2badc42f3d74360cd4426eb07e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sun, 22 Dec 2024 22:04:49 +0900 Subject: [PATCH 28/43] feat: add 'password' column in user table --- sql/__update_user_table_password.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 sql/__update_user_table_password.sql diff --git a/sql/__update_user_table_password.sql b/sql/__update_user_table_password.sql new file mode 100644 index 00000000..5fae63e6 --- /dev/null +++ b/sql/__update_user_table_password.sql @@ -0,0 +1,4 @@ +ALTER TABLE + `user` +ADD + password TEXT NOT NULL; From 5920f02733068869e92c1efd0c0117826fcb6eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sun, 22 Dec 2024 22:11:18 +0900 Subject: [PATCH 29/43] feat: add password field in User --- src/repositories/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/models.py b/src/repositories/models.py index c8931447..715be82b 100644 --- a/src/repositories/models.py +++ b/src/repositories/models.py @@ -20,7 +20,7 @@ class User(Base): created_at = Column(TIMESTAMP, nullable=False, default=func.current_timestamp()) updated_at = Column(TIMESTAMP, nullable=False, default=func.current_timestamp(), onupdate=func.current_timestamp()) is_deleted = Column(Boolean, nullable=False, default=False) - + password = Column(Text, nullable=False) # Relationships admin = relationship("Admin", back_populates="user") requested_books = relationship("RequestedBook", back_populates="user") From f1492ca75f59fe1e6b82122a689c1ad5e891839a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sun, 22 Dec 2024 23:25:36 +0900 Subject: [PATCH 30/43] feat: add hased password logic --- src/domain/services/auth_service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index 1acaf48d..739661ee 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -1,5 +1,6 @@ from fastapi import HTTPException, status from fastapi.responses import JSONResponse +from passlib.context import CryptContext from sqlalchemy.orm import Session from config import Settings @@ -16,6 +17,8 @@ async def register(request: RegisterRequest, db: Session): # If user information does not exist in the DB, create a new user if user is None: + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + hashed_pwd = pwd_context.hash(request.password) user = User( auth_id=request.user_name, auth_type='EXP', @@ -23,6 +26,7 @@ async def register(request: RegisterRequest, db: Session): github_id=request.github, instagram_id=request.instagram, user_name=request.user_name, + password=hashed_pwd, is_active=True ) db.add(user) From 10c65289e8e5ed75e7e1cf738a43b3476aa17f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sun, 22 Dec 2024 23:32:05 +0900 Subject: [PATCH 31/43] feat: add hashed password verify logic in login --- src/domain/services/auth_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index 739661ee..cc7aa322 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -9,7 +9,7 @@ from externals.firebase import sign_in_with_email_and_password from repositories.models import User - +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") async def register(request: RegisterRequest, db: Session): # Check if user information exists in the DB @@ -17,7 +17,6 @@ async def register(request: RegisterRequest, db: Session): # If user information does not exist in the DB, create a new user if user is None: - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") hashed_pwd = pwd_context.hash(request.password) user = User( auth_id=request.user_name, @@ -105,6 +104,9 @@ async def login_with_username( if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") + if not pwd_context.verify(request.password, user.password): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unvalid Password") + # Create JWT tokens token_response = create_user_tokens(user.id) response = JSONResponse(content={ From ea4b4179745273c5dd8dfab6d3a5441ad0c3f7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sun, 22 Dec 2024 23:39:05 +0900 Subject: [PATCH 32/43] fix: replace str with EmailStr for email validation in request DTO --- src/domain/schemas/auth_schemas.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/schemas/auth_schemas.py b/src/domain/schemas/auth_schemas.py index a4a5d97a..05386764 100644 --- a/src/domain/schemas/auth_schemas.py +++ b/src/domain/schemas/auth_schemas.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, EmailStr, Field class UserInfo(BaseModel): @@ -22,7 +22,7 @@ class FirebaseLoginRequest(BaseModel): class LoginRequest(BaseModel): - email: str = Field(..., example="test@test.com") + email: EmailStr = Field(..., example="test@test.com") password: str = Field(..., example="asdf1234") @@ -33,7 +33,7 @@ class LoginResponse(BaseModel): class RegisterRequest(BaseModel): - email: str = Field(..., example="test@test.com") + email: EmailStr = Field(..., example="test@test.com") password: str = Field(..., example="asdf1234") user_name: str = Field(..., example="테스트 이름") github: Optional[str] = Field(default=None) From 45f1cf3d7bd8fdfa3982e05638a5d217cba81814 Mon Sep 17 00:00:00 2001 From: sounmu Date: Tue, 24 Dec 2024 22:20:16 +0900 Subject: [PATCH 33/43] feat: Remove auth_type from user schema and add is_admin field --- src/domain/schemas/user_schemas.py | 2 +- src/domain/services/admin/user_service.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/schemas/user_schemas.py b/src/domain/schemas/user_schemas.py index 05fcfb9d..45ae034f 100644 --- a/src/domain/schemas/user_schemas.py +++ b/src/domain/schemas/user_schemas.py @@ -30,7 +30,6 @@ class DomainResPutUser(BaseModel): 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) @@ -38,6 +37,7 @@ class DomainAdminGetUserItem(BaseModel): is_active: bool = Field(title="is_active", description="활동 상태") created_at: datetime = Field(title="create_at", description="생성일시") updated_at: datetime = Field(title="update_at", description="수정일시") + is_admin: bool = Field(title="is_admin", description="관리자 권환") class DomainReqAdminPutUser(BaseModel): user_id: int = Field(title="user_id", description="관리자의 회원 ID", gt=0) diff --git a/src/domain/services/admin/user_service.py b/src/domain/services/admin/user_service.py index fc321632..92395017 100644 --- a/src/domain/services/admin/user_service.py +++ b/src/domain/services/admin/user_service.py @@ -49,7 +49,6 @@ async def service_admin_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, @@ -57,6 +56,7 @@ async def service_admin_search_users( is_active=user.is_active, created_at=user.created_at, updated_at=user.updated_at, + is_admin=True if user.admin and user.admin[-1].admin_status else False ) for user in users ] @@ -93,7 +93,6 @@ async def service_admin_read_users(db: Session) -> list[DomainAdminGetUserItem]: 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, @@ -101,6 +100,7 @@ async def service_admin_read_users(db: Session) -> list[DomainAdminGetUserItem]: is_active=user.is_active, created_at=user.created_at, updated_at=user.updated_at, + is_admin=True if user.admin and user.admin[-1].admin_status else False ) for user in users ] From 5a9ec622b36c25abacb55678cac4678929ce68b0 Mon Sep 17 00:00:00 2001 From: sounmu Date: Wed, 25 Dec 2024 23:58:14 +0900 Subject: [PATCH 34/43] feat: Add total count to book review response models --- src/routes/book_review_route.py | 3 ++- src/routes/response/book_review_response.py | 2 ++ src/routes/user_route.py | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/routes/book_review_route.py b/src/routes/book_review_route.py index a8097ed5..dc35859d 100644 --- a/src/routes/book_review_route.py +++ b/src/routes/book_review_route.py @@ -37,7 +37,8 @@ async def get_all_reviews_by_book_id( result = RouteResGetReviewListByInfoId( data=domain_res, - count=len(domain_res) + count=len(domain_res), + total=len(domain_res) ) return result diff --git a/src/routes/response/book_review_response.py b/src/routes/response/book_review_response.py index 552c08e3..375c7795 100644 --- a/src/routes/response/book_review_response.py +++ b/src/routes/response/book_review_response.py @@ -13,8 +13,10 @@ class RouteResGetReviewList(BaseModel): """ data: list[DomainResGetReviewItem] count: int = Field(description="data 배열의 요소 개수") + total: int = Field(description="data 배열의 총 요소 개수") class RouteResGetReviewListByInfoId(BaseModel): data: list[DomainResGetReviewByInfoId] count: int = Field(description="data 배열의 요소 개수") + total: int = Field(description="data 배열의 총 요소 개수") diff --git a/src/routes/user_route.py b/src/routes/user_route.py index 07322c27..ee52ac3b 100644 --- a/src/routes/user_route.py +++ b/src/routes/user_route.py @@ -127,6 +127,7 @@ async def get_all_user_reviews( result = RouteResGetReviewList( data=domain_res, - count=len(domain_res) + count=len(domain_res), + total=len(domain_res) ) return result From f86f33a0a8013a71983177b2cb24d7ae7bf2565f Mon Sep 17 00:00:00 2001 From: sounmu Date: Thu, 26 Dec 2024 00:04:38 +0900 Subject: [PATCH 35/43] fix: Replace alias for datetime in book review schemas `_datetime -> datetime` --- src/domain/schemas/book_review_schemas.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/domain/schemas/book_review_schemas.py b/src/domain/schemas/book_review_schemas.py index 480d1ea2..4bcaf838 100644 --- a/src/domain/schemas/book_review_schemas.py +++ b/src/domain/schemas/book_review_schemas.py @@ -1,4 +1,4 @@ -from datetime import datetime as _datetime +from datetime import datetime from pydantic import BaseModel, Field @@ -8,8 +8,8 @@ class DomainResGetReviewByInfoId(BaseModel): user_id: int = Field(title="user_id", description="리뷰한 사용자 ID", example=1, gt=0) user_name: str = Field(title="user_name", description="리뷰한 사용자 이름", example="test") review_content: str = Field(title="review_content", description="리뷰 내용") - 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 Review(BaseModel): @@ -17,8 +17,8 @@ class Review(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) review_content: str = Field(title="review_content", description="리뷰 내용") - 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 DomainResGetReviewItem(BaseModel): @@ -26,8 +26,8 @@ class DomainResGetReviewItem(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) review_content: str = Field(title="review_content", description="리뷰 내용") - 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()) book_title: str = Field(title="book_title", description="책 제목", example="FastAPI Tutorial") @@ -43,7 +43,7 @@ class DomainResPostReview(BaseModel): user_name: str = Field(title="user_name", description="리뷰한 사용자 이름") book_id: int = Field(title="book_id", description="리뷰한 책 정보 ID", example=1, gt=0) review_content: str = Field(title="review_content", description="리뷰 내용") - created_at: _datetime = Field(title="create_at", description="생성일시", example=_datetime.now()) + created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) class DomainReqPutReview(BaseModel): review_id: int = Field(title="book_review_id", description="리뷰 id", example=1, gt=0) From 8022e0e6c87c48b7e6696338f99af54a18a336c1 Mon Sep 17 00:00:00 2001 From: sounmu Date: Thu, 26 Dec 2024 00:06:38 +0900 Subject: [PATCH 36/43] fix: Change order of book reviews to descending by updated date --- src/domain/services/book_review_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/services/book_review_service.py b/src/domain/services/book_review_service.py index ad9e3553..d28aa51b 100644 --- a/src/domain/services/book_review_service.py +++ b/src/domain/services/book_review_service.py @@ -21,7 +21,7 @@ async def service_read_reviews_by_book_id(book_id, db: Session): select(BookReview) .options(selectinload(BookReview.user)) .where(and_(BookReview.book_id == book_id, BookReview.is_deleted == False)) - .order_by(BookReview.updated_at) + .order_by(BookReview.updated_at.desc()) ) try: reviews = db.execute(stmt).scalars().all() From 4082b1ac44801972393788578d6a8a74e0d0b8f3 Mon Sep 17 00:00:00 2001 From: sounmu Date: Thu, 26 Dec 2024 00:08:21 +0900 Subject: [PATCH 37/43] fix: Update type hint for review list response data attribute --- src/routes/response/book_review_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/response/book_review_response.py b/src/routes/response/book_review_response.py index 375c7795..c9a8d415 100644 --- a/src/routes/response/book_review_response.py +++ b/src/routes/response/book_review_response.py @@ -8,7 +8,7 @@ class RouteResGetReviewList(BaseModel): ReviewListResponse 모델은 리뷰 항목들의 목록과 해당 목록에 포함된 항목 개수를 반환하는 응답 구조입니다. Attributes: - data (List[BookReviewItem]): 리뷰 항목의 목록을 담고 있는 배열입니다. + data (list[DomainResGetReviewItem]): 리뷰 항목의 목록을 담고 있는 배열입니다. count (int): data 배열의 요소 개수를 나타냅니다. """ data: list[DomainResGetReviewItem] From 1c0100b4af5f02ce6277c1d01c97f5cb19963601 Mon Sep 17 00:00:00 2001 From: sounmu Date: Thu, 26 Dec 2024 00:17:48 +0900 Subject: [PATCH 38/43] feat: Add book title to review response and optimize query loading --- src/domain/schemas/book_review_schemas.py | 2 +- src/domain/services/book_review_service.py | 76 ++++++++++++---------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/domain/schemas/book_review_schemas.py b/src/domain/schemas/book_review_schemas.py index 4bcaf838..b53eac02 100644 --- a/src/domain/schemas/book_review_schemas.py +++ b/src/domain/schemas/book_review_schemas.py @@ -10,7 +10,7 @@ class DomainResGetReviewByInfoId(BaseModel): review_content: str = Field(title="review_content", description="리뷰 내용") created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) - + book_title: str = Field(title="book_title", description="책 제목", example="FastAPI Tutorial") class Review(BaseModel): id: int = Field(title="book_review_id", description="리뷰 정보 id", example=1, gt=0) diff --git a/src/domain/services/book_review_service.py b/src/domain/services/book_review_service.py index d28aa51b..c19a51e9 100644 --- a/src/domain/services/book_review_service.py +++ b/src/domain/services/book_review_service.py @@ -3,7 +3,7 @@ from fastapi import HTTPException, status from sqlalchemy import and_, select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session, selectinload +from sqlalchemy.orm import Session, joinedload from domain.schemas.book_review_schemas import ( DomainReqPostReview, @@ -16,16 +16,22 @@ from utils.crud_utils import delete_item, get_item -async def service_read_reviews_by_book_id(book_id, db: Session): +async def service_read_reviews_by_book_id( + book_id: int, + db: Session +) -> list[DomainResGetReviewByInfoId]: + # Using joinedload may reduce queries if the relationships are not large stmt = ( select(BookReview) - .options(selectinload(BookReview.user)) + .options( + joinedload(BookReview.user), + joinedload(BookReview.book), + ) .where(and_(BookReview.book_id == book_id, BookReview.is_deleted == False)) .order_by(BookReview.updated_at.desc()) ) try: reviews = db.execute(stmt).scalars().all() - if not reviews: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Reviews not found") @@ -34,13 +40,15 @@ async def service_read_reviews_by_book_id(book_id, db: Session): review_id=review.id, user_id=review.user_id, user_name=review.user.user_name, + book_title=review.book.book_title, review_content=review.review_content, created_at=review.created_at, updated_at=review.updated_at, ) for review in reviews ] - + except HTTPException as e: + raise e except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -51,7 +59,7 @@ async def service_read_reviews_by_book_id(book_id, db: Session): async def service_read_reviews_by_user_id( - user_id, + user_id: int, db: Session ) -> list[DomainResGetReviewItem]: stmt = ( @@ -62,37 +70,39 @@ async def service_read_reviews_by_user_id( try: reviews = db.scalars(stmt).all() # loans를 리스트로 반환 - 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 - - if not reviews: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Reviews not found" - ) - result = [] - for review in reviews: - if review.book is None: + if not reviews: raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Book with ID {review.book_id} not found for review ID {review.id}" + status_code=status.HTTP_404_NOT_FOUND, + detail="Reviews not found" ) - else: - result.append( - DomainResGetReviewItem( - review_id=review.id, - user_id=review.user_id, - book_id=review.book_id, - review_content=review.review_content, - created_at=review.created_at, - updated_at=review.updated_at, - book_title=review.book.book_title, + + result = [] + for review in reviews: + if review.book is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Book with ID {review.book_id} not found for review ID {review.id}" ) - ) + else: + result.append( + DomainResGetReviewItem( + review_id=review.id, + user_id=review.user_id, + book_id=review.book_id, + review_content=review.review_content, + created_at=review.created_at, + updated_at=review.updated_at, + book_title=review.book.book_title, + ) + ) + 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 result From b72156ea54e766ea51afc000065eb0bab4448673 Mon Sep 17 00:00:00 2001 From: sounmu Date: Thu, 26 Dec 2024 18:48:54 +0900 Subject: [PATCH 39/43] feat: Implement pagination for book and user reviews with total count in response --- src/domain/schemas/book_review_schemas.py | 12 ++++++ src/domain/services/book_review_service.py | 49 +++++++++++++++++++--- src/routes/book_review_route.py | 10 +++-- src/routes/user_route.py | 12 +++--- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/domain/schemas/book_review_schemas.py b/src/domain/schemas/book_review_schemas.py index b53eac02..7f9b32f6 100644 --- a/src/domain/schemas/book_review_schemas.py +++ b/src/domain/schemas/book_review_schemas.py @@ -12,6 +12,12 @@ class DomainResGetReviewByInfoId(BaseModel): updated_at: datetime = Field(title="update_at", description="수정일시", example=datetime.now()) book_title: str = Field(title="book_title", description="책 제목", example="FastAPI Tutorial") + +class DomainResGetReviewListByInfoId(BaseModel): + data: list[DomainResGetReviewByInfoId] + total: int = Field(description="BookReview 객체의 총 요소 개수") + + class Review(BaseModel): id: int = Field(title="book_review_id", description="리뷰 정보 id", example=1, gt=0) user_id: int = Field(title="user_id", description="리뷰한 사용자 ID", example=1, gt=0) @@ -31,6 +37,11 @@ class DomainResGetReviewItem(BaseModel): book_title: str = Field(title="book_title", description="책 제목", example="FastAPI Tutorial") +class DomainResGetReviewList(BaseModel): + data: list[DomainResGetReviewItem] + total: int = Field(description="BookReview 객체의 총 요소 개수") + + class DomainReqPostReview(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) @@ -45,6 +56,7 @@ class DomainResPostReview(BaseModel): review_content: str = Field(title="review_content", description="리뷰 내용") created_at: datetime = Field(title="create_at", description="생성일시", example=datetime.now()) + class DomainReqPutReview(BaseModel): review_id: int = Field(title="book_review_id", description="리뷰 id", example=1, gt=0) user_id: int = Field(title="user_id", description="리뷰한 사용자 ID", example=1, gt=0) diff --git a/src/domain/services/book_review_service.py b/src/domain/services/book_review_service.py index c19a51e9..b6c1f055 100644 --- a/src/domain/services/book_review_service.py +++ b/src/domain/services/book_review_service.py @@ -1,7 +1,8 @@ from datetime import datetime as _datetime +from math import ceil from fastapi import HTTPException, status -from sqlalchemy import and_, select +from sqlalchemy import and_, func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, joinedload @@ -10,6 +11,8 @@ DomainReqPutReview, DomainResGetReviewByInfoId, DomainResGetReviewItem, + DomainResGetReviewList, + DomainResGetReviewListByInfoId, DomainResPostReview, ) from repositories.models import Book, BookReview, User @@ -18,8 +21,19 @@ async def service_read_reviews_by_book_id( book_id: int, + page: int, + limit: int, db: Session -) -> list[DomainResGetReviewByInfoId]: +) -> DomainResGetReviewListByInfoId: + total = db.execute(select(func.count()).select_from(BookReview) + .where(and_(BookReview.book_id == book_id, BookReview.is_deleted == False))).scalar() + + if ceil(total/limit) < page: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Page is out of range" + ) + offset = (page - 1) * limit # Using joinedload may reduce queries if the relationships are not large stmt = ( select(BookReview) @@ -29,13 +43,14 @@ async def service_read_reviews_by_book_id( ) .where(and_(BookReview.book_id == book_id, BookReview.is_deleted == False)) .order_by(BookReview.updated_at.desc()) + .limit(limit).offset(offset) ) try: reviews = db.execute(stmt).scalars().all() if not reviews: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Reviews not found") - response = [ + result = [ DomainResGetReviewByInfoId( review_id=review.id, user_id=review.user_id, @@ -47,6 +62,12 @@ async def service_read_reviews_by_book_id( ) for review in reviews ] + + response = DomainResGetReviewListByInfoId( + data = result, + total = total + ) + except HTTPException as e: raise e except Exception as e: @@ -60,12 +81,24 @@ async def service_read_reviews_by_book_id( async def service_read_reviews_by_user_id( user_id: int, + page: int, + limit: int, db: Session -) -> list[DomainResGetReviewItem]: +) -> DomainResGetReviewList: + total = db.execute(select(func.count()).select_from(BookReview) + .where(and_(BookReview.user_id == user_id, BookReview.is_deleted == False))).scalar() + + if ceil(total/limit) < page: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Page is out of range" + ) + offset = (page - 1) * limit stmt = ( select(BookReview) .where(and_(BookReview.user_id == user_id, BookReview.is_deleted == False)) .order_by(BookReview.updated_at.desc()) + .limit(limit).offset(offset) ) try: @@ -96,6 +129,12 @@ async def service_read_reviews_by_user_id( book_title=review.book.book_title, ) ) + + response = DomainResGetReviewList( + data = result, + total = total + ) + except HTTPException as e: raise e except Exception as e: @@ -104,7 +143,7 @@ async def service_read_reviews_by_user_id( detail=f"Unexpected error occurred during retrieve: {str(e)}", ) from e - return result + return response async def service_delete_review(review_id, user_id, db: Session): diff --git a/src/routes/book_review_route.py b/src/routes/book_review_route.py index dc35859d..a27611da 100644 --- a/src/routes/book_review_route.py +++ b/src/routes/book_review_route.py @@ -31,14 +31,16 @@ ) async def get_all_reviews_by_book_id( book_id: int = Query(alias="books"), + page: int = Query(1, gt=0), + limit: int = Query(10, gt=0), db: Session = Depends(get_db), ): - domain_res = await service_read_reviews_by_book_id(book_id, db) + domain_res = await service_read_reviews_by_book_id(book_id=book_id, page=page, limit=limit, db=db) result = RouteResGetReviewListByInfoId( - data=domain_res, - count=len(domain_res), - total=len(domain_res) + data=domain_res.data, + count=len(domain_res.data), # count는 현재 page에 있는 리뷰의 개수 + total=domain_res.total # total은 총 리뷰의 개수 ) return result diff --git a/src/routes/user_route.py b/src/routes/user_route.py index ee52ac3b..84debdb1 100644 --- a/src/routes/user_route.py +++ b/src/routes/user_route.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, status +from fastapi import APIRouter, Depends, Query, status from sqlalchemy.orm import Session from dependencies import get_current_user, get_db @@ -121,13 +121,15 @@ async def put_user( ) async def get_all_user_reviews( db: Session = Depends(get_db), + page: int = Query(1, gt=0), + limit: int = Query(10, gt=0), current_user=Depends(get_current_user) ): - domain_res = await service_read_reviews_by_user_id(current_user.id, db) + domain_res = await service_read_reviews_by_user_id(user_id=current_user.id, page=page, limit=limit, db=db) result = RouteResGetReviewList( - data=domain_res, - count=len(domain_res), - total=len(domain_res) + data=domain_res.data, + count=len(domain_res.data), + total=domain_res.total ) return result From e460bf513f4cffd2789ceefdbb3ffe3a21b6bb0f Mon Sep 17 00:00:00 2001 From: sounmu Date: Thu, 26 Dec 2024 19:13:26 +0900 Subject: [PATCH 40/43] fix: Remove pagination parameters from user reviews service and route --- src/domain/services/book_review_service.py | 9 --------- src/routes/user_route.py | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/domain/services/book_review_service.py b/src/domain/services/book_review_service.py index b6c1f055..4f2ff859 100644 --- a/src/domain/services/book_review_service.py +++ b/src/domain/services/book_review_service.py @@ -81,24 +81,15 @@ async def service_read_reviews_by_book_id( async def service_read_reviews_by_user_id( user_id: int, - page: int, - limit: int, db: Session ) -> DomainResGetReviewList: total = db.execute(select(func.count()).select_from(BookReview) .where(and_(BookReview.user_id == user_id, BookReview.is_deleted == False))).scalar() - if ceil(total/limit) < page: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Page is out of range" - ) - offset = (page - 1) * limit stmt = ( select(BookReview) .where(and_(BookReview.user_id == user_id, BookReview.is_deleted == False)) .order_by(BookReview.updated_at.desc()) - .limit(limit).offset(offset) ) try: diff --git a/src/routes/user_route.py b/src/routes/user_route.py index 84debdb1..5f893ce1 100644 --- a/src/routes/user_route.py +++ b/src/routes/user_route.py @@ -125,7 +125,7 @@ async def get_all_user_reviews( limit: int = Query(10, gt=0), current_user=Depends(get_current_user) ): - domain_res = await service_read_reviews_by_user_id(user_id=current_user.id, page=page, limit=limit, db=db) + domain_res = await service_read_reviews_by_user_id(user_id=current_user.id, db=db) result = RouteResGetReviewList( data=domain_res.data, From be2a56617fe302e9778efe45739be43325cac25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Fri, 27 Dec 2024 23:07:11 +0900 Subject: [PATCH 41/43] style: adjusted indentation --- sql/__update_user_table_password.sql | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sql/__update_user_table_password.sql b/sql/__update_user_table_password.sql index 5fae63e6..901ad5d9 100644 --- a/sql/__update_user_table_password.sql +++ b/sql/__update_user_table_password.sql @@ -1,4 +1,2 @@ -ALTER TABLE - `user` -ADD - password TEXT NOT NULL; +ALTER TABLE `user` +ADD password TEXT NOT NULL; From 04baaef2fccb0f4e3ce83a7de20dcc512cb2d67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Fri, 27 Dec 2024 23:09:36 +0900 Subject: [PATCH 42/43] fix: Removed validation for duplicated names --- src/domain/services/auth_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index cc7aa322..4a09b84c 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -13,7 +13,7 @@ async def register(request: RegisterRequest, db: Session): # Check if user information exists in the DB - user = db.query(User).filter((User.user_name == request.user_name) | (User.email == request.email)).first() + user = db.query(User).filter(User.email == request.email).first() # If user information does not exist in the DB, create a new user if user is None: From d715725382a02546ef66f31e9fb6467d269b5e65 Mon Sep 17 00:00:00 2001 From: smreosms13 Date: Sat, 28 Dec 2024 17:05:28 +0900 Subject: [PATCH 43/43] chore: add passlib in requirements.txt --- requirements.txt | Bin 3106 -> 3138 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index c3a6fbe959c5f6ed0e2d32de635a55cdc6389cde..f1ed41f39238789051dc5f6d58ae627558bdbedc 100644 GIT binary patch delta 36 ocmZ1^aY$mrA2#t~hGK>shD?Se23sIBWYA+U2V#@WpV{V!Z delta 12 TcmX>ku}EUWAGXah>}sq4A~gg^