From ab99ab8832299e5cc0ce86d3796693550f79f9e6 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 13 Nov 2024 23:03:52 +0900 Subject: [PATCH 01/43] feat: post/put admin notice schemas --- src/domain/schemas/notice_schemas.py | 26 ++++++++++++++++++++ src/routes/admin/request/notice_request.py | 10 ++++++++ src/routes/admin/response/notice_response.py | 14 +++++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/routes/admin/request/notice_request.py diff --git a/src/domain/schemas/notice_schemas.py b/src/domain/schemas/notice_schemas.py index 07debbd9..51f028a3 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,28 @@ 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) + 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) + user_name: str = Field(title="user_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()]) + +class DomainReqAdminPutNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", example=1, gt=0) + user_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 DomainResAdminPutNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) + user_name: str = Field(title="user_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()]) \ No newline at end of file diff --git a/src/routes/admin/request/notice_request.py b/src/routes/admin/request/notice_request.py new file mode 100644 index 00000000..4263392c --- /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"]) \ No newline at end of file diff --git a/src/routes/admin/response/notice_response.py b/src/routes/admin/response/notice_response.py index 2e24bba3..0063b637 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) @@ -16,3 +17,16 @@ class RouteResAdminGetNoticeList(BaseModel): data: list[DomainResAdminGetNotice] count: int +class RouteResAdminPostNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) + user_name: str = Field(title="user_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()]) + +class RouteResAdminPutNotice(BaseModel): + notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) + user_name: str = Field(title="user_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()]) \ No newline at end of file From bec73d98349f4286a7a3374fa880fa791784e609 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 14 Nov 2024 19:51:57 +0900 Subject: [PATCH 02/43] fix: change http code post 201 --- src/routes/book_review_route.py | 2 +- src/routes/bookrequest_route.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/book_review_route.py b/src/routes/book_review_route.py index 14b80ac8..aa603c53 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( From 462c13903dfd3ed345428051c7980d0af9f2f3b2 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 14 Nov 2024 19:54:24 +0900 Subject: [PATCH 03/43] =?UTF-8?q?feat:=20post=20admin-notice=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/schemas/notice_schemas.py | 4 +- src/domain/services/admin/notice_service.py | 41 +++++++++++++++++- src/domain/services/notice_service.py | 3 +- src/main.py | 6 +-- src/routes/admin/notice_route.py | 44 ++++++++++++++++++-- src/routes/admin/response/notice_response.py | 4 +- 6 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/domain/schemas/notice_schemas.py b/src/domain/schemas/notice_schemas.py index 51f028a3..7e13c23e 100644 --- a/src/domain/schemas/notice_schemas.py +++ b/src/domain/schemas/notice_schemas.py @@ -20,7 +20,7 @@ class DomainResGetNotice(BaseModel): 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"]) @@ -33,7 +33,7 @@ class DomainResAdminPostNotice(BaseModel): class DomainReqAdminPutNotice(BaseModel): notice_id: int = Field(title="notice_id", description="공지사항 ID", example=1, gt=0) - user_id: int = Field(title="user_id", description="사용자 ID", example=1, 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"]) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index 4003ed7c..41742bb1 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -2,8 +2,9 @@ from sqlalchemy import 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, DomainResAdminGetNotice, DomainResAdminPostNotice +from repositories.models import Notice + async def service_admin_read_notices(page: int, limit: int, db: Session): @@ -62,3 +63,39 @@ async def service_admin_read_notice(notice_id: int, db: Session): ) return response + + + + +async def service_admin_create_notice(request: DomainReqAdminPostNotice, db: Session): + + if not request.title.strip() or not request.notice_content.strip(): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="title or notice_content is empty") + + notice = Notice( + admin_id=request.admin_id, + title=request.title, + content=request.notice_content + ) + try: + db.add(notice) + db.flush() + except Exception as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Unexpected error occurred: {str(e)}" + ) from e + + else: + db.commit() + db.refresh(notice) + + result = DomainResAdminPostNotice( + notice_id=notice.id, + admin_name=notice.user[0].user_name, + title=notice.title, + notice_content=notice.content, + created_at=notice.created_at + ) + return result + \ No newline at end of file diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index c6d127c9..8d209a9f 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -2,8 +2,9 @@ from sqlalchemy import 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): diff --git a/src/main.py b/src/main.py index 3f8de328..33dffee0 100644 --- a/src/main.py +++ b/src/main.py @@ -2,15 +2,15 @@ from fastapi.middleware.cors import CORSMiddleware from config import Settings -from routes.authentication_route import router as auth_router 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.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 from routes.books_route import router as books_router from routes.loan_route import router as loan_router -from routes.user_route import router as user_router -from routes.admin.notice_route import router as admin_notice_router from routes.notice_route import router as notice_router +from routes.user_route import router as user_router settings = Settings() diff --git a/src/routes/admin/notice_route.py b/src/routes/admin/notice_route.py index 7d82b8a0..1d37f547 100644 --- a/src/routes/admin/notice_route.py +++ b/src/routes/admin/notice_route.py @@ -1,9 +1,19 @@ -from fastapi import APIRouter, Depends, status, Query +from fastapi import APIRouter, Depends, Query, status 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 +from domain.schemas.notice_schemas import DomainReqAdminPostNotice +from domain.services.admin.notice_service import ( + service_admin_create_notice, + service_admin_read_notice, + service_admin_read_notices, +) +from routes.admin.request.notice_request import RouteReqAdminPostNotice +from routes.admin.response.notice_response import ( + RouteResAdminGetNotice, + RouteResAdminGetNoticeList, + RouteResAdminPostNotice, +) router=APIRouter( prefix="/admin/notice", @@ -57,3 +67,31 @@ async def get_notice( 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( + admin_id=current_user.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 + diff --git a/src/routes/admin/response/notice_response.py b/src/routes/admin/response/notice_response.py index 0063b637..80789009 100644 --- a/src/routes/admin/response/notice_response.py +++ b/src/routes/admin/response/notice_response.py @@ -19,14 +19,14 @@ class RouteResAdminGetNoticeList(BaseModel): class RouteResAdminPostNotice(BaseModel): notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) - user_name: str = Field(title="user_name", description="사용자 성명", examples=["사용자1", "사용자2"]) + 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) - user_name: str = Field(title="user_name", description="사용자 성명", examples=["사용자1", "사용자2"]) + 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()]) \ No newline at end of file From 87912a939438d716f2ae9ba2682b5ed70449e364 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 14 Nov 2024 19:55:50 +0900 Subject: [PATCH 04/43] fix: ruff --- src/domain/schemas/notice_schemas.py | 2 +- src/domain/services/admin/notice_service.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/domain/schemas/notice_schemas.py b/src/domain/schemas/notice_schemas.py index 7e13c23e..bf211477 100644 --- a/src/domain/schemas/notice_schemas.py +++ b/src/domain/schemas/notice_schemas.py @@ -42,4 +42,4 @@ class DomainResAdminPutNotice(BaseModel): user_name: str = Field(title="user_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()]) \ No newline at end of file + created_at: date = Field(title="created_at", description="공지사항 생성일", examples=[date.today()]) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index 41742bb1..5d880251 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -7,7 +7,7 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): - + offset=(page-1)*limit stmt =(select(Notice) @@ -25,8 +25,8 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Unexpected error occurred during retrieve: {str(e)}", ) from e - - + + response = [ DomainResAdminGetNotice( notice_id=notice.id, @@ -71,7 +71,7 @@ async def service_admin_create_notice(request: DomainReqAdminPostNotice, db: Ses if not request.title.strip() or not request.notice_content.strip(): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="title or notice_content is empty") - + notice = Notice( admin_id=request.admin_id, title=request.title, @@ -98,4 +98,3 @@ async def service_admin_create_notice(request: DomainReqAdminPostNotice, db: Ses created_at=notice.created_at ) return result - \ No newline at end of file From 20b6a6340027ca24cca019d6bdcd790cdd4c0582 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 20 Nov 2024 21:43:17 +0900 Subject: [PATCH 05/43] =?UTF-8?q?fix:=20admin=20name=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD/=20user=5Fid=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/schemas/notice_schemas.py | 5 +++-- src/domain/services/admin/notice_service.py | 1 + src/routes/admin/notice_route.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/domain/schemas/notice_schemas.py b/src/domain/schemas/notice_schemas.py index bf211477..691da33a 100644 --- a/src/domain/schemas/notice_schemas.py +++ b/src/domain/schemas/notice_schemas.py @@ -20,13 +20,14 @@ class DomainResGetNotice(BaseModel): 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) - user_name: str = Field(title="user_name", description="사용자 성명", examples=["사용자1", "사용자2"]) + 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()]) @@ -39,7 +40,7 @@ class DomainReqAdminPutNotice(BaseModel): class DomainResAdminPutNotice(BaseModel): notice_id: int = Field(title="notice_id", description="공지사항 ID", examples=[1, 2, 3], gt=0) - user_name: str = Field(title="user_name", description="사용자 성명", examples=["사용자1", "사용자2"]) + 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/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index 5d880251..a3c3ff80 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -73,6 +73,7 @@ async def service_admin_create_notice(request: DomainReqAdminPostNotice, db: Ses raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="title or notice_content is empty") notice = Notice( + user_id=request.user_id, admin_id=request.admin_id, title=request.title, content=request.notice_content diff --git a/src/routes/admin/notice_route.py b/src/routes/admin/notice_route.py index 1d37f547..5681e545 100644 --- a/src/routes/admin/notice_route.py +++ b/src/routes/admin/notice_route.py @@ -79,7 +79,8 @@ async def create_notice( current_user=Depends(get_current_admin) ): domain_req = DomainReqAdminPostNotice( - admin_id=current_user.id, + user_id=current_user.id, + admin_id=current_user.admin[0].id, title=notice_create.title, notice_content=notice_create.notice_content ) From ba9f6fa459f2a8a61ff40d7e13dcba57bda9c90f Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 20 Nov 2024 22:40:29 +0900 Subject: [PATCH 06/43] fix: change file name --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 33dffee0..f6db6c3d 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,6 @@ 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.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 @@ -11,6 +10,7 @@ from routes.loan_route import router as loan_router from routes.notice_route import router as notice_router from routes.user_route import router as user_router +from routes.admin.admin_notice_route import router as admin_notice_router settings = Settings() From be879d9123caa61e6027645e7571124707b602f7 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 20 Nov 2024 22:42:25 +0900 Subject: [PATCH 07/43] =?UTF-8?q?fix:=20ruff=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/notice_service.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index 8d209a9f..d563c617 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -7,7 +7,7 @@ async def service_read_notices(page: int, limit: int, db: Session): - + offset=(page-1)*limit stmt =(select(Notice) @@ -18,23 +18,25 @@ async def service_read_notices(page: int, limit: int, db: Session): try: notices = db.execute(stmt).scalars().all() + if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") + 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 - - + + response = [ DomainResGetNotice( notice_id=notice.id, admin_id=notice.admin_id, - admin_name=notice.user[0].user_name, + admin_name=notice.user.user_name, title=notice.title, notice_content=notice.content, - created_at=notice.created_at, + created_at=notice.created_at.date(), ) for notice in notices ] @@ -51,7 +53,7 @@ async def service_read_notice(notice_id: int, db: Session): if not notice: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notice not found") - admin_name = notice.user[0].user_name + admin_name = notice.user.user_name response = DomainResGetNotice( notice_id=notice.id, @@ -59,7 +61,7 @@ async def service_read_notice(notice_id: int, db: Session): admin_name=admin_name, title=notice.title, notice_content=notice.content, - created_at=notice.created_at + created_at=notice.created_at.date() ) return response From f222549285b8c139570d7be8e0a70d9c7a32faa5 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 20 Nov 2024 22:43:43 +0900 Subject: [PATCH 08/43] =?UTF-8?q?fix:=20user[0]->user,=20created=5Fat.date?= =?UTF-8?q?()=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/admin/notice_service.py | 12 ++++++------ .../admin/{notice_route.py => admin_notice_route.py} | 0 2 files changed, 6 insertions(+), 6 deletions(-) rename src/routes/admin/{notice_route.py => admin_notice_route.py} (100%) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index a3c3ff80..2a97d629 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -31,10 +31,10 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): DomainResAdminGetNotice( notice_id=notice.id, admin_id=notice.admin_id, - admin_name=notice.user[0].user_name, + admin_name=notice.user.user_name, title=notice.title, notice_content=notice.content, - created_at=notice.created_at, + created_at=notice.created_at.date(), ) for notice in notices ] @@ -51,7 +51,7 @@ async def service_admin_read_notice(notice_id: int, db: Session): if not notice: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notice not found") - admin_name = notice.user[0].user_name + admin_name = notice.user.user_name response = DomainResAdminGetNotice( notice_id=notice.id, @@ -59,7 +59,7 @@ async def service_admin_read_notice(notice_id: int, db: Session): admin_name=admin_name, title=notice.title, notice_content=notice.content, - created_at=notice.created_at + created_at=notice.created_at.date(), ) return response @@ -93,9 +93,9 @@ async def service_admin_create_notice(request: DomainReqAdminPostNotice, db: Ses result = DomainResAdminPostNotice( notice_id=notice.id, - admin_name=notice.user[0].user_name, + admin_name=notice.user.user_name, title=notice.title, notice_content=notice.content, - created_at=notice.created_at + created_at=notice.created_at.date(), ) return result diff --git a/src/routes/admin/notice_route.py b/src/routes/admin/admin_notice_route.py similarity index 100% rename from src/routes/admin/notice_route.py rename to src/routes/admin/admin_notice_route.py From 4d4c6e7b219e87c948c42db525c62a7f1f120bb1 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 20 Nov 2024 23:18:42 +0900 Subject: [PATCH 09/43] =?UTF-8?q?feat:=20put=20admin=20notice=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/schemas/notice_schemas.py | 2 +- src/domain/services/admin/notice_service.py | 40 +++++++++++++++++++- src/routes/admin/admin_notice_route.py | 35 ++++++++++++++++- src/routes/admin/request/notice_request.py | 2 +- src/routes/admin/response/notice_response.py | 2 +- src/routes/notice_route.py | 4 +- 6 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/domain/schemas/notice_schemas.py b/src/domain/schemas/notice_schemas.py index 691da33a..8eb29b92 100644 --- a/src/domain/schemas/notice_schemas.py +++ b/src/domain/schemas/notice_schemas.py @@ -33,7 +33,7 @@ class DomainResAdminPostNotice(BaseModel): created_at: date = Field(title="created_at", description="공지사항 생성일", examples=[date.today()]) class DomainReqAdminPutNotice(BaseModel): - notice_id: int = Field(title="notice_id", description="공지사항 ID", example=1, gt=0) + 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"]) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index 2a97d629..d1c986e0 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -2,7 +2,13 @@ from sqlalchemy import select from sqlalchemy.orm.session import Session -from domain.schemas.notice_schemas import DomainReqAdminPostNotice, DomainResAdminGetNotice, DomainResAdminPostNotice +from domain.schemas.notice_schemas import ( + DomainReqAdminPostNotice, + DomainReqAdminPutNotice, + DomainResAdminGetNotice, + DomainResAdminPostNotice, + DomainResAdminPutNotice, +) from repositories.models import Notice @@ -99,3 +105,35 @@ async def service_admin_create_notice(request: DomainReqAdminPostNotice, db: Ses created_at=notice.created_at.date(), ) return result + + +async def service_admin_update_notice(notice_id: int, request: DomainReqAdminPutNotice, db: Session): + stmt = select(Notice).where(Notice.id == notice_id) + notice = db.execute(stmt).scalar_one_or_none() + + if not notice: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Notice not found") + + try: + notice.title = request.title + notice.content = request.notice_content + db.add(notice) + db.flush() + except Exception as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Unexpected error occurred: {str(e)}" + ) from e + else: + db.commit() + db.refresh(notice) + + domain_res = DomainResAdminPutNotice( + notice_id=notice.id, + title=notice.title, + notice_content=notice.content, + admin_name=notice.user.user_name, + created_at=notice.created_at.date(), + ) + return domain_res + diff --git a/src/routes/admin/admin_notice_route.py b/src/routes/admin/admin_notice_route.py index 5681e545..6fccc355 100644 --- a/src/routes/admin/admin_notice_route.py +++ b/src/routes/admin/admin_notice_route.py @@ -2,17 +2,19 @@ from sqlalchemy.orm import Session from dependencies import get_current_admin, get_db -from domain.schemas.notice_schemas import DomainReqAdminPostNotice +from domain.schemas.notice_schemas import DomainReqAdminPostNotice, DomainReqAdminPutNotice from domain.services.admin.notice_service import ( service_admin_create_notice, service_admin_read_notice, service_admin_read_notices, + service_admin_update_notice, ) -from routes.admin.request.notice_request import RouteReqAdminPostNotice +from routes.admin.request.notice_request import RouteReqAdminPostNotice, RouteReqAdminPutNotice from routes.admin.response.notice_response import ( RouteResAdminGetNotice, RouteResAdminGetNoticeList, RouteResAdminPostNotice, + RouteResAdminPutNotice, ) router=APIRouter( @@ -96,3 +98,32 @@ async def create_notice( 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 diff --git a/src/routes/admin/request/notice_request.py b/src/routes/admin/request/notice_request.py index 4263392c..4f8aaaf2 100644 --- a/src/routes/admin/request/notice_request.py +++ b/src/routes/admin/request/notice_request.py @@ -7,4 +7,4 @@ class RouteReqAdminPostNotice(BaseModel): class RouteReqAdminPutNotice(BaseModel): title: str = Field(title="title", description="공지사항 제목", examples=["공지사항 제목1"]) - notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) \ No newline at end of file + notice_content: str = Field(title="notice content", description="공지사항 내용", examples=["공지사항 내용1"]) diff --git a/src/routes/admin/response/notice_response.py b/src/routes/admin/response/notice_response.py index 80789009..d0f154fc 100644 --- a/src/routes/admin/response/notice_response.py +++ b/src/routes/admin/response/notice_response.py @@ -29,4 +29,4 @@ class RouteResAdminPutNotice(BaseModel): 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()]) \ No newline at end of file + created_at: date = Field(title="created_at", description="공지사항 생성일", examples=[date.today()]) diff --git a/src/routes/notice_route.py b/src/routes/notice_route.py index 8c67f460..9ec3d3e8 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( From 217aa20583bd002b3f100a308f29409fd1e9871b Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 21 Nov 2024 16:08:19 +0900 Subject: [PATCH 10/43] fix: 404 error --- src/domain/services/admin/notice_service.py | 2 ++ src/domain/services/notice_service.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index d1c986e0..91b793ad 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -26,6 +26,8 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): notices = db.execute(stmt).scalars().all() if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") + except HTTPException as e: + raise e except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index d563c617..f8655d5c 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -21,7 +21,8 @@ async def service_read_notices(page: int, limit: int, db: Session): if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") - + except HTTPException as e: + raise e except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, From a2b41d8aaf8574bf26a68820c266afa6807b6002 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 21 Nov 2024 16:08:39 +0900 Subject: [PATCH 11/43] =?UTF-8?q?fix:=20page=3D1,=20limit=3D7=20default=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/notice_route.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/notice_route.py b/src/routes/notice_route.py index 9ec3d3e8..6584abb0 100644 --- a/src/routes/notice_route.py +++ b/src/routes/notice_route.py @@ -19,8 +19,8 @@ ) 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) ): From dcc85c9f9272080ec0a197c9a39845a684d2dd5a Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 21 Nov 2024 16:09:11 +0900 Subject: [PATCH 12/43] =?UTF-8?q?fix:=20page=3D1,=20limit=3D7=20default=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/admin/admin_notice_route.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/admin/admin_notice_route.py b/src/routes/admin/admin_notice_route.py index 6fccc355..74e11b0a 100644 --- a/src/routes/admin/admin_notice_route.py +++ b/src/routes/admin/admin_notice_route.py @@ -31,8 +31,8 @@ ) 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_admin) ): From e465f4b5e12d2d35b5ebf71701eb178c0700166c Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 21 Nov 2024 16:28:00 +0900 Subject: [PATCH 13/43] =?UTF-8?q?fix:=20404=20Error=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/admin/notice_service.py | 17 +++++++---------- src/domain/services/notice_service.py | 17 +++++++---------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index 91b793ad..ff200615 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -26,16 +26,7 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): notices = db.execute(stmt).scalars().all() if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") - 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 - - - response = [ + response =[ DomainResAdminGetNotice( notice_id=notice.id, admin_id=notice.admin_id, @@ -47,6 +38,12 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): for notice in notices ] + 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 response diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index f8655d5c..83cb746f 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -21,16 +21,7 @@ async def service_read_notices(page: int, limit: int, db: Session): if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") - 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 - - - response = [ + response = [ DomainResGetNotice( notice_id=notice.id, admin_id=notice.admin_id, @@ -42,6 +33,12 @@ async def service_read_notices(page: int, limit: int, db: Session): for notice in notices ] + 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 response From 26c75a01d0fe92897a4976997f61f4b283b13201 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 21 Nov 2024 16:35:00 +0900 Subject: [PATCH 14/43] =?UTF-8?q?fix:=20count->total=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin/admin_notice_route.py | 2 +- src/routes/admin/response/notice_response.py | 2 +- src/routes/notice_route.py | 2 +- src/routes/response/notice_response.py | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes/admin/admin_notice_route.py b/src/routes/admin/admin_notice_route.py index 74e11b0a..58446122 100644 --- a/src/routes/admin/admin_notice_route.py +++ b/src/routes/admin/admin_notice_route.py @@ -39,7 +39,7 @@ async def get_all_notices( domain_res = await service_admin_read_notices(page, limit, db) response = RouteResAdminGetNoticeList( data=domain_res, - count=len(domain_res) + total=len(domain_res) ) return response diff --git a/src/routes/admin/response/notice_response.py b/src/routes/admin/response/notice_response.py index d0f154fc..8055762e 100644 --- a/src/routes/admin/response/notice_response.py +++ b/src/routes/admin/response/notice_response.py @@ -15,7 +15,7 @@ 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) diff --git a/src/routes/notice_route.py b/src/routes/notice_route.py index 6584abb0..8a78bbe2 100644 --- a/src/routes/notice_route.py +++ b/src/routes/notice_route.py @@ -27,7 +27,7 @@ async def get_all_notices( domain_res = await service_read_notices(page, limit, db) response = RouteResGetNoticeList( data=domain_res, - count=len(domain_res) + total=len(domain_res) ) return response diff --git a/src/routes/response/notice_response.py b/src/routes/response/notice_response.py index f573d673..9131dff5 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) @@ -14,5 +15,5 @@ class RouteResGetNotice(BaseModel): class RouteResGetNoticeList(BaseModel): data: list[DomainResGetNotice] - count: int + total: int From ed7e35c3795ffe727f3ecfc8aa94b077988c9eb9 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Thu, 21 Nov 2024 22:30:51 +0900 Subject: [PATCH 15/43] =?UTF-8?q?fix:=20total=20=EC=B6=94=EA=B0=80,=20is?= =?UTF-8?q?=5Fdeleted=3D=3DFalse=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/admin/notice_service.py | 41 ++++++++++++-------- src/domain/services/notice_service.py | 11 ++++-- src/routes/admin/admin_notice_route.py | 5 ++- src/routes/admin/response/notice_response.py | 1 + src/routes/notice_route.py | 5 ++- src/routes/response/notice_response.py | 1 + 6 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index ff200615..c7223b15 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -1,5 +1,5 @@ from fastapi import HTTPException, status -from sqlalchemy import select +from sqlalchemy import and_, select from sqlalchemy.orm.session import Session from domain.schemas.notice_schemas import ( @@ -17,26 +17,33 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): offset=(page-1)*limit stmt =(select(Notice) + .where(Notice.is_deleted == False) .order_by(Notice.created_at.desc()) .limit(limit) .offset(offset) ) + count_stmt=select(Notice).where(Notice.is_deleted == False) + try: notices = db.execute(stmt).scalars().all() + total=len(db.execute(count_stmt).scalars().all()) if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") - response =[ - DomainResAdminGetNotice( - notice_id=notice.id, - admin_id=notice.admin_id, - admin_name=notice.user.user_name, - title=notice.title, - notice_content=notice.content, - created_at=notice.created_at.date(), - ) - for notice in notices - ] + elif not total: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Fetch incorrect total value") + response = [ + DomainResAdminGetNotice( + notice_id=notice.id, + admin_id=notice.admin_id, + admin_name=notice.user.user_name, + title=notice.title, + notice_content=notice.content, + created_at=notice.created_at.date(), + ) + for notice in notices + ] + except Exception as e: raise HTTPException( @@ -44,13 +51,13 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): detail=f"Unexpected error occurred during retrieve: {str(e)}", ) from e - return response + return response, total async def service_admin_read_notice(notice_id: int, db: Session): - stmt = select(Notice).where(Notice.id == notice_id) + stmt = select(Notice).where(and_(Notice.id == notice_id, Notice.is_deleted == False)) notice = db.execute(stmt).scalar() if not notice: @@ -89,7 +96,8 @@ async def service_admin_create_notice(request: DomainReqAdminPostNotice, db: Ses except Exception as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Unexpected error occurred: {str(e)}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error occurred while creating notice: {str(e)}" ) from e else: @@ -121,7 +129,8 @@ async def service_admin_update_notice(notice_id: int, request: DomainReqAdminPut except Exception as e: db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Unexpected error occurred: {str(e)}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Unexpected error occurred while updating notice: {str(e)}" ) from e else: db.commit() diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index 83cb746f..6d7f0930 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -1,5 +1,5 @@ from fastapi import HTTPException, status -from sqlalchemy import select +from sqlalchemy import and_, select from sqlalchemy.orm.session import Session from domain.schemas.notice_schemas import DomainResGetNotice @@ -11,6 +11,7 @@ async def service_read_notices(page: int, limit: int, db: Session): offset=(page-1)*limit stmt =(select(Notice) + .where(Notice.is_deleted == False) .order_by(Notice.created_at.desc()) .limit(limit) .offset(offset) @@ -18,9 +19,13 @@ async def service_read_notices(page: int, limit: int, db: Session): try: notices = db.execute(stmt).scalars().all() + total=len(db.execute(stmt).scalars().all()) if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") + elif not total: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Fetch incorrect total value") + response = [ DomainResGetNotice( notice_id=notice.id, @@ -39,13 +44,13 @@ async def service_read_notices(page: int, limit: int, db: Session): detail=f"Unexpected error occurred during retrieve: {str(e)}", ) from e - return response + return response, total async def service_read_notice(notice_id: int, db: Session): - stmt = select(Notice).where(Notice.id == notice_id) + stmt = select(Notice).where(and_(Notice.id == notice_id, Notice.is_deleted == False)) notice = db.execute(stmt).scalar() if not notice: diff --git a/src/routes/admin/admin_notice_route.py b/src/routes/admin/admin_notice_route.py index 58446122..5b50d1ef 100644 --- a/src/routes/admin/admin_notice_route.py +++ b/src/routes/admin/admin_notice_route.py @@ -36,10 +36,11 @@ async def get_all_notices( db: Session=Depends(get_db), current_user=Depends(get_current_admin) ): - domain_res = await service_admin_read_notices(page, limit, db) + domain_res, total = await service_admin_read_notices(page, limit, db) response = RouteResAdminGetNoticeList( data=domain_res, - total=len(domain_res) + total=total, + count=len(domain_res) ) return response diff --git a/src/routes/admin/response/notice_response.py b/src/routes/admin/response/notice_response.py index 8055762e..a4c999ef 100644 --- a/src/routes/admin/response/notice_response.py +++ b/src/routes/admin/response/notice_response.py @@ -15,6 +15,7 @@ class RouteResAdminGetNotice(BaseModel): class RouteResAdminGetNoticeList(BaseModel): data: list[DomainResAdminGetNotice] + count: int total: int class RouteResAdminPostNotice(BaseModel): diff --git a/src/routes/notice_route.py b/src/routes/notice_route.py index 8a78bbe2..6f4a9dd3 100644 --- a/src/routes/notice_route.py +++ b/src/routes/notice_route.py @@ -24,10 +24,11 @@ async def get_all_notices( 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=len(domain_res) + total=total, + count=len(domain_res) ) return response diff --git a/src/routes/response/notice_response.py b/src/routes/response/notice_response.py index 9131dff5..9fef4df8 100644 --- a/src/routes/response/notice_response.py +++ b/src/routes/response/notice_response.py @@ -15,5 +15,6 @@ class RouteResGetNotice(BaseModel): class RouteResGetNoticeList(BaseModel): data: list[DomainResGetNotice] + count: int total: int From 3fa4ee7a698a13a7ccdec060f0d4ebde0548e0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 15:42:59 +0900 Subject: [PATCH 16/43] feat: put access_token in header --- src/domain/services/auth_service.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index eb0eb7c5..8cd182e3 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -1,4 +1,5 @@ from fastapi import HTTPException, status +from fastapi.responses import JSONResponse from sqlalchemy.orm import Session from config import Settings @@ -30,16 +31,14 @@ 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["Authentication"] = token_response["access_token"] + return response # firebase를 사용한 로그인 @@ -107,10 +106,4 @@ async def login_with_username( return { "token": token_response, - "user": { - "id": user.id, - "user_name": user.user_name, - "is_active": user.is_active, - "email": user.email - } } From 38da13f0b0f94b5f44ad07b38dd655562c3b303f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 15:44:09 +0900 Subject: [PATCH 17/43] feat: token expired --- src/dependencies.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dependencies.py b/src/dependencies.py index 8a814f66..c5a7dc08 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import date, datetime from fastapi import Depends, Header, HTTPException, status from jose import jwt @@ -25,6 +25,11 @@ async def get_current_user(token=Header(None), db: Session = Depends(get_db)): ) payload = jwt.decode(token, key=Settings().JWT_SECRET_KEY, algorithms=Settings().JWT_ALGORITHM) user_id: int = int(payload.get("sub")) + if payload.get("exp") < datetime.now(): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token expired", + ) if user_id is None: raise credentials_exception user = db.query(User).filter(User.id == user_id).first() From d0b270cd11bf484968e64fc45a1a4cffd5054ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 15:44:55 +0900 Subject: [PATCH 18/43] style: change authentication -> autorizaiton --- 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 8cd182e3..8a7e9dc0 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -37,7 +37,7 @@ async def register(request: RegisterRequest, db: Session): "is_active": user.is_active, "email": user.email }, status_code=status.HTTP_201_CREATED) - response.headers["Authentication"] = token_response["access_token"] + response.headers["Authorization"] = token_response["access_token"] return response # firebase를 사용한 로그인 From fdac992cc353e7a2e93cde9c5e988a4a9236b58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 16:02:35 +0900 Subject: [PATCH 19/43] fix: timestamp payload.exp --- src/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dependencies.py b/src/dependencies.py index c5a7dc08..6c70e554 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -25,7 +25,7 @@ async def get_current_user(token=Header(None), db: Session = Depends(get_db)): ) payload = jwt.decode(token, key=Settings().JWT_SECRET_KEY, algorithms=Settings().JWT_ALGORITHM) user_id: int = int(payload.get("sub")) - if payload.get("exp") < datetime.now(): + if datetime.fromtimestamp(payload.get("exp")) < datetime.now(): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired", From e4723aa4484ff413063644a3e2fca6bfe0c6c524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 16:13:47 +0900 Subject: [PATCH 20/43] feat: refresh token in Cookie --- src/domain/services/auth_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index 8a7e9dc0..1f3b88cf 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -38,6 +38,8 @@ async def register(request: RegisterRequest, db: Session): "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를 사용한 로그인 From f01ab7309f7723ab195be24cc9334851f82199fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 16:14:31 +0900 Subject: [PATCH 21/43] feat: add "Authorization" in expose_header --- src/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.py b/src/main.py index 815165d6..2411c273 100644 --- a/src/main.py +++ b/src/main.py @@ -45,7 +45,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) @@ -85,4 +85,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 서버입니다!"} From 252a0fa8a5b082296c56650ab657437be449976a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 16:17:48 +0900 Subject: [PATCH 22/43] feat: modify token in login_with_username --- src/domain/services/auth_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index 1f3b88cf..078b4e4b 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -105,7 +105,7 @@ async def login_with_username( # Create JWT tokens token_response = create_user_tokens(user.id) - - return { - "token": token_response, - } + response = JSONResponse() + response.headers["Authorization"] = token_response["access_token"] + response.set_cookie(key="refresh_token", value=token_response["refresh_token"]) + return response From 2821b0e1e0c80f40a1edfda56a1d1511fc222815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:01:11 +0900 Subject: [PATCH 23/43] fix: token expired with 401 code --- src/dependencies.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/dependencies.py b/src/dependencies.py index 6c70e554..3deb4e15 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -23,19 +23,25 @@ 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 datetime.fromtimestamp(payload.get("exp")) < datetime.now(): + 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 + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise credentials_exception + return user + except jwt.ExpiredSignatureError as err: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token expired", - ) - 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 + detail="Token has expired", + ) from err + except jwt.InvalidTokenError as err: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + ) from err def get_current_active_user(user: User = Depends(get_current_user)): From 1e819195d60ec7e5906e9e7c0ce153d5c02d3bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:09:04 +0900 Subject: [PATCH 24/43] fix: add "user" in body in login_with_username --- src/domain/services/auth_service.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index 078b4e4b..3e880140 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -1,10 +1,10 @@ -from fastapi import HTTPException, status +from fastapi import HTTPException, Request, 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 @@ -105,7 +105,12 @@ async def login_with_username( # Create JWT tokens token_response = create_user_tokens(user.id) - response = JSONResponse() + 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 From df6cf3ae6e3935f00ab2ca235177e31746118284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:11:08 +0900 Subject: [PATCH 25/43] style: delete import datetime --- src/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dependencies.py b/src/dependencies.py index 3deb4e15..2e847165 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -1,4 +1,4 @@ -from datetime import date, datetime +from datetime import date from fastapi import Depends, Header, HTTPException, status from jose import jwt From f8afce49d6c1efb2fa24762fd7ad4c12b3684bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:15:17 +0900 Subject: [PATCH 26/43] fix: Invalid token 400 error --- src/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dependencies.py b/src/dependencies.py index 2e847165..9c80299e 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -39,7 +39,7 @@ async def get_current_user(token=Header(None), db: Session = Depends(get_db)): ) from err except jwt.InvalidTokenError as err: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, + status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token", ) from err From 142abdbaca9b7a4e0b151ac0a508cb3370954823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:20:29 +0900 Subject: [PATCH 27/43] feat: refresh_user_tokens --- src/domain/services/token_service.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/domain/services/token_service.py b/src/domain/services/token_service.py index 4914a937..b743a63b 100644 --- a/src/domain/services/token_service.py +++ b/src/domain/services/token_service.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta, timezone +from fastapi import HTTPException, status from jose import jwt from config import Settings @@ -70,3 +71,24 @@ def create_user_tokens(user_id: int) -> 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) + # 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 jwt.ExpiredSignatureError as err: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Refresh Token has expired", + ) from err + except jwt.InvalidTokenError as err: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid token", + ) from err From 370bdc5b43f8b1136f0297588367aee4bf190d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:21:40 +0900 Subject: [PATCH 28/43] feat: service_refresh_token --- src/domain/services/auth_service.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index 3e880140..b78e8728 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -114,3 +114,15 @@ async def login_with_username( 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): + if not refresh_token or not access_token: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Token is missed" + ) + token_response = refresh_user_tokens(access_token, refresh_token) + response = JSONResponse(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 From f36f6300750e770bbdcb49ac5b4e648cf6629bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:22:15 +0900 Subject: [PATCH 29/43] feat: POST refresh-token route --- src/routes/authentication_route.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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) From 1c7c9d162f187b2b472c55a793b5ad7e2df79280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:31:48 +0900 Subject: [PATCH 30/43] fix: modify jwt error --- src/dependencies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dependencies.py b/src/dependencies.py index 9c80299e..1b4c4859 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -1,7 +1,7 @@ from datetime import date from fastapi import Depends, Header, HTTPException, status -from jose import jwt +from jose import ExpiredSignatureError, JWTError, jwt from sqlalchemy.orm import Session from config import Settings @@ -32,12 +32,12 @@ async def get_current_user(token=Header(None), db: Session = Depends(get_db)): if user is None: raise credentials_exception return user - except jwt.ExpiredSignatureError as err: + except ExpiredSignatureError as err: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has expired", ) from err - except jwt.InvalidTokenError as err: + except JWTError as err: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token", From 60d05b17d80391470acca1752e2f36b7ee1931e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:32:24 +0900 Subject: [PATCH 31/43] fix: add content=None in JSONresponse --- src/domain/services/auth_service.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/domain/services/auth_service.py b/src/domain/services/auth_service.py index b78e8728..aef6eb97 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -116,13 +116,8 @@ async def login_with_username( return response async def service_refresh_token(access_token: str, refresh_token: str): - if not refresh_token or not access_token: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Token is missed" - ) token_response = refresh_user_tokens(access_token, refresh_token) - response = JSONResponse(status_code=status.HTTP_202_ACCEPTED) + 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 From a92a8baa737d2ad9e1793d18a2860252f00bf558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:33:54 +0900 Subject: [PATCH 32/43] fix: add access token validation of expiration --- src/domain/services/token_service.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/domain/services/token_service.py b/src/domain/services/token_service.py index b743a63b..77409f7d 100644 --- a/src/domain/services/token_service.py +++ b/src/domain/services/token_service.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta, timezone from fastapi import HTTPException, status -from jose import jwt +from jose import ExpiredSignatureError, JWTError, jwt from config import Settings @@ -76,18 +76,23 @@ 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 jwt.ExpiredSignatureError as err: + except ExpiredSignatureError as err: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Refresh Token has expired", ) from err - except jwt.InvalidTokenError as err: + except JWTError as err: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token", From 597a91c73deb239c18f8dc8bcc448252b359e343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Sat, 23 Nov 2024 17:34:51 +0900 Subject: [PATCH 33/43] style: delete import Request --- 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 aef6eb97..ca67fb0e 100644 --- a/src/domain/services/auth_service.py +++ b/src/domain/services/auth_service.py @@ -1,4 +1,4 @@ -from fastapi import HTTPException, Request, status +from fastapi import HTTPException, status from fastapi.responses import JSONResponse from sqlalchemy.orm import Session From 958b291d4b95071bd8bce1ba7aff25130a31f1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EC=9C=A0=EB=82=98?= Date: Tue, 26 Nov 2024 21:01:36 +0900 Subject: [PATCH 34/43] fix: db.query.filter -> select.where --- src/dependencies.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dependencies.py b/src/dependencies.py index 1b4c4859..98349578 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -2,6 +2,7 @@ from fastapi import Depends, Header, HTTPException, status from jose import ExpiredSignatureError, JWTError, jwt +from sqlalchemy import select from sqlalchemy.orm import Session from config import Settings @@ -28,7 +29,8 @@ async def get_current_user(token=Header(None), db: Session = Depends(get_db)): 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() + stmt = select(User).where(User.id == user_id) + user = db.execute(stmt).scalar_one() if user is None: raise credentials_exception return user From 044097f4bfd15603c6e5f5c2812ab5199f971e1c Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 27 Nov 2024 21:42:02 +0900 Subject: [PATCH 35/43] =?UTF-8?q?fix:=20offset=EB=B3=B4=EB=8B=A4=20?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A9=B4=20400=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/admin/notice_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index c7223b15..b8da8031 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -32,6 +32,8 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") elif not total: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Fetch incorrect total value") + elif total < offset: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Requested page is out of range") response = [ DomainResAdminGetNotice( notice_id=notice.id, From d096689422501e2a211b1cc1565c8d5dee01cd63 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 27 Nov 2024 22:00:03 +0900 Subject: [PATCH 36/43] =?UTF-8?q?fix:=20error=20404=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/admin/notice_service.py | 10 ++++++---- src/domain/services/notice_service.py | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index b8da8031..685eb91c 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -28,12 +28,13 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): try: notices = db.execute(stmt).scalars().all() total=len(db.execute(count_stmt).scalars().all()) - if not notices: + if total < offset: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Requested page is out of range") + elif not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") elif not total: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Fetch incorrect total value") - elif total < offset: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Requested page is out of range") + response = [ DomainResAdminGetNotice( notice_id=notice.id, @@ -45,7 +46,8 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): ) for notice in notices ] - + except HTTPException as e: + raise e except Exception as e: raise HTTPException( diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index 6d7f0930..931e0e5e 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -20,7 +20,8 @@ async def service_read_notices(page: int, limit: int, db: Session): try: notices = db.execute(stmt).scalars().all() total=len(db.execute(stmt).scalars().all()) - + if total < offset: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Requested page is out of range") if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") elif not total: @@ -37,6 +38,8 @@ async def service_read_notices(page: int, limit: int, db: Session): ) for notice in notices ] + except HTTPException as e: + raise e except Exception as e: raise HTTPException( From 4b393bb9a599179faacd9a3ff78cb52765f94d41 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 27 Nov 2024 22:07:23 +0900 Subject: [PATCH 37/43] fix: ruff --- src/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.py b/src/main.py index 78c049be..ed4fe3a9 100644 --- a/src/main.py +++ b/src/main.py @@ -14,7 +14,6 @@ from routes.notice_route import router as notice_router from routes.user_route import router as user_router - settings = Settings() app = FastAPI( From 30eaa003ad8c75ef88b65694f4d1ab826280c6ef Mon Sep 17 00:00:00 2001 From: Moonsu Kang Date: Wed, 27 Nov 2024 22:14:52 +0900 Subject: [PATCH 38/43] =?UTF-8?q?Feat/admin-loans=20=EB=8C=80=EC=B6=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: admin-loans 대출 조회 기능 추가 * feat: current와 completed 통합, 스키마 이름 내용 변경 * feat: 어드민 유저 이름 검색 기능 구현 * feat: 어드민 도서 제목 검색 구현 * fix: GET books/{book_id} 버그 수정 * fix: get_current_admin으로 변경 * feat: admin user, book 검색 기능 개선 * feat: Admin User 검색 기능 None 검색도 허용 * feat: loan 검색, 조회 기능 추가, 코드 가독성 개선, book & user 검색 조회 기능 구현 * feat: 라우트에서 Annotated 사용, book_service 먼저 full text index 적용 * feat: loan_service, user_service에도 full text index 추가, 도메인 스키마 추가, 라우트 단에서 리스트 만들기로 변경 * fix: 서비스 함수 반환 list[] * feat: search_ 에서 read_ 함수 분리, schemas/admin/... 삭제 * fix: 라우트 response 스키마 import 수정 * style: tags in admin routes --------- Co-authored-by: smreosms13 <76930385+smreosms13@users.noreply.github.com> --- src/domain/schemas/book_schemas.py | 22 +++- src/domain/schemas/loan_schemas.py | 42 ++++--- src/domain/schemas/user_schemas.py | 13 ++ src/domain/services/admin/__init__.py | 0 src/domain/services/admin/book_service.py | 135 ++++++++++++++++++++- src/domain/services/admin/loan_service.py | 125 +++++++++++++++++++ src/domain/services/admin/user_service.py | 107 ++++++++++++++++ src/main.py | 11 +- src/routes/admin/__init__.py | 0 src/routes/admin/book_route.py | 79 ++++++++++++ src/routes/admin/loan_route.py | 74 +++++++++++ src/routes/admin/response/book_response.py | 8 ++ src/routes/admin/response/loan_response.py | 9 ++ src/routes/admin/response/user_response.py | 8 ++ src/routes/admin/user_route.py | 70 +++++++++++ src/routes/response/book_response.py | 5 +- 16 files changed, 686 insertions(+), 22 deletions(-) create mode 100644 src/domain/services/admin/__init__.py create mode 100644 src/domain/services/admin/loan_service.py create mode 100644 src/domain/services/admin/user_service.py create mode 100644 src/routes/admin/__init__.py create mode 100644 src/routes/admin/book_route.py create mode 100644 src/routes/admin/loan_route.py create mode 100644 src/routes/admin/response/loan_response.py create mode 100644 src/routes/admin/response/user_response.py create mode 100644 src/routes/admin/user_route.py 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/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/user_service.py b/src/domain/services/admin/user_service.py new file mode 100644 index 00000000..a4e08117 --- /dev/null +++ b/src/domain/services/admin/user_service.py @@ -0,0 +1,107 @@ +from fastapi import HTTPException, status +from sqlalchemy import select, text +from sqlalchemy.orm import Session, selectinload + +from domain.schemas.user_schemas import DomainAdminGetUserItem +from repositories.models import User + + +async def service_admin_search_users( + user_name: str | None, + authority: bool | None, + active: bool | None, + db: Session +) -> 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/main.py b/src/main.py index 2411c273..3d2f4b03 100644 --- a/src/main.py +++ b/src/main.py @@ -4,8 +4,13 @@ from starlette.exceptions import HTTPException as StarletteHTTPException from config import Settings + from routes.admin.admin_books_route import router as admin_books_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.notice_route import router as admin_notice_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 @@ -54,9 +59,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) 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/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/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/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/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()) + From 95819cfacd70eb0cd2183fdbf80e0679c6de09a4 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 27 Nov 2024 22:52:09 +0900 Subject: [PATCH 39/43] =?UTF-8?q?fix:=20count=5Fstmt=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20error=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/admin/notice_service.py | 23 ++++++++------------- src/domain/services/notice_service.py | 20 +++++++----------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index 685eb91c..49228a8b 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -1,5 +1,5 @@ from fastapi import HTTPException, status -from sqlalchemy import and_, select +from sqlalchemy import and_, func, select from sqlalchemy.orm.session import Session from domain.schemas.notice_schemas import ( @@ -16,24 +16,19 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): offset=(page-1)*limit - stmt =(select(Notice) - .where(Notice.is_deleted == False) - .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: - notices = db.execute(stmt).scalars().all() - total=len(db.execute(count_stmt).scalars().all()) - if total < offset: + + total=db.execute(total_stmt).scalar() + if total < page*limit: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Requested page is out of range") - elif not notices: + + notices = db.execute(stmt).scalars().all() + if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") - elif not total: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Fetch incorrect total value") response = [ DomainResAdminGetNotice( diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index 931e0e5e..4c5fd69c 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -1,5 +1,5 @@ from fastapi import HTTPException, status -from sqlalchemy import and_, select +from sqlalchemy import and_, func, select from sqlalchemy.orm.session import Session from domain.schemas.notice_schemas import DomainResGetNotice @@ -10,22 +10,18 @@ async def service_read_notices(page: int, limit: int, db: Session): offset=(page-1)*limit - stmt =(select(Notice) - .where(Notice.is_deleted == False) - .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: - notices = db.execute(stmt).scalars().all() - total=len(db.execute(stmt).scalars().all()) - if total < offset: + total=db.execute(total_stmt).scalar() + if total < page*limit: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Requested page is out of range") + + notices = db.execute(stmt).scalars().all() if not notices: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notices not found") - elif not total: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Fetch incorrect total value") response = [ DomainResGetNotice( From 3e057c3986a478612c198a17f4b2a641d0ca0f40 Mon Sep 17 00:00:00 2001 From: smreosms13 <76930385+smreosms13@users.noreply.github.com> Date: Wed, 27 Nov 2024 23:08:31 +0900 Subject: [PATCH 40/43] Revert "Merge pull request #88 from kucc/exp" From a4a829b43833c2193959b95702d9983259e8302d Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 27 Nov 2024 23:19:13 +0900 Subject: [PATCH 41/43] fix: ruff --- src/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main.py b/src/main.py index db3aa84f..28aada0b 100644 --- a/src/main.py +++ b/src/main.py @@ -4,13 +4,11 @@ from starlette.exceptions import HTTPException as StarletteHTTPException from config import Settings - from routes.admin.admin_books_route import router as admin_books_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 From c197e04509c577d8fae1bae2323e7e395bf8e228 Mon Sep 17 00:00:00 2001 From: ywnjeong Date: Wed, 27 Nov 2024 23:20:02 +0900 Subject: [PATCH 42/43] =?UTF-8?q?fix:=20page,=20limit=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/notice_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/domain/services/notice_service.py b/src/domain/services/notice_service.py index 4c5fd69c..8776ab3e 100644 --- a/src/domain/services/notice_service.py +++ b/src/domain/services/notice_service.py @@ -1,3 +1,5 @@ +from math import ceil + from fastapi import HTTPException, status from sqlalchemy import and_, func, select from sqlalchemy.orm.session import Session @@ -16,7 +18,7 @@ async def service_read_notices(page: int, limit: int, db: Session): try: total=db.execute(total_stmt).scalar() - if total < page*limit: + if ceil(total/limit) Date: Wed, 27 Nov 2024 23:20:51 +0900 Subject: [PATCH 43/43] =?UTF-8?q?feat:=20delete=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/services/admin/notice_service.py | 8 +++++++- src/routes/admin/admin_notice_route.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/domain/services/admin/notice_service.py b/src/domain/services/admin/notice_service.py index 49228a8b..b3560b6b 100644 --- a/src/domain/services/admin/notice_service.py +++ b/src/domain/services/admin/notice_service.py @@ -1,3 +1,5 @@ +from math import ceil + from fastapi import HTTPException, status from sqlalchemy import and_, func, select from sqlalchemy.orm.session import Session @@ -10,6 +12,7 @@ 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): @@ -23,7 +26,7 @@ async def service_admin_read_notices(page: int, limit: int, db: Session): try: total=db.execute(total_stmt).scalar() - if total < page*limit: + if ceil(total/limit)