diff --git a/.gitignore b/.gitignore index 5fb2f2f..665a501 100644 --- a/.gitignore +++ b/.gitignore @@ -205,4 +205,6 @@ gradle-app.setting .idea/ db_dev.mv.db +db_dev.trace.db +api-docs.json # End of https://www.toptal.com/developers/gitignore/api/nextjs,intellij,java,gradle \ No newline at end of file diff --git a/backend/src/main/resources/application-h2.yml b/api-docs similarity index 100% rename from backend/src/main/resources/application-h2.yml rename to api-docs diff --git a/api-docs.json b/api-docs.json new file mode 100644 index 0000000..1cda873 --- /dev/null +++ b/api-docs.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"API 서버","version":"v1"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"tags":[{"name":"QuestionController","description":" qna 질문 컨트롤러"},{"name":"Answer Controller","description":"관리자 전용 QnA 답변 API"},{"name":"Review","description":"Review API"},{"name":"Book","description":"Book API"},{"name":"DeliveryInformationController","description":"배송 정보 컨트롤러"},{"name":"Order","description":"관리자 주문 API"},{"name":"관리자 - 상품(도서) 관리","description":"관리자 상품 관리 API"},{"name":"Member","description":"Member API"},{"name":"DetailOrder","description":"관리자 주문 상세 API"},{"name":"Cart","description":"Cart API"}],"paths":{"/reviews/{reviewId}":{"put":{"tags":["Review"],"summary":"리뷰 수정","operationId":"updateReview","parameters":[{"name":"reviewId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"content","in":"query","required":true,"schema":{"type":"string"}},{"name":"rating","in":"query","required":true,"schema":{"type":"number","format":"double"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}},"delete":{"tags":["Review"],"summary":"리뷰 삭제","operationId":"deleteReview","parameters":[{"name":"reviewId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/my/question/{id}":{"get":{"tags":["QuestionController"],"summary":"사용자의 특정 qna 질문 조회","operationId":"getQuestion","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}},"put":{"tags":["QuestionController"],"summary":"사용자의 특정 qna 질문 수정","operationId":"modifyQuesiton","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReqQuestionDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}},"delete":{"tags":["QuestionController"],"summary":"사용자의 특정 qna 질문 삭제","operationId":"removeQuesiton","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/my/deliveryInformation/{id}":{"put":{"tags":["DeliveryInformationController"],"summary":"배송 정보 갱신 (한개)","operationId":"putDeliveryInformation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReqDeliveryInformationDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}},"delete":{"tags":["DeliveryInformationController"],"summary":"배송 정보 삭제 (한개)","operationId":"deleteDeliveryInformation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/cart":{"get":{"tags":["Cart"],"summary":"장바구니 조회","operationId":"getCart","responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CartResponseDto"}}}}}}},"put":{"tags":["Cart"],"summary":"장바구니 수정 json","operationId":"updateCartItems","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartRequestDto"}}},"required":true},"responses":{"200":{"description":"OK"}}},"post":{"tags":["Cart"],"summary":"장바구니 추가","operationId":"addCart","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartRequestDto"}}},"required":true},"responses":{"200":{"description":"OK"}}},"delete":{"tags":["Cart"],"summary":"장바구니 삭제","operationId":"deleteBook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartRequestDto"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/api/auth/me/my":{"get":{"tags":["Member"],"summary":"사용자 정보 조회","operationId":"getMyPage","responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}},"put":{"tags":["Member"],"summary":"사용자 정보 갱신","operationId":"putMyPage","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutReqMemberMyPageDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/admin/dashboard/questions/{questionId}/answers/{answerId}":{"get":{"tags":["Answer Controller"],"summary":"사용자가 작성한 QnA 질문의 상세 답변 조회 (관리자 전용)","operationId":"getAnswer","parameters":[{"name":"questionId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"answerId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/AnswerDto"}}}}}},"put":{"tags":["Answer Controller"],"summary":"질문에 대한 답변 수정 (관리자 전용)","operationId":"modifyAnswer","parameters":[{"name":"questionId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"answerId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReqAnswerDto"}}},"required":true},"responses":{"200":{"description":"OK"}}},"delete":{"tags":["Answer Controller"],"summary":"질문에 대한 답변 삭제 (관리자 전용)","operationId":"deleteAnswer","parameters":[{"name":"questionId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"answerId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK"}}}},"/reviews/{book-id}":{"post":{"tags":["Review"],"summary":"리뷰 등록","operationId":"createReview","parameters":[{"name":"book-id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewRequestDto"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/my/question":{"get":{"tags":["QuestionController"],"summary":"사용자가 작성한 qna 질문 목록 조회","operationId":"getQuesitons","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}},"post":{"tags":["QuestionController"],"summary":"사용자가 qna 질문 등록","operationId":"postQuesiton","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReqQuestionDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/my/orders/create":{"post":{"tags":["order-controller"],"operationId":"createOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequestDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/OrderResponseDto"}}}}}}},"/my/orders/create/fast":{"post":{"tags":["order-controller"],"operationId":"createFastOrder","parameters":[{"name":"bookId","in":"query","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"quantity","in":"query","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequestDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/OrderResponseDto"}}}}}}},"/my/deliveryInformation":{"post":{"tags":["DeliveryInformationController"],"summary":"배송 정보 등록(최대 5개)","operationId":"postDeliveryInformation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReqDeliveryInformationDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/cart/anonymous":{"post":{"tags":["Cart"],"operationId":"getAnonymousCart","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CartRequestDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CartResponseDto"}}}}}}}},"/api/auth/refresh":{"post":{"tags":["auth-controller"],"operationId":"refreshAccessToken","parameters":[{"name":"refreshToken","in":"cookie","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/api/auth/me/logout":{"post":{"tags":["Member"],"operationId":"logout","responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/admin/login":{"post":{"tags":["admin-controller"],"operationId":"adminLogin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminLoginDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/admin/dashboard/questions/{questionId}/answers":{"post":{"tags":["Answer Controller"],"summary":"질문에 답변 등록 (관리자 전용)","operationId":"postAnswer","parameters":[{"name":"questionId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReqAnswerDto"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/admin/books/search":{"post":{"tags":["관리자 - 상품(도서) 관리"],"summary":"도서 검색","operationId":"searchBooks","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminBookSearchDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminBookSearchListDto"}}}}}}}},"/admin/books/register":{"post":{"tags":["관리자 - 상품(도서) 관리"],"summary":"도서 등록","operationId":"registerBook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminBookRegisterDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"string"}}}}}}},"/admin/detail-orders/{detailOrderId}/status":{"patch":{"tags":["DetailOrder"],"summary":"상세 주문 배송 상태 수정","description":"상세 주문 ID를 이용해 배송 상태를 변경합니다.","operationId":"updateDetailStatus","parameters":[{"name":"detailOrderId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDetailOrderStatusRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/AdminDetailOrderDTO"}}}}}}},"/admin/books/{bookId}":{"get":{"tags":["관리자 - 상품(도서) 관리"],"summary":"도서 상세 조회","description":"상품(도서)의 상세 정보를 조회한다.","operationId":"getBookDetail","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/AdminBookDetailDto"}}}}}},"delete":{"tags":["관리자 - 상품(도서) 관리"],"summary":"도서 삭제","description":"특정 도서를 삭제한다.","operationId":"deleteBook_1","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"string"}}}}}},"patch":{"tags":["관리자 - 상품(도서) 관리"],"summary":"도서 수정","description":"특정 도서 정보를 수정한다.","operationId":"updateBookPart","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminBookUpdateDto"}}},"required":true},"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/AdminBookDetailDto"}}}}}}},"/reviews":{"get":{"tags":["Review"],"summary":"전체 리뷰 조회","operationId":"getAllReviews","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"pageSize","in":"query","required":false,"schema":{"maximum":9223372036854775807,"minimum":0,"type":"integer","format":"int32","default":10}},{"name":"reviewSortType","in":"query","required":false,"schema":{"type":"string","enum":["CREATE_AT_DESC","CREATE_AT_ASC","RATING_DESC","RATING_ASC"],"default":"CREATE_AT_DESC"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageReviewResponseDto"}}}}}}},"/reviews/{bookId}":{"get":{"tags":["Review"],"summary":"특정 도서 리뷰 조회","operationId":"getReviewsById","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"pageSize","in":"query","required":false,"schema":{"maximum":9223372036854775807,"minimum":0,"type":"integer","format":"int32","default":10}},{"name":"reviewSortType","in":"query","required":false,"schema":{"type":"string","enum":["CREATE_AT_DESC","CREATE_AT_ASC","RATING_DESC","RATING_ASC"],"default":"CREATE_AT_DESC"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageReviewResponseDto"}}}}}}},"/my/orders":{"get":{"tags":["order-controller"],"operationId":"getOrdersByMember","parameters":[{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageOrderDTO"}}}}}}},"/my/orders/{orderId}/details":{"get":{"tags":["detail-order-controller"],"operationId":"getDetailOrdersByOrderIdAndMember","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DetailOrderDto"}}}}}}}},"/my/orders/payment":{"get":{"tags":["order-controller"],"operationId":"payment","responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PaymentResponseDto"}}}}}}},"/books":{"get":{"tags":["Book"],"summary":"전체 도서 조회","operationId":"getAllBooks","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"pageSize","in":"query","required":false,"schema":{"maximum":9223372036854775807,"minimum":0,"type":"integer","format":"int32","default":10}},{"name":"bookSortType","in":"query","required":false,"schema":{"type":"string","enum":["PUBLISHED_DATE","SALES_POINT","RATING","REVIEW_COUNT"],"default":"PUBLISHED_DATE"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageBookResponseDto"}}}}}}},"/books/{bookId}":{"get":{"tags":["Book"],"summary":"특정 도서 조회","operationId":"getBookById","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/BookResponseDto"}}}}}}},"/books/search":{"get":{"tags":["Book"],"summary":"도서 검색 (제목, 저자, ISBN13, 출판사 검색)","operationId":"searchBooks_1","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"pageSize","in":"query","required":false,"schema":{"maximum":9223372036854775807,"minimum":0,"type":"integer","format":"int32","default":10}},{"name":"bookSortType","in":"query","required":false,"schema":{"type":"string","enum":["PUBLISHED_DATE","SALES_POINT","RATING","REVIEW_COUNT"],"default":"PUBLISHED_DATE"}},{"name":"searchType","in":"query","required":false,"schema":{"type":"string","enum":["TITLE","AUTHOR","ISBN13","PUBLISHER"],"default":"TITLE"}},{"name":"keyword","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageBookResponseDto"}}}}}}},"/api/auth/me":{"get":{"tags":["Member"],"operationId":"getUserInfo","responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/admin/orders":{"get":{"tags":["Order"],"summary":"전체 회원 주문 조회","operationId":"getAllOrders","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":10}},{"name":"sortType","in":"query","required":false,"schema":{"type":"string","enum":["ORDER_DATE","TOTAL_PRICE","STATUS"],"default":"ORDER_DATE"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageAdminOrderDTO"}}}}}}},"/admin/orders/{orderId}/details":{"get":{"tags":["DetailOrder"],"summary":"상세 주문 조회","description":"상세 주문 ID를 이용해 정보를 조회합니다.","operationId":"getOrderDetails","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":10}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"book-title"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageAdminDetailOrderDTO"}}}}}}},"/admin/dashboard/questions":{"get":{"tags":["admin-question-controller"],"operationId":"getAdminQuestions","parameters":[{"name":"keyword","in":"query","required":false,"schema":{"type":"string"}},{"name":"hasAnswer","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageDtoAdminQuestionDto"}}}}}}},"/admin/dashboard/questions/{id}":{"get":{"tags":["admin-question-controller"],"operationId":"getAdminQuestion","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"object"}}}}}}},"/admin/dashboard/question/{questionId}/answer":{"get":{"tags":["Answer Controller"],"summary":"사용자가 작성한 QnA 질문에 대한 답변 조회","operationId":"getAnswers","parameters":[{"name":"questionId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/GetResAnswersDto"}}}}}}},"/admin/books":{"get":{"tags":["관리자 - 상품(도서) 관리"],"summary":"전체 도서 조회","description":"DB 전체 도서를 조회한다.(페이징)","operationId":"getAllBooks_1","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"pageSize","in":"query","required":false,"schema":{"maximum":9223372036854775807,"minimum":0,"type":"integer","format":"int32","default":10}},{"name":"bookSortType","in":"query","required":false,"schema":{"type":"string","enum":["PUBLISHED_DATE","SALES_POINT","RATING","REVIEW_COUNT"],"default":"PUBLISHED_DATE"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"$ref":"#/components/schemas/PageAdminBookListDto"}}}}}}},"/my/orders/{orderId}":{"delete":{"tags":["order-controller"],"operationId":"deleteOrder","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"accessToken","in":"cookie","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json;charset=UTF-8":{"schema":{"type":"string"}}}}}}},"/admin/dashboard/questions/{questionid}":{"delete":{"tags":["admin-question-controller"],"operationId":"deleteQuestion","parameters":[{"name":"questionid","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"OK"}}}}},"components":{"schemas":{"ReqQuestionDto":{"required":["content","title"],"type":"object","properties":{"title":{"type":"string"},"content":{"type":"string"}}},"ReqDeliveryInformationDto":{"required":["addressName","detailAddress","isDefaultAddress","phone","postCode","recipient"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"addressName":{"type":"string"},"postCode":{"type":"string"},"detailAddress":{"type":"string"},"recipient":{"type":"string"},"phone":{"pattern":"^\\d{3}-\\d{4}-\\d{4}$","type":"string"},"isDefaultAddress":{"type":"boolean"}}},"CartItemRequestDto":{"required":["bookId"],"type":"object","properties":{"bookId":{"type":"integer","format":"int64"},"quantity":{"minimum":1,"type":"integer","format":"int32"},"isAddToCart":{"type":"boolean"}}},"CartRequestDto":{"required":["cartItems"],"type":"object","properties":{"cartItems":{"type":"array","items":{"$ref":"#/components/schemas/CartItemRequestDto"}}}},"PutReqMemberMyPageDto":{"required":["name","phoneNumber"],"type":"object","properties":{"name":{"type":"string"},"phoneNumber":{"type":"string"},"profileImageUrl":{"type":"string"}}},"ReqAnswerDto":{"required":["content"],"type":"object","properties":{"content":{"type":"string"}}},"ReviewRequestDto":{"type":"object","properties":{"content":{"type":"string"},"rating":{"type":"number","format":"double"}}},"OrderRequestDto":{"type":"object","properties":{"postCode":{"type":"string"},"fullAddress":{"type":"string"},"recipient":{"type":"string"},"phone":{"pattern":"^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$","type":"string"},"paymentMethod":{"type":"string"}}},"OrderResponseDto":{"type":"object","properties":{"orderId":{"type":"integer","format":"int64"}}},"CartResponseDto":{"type":"object","properties":{"bookId":{"type":"integer","format":"int64"},"quantity":{"type":"integer","format":"int32"},"title":{"type":"string"},"price":{"type":"integer","format":"int32"},"coverImage":{"type":"string"}}},"AdminLoginDto":{"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AdminBookSearchDto":{"type":"object","properties":{"title":{"type":"string"},"author":{"type":"string"},"isbn13":{"type":"string"}}},"AdminBookSearchListDto":{"type":"object","properties":{"title":{"type":"string"},"author":{"type":"string"},"publisher":{"type":"string"},"pubDate":{"type":"string"},"categoryName":{"type":"string"},"isbn13":{"type":"string"}}},"AdminBookRegisterDto":{"type":"object","properties":{"isbn13":{"type":"string"}}},"UpdateDetailOrderStatusRequest":{"type":"object","properties":{"status":{"type":"string","description":"변경할 배송 상태","enum":["PENDING","SHIPPING","DELIVERED","RETURNED"]}}},"AdminDetailOrderDTO":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"orderId":{"type":"integer","format":"int64"},"modifyDate":{"type":"string","format":"date-time"},"bookTitle":{"type":"string"},"bookQuantity":{"type":"integer","format":"int32"},"deliveryStatus":{"type":"string"}}},"AdminBookUpdateDto":{"type":"object","properties":{"title":{"type":"string"},"author":{"type":"string"},"isbn":{"type":"string"},"isbn13":{"type":"string"},"publisher":{"type":"string"},"pubDate":{"type":"string","format":"date"},"priceStandard":{"type":"integer","format":"int32"},"priceSales":{"type":"integer","format":"int32"},"salesPoint":{"type":"integer","format":"int64"},"stock":{"type":"integer","format":"int32"},"status":{"type":"integer","format":"int32"},"toc":{"type":"string"},"coverImage":{"type":"string"},"categoryId":{"type":"integer","format":"int32"},"description":{"type":"string"},"descriptionImage":{"type":"string"}}},"AdminBookDetailDto":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"title":{"type":"string"},"author":{"type":"string"},"publisher":{"type":"string"},"pubDate":{"type":"string","format":"date"},"category":{"type":"string"},"isbn":{"type":"string"},"isbn13":{"type":"string"},"coverImage":{"type":"string"},"toc":{"type":"string"},"description":{"type":"string"},"descriptionImage":{"type":"string"},"priceStandard":{"type":"integer","format":"int32"},"pricesSales":{"type":"integer","format":"int32"},"stock":{"type":"integer","format":"int32"},"status":{"type":"integer","format":"int32"},"salesPoint":{"type":"integer","format":"int64"},"rating":{"type":"number","format":"double"},"reviewCount":{"type":"integer","format":"int64"}}},"PageReviewResponseDto":{"type":"object","properties":{"totalPages":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"first":{"type":"boolean"},"last":{"type":"boolean"},"numberOfElements":{"type":"integer","format":"int32"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"size":{"type":"integer","format":"int32"},"content":{"type":"array","items":{"$ref":"#/components/schemas/ReviewResponseDto"}},"number":{"type":"integer","format":"int32"},"sort":{"$ref":"#/components/schemas/SortObject"},"empty":{"type":"boolean"}}},"PageableObject":{"type":"object","properties":{"pageSize":{"type":"integer","format":"int32"},"pageNumber":{"type":"integer","format":"int32"},"paged":{"type":"boolean"},"unpaged":{"type":"boolean"},"offset":{"type":"integer","format":"int64"},"sort":{"$ref":"#/components/schemas/SortObject"}}},"ReviewResponseDto":{"type":"object","properties":{"bookId":{"type":"integer","format":"int64"},"reviewId":{"type":"integer","format":"int64"},"memberId":{"type":"integer","format":"int64"},"content":{"type":"string"},"rating":{"type":"number","format":"double"},"createDate":{"type":"string","format":"date-time"},"modifyDate":{"type":"string","format":"date-time"}}},"SortObject":{"type":"object","properties":{"sorted":{"type":"boolean"},"unsorted":{"type":"boolean"},"empty":{"type":"boolean"}}},"Pageable":{"type":"object","properties":{"page":{"minimum":0,"type":"integer","format":"int32"},"size":{"minimum":1,"type":"integer","format":"int32"},"sort":{"type":"array","items":{"type":"string"}}}},"OrderDTO":{"type":"object","properties":{"orderId":{"type":"integer","format":"int64"},"orderStatus":{"type":"string"},"totalPrice":{"type":"integer","format":"int64"},"createDate":{"type":"string","format":"date-time"}}},"PageOrderDTO":{"type":"object","properties":{"totalPages":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"first":{"type":"boolean"},"last":{"type":"boolean"},"numberOfElements":{"type":"integer","format":"int32"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"size":{"type":"integer","format":"int32"},"content":{"type":"array","items":{"$ref":"#/components/schemas/OrderDTO"}},"number":{"type":"integer","format":"int32"},"sort":{"$ref":"#/components/schemas/SortObject"},"empty":{"type":"boolean"}}},"DetailOrderDto":{"type":"object","properties":{"orderId":{"type":"integer","format":"int64"},"bookId":{"type":"integer","format":"int64"},"bookQuantity":{"type":"integer","format":"int32"},"deliveryStatus":{"type":"string","enum":["PENDING","SHIPPING","DELIVERED","RETURNED"]}}},"PaymentResponseDto":{"type":"object","properties":{"cartList":{"type":"array","items":{"$ref":"#/components/schemas/CartResponseDto"}},"priceStandard":{"type":"integer","format":"int64"},"pricesSales":{"type":"integer","format":"int64"}}},"BookResponseDto":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"title":{"type":"string"},"author":{"type":"string"},"isbn":{"type":"string"},"isbn13":{"type":"string"},"publisher":{"type":"string"},"pubDate":{"type":"string","format":"date"},"priceStandard":{"type":"integer","format":"int32"},"priceSales":{"type":"integer","format":"int32"},"salesPoint":{"type":"integer","format":"int64"},"stock":{"type":"integer","format":"int32"},"status":{"type":"integer","format":"int32"},"rating":{"type":"number","format":"double"},"toc":{"type":"string"},"reviewCount":{"type":"integer","format":"int64"},"coverImage":{"type":"string"},"categoryId":{"type":"integer","format":"int32"},"description":{"type":"string"},"descriptionImage":{"type":"string"},"averageRating":{"type":"number","format":"double"}}},"PageBookResponseDto":{"type":"object","properties":{"totalPages":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"first":{"type":"boolean"},"last":{"type":"boolean"},"numberOfElements":{"type":"integer","format":"int32"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"size":{"type":"integer","format":"int32"},"content":{"type":"array","items":{"$ref":"#/components/schemas/BookResponseDto"}},"number":{"type":"integer","format":"int32"},"sort":{"$ref":"#/components/schemas/SortObject"},"empty":{"type":"boolean"}}},"AdminOrderDTO":{"type":"object","properties":{"orderId":{"type":"integer","format":"int64"},"createdDate":{"type":"string","format":"date-time"},"totalPrice":{"type":"integer","format":"int64"},"status":{"type":"string"},"detailOrders":{"type":"array","items":{"$ref":"#/components/schemas/AdminDetailOrderDTO"}}}},"PageAdminOrderDTO":{"type":"object","properties":{"totalPages":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"first":{"type":"boolean"},"last":{"type":"boolean"},"numberOfElements":{"type":"integer","format":"int32"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"size":{"type":"integer","format":"int32"},"content":{"type":"array","items":{"$ref":"#/components/schemas/AdminOrderDTO"}},"number":{"type":"integer","format":"int32"},"sort":{"$ref":"#/components/schemas/SortObject"},"empty":{"type":"boolean"}}},"PageAdminDetailOrderDTO":{"type":"object","properties":{"totalPages":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"first":{"type":"boolean"},"last":{"type":"boolean"},"numberOfElements":{"type":"integer","format":"int32"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"size":{"type":"integer","format":"int32"},"content":{"type":"array","items":{"$ref":"#/components/schemas/AdminDetailOrderDTO"}},"number":{"type":"integer","format":"int32"},"sort":{"$ref":"#/components/schemas/SortObject"},"empty":{"type":"boolean"}}},"AdminQuestionDto":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"title":{"type":"string"},"content":{"type":"string"},"memberEmail":{"type":"string"},"createDate":{"type":"string"},"hasAnswer":{"type":"boolean"},"answer":{"$ref":"#/components/schemas/AnswerDto"}}},"AnswerDto":{"required":["content"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"createDate":{"type":"string","format":"date-time"},"modifyDate":{"type":"string","format":"date-time"},"content":{"type":"string"}}},"PageDtoAdminQuestionDto":{"required":["currentPageNumber","items","pageSize","totalItems","totalPages"],"type":"object","properties":{"currentPageNumber":{"type":"integer","format":"int32"},"pageSize":{"type":"integer","format":"int32"},"totalPages":{"type":"integer","format":"int64"},"totalItems":{"type":"integer","format":"int64"},"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminQuestionDto"}}}},"GetResAnswersDto":{"type":"object"},"AdminBookListDto":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"title":{"type":"string"},"author":{"type":"string"},"publisher":{"type":"string"},"pubDate":{"type":"string","format":"date"},"categoryName":{"type":"string"},"coverImage":{"type":"string"},"priceStandard":{"type":"integer","format":"int32"},"pricesSales":{"type":"integer","format":"int32"},"stock":{"type":"integer","format":"int32"},"status":{"type":"integer","format":"int32"}}},"PageAdminBookListDto":{"type":"object","properties":{"totalPages":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"first":{"type":"boolean"},"last":{"type":"boolean"},"numberOfElements":{"type":"integer","format":"int32"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"size":{"type":"integer","format":"int32"},"content":{"type":"array","items":{"$ref":"#/components/schemas/AdminBookListDto"}},"number":{"type":"integer","format":"int32"},"sort":{"$ref":"#/components/schemas/SortObject"},"empty":{"type":"boolean"}}}}}}mponents/schemas/SortObject"},"first":{"type":"boolean"},"last":{"type":"boolean"},"numberOfElements":{"type":"integer","format":"int32"},"pageable":{"$ref":"#/components/schemas/PageableObject"},"empty":{"type":"boolean"}}}}}} \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 2a3ab38..f618d3b 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -44,3 +44,9 @@ src/main/java/com/ll/nbe342team8/domain/member/member/dto/KakaoProfile.java src/main/java/com/ll/nbe342team8/domain/member/member/dto/KakaoTokenResponse.java src/main/java/com/ll/nbe342team8/domain/member/member/dto/KakaoUserProperties.java src/main/java/com/ll/nbe342team8/domain/member/member/dto/KakaoUserResponse.java + +# Ignore application configuration files +application.yml +application-dev.yml +application-prod.yml +application-secret.yml \ No newline at end of file diff --git a/backend/build.gradle b/backend/build.gradle index a6f702c..ff903d4 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -25,10 +25,13 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - //implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") annotationProcessor 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' @@ -44,6 +47,8 @@ dependencies { runtimeOnly 'com.h2database:h2' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' //Swagger + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.0' // Jackson JSR310 + compileOnly 'javax.servlet:javax.servlet-api:4.0.1' } tasks.named('test') { diff --git a/backend/frontend/app/backend/api/schema.d.ts b/backend/frontend/app/backend/api/schema.d.ts new file mode 100644 index 0000000..7d60999 --- /dev/null +++ b/backend/frontend/app/backend/api/schema.d.ts @@ -0,0 +1,2229 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/reviews/{reviewId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** 리뷰 수정 */ + put: operations["updateReview"]; + post?: never; + /** 리뷰 삭제 */ + delete: operations["deleteReview"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/question/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 사용자의 특정 qna 질문 조회 */ + get: operations["getQuestion"]; + /** 사용자의 특정 qna 질문 수정 */ + put: operations["modifyQuesiton"]; + post?: never; + /** 사용자의 특정 qna 질문 삭제 */ + delete: operations["removeQuesiton"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/deliveryInformation/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** 배송 정보 갱신 (한개) */ + put: operations["putDeliveryInformation"]; + post?: never; + /** 배송 정보 삭제 (한개) */ + delete: operations["deleteDeliveryInformation"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/cart": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 장바구니 조회 */ + get: operations["getCart"]; + /** 장바구니 수정 json */ + put: operations["updateCartItems"]; + /** 장바구니 추가 */ + post: operations["addCart"]; + /** 장바구니 삭제 */ + delete: operations["deleteBook"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/me/my": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 사용자 정보 조회 */ + get: operations["getMyPage"]; + /** 사용자 정보 갱신 */ + put: operations["putMyPage"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/dashboard/question/{questionId}/answer/{answerId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 사용자가 작성한 qna 질문의 상세 답변 조회 */ + get: operations["getAnswer"]; + /** 사용자가 작성한 qna 질문에 답변 수정(관리자 전용) */ + put: operations["modifyAnswer"]; + post?: never; + /** 사용자가 작성한 qna 질문에 답변 삭제(관리자 전용) */ + delete: operations["deleteAnswer"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/reviews/{book-id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 리뷰 등록 */ + post: operations["createReview"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/question": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 사용자가 작성한 qna 질문 목록 조회 */ + get: operations["getQuesitons"]; + put?: never; + /** 사용자가 qna 질문 등록 */ + post: operations["postQuesiton"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders/create": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders/create/fast": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createFastOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/deliveryInformation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 배송 정보 등록(최대 5개) */ + post: operations["postDeliveryInformation"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/cart/anonymous": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["getAnonymousCart"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["refreshAccessToken"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/me/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["logout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["adminLogin"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/dashboard/question/{questionId}/answer": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 사용자가 작성한 qna 질문에 대한 답변 조회 */ + get: operations["getAnswers"]; + put?: never; + /** 사용자가 작성한 qna 질문에 답변 등록(관리자 전용) */ + post: operations["postAnswer"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/books/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 도서 검색 */ + post: operations["searchBooks"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/books/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 도서 등록 */ + post: operations["registerBook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/detail-orders/{detailOrderId}/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * 상세 주문 배송 상태 수정 + * @description 상세 주문 ID를 이용해 배송 상태를 변경합니다. + */ + patch: operations["updateDetailStatus"]; + trace?: never; + }; + "/admin/books/{bookId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * 도서 상세 조회 + * @description 상품(도서)의 상세 정보를 조회한다. + */ + get: operations["getBookDetail"]; + put?: never; + post?: never; + /** + * 도서 삭제 + * @description 특정 도서를 삭제한다. + */ + delete: operations["deleteBook_1"]; + options?: never; + head?: never; + /** + * 도서 수정 + * @description 특정 도서 정보를 수정한다. + */ + patch: operations["updateBookPart"]; + trace?: never; + }; + "/reviews": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 전체 리뷰 조회 */ + get: operations["getAllReviews"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/reviews/{bookId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 특정 도서 리뷰 조회 */ + get: operations["getReviewsById"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getOrders"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders/{orderId}/details": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getDetailOrdersByOrderIdAndMemberId"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders/payment": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["payment"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/event/banners": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getBannerImages"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/books": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 전체 도서 조회 */ + get: operations["getAllBooks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/books/{bookId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 특정 도서 조회 */ + get: operations["getBookById"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/books/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 도서 검색 (제목, 저자, ISBN13, 출판사 검색) */ + get: operations["searchBooks_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getUserInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/orders": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 전체 회원 주문 조회 */ + get: operations["getAllOrders"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/orders/{orderId}/details": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * 상세 주문 조회 + * @description 상세 주문 ID를 이용해 정보를 조회합니다. + */ + get: operations["getOrderDetails"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/dashboard/questions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getAdminQuestions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/dashboard/questions/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getAdminQuestion"]; + put?: never; + post?: never; + delete: operations["deleteQuestion"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/books": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * 전체 도서 조회 + * @description DB 전체 도서를 조회한다.(페이징) + */ + get: operations["getAllBooks_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders/{orderId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["deleteOrder"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + ReqQuestionDto: { + title: string; + content: string; + }; + ReqDeliveryInformationDto: { + /** Format: int64 */ + id?: number; + addressName: string; + postCode: string; + detailAddress: string; + recipient: string; + phone: string; + isDefaultAddress: boolean; + }; + CartItemRequestDto: { + /** Format: int64 */ + bookId: number; + /** Format: int32 */ + quantity?: number; + isAddToCart?: boolean; + }; + CartRequestDto: { + cartItems: components["schemas"]["CartItemRequestDto"][]; + }; + PutReqMemberMyPageDto: { + name: string; + phoneNumber: string; + }; + ReqAnswerDto: { + content: string; + }; + ReviewRequestDto: { + content?: string; + /** Format: double */ + rating?: number; + }; + OrderRequestDto: { + postCode?: string; + fullAddress?: string; + recipient?: string; + phone?: string; + paymentMethod?: string; + }; + OrderResponseDto: { + /** Format: int64 */ + orderId?: number; + }; + CartResponseDto: { + /** Format: int64 */ + bookId?: number; + /** Format: int32 */ + quantity?: number; + title?: string; + /** Format: int32 */ + price?: number; + coverImage?: string; + }; + AdminLoginDto: { + username?: string; + password?: string; + }; + AdminBookSearchDto: { + title?: string; + author?: string; + isbn13?: string; + }; + AdminBookSearchListDto: { + title?: string; + author?: string; + publisher?: string; + pubDate?: string; + categoryName?: string; + isbn13?: string; + }; + AdminBookRegisterDto: { + isbn13?: string; + }; + UpdateDetailOrderStatusRequest: { + /** + * @description 변경할 배송 상태 + * @enum {string} + */ + status?: "PENDING" | "SHIPPED" | "DELIVERED"; + }; + AdminDetailOrderDTO: { + /** Format: int64 */ + id?: number; + /** Format: int64 */ + orderId?: number; + /** Format: date-time */ + modifyDate?: string; + bookTitle?: string; + /** Format: int32 */ + bookQuantity?: number; + deliveryStatus?: string; + }; + AdminBookUpdateDto: { + title?: string; + author?: string; + isbn?: string; + isbn13?: string; + publisher?: string; + /** Format: date */ + pubDate?: string; + /** Format: int32 */ + priceStandard?: number; + /** Format: int32 */ + priceSales?: number; + /** Format: int64 */ + salesPoint?: number; + /** Format: int32 */ + stock?: number; + /** Format: int32 */ + status?: number; + toc?: string; + coverImage?: string; + /** Format: int32 */ + categoryId?: number; + description?: string; + descriptionImage?: string; + }; + AdminBookDetailDto: { + /** Format: int64 */ + id?: number; + title?: string; + author?: string; + publisher?: string; + /** Format: date */ + pubDate?: string; + category?: string; + isbn?: string; + isbn13?: string; + coverImage?: string; + toc?: string; + description?: string; + descriptionImage?: string; + /** Format: int32 */ + priceStandard?: number; + /** Format: int32 */ + pricesSales?: number; + /** Format: int32 */ + stock?: number; + /** Format: int32 */ + status?: number; + /** Format: int64 */ + salesPoint?: number; + /** Format: double */ + rating?: number; + /** Format: int64 */ + reviewCount?: number; + }; + PageReviewResponseDto: { + /** Format: int32 */ + totalPages?: number; + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["ReviewResponseDto"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + pageable?: components["schemas"]["PageableObject"]; + /** Format: int32 */ + numberOfElements?: number; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + PageableObject: { + /** Format: int64 */ + offset?: number; + sort?: components["schemas"]["SortObject"]; + paged?: boolean; + /** Format: int32 */ + pageNumber?: number; + /** Format: int32 */ + pageSize?: number; + unpaged?: boolean; + }; + ReviewResponseDto: { + /** Format: int64 */ + bookId?: number; + /** Format: int64 */ + reviewId?: number; + /** Format: int64 */ + memberId?: number; + content?: string; + /** Format: double */ + rating?: number; + /** Format: date-time */ + createDate?: string; + /** Format: date-time */ + modifyDate?: string; + }; + SortObject: { + empty?: boolean; + sorted?: boolean; + unsorted?: boolean; + }; + OrderDTO: { + /** Format: int64 */ + orderId?: number; + orderStatus?: string; + /** Format: int64 */ + totalPrice?: number; + }; + DetailOrderDto: { + /** Format: int64 */ + orderId?: number; + /** Format: int64 */ + bookId?: number; + /** Format: int32 */ + bookQuantity?: number; + /** @enum {string} */ + deliveryStatus?: "PENDING" | "SHIPPING" | "DELIVERED" | "RETURNED"; + }; + PaymentResponseDto: { + cartList?: components["schemas"]["CartResponseDto"][]; + /** Format: int64 */ + priceStandard?: number; + /** Format: int64 */ + pricesSales?: number; + }; + BookResponseDto: { + /** Format: int64 */ + id?: number; + title?: string; + author?: string; + isbn?: string; + isbn13?: string; + publisher?: string; + /** Format: date */ + pubDate?: string; + /** Format: int32 */ + priceStandard?: number; + /** Format: int32 */ + priceSales?: number; + /** Format: int64 */ + salesPoint?: number; + /** Format: int32 */ + stock?: number; + /** Format: int32 */ + status?: number; + /** Format: double */ + rating?: number; + toc?: string; + /** Format: int64 */ + reviewCount?: number; + coverImage?: string; + /** Format: int32 */ + categoryId?: number; + description?: string; + descriptionImage?: string; + /** Format: double */ + averageRating?: number; + }; + PageBookResponseDto: { + /** Format: int32 */ + totalPages?: number; + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["BookResponseDto"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + pageable?: components["schemas"]["PageableObject"]; + /** Format: int32 */ + numberOfElements?: number; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + AdminOrderDTO: { + /** Format: int64 */ + orderId?: number; + /** Format: date-time */ + createdDate?: string; + /** Format: int64 */ + totalPrice?: number; + status?: string; + detailOrders?: components["schemas"]["AdminDetailOrderDTO"][]; + }; + PageAdminOrderDTO: { + /** Format: int32 */ + totalPages?: number; + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["AdminOrderDTO"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + pageable?: components["schemas"]["PageableObject"]; + /** Format: int32 */ + numberOfElements?: number; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + PageAdminDetailOrderDTO: { + /** Format: int32 */ + totalPages?: number; + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["AdminDetailOrderDTO"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + pageable?: components["schemas"]["PageableObject"]; + /** Format: int32 */ + numberOfElements?: number; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + Pageable: { + /** Format: int32 */ + page?: number; + /** Format: int32 */ + size?: number; + sort?: string[]; + }; + AdminQuestionDto: { + /** Format: int64 */ + id?: number; + title?: string; + content?: string; + memberEmail?: string; + createDate?: string; + hasAnswer?: boolean; + answer?: components["schemas"]["AnswerDto"]; + }; + AnswerDto: { + /** Format: int64 */ + id?: number; + /** Format: date-time */ + createDate?: string; + /** Format: date-time */ + modifyDate?: string; + content: string; + }; + PageDtoAdminQuestionDto: { + /** Format: int32 */ + currentPageNumber: number; + /** Format: int32 */ + pageSize: number; + /** Format: int64 */ + totalPages: number; + /** Format: int64 */ + totalItems: number; + items: components["schemas"]["AdminQuestionDto"][]; + }; + AdminBookListDto: { + /** Format: int64 */ + id?: number; + title?: string; + author?: string; + publisher?: string; + /** Format: date */ + pubDate?: string; + categoryName?: string; + coverImage?: string; + /** Format: int32 */ + priceStandard?: number; + /** Format: int32 */ + pricesSales?: number; + /** Format: int32 */ + stock?: number; + /** Format: int32 */ + status?: number; + }; + PageAdminBookListDto: { + /** Format: int32 */ + totalPages?: number; + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["AdminBookListDto"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + pageable?: components["schemas"]["PageableObject"]; + /** Format: int32 */ + numberOfElements?: number; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + updateReview: { + parameters: { + query: { + content: string; + rating: number; + }; + header?: never; + path: { + reviewId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + deleteReview: { + parameters: { + query?: never; + header?: never; + path: { + reviewId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + getQuestion: { + parameters: { + query: { + id: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + modifyQuesiton: { + parameters: { + query: { + id: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqQuestionDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + removeQuesiton: { + parameters: { + query: { + id: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + putDeliveryInformation: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqDeliveryInformationDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + deleteDeliveryInformation: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + getCart: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["CartResponseDto"][]; + }; + }; + }; + }; + updateCartItems: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CartRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + addCart: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CartRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteBook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CartRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMyPage: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + putMyPage: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PutReqMemberMyPageDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + getAnswer: { + parameters: { + query?: never; + header?: never; + path: { + questionId: number; + answerId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + modifyAnswer: { + parameters: { + query?: never; + header?: never; + path: { + questionId: number; + answerId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqAnswerDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + deleteAnswer: { + parameters: { + query?: never; + header?: never; + path: { + questionId: number; + answerId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + createReview: { + parameters: { + query?: never; + header?: never; + path: { + "book-id": number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReviewRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getQuesitons: { + parameters: { + query?: { + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + postQuesiton: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqQuestionDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + createOrder: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrderRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["OrderResponseDto"]; + }; + }; + }; + }; + createFastOrder: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrderRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["OrderResponseDto"]; + }; + }; + }; + }; + postDeliveryInformation: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqDeliveryInformationDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + getAnonymousCart: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CartRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["CartResponseDto"][]; + }; + }; + }; + }; + refreshAccessToken: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: { + refreshToken?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + adminLogin: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdminLoginDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + getAnswers: { + parameters: { + query?: never; + header?: never; + path: { + questionId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + postAnswer: { + parameters: { + query?: never; + header?: never; + path: { + questionId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqAnswerDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + searchBooks: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdminBookSearchDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["AdminBookSearchListDto"][]; + }; + }; + }; + }; + registerBook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdminBookRegisterDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": string; + }; + }; + }; + }; + updateDetailStatus: { + parameters: { + query?: never; + header?: never; + path: { + detailOrderId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateDetailOrderStatusRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["AdminDetailOrderDTO"]; + }; + }; + }; + }; + getBookDetail: { + parameters: { + query?: never; + header?: never; + path: { + bookId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["AdminBookDetailDto"]; + }; + }; + }; + }; + deleteBook_1: { + parameters: { + query?: never; + header?: never; + path: { + bookId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": string; + }; + }; + }; + }; + updateBookPart: { + parameters: { + query?: never; + header?: never; + path: { + bookId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdminBookUpdateDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["AdminBookDetailDto"]; + }; + }; + }; + }; + getAllReviews: { + parameters: { + query?: { + page?: number; + pageSize?: number; + reviewSortType?: "CREATE_AT_DESC" | "CREATE_AT_ASC" | "RATING_DESC" | "RATING_ASC"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageReviewResponseDto"]; + }; + }; + }; + }; + getReviewsById: { + parameters: { + query?: { + page?: number; + pageSize?: number; + reviewSortType?: "CREATE_AT_DESC" | "CREATE_AT_ASC" | "RATING_DESC" | "RATING_ASC"; + }; + header?: never; + path: { + bookId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageReviewResponseDto"]; + }; + }; + }; + }; + getOrders: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: { + accessToken?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["OrderDTO"][]; + }; + }; + }; + }; + getDetailOrdersByOrderIdAndMemberId: { + parameters: { + query: { + memberId: number; + }; + header?: never; + path: { + orderId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["DetailOrderDto"][]; + }; + }; + }; + }; + payment: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PaymentResponseDto"]; + }; + }; + }; + }; + getBannerImages: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": string[]; + }; + }; + }; + }; + getAllBooks: { + parameters: { + query?: { + page?: number; + pageSize?: number; + bookSortType?: "PUBLISHED_DATE" | "SALES_POINT" | "RATING" | "REVIEW_COUNT"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageBookResponseDto"]; + }; + }; + }; + }; + getBookById: { + parameters: { + query?: never; + header?: never; + path: { + bookId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["BookResponseDto"]; + }; + }; + }; + }; + searchBooks_1: { + parameters: { + query: { + page?: number; + pageSize?: number; + bookSortType?: "PUBLISHED_DATE" | "SALES_POINT" | "RATING" | "REVIEW_COUNT"; + searchType?: "TITLE" | "AUTHOR" | "ISBN13" | "PUBLISHER"; + keyword: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageBookResponseDto"]; + }; + }; + }; + }; + getUserInfo: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + getAllOrders: { + parameters: { + query?: { + page?: number; + pageSize?: number; + sortType?: "ORDER_DATE" | "TOTAL_PRICE" | "STATUS"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageAdminOrderDTO"]; + }; + }; + }; + }; + getOrderDetails: { + parameters: { + query?: { + page?: number; + size?: number; + sort?: string; + }; + header?: never; + path: { + orderId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageAdminDetailOrderDTO"]; + }; + }; + }; + }; + getAdminQuestions: { + parameters: { + query: { + keyword?: string; + hasAnswer?: boolean; + pageable: components["schemas"]["Pageable"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageDtoAdminQuestionDto"]; + }; + }; + }; + }; + getAdminQuestion: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + deleteQuestion: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAllBooks_1: { + parameters: { + query?: { + page?: number; + pageSize?: number; + bookSortType?: "PUBLISHED_DATE" | "SALES_POINT" | "RATING" | "REVIEW_COUNT"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageAdminBookListDto"]; + }; + }; + }; + }; + deleteOrder: { + parameters: { + query?: never; + header?: never; + path: { + orderId: number; + }; + cookie?: { + accessToken?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": string; + }; + }; + }; + }; +} diff --git a/backend/frontend/types/schema.d.ts b/backend/frontend/types/schema.d.ts new file mode 100644 index 0000000..cf807ab --- /dev/null +++ b/backend/frontend/types/schema.d.ts @@ -0,0 +1,1514 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/reviews/{reviewId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** 리뷰 수정 */ + put: operations["updateReview"]; + post?: never; + /** 리뷰 삭제 */ + delete: operations["deleteReview"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/deliveryInformation/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put: operations["putDeliveryInformation"]; + post?: never; + delete: operations["deleteDeliveryInformation"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/cart": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 장바구니 조회 */ + get: operations["getCart"]; + /** 장바구니 수정 json */ + put: operations["updateCartItems"]; + /** 장바구니 추가 */ + post: operations["addCart"]; + /** 장바구니 삭제 */ + delete: operations["deleteBook"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/me/my": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getMyPage"]; + put: operations["putMyPage"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/reviews/{book-id}/{member-id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 리뷰 등록 */ + post: operations["createReview"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/question": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["postQuesiton"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders/add-dummy": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["addDummyOrders"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/deliveryInformation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["postDeliveryInformation"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/books/admin/books": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["addBook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/me/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["logout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["adminLogin"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/books/admin/books/{bookId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations["updateBookPart"]; + trace?: never; + }; + "/admin/detail-orders/{detailOrderId}/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * 상세 주문 배송 상태 수정 + * @description 상세 주문 ID를 이용해 배송 상태를 변경합니다. + */ + patch: operations["updateDetailStatus"]; + trace?: never; + }; + "/reviews": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 전체 리뷰 조회 */ + get: operations["getAllReviews"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/reviews/{bookId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 특정 도서 리뷰 조회 */ + get: operations["getReviewsById"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getOrders"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/event/banners": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getBannerImages"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/books": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 전체 도서 조회 */ + get: operations["getAllBooks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/books/{bookId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 특정 도서 조회 */ + get: operations["getBookById"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/books/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 도서 검색 (제목, 저자, ISBN13, 출판사 검색) */ + get: operations["searchBooks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/orders/{orderId}/details": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getDetailOrdersByOrderIdAndOauthId"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getUserInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/orders": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 전체 회원 주문 조회 */ + get: operations["getAllOrders"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/admin/orders/{orderId}/details": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * 상세 주문 조회 + * @description 상세 주문 ID를 이용해 정보를 조회합니다. + */ + get: operations["getOrderDetails"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/my/orders/{orderId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["deleteOrder"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + ReqDeliveryInformationDto: { + /** Format: int64 */ + id?: number; + addressName: string; + postCode: string; + detailAddress: string; + recipient: string; + phone: string; + isDefaultAddress: boolean; + }; + CartItemRequestDto: { + /** Format: int64 */ + bookId?: number; + /** Format: int32 */ + quantity?: number; + }; + CartRequestDto: { + cartItems?: components["schemas"]["CartItemRequestDto"][]; + }; + PutReqMemberMyPageDto: { + name: string; + phoneNumber: string; + }; + Book: { + /** Format: date-time */ + createDate?: string; + /** Format: date-time */ + modifyDate?: string; + /** Format: int64 */ + id?: number; + title: string; + author: string; + isbn?: string; + isbn13: string; + /** Format: date */ + pubDate: string; + /** Format: int32 */ + priceStandard: number; + /** Format: int32 */ + pricesSales: number; + /** Format: int32 */ + stock: number; + /** Format: int32 */ + status: number; + /** Format: double */ + rating?: number; + /** Format: double */ + averageRating?: number; + toc?: string; + coverImage?: string; + description?: string; + descriptionImage?: string; + /** Format: int64 */ + salesPoint?: number; + /** Format: int64 */ + reviewCount?: number; + publisher?: string; + review?: components["schemas"]["Review"][]; + }; + Cart: { + /** Format: date-time */ + createDate?: string; + /** Format: date-time */ + modifyDate?: string; + /** Format: int64 */ + id?: number; + /** Format: int32 */ + quantity?: number; + }; + DeliveryInformation: { + /** Format: date-time */ + createDate?: string; + /** Format: date-time */ + modifyDate?: string; + /** Format: int64 */ + id?: number; + addressName?: string; + postCode?: string; + detailAddress?: string; + isDefaultAddress?: boolean; + recipient?: string; + phone?: string; + member?: components["schemas"]["Member"]; + }; + GrantedAuthority: { + authority?: string; + }; + Member: { + /** Format: date-time */ + createDate?: string; + /** Format: date-time */ + modifyDate?: string; + /** Format: int64 */ + id?: number; + name?: string; + phoneNumber?: string; + /** @enum {string} */ + memberType?: "USER" | "ADMIN"; + oauthId?: string; + email?: string; + password?: string; + username?: string; + deliveryInformations?: components["schemas"]["DeliveryInformation"][]; + carts?: components["schemas"]["Cart"][]; + authorities?: components["schemas"]["GrantedAuthority"][]; + }; + Review: { + /** Format: date-time */ + createDate?: string; + /** Format: date-time */ + modifyDate?: string; + /** Format: int64 */ + id?: number; + book?: components["schemas"]["Book"]; + member?: components["schemas"]["Member"]; + content?: string; + /** Format: double */ + rating?: number; + }; + ReqQuestionDto: { + content: string; + title: string; + }; + AdminLoginDto: { + username?: string; + password?: string; + }; + BookPatchRequestDto: { + title?: string; + author?: string; + isbn?: string; + isbn13?: string; + /** Format: date */ + pubDate?: string; + /** Format: int32 */ + priceStandard?: number; + /** Format: int32 */ + priceSales?: number; + /** Format: int32 */ + stock?: number; + /** Format: int32 */ + status?: number; + /** Format: double */ + rating?: number; + toc?: string; + cover?: string; + description?: string; + descriptionImage?: string; + categoryId?: components["schemas"]["Category"]; + validStatus?: boolean; + }; + Category: { + /** Format: int64 */ + id?: number; + /** Format: int32 */ + categoryId: number; + categoryName: string; + mall: string; + depth1: string; + depth2?: string; + depth3?: string; + depth4?: string; + depth5?: string; + books?: components["schemas"]["Book"][]; + category?: string; + }; + BookResponseDto: { + /** Format: int64 */ + id?: number; + title?: string; + author?: string; + isbn?: string; + isbn13?: string; + publisher?: string; + /** Format: date */ + pubDate?: string; + /** Format: int32 */ + priceStandard?: number; + /** Format: int32 */ + priceSales?: number; + /** Format: int64 */ + salesPoint?: number; + /** Format: int32 */ + stock?: number; + /** Format: int32 */ + status?: number; + /** Format: double */ + rating?: number; + toc?: string; + /** Format: int64 */ + reviewCount?: number; + coverImage?: string; + /** Format: int32 */ + categoryId?: number; + description?: string; + descriptionImage?: string; + /** Format: double */ + averageRating?: number; + }; + UpdateDetailOrderStatusRequest: { + /** + * @description 변경할 배송 상태 + * @enum {string} + */ + status?: "PENDING" | "SHIPPED" | "DELIVERED"; + }; + AdminDetailOrderDTO: { + /** Format: int64 */ + id?: number; + /** Format: int64 */ + orderId?: number; + /** Format: date-time */ + modifyDate?: string; + bookTitle?: string; + /** Format: int32 */ + bookQuantity?: number; + deliveryStatus?: string; + }; + PageReviewResponseDto: { + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + totalPages?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["ReviewResponseDto"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + /** Format: int32 */ + numberOfElements?: number; + pageable?: components["schemas"]["PageableObject"]; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + PageableObject: { + /** Format: int64 */ + offset?: number; + sort?: components["schemas"]["SortObject"]; + paged?: boolean; + /** Format: int32 */ + pageNumber?: number; + /** Format: int32 */ + pageSize?: number; + unpaged?: boolean; + }; + ReviewResponseDto: { + /** Format: int64 */ + bookId?: number; + /** Format: int64 */ + reviewId?: number; + author?: string; + content?: string; + /** Format: double */ + rating?: number; + /** Format: date-time */ + createDate?: string; + }; + SortObject: { + empty?: boolean; + sorted?: boolean; + unsorted?: boolean; + }; + OrderDTO: { + /** Format: int64 */ + orderId?: number; + orderStatus?: string; + /** Format: int64 */ + totalPrice?: number; + }; + CartResponseDto: { + /** Format: int64 */ + memberId?: number; + /** Format: int64 */ + bookId?: number; + /** Format: int32 */ + quantity?: number; + title?: string; + /** Format: int32 */ + price?: number; + coverImage?: string; + }; + PageBookResponseDto: { + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + totalPages?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["BookResponseDto"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + /** Format: int32 */ + numberOfElements?: number; + pageable?: components["schemas"]["PageableObject"]; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + DetailOrderDto: { + /** Format: int64 */ + orderId?: number; + /** Format: int64 */ + bookId?: number; + /** Format: int32 */ + bookQuantity?: number; + /** @enum {string} */ + deliveryStatus?: "PENDING" | "SHIPPING" | "DELIVERED" | "RETURNED"; + }; + AdminOrderDTO: { + /** Format: int64 */ + orderId?: number; + /** Format: date-time */ + createdDate?: string; + /** Format: int64 */ + totalPrice?: number; + status?: string; + detailOrders?: components["schemas"]["AdminDetailOrderDTO"][]; + }; + PageAdminOrderDTO: { + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + totalPages?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["AdminOrderDTO"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + /** Format: int32 */ + numberOfElements?: number; + pageable?: components["schemas"]["PageableObject"]; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + PageAdminDetailOrderDTO: { + /** Format: int64 */ + totalElements?: number; + /** Format: int32 */ + totalPages?: number; + /** Format: int32 */ + size?: number; + content?: components["schemas"]["AdminDetailOrderDTO"][]; + /** Format: int32 */ + number?: number; + sort?: components["schemas"]["SortObject"]; + /** Format: int32 */ + numberOfElements?: number; + pageable?: components["schemas"]["PageableObject"]; + first?: boolean; + last?: boolean; + empty?: boolean; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + updateReview: { + parameters: { + query: { + content: string; + rating: number; + }; + header?: never; + path: { + reviewId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteReview: { + parameters: { + query?: never; + header?: never; + path: { + reviewId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + putDeliveryInformation: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqDeliveryInformationDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + deleteDeliveryInformation: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + getCart: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["CartResponseDto"][]; + }; + }; + }; + }; + updateCartItems: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CartRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + addCart: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CartRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteBook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CartRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getMyPage: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + putMyPage: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PutReqMemberMyPageDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + createReview: { + parameters: { + query?: never; + header?: never; + path: { + "book-id": number; + "member-id": number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Review"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + postQuesiton: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqQuestionDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + addDummyOrders: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: { + accessToken?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": string; + }; + }; + }; + }; + postDeliveryInformation: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReqDeliveryInformationDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + addBook: { + parameters: { + query?: { + isbn13?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + adminLogin: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdminLoginDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + updateBookPart: { + parameters: { + query?: never; + header?: never; + path: { + bookId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BookPatchRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["BookResponseDto"]; + }; + }; + }; + }; + updateDetailStatus: { + parameters: { + query?: never; + header?: never; + path: { + detailOrderId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateDetailOrderStatusRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["AdminDetailOrderDTO"]; + }; + }; + }; + }; + getAllReviews: { + parameters: { + query?: { + page?: number; + pageSize?: number; + reviewSortType?: "CREATE_AT_DESC" | "CREATE_AT_ASC" | "RATING_DESC" | "RATING_ASC"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageReviewResponseDto"]; + }; + }; + }; + }; + getReviewsById: { + parameters: { + query?: { + page?: number; + pageSize?: number; + reviewSortType?: "CREATE_AT_DESC" | "CREATE_AT_ASC" | "RATING_DESC" | "RATING_ASC"; + }; + header?: never; + path: { + bookId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageReviewResponseDto"]; + }; + }; + }; + }; + getOrders: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: { + accessToken?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["OrderDTO"][]; + }; + }; + }; + }; + getBannerImages: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": string[]; + }; + }; + }; + }; + getAllBooks: { + parameters: { + query?: { + page?: number; + pageSize?: number; + bookSortType?: "PUBLISHED_DATE" | "SALES_POINT" | "RATING" | "REVIEW_COUNT"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageBookResponseDto"]; + }; + }; + }; + }; + getBookById: { + parameters: { + query?: never; + header?: never; + path: { + bookId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["BookResponseDto"]; + }; + }; + }; + }; + searchBooks: { + parameters: { + query: { + page?: number; + pageSize?: number; + bookSortType?: "PUBLISHED_DATE" | "SALES_POINT" | "RATING" | "REVIEW_COUNT"; + searchType?: "TITLE" | "AUTHOR" | "ISBN13" | "PUBLISHER"; + keyword: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageBookResponseDto"]; + }; + }; + }; + }; + getDetailOrdersByOrderIdAndOauthId: { + parameters: { + query: { + oauthId: string; + }; + header?: never; + path: { + orderId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["DetailOrderDto"][]; + }; + }; + }; + }; + getUserInfo: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": Record; + }; + }; + }; + }; + getAllOrders: { + parameters: { + query?: { + page?: number; + pageSize?: number; + sortType?: "ORDER_DATE" | "TOTAL_PRICE" | "STATUS"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageAdminOrderDTO"]; + }; + }; + }; + }; + getOrderDetails: { + parameters: { + query?: { + page?: number; + size?: number; + sort?: string; + }; + header?: never; + path: { + orderId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": components["schemas"]["PageAdminDetailOrderDTO"]; + }; + }; + }; + }; + deleteOrder: { + parameters: { + query?: never; + header?: never; + path: { + orderId: number; + }; + cookie?: { + accessToken?: string; + }; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json;charset=UTF-8": string; + }; + }; + }; + }; +} diff --git a/backend/gradlew b/backend/gradlew old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/admin/controller/AdminController.java b/backend/src/main/java/com/ll/nbe342team8/domain/admin/controller/AdminController.java new file mode 100644 index 0000000..c5e28a0 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/admin/controller/AdminController.java @@ -0,0 +1,78 @@ +package com.ll.nbe342team8.domain.admin.controller; + +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.ll.nbe342team8.domain.admin.dto.AdminLoginDto; +import com.ll.nbe342team8.domain.jwt.JwtService; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.repository.MemberRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/admin") +public class AdminController { + + private final JwtService jwtService; + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + + @PostMapping("/login") + public ResponseEntity adminLogin(@RequestBody AdminLoginDto loginDto) { + try { + // 관리자 계정 조회 (이메일 기준) + Optional optionalMember = memberRepository.findByEmailAndMemberType(loginDto.getUsername(), Member.MemberType.ADMIN); + if (optionalMember.isEmpty()) { + log.error("로그인 실패: 해당 이메일의 관리자 계정이 존재하지 않음"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인 실패 - 계정을 찾을 수 없음"); + } + Member admin = optionalMember.get(); + + // 비밀번호 확인 + boolean isPasswordCorrect = passwordEncoder.matches(loginDto.getPassword(), admin.getPassword()); + + if (!isPasswordCorrect) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인 실패 - 비밀번호 불일치"); + } + + // 이메일을 이용한 로그인 후 oAuthId로 JWT 발급 + String accessToken = jwtService.generateToken(admin); + String refreshToken = jwtService.generateRefreshToken(admin); + + ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", accessToken) + .httpOnly(true) + .secure(true) + .sameSite("None") + .path("/") + .maxAge(60 * 60) // 1시간 + .build(); + + ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) + .secure(true) + .sameSite("None") + .path("/api/auth/refresh") + .maxAge(7 * 24 * 60 * 60) // 7일 + .build(); + + return ResponseEntity.ok() + .header("Set-Cookie", accessTokenCookie.toString()) + .header("Set-Cookie", refreshTokenCookie.toString()) + .body("관리자 로그인 성공"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인 실패"); + } + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/admin/controller/AdminQuestionController.java b/backend/src/main/java/com/ll/nbe342team8/domain/admin/controller/AdminQuestionController.java new file mode 100644 index 0000000..dec5257 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/admin/controller/AdminQuestionController.java @@ -0,0 +1,77 @@ +package com.ll.nbe342team8.domain.admin.controller; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ll.nbe342team8.domain.admin.dto.AdminQuestionDto; +import com.ll.nbe342team8.domain.admin.service.AdminQuestionService; +import com.ll.nbe342team8.domain.qna.answer.entity.Answer; +import com.ll.nbe342team8.domain.qna.answer.repository.AnswerRepository; +import com.ll.nbe342team8.domain.qna.question.entity.Question; +import com.ll.nbe342team8.domain.qna.question.repository.QuestionRepository; +import com.ll.nbe342team8.standard.PageDto.PageDto; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/admin/dashboard") +@Slf4j +public class AdminQuestionController { + + private final AdminQuestionService adminQuestionService; + private final QuestionRepository questionRepository; + private final AnswerRepository answerRepository; + + // 전체 질문 조회 + @GetMapping("/questions") + public ResponseEntity> getAdminQuestions( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Boolean hasAnswer, + @PageableDefault(size = 10, sort = "createDate", direction = Sort.Direction.DESC) Pageable pageable) { + + PageDto questions = adminQuestionService.getQuestionsForAdmin(keyword, hasAnswer, pageable); + return ResponseEntity.ok(questions); + } + + // 특정 질문 조회 + @GetMapping("/questions/{id}") + public ResponseEntity getAdminQuestion(@PathVariable Long id) { + Question question = adminQuestionService.getQuestionById(id); + if (question == null) { + return ResponseEntity.notFound().build(); + } + AdminQuestionDto dto = new AdminQuestionDto(question); + + return ResponseEntity.ok(dto); + } + + // 질문 삭제 + @DeleteMapping("/questions/{questionid}") + @Transactional + public ResponseEntity deleteQuestion(@PathVariable Long questionid) { + + Question question = questionRepository.findById(questionid) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 질문입니다: " + questionid)); + + List answers = answerRepository.findByQuestionId(questionid); + answerRepository.deleteAll(answers); + + questionRepository.delete(question); + + return ResponseEntity.ok().build(); + } + +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/admin/dto/AdminLoginDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/admin/dto/AdminLoginDto.java new file mode 100644 index 0000000..21d4725 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/admin/dto/AdminLoginDto.java @@ -0,0 +1,12 @@ +package com.ll.nbe342team8.domain.admin.dto; + + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AdminLoginDto { + private String username; + private String password; +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/admin/dto/AdminQuestionDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/admin/dto/AdminQuestionDto.java new file mode 100644 index 0000000..3d4d172 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/admin/dto/AdminQuestionDto.java @@ -0,0 +1,27 @@ +package com.ll.nbe342team8.domain.admin.dto; + +import com.ll.nbe342team8.domain.qna.answer.dto.AnswerDto; +import com.ll.nbe342team8.domain.qna.question.entity.Question; + +import lombok.Getter; + +@Getter +public class AdminQuestionDto { + private final Long id; + private final String title; + private final String content; + private final String memberEmail; + private final String createDate; + private final boolean hasAnswer; + private final AnswerDto answer; + + public AdminQuestionDto(Question question) { + this.id = question.getId(); + this.title = question.getTitle(); + this.content = question.getContent(); + this.memberEmail = (question.getMember() != null) ? question.getMember().getEmail() : "탈퇴한 회원"; + this.createDate = question.getCreateDate().toString(); + this.hasAnswer = question.getAnswers() != null && !question.getAnswers().isEmpty(); + this.answer = (this.hasAnswer) ? new AnswerDto(question.getAnswers().getFirst()) : null; + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/admin/exception/AdminException.java b/backend/src/main/java/com/ll/nbe342team8/domain/admin/exception/AdminException.java new file mode 100644 index 0000000..567ed09 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/admin/exception/AdminException.java @@ -0,0 +1,17 @@ +package com.ll.nbe342team8.domain.admin.exception; + +import org.springframework.http.HttpStatus; + +import com.ll.nbe342team8.global.exceptions.ErrorCode; + +import lombok.Getter; + +@Getter +public class AdminException extends RuntimeException { + private final HttpStatus status; + + public AdminException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.status = errorCode.getStatus(); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/admin/service/AdminQuestionService.java b/backend/src/main/java/com/ll/nbe342team8/domain/admin/service/AdminQuestionService.java new file mode 100644 index 0000000..77520c3 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/admin/service/AdminQuestionService.java @@ -0,0 +1,72 @@ +package com.ll.nbe342team8.domain.admin.service; + +import com.ll.nbe342team8.domain.admin.dto.AdminQuestionDto; +import com.ll.nbe342team8.domain.qna.question.entity.Question; +import com.ll.nbe342team8.domain.qna.question.repository.QuestionRepository; +import com.ll.nbe342team8.global.exceptions.ServiceException; +import com.ll.nbe342team8.standard.PageDto.PageDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AdminQuestionService { + private final QuestionRepository questionRepository; + + public PageDto getQuestionsForAdmin(String keyword, Boolean hasAnswer, Pageable pageable) { + Page questionPage = fetchQuestions(keyword, hasAnswer, pageable); + return new PageDto<>(questionPage.map(AdminQuestionDto::new)); + } + + //검색 조건(키워드, 답변 상태)에 맞는 질문 목록을 조회 + private Page fetchQuestions(String keyword, Boolean hasAnswer, Pageable pageable) { + if (isBlank(keyword) && hasAnswer == null) { + return questionRepository.findAll(pageable); + } + + if (!isBlank(keyword)) { + return searchByKeywordAndAnswerStatus(keyword.trim(), hasAnswer, pageable); + } + + return searchByAnswerStatus(hasAnswer, pageable); + } + + //문자열이 비어있는지 확인 + private boolean isBlank(String str) { + return str == null || str.trim().isEmpty(); + } + + //키워드 및 답변 상태에 따라 질문 검색 + private Page searchByKeywordAndAnswerStatus(String keyword, Boolean hasAnswer, Pageable pageable) { + if (hasAnswer == null) { + return questionRepository.findByTitleContainingOrContentContaining(keyword, keyword, pageable); + } + return hasAnswer + ? questionRepository.findByAnswersIsNotEmptyAndTitleContainingOrContentContaining(keyword, keyword, pageable) + : questionRepository.findByAnswersIsEmptyAndTitleContainingOrContentContaining(keyword, keyword, pageable); + } + + // 답변 상태에 따라 질문 검색 + private Page searchByAnswerStatus(Boolean hasAnswer, Pageable pageable) { + return hasAnswer + ? questionRepository.findByAnswersIsNotEmpty(pageable) + : questionRepository.findByAnswersIsEmpty(pageable); + } + + //ID를 기반으로 특정 질문을 조회 + public Question getQuestionById(Long id) { + return questionRepository.findById(id) + .orElseThrow(() -> new ServiceException(404, "해당 질문을 찾을 수 없습니다.")); + } + + + //관리자 권한으로 질문 삭제 + @PreAuthorize("hasRole('ADMIN')") + public void deleteQuestion(Long id) { + Question question = getQuestionById(id); + questionRepository.delete(question); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/controller/AdminBookController.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/controller/AdminBookController.java new file mode 100644 index 0000000..fa9af46 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/controller/AdminBookController.java @@ -0,0 +1,101 @@ +package com.ll.nbe342team8.domain.book.book.controller; + +import java.net.URI; +import java.util.List; + +import org.hibernate.validator.constraints.Range; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ll.nbe342team8.domain.book.book.dto.request.AdminBookRegisterDto; +import com.ll.nbe342team8.domain.book.book.dto.request.AdminBookSearchDto; +import com.ll.nbe342team8.domain.book.book.dto.request.AdminBookUpdateDto; +import com.ll.nbe342team8.domain.book.book.dto.response.AdminBookDetailDto; +import com.ll.nbe342team8.domain.book.book.dto.response.AdminBookListDto; +import com.ll.nbe342team8.domain.book.book.dto.response.AdminBookSearchListDto; +import com.ll.nbe342team8.domain.book.book.entity.Book; +import com.ll.nbe342team8.domain.book.book.service.AdminBookService; +import com.ll.nbe342team8.domain.book.book.service.BookService; +import com.ll.nbe342team8.domain.book.book.type.BookSortType; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/admin/books") +@Tag(name = "관리자 - 상품(도서) 관리", description = "관리자 상품 관리 API") +@RequiredArgsConstructor +public class AdminBookController { + + private final BookService bookService; + private final AdminBookService adminBookService; + + @GetMapping + @Operation(summary = "전체 도서 조회", description = "DB 전체 도서를 조회한다.(페이징)") + public Page getAllBooks( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") @Range(min = 0, max = 100) int pageSize, + @RequestParam(defaultValue = "PUBLISHED_DATE") BookSortType bookSortType) { + + Page books = bookService.getAllBooks(page, pageSize, bookSortType); + return books.map(AdminBookListDto::from); + } + + // 외부 API에서 도서 정보를 가져와서 등록하는 기능(개선 예정) + @PostMapping("/search") + @Operation(summary = "도서 검색") + public ResponseEntity> searchBooks(@RequestBody AdminBookSearchDto request) { + List responses = adminBookService.searchBooks(request); + return ResponseEntity.ok(responses); + } + + @PostMapping("/register") + @Operation(summary = "도서 등록") + public ResponseEntity registerBook(@RequestBody AdminBookRegisterDto request) { + boolean isRegistered = adminBookService.registerBook(request.isbn13()); + + if (isRegistered) { + URI location = URI.create("/books/" + request.isbn13()); + return ResponseEntity.created(location).body("도서가 성공적으로 등록되었습니다."); + } else { + return ResponseEntity.badRequest().body("등록 실패: 해당 도서를 찾을 수 없습니다."); + } + } + + @GetMapping("/{bookId}") + @Operation(summary = "도서 상세 조회", description = "상품(도서)의 상세 정보를 조회한다.") + public ResponseEntity getBookDetail(@PathVariable Long bookId) { + AdminBookDetailDto bookDetail = adminBookService.getBookDetail(bookId); + + return ResponseEntity.ok(bookDetail); + } + + @PatchMapping("/{bookId}") + @Operation(summary = "도서 수정", description = "특정 도서 정보를 수정한다.") + public ResponseEntity updateBookPart( + @PathVariable Long bookId, + @RequestBody AdminBookUpdateDto requestDto) { + + AdminBookDetailDto updatedBook = adminBookService.updateBookPart(bookId, requestDto); + + return ResponseEntity.ok(updatedBook); + } + + @DeleteMapping("/{bookId}") + @Operation(summary = "도서 삭제", description = "특정 도서를 삭제한다.") + public ResponseEntity deleteBook(@PathVariable Long bookId) { + adminBookService.deleteBook(bookId); + + return ResponseEntity.ok("도서가 성공적으로 삭제되었습니다."); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/controller/BookController.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/controller/BookController.java index 743db2b..5b47892 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/controller/BookController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/controller/BookController.java @@ -1,17 +1,23 @@ package com.ll.nbe342team8.domain.book.book.controller; -import com.ll.nbe342team8.domain.book.book.dto.BookPatchRequestDto; +import org.hibernate.validator.constraints.Range; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import com.ll.nbe342team8.domain.book.book.dto.BookResponseDto; import com.ll.nbe342team8.domain.book.book.entity.Book; import com.ll.nbe342team8.domain.book.book.service.BookService; -import com.ll.nbe342team8.domain.book.book.type.SortType; +import com.ll.nbe342team8.domain.book.book.type.BookSortType; +import com.ll.nbe342team8.domain.book.book.type.SearchType; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; @Slf4j @RestController @@ -19,55 +25,35 @@ @Tag(name = "Book", description = "Book API") @RequiredArgsConstructor public class BookController { - private final BookService bookService; - - @GetMapping - @Operation(summary = "전체 도서 조회") - public Page getAllBooks(@RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int pageSize, - @RequestParam(defaultValue = "PUBLISHED_DATE") SortType sortType) { - - Page books = bookService.getAllBooks(page, pageSize, sortType); - return books.map(BookResponseDto::from); - } - - @Operation(summary = "특정 도서 조회") - @GetMapping("/{book-id}") - public BookResponseDto getBookById(@PathVariable("book-id") long bookId) { - Book book = bookService.getBookById(bookId); - return BookResponseDto.from(book); - } - - @Operation(summary = "특정 도서 댓글 조회") - @GetMapping("/{book-id}/review") - public void getBookReview(@PathVariable("book-id") long bookId) { - - } - - @Operation(summary = "도서 이름 검색") - @GetMapping("/search") - public Page searchBooks(@RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int pageSize, - @RequestParam(defaultValue = "PUBLISHED_DATE") SortType sortType, - @RequestParam String title){ - Page books = bookService.searchBooks(page, pageSize, sortType, title); - return books.map(BookResponseDto::from); - } - - @PostMapping("/admin/books") - public ResponseEntity addBook(@RequestParam(required = false) String isbn13) { - if (isbn13 == null) { - return ResponseEntity.badRequest().body("ISBN13 값을 포함해야 합니다."); - } - - return ResponseEntity.ok("요청 성공: 확인 완료."); - } - - @PatchMapping("/admin/books/{bookId}") - public ResponseEntity updateBookPart(@PathVariable("bookId") Long bookId, - @RequestBody BookPatchRequestDto requestDto) { - BookResponseDto updatedBook = bookService.updateBookPart(bookId, requestDto); - - return ResponseEntity.ok(updatedBook); - } + private final BookService bookService; + + @GetMapping + @Operation(summary = "전체 도서 조회") + public Page getAllBooks(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") @Range(min = 0, max = 100) int pageSize, + @RequestParam(defaultValue = "PUBLISHED_DATE") BookSortType bookSortType) { + + Page books = bookService.getAllBooks(page, pageSize, bookSortType); + return books.map(BookResponseDto::from); + } + + @Operation(summary = "특정 도서 조회") + @GetMapping("/{bookId}") + public BookResponseDto getBookById(@PathVariable Long bookId) { + Book book = bookService.getBookById(bookId); + return BookResponseDto.from(book); + } + + @Operation(summary = "도서 검색 (제목, 저자, ISBN13, 출판사 검색)") + @GetMapping("/search") + public Page searchBooks( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") @Range(min = 0, max = 100) int pageSize, + @RequestParam(defaultValue = "PUBLISHED_DATE") BookSortType bookSortType, + @RequestParam(defaultValue = "TITLE") SearchType searchType, + @RequestParam String keyword) { + + Page books = bookService.searchBooks(page, pageSize, bookSortType, searchType, keyword); + return books.map(BookResponseDto::from); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookDto.java new file mode 100644 index 0000000..bea442c --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookDto.java @@ -0,0 +1,13 @@ +package com.ll.nbe342team8.domain.book.book.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +public class BookDto { +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookPatchRequestDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookPatchRequestDto.java index 455f416..d1e1aae 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookPatchRequestDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookPatchRequestDto.java @@ -39,7 +39,7 @@ public boolean isValidStatus() { private Integer status; @DecimalMin(value = "0.0", message = "평점은 0 이상이어야 합니다.") - private Float rating; + private Double rating; private String toc; private String cover; diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookResponseDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookResponseDto.java index f90d7bc..e5db2f7 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookResponseDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/BookResponseDto.java @@ -1,56 +1,107 @@ package com.ll.nbe342team8.domain.book.book.dto; +import java.time.LocalDate; + import com.ll.nbe342team8.domain.book.book.entity.Book; import com.ll.nbe342team8.domain.book.category.entity.Category; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import java.time.LocalDate; -import java.time.LocalDateTime; +public record BookResponseDto( + Long id, + String title, + String author, + String isbn, + String isbn13, + String publisher, + LocalDate pubDate, + int priceStandard, + int priceSales, + Long salesPoint, + int stock, + int status, + Double rating, + String toc, + Long reviewCount, + String coverImage, + Integer categoryId, + String description, + String descriptionImage, + Double averageRating // 추가한 평균 평점 필드 +) { + public static BookResponseDto from(Book book) { + return new BookResponseDto( + book.getId(), + book.getTitle(), + book.getAuthor(), + book.getIsbn(), + book.getIsbn13(), + book.getPublisher(), + book.getPubDate(), + book.getPriceStandard(), + book.getPricesSales(), + book.getSalesPoint(), + book.getStock(), + book.getStatus(), + book.getRating(), + book.getToc(), + book.getReviewCount(), + book.getCoverImage(), + book.getCategoryId().getCategoryId(), + book.getDescription(), + book.getDescriptionImage(), + book.getAverageRating() + ); + } + + // 외부 API ExternalBookDto -> 내부 DTO 변환 + // 여기서는 @NotNull인 필드에 대해 null 체크 후 기본값 적용 + public static BookResponseDto from(ExternalBookDto externalBookDto) { + return new BookResponseDto( + null, // id는 DB 저장 시 자동 생성 + externalBookDto.title() != null ? externalBookDto.title() : "", // 필수: 제목 + externalBookDto.author() != null ? externalBookDto.author() : "", // 필수: 저자 + externalBookDto.isbn() != null ? externalBookDto.isbn() : "0000000000000", // 선택: ISBN + externalBookDto.isbn13() != null ? externalBookDto.isbn13() : "9999999999999", // 필수: ISBN13 + externalBookDto.publisher() != null ? externalBookDto.publisher() : "Unknown", // 선택: 출판사 + externalBookDto.pubDate() != null ? externalBookDto.pubDate() : LocalDate.of(9999, 12, 31), // 필수: 출판일 + externalBookDto.priceStandard() != null ? externalBookDto.priceStandard() : 9999999, // 필수: 정가 + externalBookDto.priceSales() != null ? externalBookDto.priceSales() : 9999999, // 필수: 판매가 + 0L, // 필수: 판매 포인트 (기본값 0L) + 0, // 필수: 재고 (외부 데이터에 없으므로 0 기본) + 0, // 필수: 판매 상태 (외부 데이터에 없으므로 0 기본, 품절) + 0.0, // 선택: 평점 (없으면 0.0 기본값) + externalBookDto.toc() != null ? externalBookDto.toc() : "", // 선택: 목차 + 0L, // 선택: 리뷰 수 (없으면 0L 기본값) + externalBookDto.cover() != null ? externalBookDto.cover() : "", // 선택: 커버 이미지 + externalBookDto.categoryId() != null ? externalBookDto.categoryId() : 99999, // 필수: 카테고리 ID (없으면 99999로 기본) + externalBookDto.description() != null ? externalBookDto.description() : "", // 선택: 상세 설명 + externalBookDto.descriptionImage() != null ? externalBookDto.descriptionImage() : "", // 선택: 상세 설명 이미지 + 0.0 // 선택: 평균 평점 (없으면 0.0 기본값) + ); + } -public record BookResponseDto(Long id, - String title, - String author, - String isbn, - String isbn13, - String publisher, - LocalDate pubDate, - int priceStandard, - int priceSales, - long salesPoint, - int stock, - int status, - float rating, - String toc, - long reviewCount, - String coverImage, - Integer categoryId, - String description, - String descriptionImage) { - public static BookResponseDto from(Book book){ - return new BookResponseDto( - book.getId(), - book.getTitle(), - book.getAuthor(), - book.getIsbn(), - book.getIsbn13(), - book.getPublisher(), - book.getPubDate(), - book.getPriceStandard(), - book.getPricesSales(), - book.getSalesPoint(), - book.getStock(), - book.getStatus(), - book.getRating(), - book.getToc(), - book.getReviewCount(), - book.getCoverImage(), - book.getCategoryId().getCategoryId(), - book.getDescription(), - book.getDescriptionImage() - ); - } + // BookResponseDto -> Book 엔티티 변환 + public Book toEntity(Category category) { + return Book.builder() + .title(this.title != null ? this.title : "") // 필수: 제목 + .author(this.author != null ? this.author : "") // 필수: 저자 + .publisher(this.publisher) // 선택: 출판사 + .isbn(this.isbn) // 선택: ISBN + .isbn13(this.isbn13 != null ? this.isbn13 : "9999999999999") // 필수: ISBN13 + .pubDate(this.pubDate != null ? this.pubDate : LocalDate.of(9999, 12, 31)) // 필수: 출판일 + .priceStandard(this.priceStandard) // 필수: 정가 + .pricesSales(this.priceSales) // 필수: 판매가 + .stock(this.stock) // 필수: 재고 + .status(this.status) // 필수: 상태 + .salesPoint(this.salesPoint != null ? this.salesPoint : 0L) // 선택: 판매량 + .reviewCount(this.reviewCount != null ? this.reviewCount : 0L) // 선택: 리뷰 수 + .rating(this.rating != null ? this.rating : 0.0) // 선택: 평점 + .averageRating(this.averageRating != null ? this.averageRating : 0.0) // 선택: 평균 평점 + .toc(this.toc != null ? this.toc : "") // 선택: 목차 + .coverImage(this.coverImage != null ? this.coverImage : "") // 선택: 커버 이미지 + .description(this.description != null ? this.description : "") // 선택: 상세 설명 + .descriptionImage(this.descriptionImage != null ? this.descriptionImage : "")// 선택: 상세 설명 이미지 + .categoryId(category) // 필수: 카테고리 (외부에서 제공해야 함) + .build(); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/ExternalBookDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/ExternalBookDto.java index c27f325..7d5a2c9 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/ExternalBookDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/ExternalBookDto.java @@ -1,29 +1,24 @@ package com.ll.nbe342team8.domain.book.book.dto; -import com.ll.nbe342team8.domain.book.category.entity.Category; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; +import java.time.LocalDate; -@Getter -@Setter -@Builder -@AllArgsConstructor -public class ExternalBookDto { - - private String title; // 제목 - private String author; // 저자 - private String publisher; // 출판사 - private String pubDate; // 출판일 - private String isbn; // ISBN - private String isbn13; // ISBN13 - private int priceStandard; // 정가 - private int priceSales; // 판매가 - private String toc; // 목차 - private String cover; // 커버 이미지 URL - private String description; // 상세 설명 - private String descriptionImage; // 상세 설명 이미지 URL - private Category categoryId; // 카테고리 +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +// 외부 API에서 받아온 도서 정보를 담는 DTO +@JsonIgnoreProperties(ignoreUnknown = true) +public record ExternalBookDto( + String title, // 제목 + String author, // 저자 + String publisher, // 출판사 + LocalDate pubDate, // 출판일 + String isbn, // ISBN + String isbn13, // ISBN13 + Integer priceStandard, // 정가 + Integer priceSales, // 판매가 + String toc, // 목차 + String cover, // 커버 이미지 URL + String description, // 상세 설명 + String descriptionImage, // 상세 설명 이미지 URL + Integer categoryId // 알라딘 API에서 받아온 카테고리 ID (Category Pk 아님) +) { } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookRegisterDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookRegisterDto.java new file mode 100644 index 0000000..1b58181 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookRegisterDto.java @@ -0,0 +1,7 @@ +package com.ll.nbe342team8.domain.book.book.dto.request; + +// 관리자 페이지에서 도서 등록 요청할 때 사용하는 DTO +public record AdminBookRegisterDto( + String isbn13 +) { +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookSearchDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookSearchDto.java new file mode 100644 index 0000000..c5dcf9d --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookSearchDto.java @@ -0,0 +1,9 @@ +package com.ll.nbe342team8.domain.book.book.dto.request; + +// 관리자 페이지에서 등록할 도서 검색할 때 사용하는 DTO +public record AdminBookSearchDto( + String title, + String author, + String isbn13 +) { +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookUpdateDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookUpdateDto.java new file mode 100644 index 0000000..15bdf60 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/request/AdminBookUpdateDto.java @@ -0,0 +1,24 @@ +package com.ll.nbe342team8.domain.book.book.dto.request; + +import java.time.LocalDate; + +// 관리자 페이지에서 도서 정보 수정할 때 사용하는 DTO +public record AdminBookUpdateDto( + String title, + String author, + String isbn, + String isbn13, + String publisher, + LocalDate pubDate, + Integer priceStandard, + Integer priceSales, + Long salesPoint, + Integer stock, + Integer status, + String toc, + String coverImage, + Integer categoryId, + String description, + String descriptionImage +) { +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookDetailDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookDetailDto.java new file mode 100644 index 0000000..d7938f5 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookDetailDto.java @@ -0,0 +1,52 @@ +package com.ll.nbe342team8.domain.book.book.dto.response; + +import java.time.LocalDate; + +import com.ll.nbe342team8.domain.book.book.entity.Book; + +// 관리자 페이지에서 도서 상세 정보를 조회할 때 사용하는 DTO +public record AdminBookDetailDto( + Long id, + String title, + String author, + String publisher, + LocalDate pubDate, + String category, + String isbn, + String isbn13, + String coverImage, + String toc, + String description, + String descriptionImage, + int priceStandard, + int pricesSales, + int stock, + int status, + long salesPoint, + Double rating, + long reviewCount +) { + public static AdminBookDetailDto from(Book book) { + return new AdminBookDetailDto( + book.getId(), + book.getTitle(), + book.getAuthor(), + book.getPublisher(), + book.getPubDate(), + book.getCategoryId().getCategory(), // 전체 카테고리(대>중>소) 이름 + book.getIsbn(), + book.getIsbn13(), + book.getCoverImage(), + book.getToc(), + book.getDescription(), + book.getDescriptionImage(), + book.getPriceStandard(), + book.getPricesSales(), + book.getStock(), + book.getStatus(), + book.getSalesPoint(), + book.getRating(), + book.getReviewCount() + ); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookListDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookListDto.java new file mode 100644 index 0000000..fdc026a --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookListDto.java @@ -0,0 +1,36 @@ +package com.ll.nbe342team8.domain.book.book.dto.response; + +import java.time.LocalDate; + +import com.ll.nbe342team8.domain.book.book.entity.Book; + +// 관리자 페이지에서 도서 목록을 조회할 때 사용하는 DTO +public record AdminBookListDto( + Long id, // 책 ID + String title, // 제목 + String author, // 저자 + String publisher, // 출판사 + LocalDate pubDate, // 출판일 + String categoryName, // 카테고리 이름 + String coverImage, // 커버 이미지 URL + int priceStandard, // 정가 + int pricesSales, // 판매가 + int stock, // 재고 + int status // 판매 상태 +) { + public static AdminBookListDto from(Book book) { + return new AdminBookListDto( + book.getId(), + book.getTitle(), + book.getAuthor(), + book.getPublisher(), + book.getPubDate(), + book.getCategoryId().getCategoryName(), + book.getCoverImage(), + book.getPriceStandard(), + book.getPricesSales(), + book.getStock(), + book.getStatus() + ); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookSearchListDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookSearchListDto.java new file mode 100644 index 0000000..9e74f66 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/dto/response/AdminBookSearchListDto.java @@ -0,0 +1,24 @@ +package com.ll.nbe342team8.domain.book.book.dto.response; + +import com.ll.nbe342team8.domain.book.book.entity.Book; + +// 관리자 페이지에서 도서 등록을 위해 알라딘API 도서 검색 시 사용하는 DTO +public record AdminBookSearchListDto( + String title, + String author, + String publisher, + String pubDate, + String categoryName, + String isbn13 +) { + public static AdminBookSearchListDto from(Book book) { + return new AdminBookSearchListDto( + book.getTitle(), + book.getAuthor(), + book.getPublisher(), + book.getPubDate().toString(), + book.getCategoryId().getCategoryName(), + book.getIsbn13() + ); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/entity/Book.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/entity/Book.java index c8eff41..ed68130 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/entity/Book.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/entity/Book.java @@ -1,108 +1,104 @@ package com.ll.nbe342team8.domain.book.book.entity; +import java.time.LocalDate; +import java.util.List; + +import org.hibernate.annotations.Formula; + import com.fasterxml.jackson.annotation.JsonIgnore; -import com.ll.nbe342team8.domain.book.book.dto.BookPatchRequestDto; import com.ll.nbe342team8.domain.book.category.entity.Category; import com.ll.nbe342team8.domain.book.review.entity.Review; import com.ll.nbe342team8.global.jpa.entity.BaseTime; -import jakarta.persistence.*; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDate; -import java.util.List; - @Entity @Getter -@Builder +@Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor public class Book extends BaseTime { - @Column(length = 100) - @NotNull - private String title; // 제목 - - @NotNull - private String author; // 저자 + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT + private Long id; - @Column(name = "isbn") - private String isbn; // ISBN + @Column(length = 100) + @NotNull + private String title; // 제목 - @NotNull - private String isbn13; // ISBN13 + @NotNull + private String author; // 저자 - @NotNull - private LocalDate pubDate; //출판일 + private String publisher; // 출판사 - @NotNull - private int priceStandard; // 정가 + private String isbn; // ISBN - @NotNull - private int pricesSales; // 판매가 + @NotNull + private String isbn13; // ISBN13 - @NotNull - private int stock; // 재고 + @NotNull + private LocalDate pubDate; //출판일 - @NotNull - private int status; // 판매 상태 + @NotNull + private Integer priceStandard; // 정가 - private float rating; // 평점 + @NotNull + private Integer pricesSales; // 판매가 - private String toc; // 목차 + @NotNull + private Integer stock; // 재고 - private String coverImage; // 커버 이미지 URL + @NotNull + private Integer status; // 판매 상태 - private String description; // 상세페이지 글 + private Double rating = 0.0; // 평점 - @Column(name = "description2") - private String descriptionImage; + @Formula("CASE WHEN review_count = 0 THEN 0 ELSE rating / review_count END") + private Double averageRating; //평균 평점 - private long salesPoint; + @Column(columnDefinition = "TEXT") + private String toc; // 목차 - private long reviewCount; + private String coverImage; // 커버 이미지 URL - private String publisher; + private String description; // 상세페이지 글 - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @NotNull - @JoinColumn(name = "category_id") // @@@실행 이상하면 지워보기@@@ - private Category categoryId; // 카테고리 + private String descriptionImage; - @OneToMany(mappedBy = "book", fetch = FetchType.LAZY) - private List review; + private Long salesPoint; - public void createReview(float rating) { - this.reviewCount++; - this.rating += rating; - } + private Long reviewCount; // 리뷰 수 - public void deleteReview(float rating) { - this.reviewCount--; - this.rating -= rating; - } + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @NotNull + @JoinColumn(name = "category_id", referencedColumnName = "id") // 외래키 + private Category categoryId; // 카테고리 - public void update(BookPatchRequestDto requestDto) { - if (requestDto.getTitle() != null) this.title = requestDto.getTitle(); - if (requestDto.getAuthor() != null) this.author = requestDto.getAuthor(); - if (requestDto.getIsbn() != null) this.isbn = requestDto.getIsbn(); - if (requestDto.getIsbn13() != null) this.isbn13 = requestDto.getIsbn13(); - if (requestDto.getPubDate() != null) this.pubDate = requestDto.getPubDate(); - if (requestDto.getPriceStandard() != null) this.priceStandard = requestDto.getPriceStandard(); - if (requestDto.getPriceSales() != null) this.pricesSales = requestDto.getPriceSales(); - if (requestDto.getStock() != null) this.stock = requestDto.getStock(); - if (requestDto.getStatus() != null) this.status = requestDto.getStatus(); - if (requestDto.getRating() != null) this.rating = requestDto.getRating(); - if (requestDto.getToc() != null) this.toc = requestDto.getToc(); - if (requestDto.getCover() != null) this.coverImage = requestDto.getCover(); - if (requestDto.getDescription() != null) this.description = requestDto.getDescription(); - if (requestDto.getDescriptionImage() != null) this.descriptionImage = requestDto.getDescriptionImage(); - if (requestDto.getCategoryId() != null) this.categoryId = requestDto.getCategoryId(); - } + @OneToMany(mappedBy = "book", fetch = FetchType.LAZY) + private List review; + public void createReview(Double rating) { + this.reviewCount++; + this.rating += rating; + } + public void deleteReview(Double rating) { + this.reviewCount--; + this.rating -= rating; + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/repository/BookRepository.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/repository/BookRepository.java index c8b531c..7e9e6bc 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/repository/BookRepository.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/repository/BookRepository.java @@ -1,25 +1,48 @@ package com.ll.nbe342team8.domain.book.book.repository; -import com.ll.nbe342team8.domain.book.book.entity.Book; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.Optional; +import com.ll.nbe342team8.domain.book.book.entity.Book; +import com.ll.nbe342team8.global.util.BookSpecifications; @Repository -public interface BookRepository extends JpaRepository { - List findAll(); +public interface BookRepository extends JpaRepository, JpaSpecificationExecutor { + List findAll(); + + Page findAll(Pageable pageable); + + // 도서 등록시 중복 확인 + boolean existsByIsbn13(String isbn13); + + // 도서 등록을 위한 도서 검색 + default List dynamicSearch(String title, String author, String isbn13) { + Specification spec = Specification.where(null); // 기본값 + + if (title != null && !title.isEmpty()) { + spec = spec.and(BookSpecifications.hasTitle(title)); + } + if (author != null && !author.isEmpty()) { + spec = spec.and(BookSpecifications.hasAuthor(author)); + } + if (isbn13 != null && !isbn13.isEmpty()) { + spec = spec.and(BookSpecifications.hasIsbn(isbn13)); + } + + return findAll(spec); + } - Page findAll(Pageable pageable); + Page findBooksByTitleContaining(String title, Pageable pageable); - Page findBooksByTitleContaining(String title, Pageable pageable); + Page findBooksByAuthorContaining(String author, Pageable pageable); - // 상품 조회 - Optional findByIsbn13(String isbn13); + Page findBooksByIsbn13(String isbn13, Pageable pageable); - // 상품시 중목 확인 - boolean existsByIsbn13(String isbn13); + Page findBooksByPublisherContaining(String publisher, Pageable pageable); } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/AdminBookService.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/AdminBookService.java new file mode 100644 index 0000000..3de4eb5 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/AdminBookService.java @@ -0,0 +1,123 @@ +package com.ll.nbe342team8.domain.book.book.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.ll.nbe342team8.domain.book.book.dto.BookResponseDto; +import com.ll.nbe342team8.domain.book.book.dto.ExternalBookDto; +import com.ll.nbe342team8.domain.book.book.dto.request.AdminBookSearchDto; +import com.ll.nbe342team8.domain.book.book.dto.request.AdminBookUpdateDto; +import com.ll.nbe342team8.domain.book.book.dto.response.AdminBookDetailDto; +import com.ll.nbe342team8.domain.book.book.dto.response.AdminBookSearchListDto; +import com.ll.nbe342team8.domain.book.book.entity.Book; +import com.ll.nbe342team8.domain.book.book.repository.BookRepository; +import com.ll.nbe342team8.domain.book.category.entity.Category; +import com.ll.nbe342team8.domain.book.category.repository.CategoryRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AdminBookService { + + private final ExternalBookApiService externalBookApiService; + private final CategoryRepository categoryRepository; + private final BookRepository bookRepository; + + // 도서 상품 검색 + public List searchBooks(AdminBookSearchDto request) { + return bookRepository.dynamicSearch( + request.title(), + request.author(), + request.isbn13() + ).stream() + .map(AdminBookSearchListDto::from) + .toList(); + } + + + // 도서 상품 등록 + @Transactional + public boolean registerBook(String isbn13) { + // 기존 DB에서 중복 확인 + if (bookRepository.existsByIsbn13(isbn13)) { + throw new IllegalArgumentException("ISBN " + isbn13 + "은(는) 이미 등록된 도서입니다."); + } + + // 외부 API에서 도서 정보 조회 + ExternalBookDto apiResponse = externalBookApiService.searchBookByIsbn13(isbn13); + if (apiResponse == null) { + throw new IllegalStateException("외부 API에서 ISBN " + isbn13 + "에 대한 도서를 찾을 수 없습니다."); + } + + // 외부 API에서 받은 categoryNum을 category 테이블의 id와 매칭 + Category category = categoryRepository.findByCategoryId(apiResponse.categoryId()) + .orElseThrow(() -> new RuntimeException("해당 category_id가 존재하지 않습니다: " + apiResponse.categoryId())); + + // 변환 + BookResponseDto bookResponse = BookResponseDto.from(apiResponse); + + // DB 저장 + Book book = bookResponse.toEntity(category); + bookRepository.save(book); + + return true; + } + + // 도서 상품 상세 조회 + @Transactional(readOnly = true) + public AdminBookDetailDto getBookDetail(Long bookId) { + Book book = bookRepository.findById(bookId) + .orElseThrow(() -> new IllegalArgumentException("해당 도서를 찾을 수 없습니다. ID: " + bookId)); + + return AdminBookDetailDto.from(book); + } + + // 도서 상품 수정 + @Transactional + public AdminBookDetailDto updateBookPart(Long bookId, AdminBookUpdateDto request) { + Book book = bookRepository.findById(bookId) + .orElseThrow(() -> new IllegalArgumentException("해당 도서를 찾을 수 없습니다. ID: " + bookId)); + + Book updatedBook = book.toBuilder() + .title(request.title() != null ? request.title() : book.getTitle()) + .author(request.author() != null ? request.author() : book.getAuthor()) + .isbn(request.isbn() != null ? request.isbn() : book.getIsbn()) + .isbn13(request.isbn13() != null ? request.isbn13() : book.getIsbn13()) + .publisher(request.publisher() != null ? request.publisher() : book.getPublisher()) + .pubDate(request.pubDate() != null ? request.pubDate() : book.getPubDate()) + .priceStandard(request.priceStandard() != null ? request.priceStandard() : book.getPriceStandard()) + .pricesSales(request.priceSales() != null ? request.priceSales() : book.getPricesSales()) + .salesPoint(request.salesPoint() != null ? request.salesPoint() : book.getSalesPoint()) + .stock(request.stock() != null ? request.stock() : book.getStock()) + .status(request.status() != null ? request.status() : book.getStatus()) + .toc(request.toc() != null ? request.toc() : book.getToc()) + .coverImage(request.coverImage() != null ? request.coverImage() : book.getCoverImage()) + .description(request.description() != null ? request.description() : book.getDescription()) + .descriptionImage(request.descriptionImage() != null ? request.descriptionImage() : book.getDescriptionImage()) + .build(); + + // 카테고리 변경이 필요한 경우만 업데이트 + if (request.categoryId() != null) { + updatedBook = updatedBook.toBuilder() + .categoryId(categoryRepository.findById(request.categoryId().longValue()) + .orElseThrow(() -> new IllegalArgumentException("해당 카테고리를 찾을 수 없습니다."))) + .build(); + } + + bookRepository.save(updatedBook); + + return AdminBookDetailDto.from(updatedBook); + } + + // 도서 상품 삭제 + @Transactional + public void deleteBook(Long bookId) { + Book book = bookRepository.findById(bookId) + .orElseThrow(() -> new IllegalArgumentException("해당 도서를 찾을 수 없습니다. ID: " + bookId)); + + bookRepository.delete(book); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/BookService.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/BookService.java index d2e9f26..6ab1a42 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/BookService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/BookService.java @@ -1,113 +1,93 @@ package com.ll.nbe342team8.domain.book.book.service; -import com.ll.nbe342team8.domain.book.book.dto.BookPatchRequestDto; -import com.ll.nbe342team8.domain.book.book.dto.BookResponseDto; -import com.ll.nbe342team8.domain.book.book.dto.ExternalBookDto; -import com.ll.nbe342team8.domain.book.book.entity.Book; -import com.ll.nbe342team8.domain.book.book.repository.BookRepository; -import com.ll.nbe342team8.domain.book.book.type.SortType; -import jakarta.persistence.EntityNotFoundException; -import lombok.RequiredArgsConstructor; +import java.util.ArrayList; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; +import com.ll.nbe342team8.domain.book.book.entity.Book; +import com.ll.nbe342team8.domain.book.book.repository.BookRepository; +import com.ll.nbe342team8.domain.book.book.type.BookSortType; +import com.ll.nbe342team8.domain.book.book.type.SearchType; +import com.ll.nbe342team8.global.exceptions.ServiceException; + +import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class BookService { - private final ExternalBookApiService externalBookApiService; - private final BookRepository bookRepository; - - public Page getAllBooks(int page, int pageSize, SortType sortType) { - List sorts = new ArrayList<>(); - sorts.add(sortType.getOrder()); - - Pageable pageable = PageRequest.of(page, pageSize, Sort.by(sorts)); - return bookRepository.findAll(pageable); - } - - public Book getBookById(Long id) { - if (id == null) { - throw new IllegalArgumentException("ID 값이 null입니다."); - } - return bookRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID(" + id + ")의 책을 찾을 수 없습니다.")); - } - - public long count() { - return bookRepository.count(); - } - - public Book create(Book book) { - return bookRepository.save(book); - } - - public Book createReview(Book book, float rating) { - book.createReview(rating); - return bookRepository.save(book); - } - - public Book deleteReview(Book book, float rating) { - book.deleteReview(rating); - return bookRepository.save(book); - } - - public Page searchBooks(int page, int pageSize, SortType sortType, String title) { - List sorts = new ArrayList<>(); - sorts.add(sortType.getOrder()); - - Pageable pageable = PageRequest.of(page, pageSize, Sort.by(sorts)); - return bookRepository.findBooksByTitleContaining(title, pageable); - } - - // 도서 추가 - @Transactional - public Book addBook(String isbn13) { - - if (bookRepository.existsByIsbn13(isbn13)) { - throw new IllegalArgumentException("이미 등록된 도서입니다."); - } - - ExternalBookDto externalBookDto = externalBookApiService.searchBook(isbn13); - - Book book = mapToEntity(externalBookDto); - - return bookRepository.save(book); - } - - private Book mapToEntity(ExternalBookDto dto) { - return Book.builder() - .title(dto.getTitle()) - .author(dto.getAuthor()) - .isbn(dto.getIsbn()) - .isbn13(dto.getIsbn13()) - .pubDate(LocalDate.parse(dto.getPubDate())) - .priceStandard(dto.getPriceStandard()) - .pricesSales(dto.getPriceSales()) - .toc(dto.getToc()) - .coverImage(dto.getCover()) - .description(dto.getDescription()) - .descriptionImage(dto.getDescriptionImage()) - .categoryId(dto.getCategoryId()) - .build(); - } - - // 도서 정보 수정 - @Transactional - public BookResponseDto updateBookPart(Long bookId, BookPatchRequestDto requestDto) { - Book book = bookRepository.findById(bookId) - .orElseThrow(() -> new EntityNotFoundException("책을 찾을 수 없습니다.")); - - book.update(requestDto); // DTO 에서 null 이 아닌 값만 업데이트 - - return BookResponseDto.from(book); - } -} \ No newline at end of file + private final ExternalBookApiService externalBookApiService; + private final BookRepository bookRepository; + + public Page getAllBooks(int page, int pageSize, BookSortType bookSortType) { + List sorts = new ArrayList<>(); + sorts.add(bookSortType.getOrder()); + + if (!(bookSortType == BookSortType.PUBLISHED_DATE)) { // 출간일을 보조 정렬 기준으로 추가 + BookSortType baseSort = BookSortType.PUBLISHED_DATE; + sorts.add(baseSort.getOrder()); + } + + Pageable pageable = PageRequest.of(page, pageSize, Sort.by(sorts)); + return bookRepository.findAll(pageable); + } + + public Book getBookById(Long id) { + // Todo: GlobalExceptionHandler 를 통해 처리하도록 수정 + + return bookRepository.findById(id) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "id에 해당하는 책이 없습니다.")); + } + + public long count() { + return bookRepository.count(); + } + + public Book create(Book book) { + return bookRepository.save(book); + } + + public Book createReview(Book book, Double rating) { + book.createReview(rating); + return bookRepository.save(book); + } + + public Book deleteReview(Book book, Double rating) { + book.deleteReview(rating); + return bookRepository.save(book); + } + + @Transactional(readOnly = true) + public Page searchBooks(int page, int pageSize, BookSortType bookSortType, SearchType searchType, String keyword) { + Pageable pageable; + // 판매량, 평점, 리뷰 정렬 시 보조 정렬 기준으로 출간일(pubDate) 적용 + if (bookSortType == BookSortType.SALES_POINT || bookSortType == BookSortType.RATING || bookSortType == BookSortType.REVIEW_COUNT) { + pageable = PageRequest.of(page, pageSize, Sort.by( + new Sort.Order(bookSortType.getOrder().getDirection(), bookSortType.getOrder().getProperty()), + new Sort.Order(Sort.Direction.DESC, "pubDate") + )); + } else { + pageable = PageRequest.of(page, pageSize, Sort.by(bookSortType.getOrder())); + } + + switch (searchType) { + case AUTHOR: + return bookRepository.findBooksByAuthorContaining(keyword, pageable); + case ISBN13: + return bookRepository.findBooksByIsbn13(keyword, pageable); + case PUBLISHER: + return bookRepository.findBooksByPublisherContaining(keyword, pageable); + case TITLE: + default: + return bookRepository.findBooksByTitleContaining(keyword, pageable); + } + } +} + diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/ExternalBookApiService.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/ExternalBookApiService.java index 9261040..df43182 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/ExternalBookApiService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/service/ExternalBookApiService.java @@ -1,38 +1,63 @@ package com.ll.nbe342team8.domain.book.book.service; -import com.ll.nbe342team8.domain.book.book.dto.ExternalBookDto; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.ll.nbe342team8.domain.book.book.dto.ExternalBookDto; + @Service public class ExternalBookApiService { - private final WebClient webClient; - private final String ttbkey; - - public ExternalBookApiService(WebClient.Builder webClientBuilder, - @Value("${aladin.ttbkey}") String ttbkey) { - this.webClient = webClientBuilder.baseUrl("http://www.aladin.co.kr/ttb/api/").build(); - this.ttbkey = ttbkey; - } - - public ExternalBookDto searchBook(String isbn13) { - if (isbn13 == null || isbn13.isEmpty()) { - throw new IllegalArgumentException("ISBN13 값은 필수입니다."); - } - - String url = "http://www.aladin.co.kr/ttb/api/ItemLookUp.aspx?" - + "ttbkey=ttbcameogu1634001" // 하드코딩 (추후수정) - + "&itemIdType=ISBN13" - + "&ItemId=" + isbn13 - + "&output=js" - + "&Cover=Big"; - - return webClient.get() - .uri(url) // 호출할 URL - .retrieve() // 응답 데이터를 가져옴 - .bodyToMono(ExternalBookDto.class) // 응답 데이터를 ExternalBookDto 로 변환 - .block(); // 비동기 호출을 동기적으로 처리 - } + private final WebClient webClient; + private final String ttbkey; + private final ObjectMapper objectMapper; + + public ExternalBookApiService(WebClient.Builder webClientBuilder, @Value("${aladin.ttbkey}") String ttbkey) { + this.webClient = webClientBuilder.baseUrl("http://www.aladin.co.kr/ttb/api/").build(); + this.ttbkey = ttbkey; + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new JavaTimeModule()); + } + + public ExternalBookDto searchBookByIsbn13(String isbn13) { + if (isbn13 == null || isbn13.isEmpty()) { + throw new IllegalArgumentException("ISBN13 값이 없습니다."); + } + + String url = String.format("ItemLookUp.aspx?ttbkey=%s&itemIdType=ISBN13&ItemId=%s&output=JS&Cover=Big", + ttbkey, isbn13); + + try { + // 응답을 String으로 받고 JSON 변환 + String jsonResponse = webClient.get() + .uri(url) + .retrieve() + .bodyToMono(String.class) // 응답을 String으로 받음 + .block(); // 비동기 -> 동기 변환 + + if (jsonResponse == null || jsonResponse.isEmpty()) { + System.out.println("API 응답이 비어 있습니다."); + return null; + } + + // JSON을 객체로 변환 + JsonNode rootNode = objectMapper.readTree(jsonResponse); + JsonNode itemNode = rootNode.path("item").get(0); // 첫 번째 도서 정보 추출 + + if (itemNode.isMissingNode()) { + System.out.println("API 응답에 도서 정보가 없습니다."); + return null; + } + + return objectMapper.treeToValue(itemNode, ExternalBookDto.class); + + } catch (Exception e) { + System.out.println("API 요청 또는 응답 변환 오류: " + e.getMessage()); + return null; + } + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/BookSortType.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/BookSortType.java new file mode 100644 index 0000000..26c85b7 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/BookSortType.java @@ -0,0 +1,20 @@ +package com.ll.nbe342team8.domain.book.book.type; + +import org.springframework.data.domain.Sort; + +import com.ll.nbe342team8.global.types.Sortable; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BookSortType implements Sortable { + PUBLISHED_DATE("pubDate", Sort.Direction.DESC), // 출간일순 + SALES_POINT("salesPoint", Sort.Direction.ASC), // 판매량순 + RATING("averageRating", Sort.Direction.DESC), // 평점순 + REVIEW_COUNT("reviewCount", Sort.Direction.DESC); // 리뷰 많은순 + + private final String field; + private final Sort.Direction direction; +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/SearchType.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/SearchType.java new file mode 100644 index 0000000..9047e7c --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/SearchType.java @@ -0,0 +1,5 @@ +package com.ll.nbe342team8.domain.book.book.type; + +public enum SearchType { + TITLE, AUTHOR, ISBN13, PUBLISHER +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/SortType.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/SortType.java deleted file mode 100644 index e6e7b25..0000000 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/book/type/SortType.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ll.nbe342team8.domain.book.book.type; - -import org.springframework.data.domain.Sort; - -public enum SortType { - PUBLISHED_DATE("pubDate", Sort.Direction.DESC), // 출간일순 - SALES_POINT("salesPoint", Sort.Direction.ASC), // 판매량순 - RATING("rating", Sort.Direction.DESC), // 평점순 - REVIEW_COUNT("reviewCount", Sort.Direction.DESC); // 리뷰 많은순 - - private final String field; - private final Sort.Direction direction; - - SortType(String field, Sort.Direction direction) { - this.field = field; - this.direction = direction; - } - - public Sort.Order getOrder() { - return new Sort.Order(direction, field); - } -} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/category/entity/Category.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/category/entity/Category.java index 677362f..450a565 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/category/entity/Category.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/category/entity/Category.java @@ -1,9 +1,15 @@ package com.ll.nbe342team8.domain.book.category.entity; +import java.util.ArrayList; +import java.util.List; + import com.ll.nbe342team8.domain.book.book.entity.Book; -import com.ll.nbe342team8.global.jpa.entity.BaseEntity; -import jakarta.persistence.CascadeType; + +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; @@ -11,37 +17,39 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.ArrayList; -import java.util.List; - @Entity @Getter @Builder @NoArgsConstructor @AllArgsConstructor -public class Category extends BaseEntity { - @NotNull - private Integer categoryId; +public class Category { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT + private Long id; + + @NotNull + @Column(name = "category_id") + private Integer categoryId; - @NotNull - private String categoryName; + @NotNull + private String categoryName; - @NotNull - private String mall; + @NotNull + private String mall; - @NotNull - private String depth1; + @NotNull + private String depth1; - private String depth2; + private String depth2; - private String depth3; + private String depth3; - private String depth4; + private String depth4; - private String depth5; + private String depth5; - @OneToMany(mappedBy = "categoryId", cascade = CascadeType.ALL) - private List books = new ArrayList<>(); + @OneToMany(mappedBy = "categoryId") + private List books = new ArrayList<>(); - private String category; // 카테고리 종류 ex) 국내도서 > 경제/경영 > 재테크/금융 > 재테크 > 부자되는법 + private String category; // 카테고리 종류 ex) 국내도서 > 경제/경영 > 재테크/금융 > 재테크 > 부자되는법 } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/category/repository/CategoryRepository.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/category/repository/CategoryRepository.java index 4a96fd9..283e997 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/category/repository/CategoryRepository.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/category/repository/CategoryRepository.java @@ -1,9 +1,13 @@ package com.ll.nbe342team8.domain.book.category.repository; -import com.ll.nbe342team8.domain.book.category.entity.Category; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import com.ll.nbe342team8.domain.book.category.entity.Category; + @Repository public interface CategoryRepository extends JpaRepository { + Optional findByCategoryId(Integer categoryId); } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/controller/ReviewController.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/controller/ReviewController.java index 0609b4f..bbd2a42 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/controller/ReviewController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/controller/ReviewController.java @@ -2,16 +2,22 @@ import com.ll.nbe342team8.domain.book.book.entity.Book; import com.ll.nbe342team8.domain.book.book.service.BookService; +import com.ll.nbe342team8.domain.book.review.dto.ReviewRequestDto; import com.ll.nbe342team8.domain.book.review.dto.ReviewResponseDto; import com.ll.nbe342team8.domain.book.review.entity.Review; import com.ll.nbe342team8.domain.book.review.service.ReviewService; -import com.ll.nbe342team8.domain.book.review.type.SortType; +import com.ll.nbe342team8.domain.book.review.type.ReviewSortType; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.domain.oauth.SecurityUser; +import com.ll.nbe342team8.global.exceptions.ServiceException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.hibernate.validator.constraints.Range; import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -20,6 +26,8 @@ @RequestMapping("/reviews") public class ReviewController { + //Todo: 모든 컨트롤러 메서드가 적절한 HttpStatus 코드를 반환하도록 수정 + private final ReviewService reviewService; private final BookService bookService; private final MemberService memberService; @@ -27,55 +35,66 @@ public class ReviewController { @GetMapping @Operation(summary = "전체 리뷰 조회") public Page getAllReviews(@RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int pageSize, - @RequestParam(defaultValue = "CREATE_AT_DESC") SortType sortType) { - Page reviews = reviewService.getAllReviews(page, pageSize, sortType); + @RequestParam(defaultValue = "10") @Range(min = 0, max = 100) int pageSize, + @RequestParam(defaultValue = "CREATE_AT_DESC") ReviewSortType reviewSortType) { + Page reviews = reviewService.getAllReviews(page, pageSize, reviewSortType); return reviews.map(ReviewResponseDto::from); } - @GetMapping("/{book-id}") + @GetMapping("/{bookId}") @Operation(summary = "특정 도서 리뷰 조회") - public Page getReviewsById(@PathVariable("book-id") Long bookId, + public Page getReviewsById(@PathVariable Long bookId, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int pageSize, - @RequestParam(defaultValue = "CREATE_AT_DESC") SortType sortType) { - Page reviews = reviewService.getReviewsById(bookId, page, pageSize, sortType); + @RequestParam(defaultValue = "10") @Range(min = 0, max = 100) int pageSize, + @RequestParam(defaultValue = "CREATE_AT_DESC") ReviewSortType reviewSortType) { + Page reviews = reviewService.getReviewsById(bookId, page, pageSize, reviewSortType); return reviews.map(ReviewResponseDto::from); } - @DeleteMapping("/{review-id}") + @DeleteMapping("/{reviewId}") @Operation(summary = "리뷰 삭제") - public void deleteReview(@PathVariable("review-id") Long reviewId) { + public ResponseEntity deleteReview(@PathVariable Long reviewId, + @AuthenticationPrincipal SecurityUser securityUser) { + + Review review = reviewService.getReviewById(reviewId); + + if(review.getMember().getId() != securityUser.getMember().getId()){ + return ResponseEntity.badRequest().body("본인의 리뷰만 삭제할 수 있습니다."); + } + reviewService.deleteReview(reviewId); + return ResponseEntity.ok("리뷰를 삭제했습니다."); } - @PutMapping("/{review-id}") + @PutMapping("/{reviewId}") @Operation(summary = "리뷰 수정") - public void updateReview(@PathVariable("review-id") Long reviewId, + public ResponseEntity updateReview(@PathVariable Long reviewId, @RequestParam(name = "content") String content, - @RequestParam(name = "rating") float rating) { + @RequestParam(name = "rating") Double rating, + @AuthenticationPrincipal SecurityUser securityUser) { + + Review review = reviewService.getReviewById(reviewId); + if(review.getMember().getId() != securityUser.getMember().getId()){ + return ResponseEntity.badRequest().body("본인의 리뷰만 수정할 수 있습니다."); + } reviewService.updateReview(reviewId, content, rating); + return ResponseEntity.ok("리뷰를 수정했습니다."); } - @PostMapping("/{book-id}/{member-id}") + @PostMapping("/{book-id}") @Operation(summary = "리뷰 등록") public void createReview(@PathVariable("book-id") Long bookId, - @PathVariable("member-id") Long memberId, - @RequestBody Review req){ + @AuthenticationPrincipal SecurityUser securityUser, + @RequestBody ReviewRequestDto req){ Book book = bookService.getBookById(bookId); - Member member = memberService.getMemberById(memberId); + Member member = securityUser.getMember(); - Review review = Review.builder() - .book(book) - .member(member) - .content(req.getContent()) - .rating(req.getRating()) - .build(); + Review review = Review.create(book, member, req.content(), req.rating()); - reviewService.create(review, req.getRating()); + reviewService.create(review, req.rating()); } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/dto/ReviewRequestDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/dto/ReviewRequestDto.java new file mode 100644 index 0000000..a3cf1d8 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/dto/ReviewRequestDto.java @@ -0,0 +1,19 @@ +package com.ll.nbe342team8.domain.book.review.dto; + +import com.ll.nbe342team8.domain.book.review.entity.Review; + +import java.time.LocalDateTime; + +public record ReviewRequestDto( + String content, + Double rating +) { + + public static ReviewRequestDto from(Review review){ + return new ReviewRequestDto( + review.getContent(), + review.getRating() + ); + } +} + diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/dto/ReviewResponseDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/dto/ReviewResponseDto.java index ab84661..be35bcb 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/dto/ReviewResponseDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/dto/ReviewResponseDto.java @@ -8,20 +8,22 @@ public record ReviewResponseDto( Long bookId, Long reviewId, - String author, + Long memberId, String content, - float rating, - LocalDateTime createDate + Double rating, + LocalDateTime createDate, + LocalDateTime modifyDate ) { public static ReviewResponseDto from(Review review){ return new ReviewResponseDto( review.getBook().getId(), review.getId(), - review.getMember().getName(), + review.getMember().getId(), review.getContent(), review.getRating(), - review.getCreateDate() + review.getCreateDate(), + review.getModifyDate() ); } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/entity/Review.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/entity/Review.java index 2dca112..3f37a19 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/entity/Review.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/entity/Review.java @@ -3,9 +3,8 @@ import com.ll.nbe342team8.domain.book.book.entity.Book; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.global.jpa.entity.BaseTime; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.*; @Entity @@ -15,18 +14,38 @@ @AllArgsConstructor public class Review extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT + private Long id; + + @NonNull @ManyToOne(fetch = FetchType.LAZY) Book book; + @NonNull @ManyToOne(fetch = FetchType.LAZY) Member member; + @NonNull String content; - float rating; + @NonNull + Double rating; - public void update(String content, float rating){ + public Review(Book book, Member member, String content, Double rating) { + this.book = book; + this.member = member; this.content = content; this.rating = rating; } + + public void update(String content, Double rating){ + this.content = content; + this.rating = rating; + } + + // 정적 팩토리 메서드 - 컨트롤러나 서비스 단에서 빌더 패턴을 사용하지 않고 객체를 생성할 수 있도록 함. + public static Review create(Book book, Member member, String content, Double rating){ + return new Review(book, member, content, rating); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/service/ReviewService.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/service/ReviewService.java index 5ab00af..fccde55 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/service/ReviewService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/service/ReviewService.java @@ -5,18 +5,18 @@ import com.ll.nbe342team8.domain.book.review.dto.ReviewResponseDto; import com.ll.nbe342team8.domain.book.review.entity.Review; import com.ll.nbe342team8.domain.book.review.repository.ReviewRepository; -import com.ll.nbe342team8.domain.book.review.type.SortType; +import com.ll.nbe342team8.domain.book.review.type.ReviewSortType; +import com.ll.nbe342team8.global.exceptions.ServiceException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Optional; @Service @RequiredArgsConstructor @@ -24,32 +24,33 @@ public class ReviewService { private final ReviewRepository reviewRepository; private final BookService bookService; - public Page getAllReviews(int page, int pageSize, SortType sortType) { + public Page getAllReviews(int page, int pageSize, ReviewSortType reviewSortType) { List sorts = new ArrayList<>(); - sorts.add(sortType.getOrder()); + sorts.add(reviewSortType.getOrder()); Pageable pageable = PageRequest.of(page, pageSize, Sort.by(sorts)); return reviewRepository.findAll(pageable); } - public Page getReviewsById(Long bookId, int page, int pageSize, SortType sortType) { + public Page getReviewsById(Long bookId, int page, int pageSize, ReviewSortType reviewSortType) { List sorts = new ArrayList<>(); - sorts.add(sortType.getOrder()); + sorts.add(reviewSortType.getOrder()); Pageable pageable = PageRequest.of(page, pageSize, Sort.by(sorts)); return reviewRepository.findAllByBookId(bookId, pageable); } public Review getReviewById(Long reviewId) { - return reviewRepository.findById(reviewId).orElseThrow(() -> new IllegalArgumentException()); + return reviewRepository.findById(reviewId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "id에 해당하는 리뷰가 없습니다.")); } - public Review create(Review review, float rating) { + public Review create(Review review, Double rating) { bookService.createReview(review.getBook(), rating); return reviewRepository.save(review); } - public ReviewResponseDto updateReview(Long reviewId, String content, float rating) { + public ReviewResponseDto updateReview(Long reviewId, String content, Double rating) { Book book = getReviewById(reviewId).getBook(); Review review = getReviewById(reviewId); diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/type/SortType.java b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/type/ReviewSortType.java similarity index 66% rename from backend/src/main/java/com/ll/nbe342team8/domain/book/review/type/SortType.java rename to backend/src/main/java/com/ll/nbe342team8/domain/book/review/type/ReviewSortType.java index aaf6301..067b614 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/book/review/type/SortType.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/book/review/type/ReviewSortType.java @@ -1,8 +1,13 @@ package com.ll.nbe342team8.domain.book.review.type; +import com.ll.nbe342team8.global.types.Sortable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Sort; -public enum SortType { +@Getter +@RequiredArgsConstructor +public enum ReviewSortType implements Sortable { CREATE_AT_DESC("createDate", Sort.Direction.DESC), // 최근 등록순 CREATE_AT_ASC("createDate", Sort.Direction.ASC), // 과거 등록순 RATING_DESC("rating", Sort.Direction.DESC), // 평점 높은 순 @@ -10,13 +15,4 @@ public enum SortType { private final String field; private final Sort.Direction direction; - - SortType(String field, Sort.Direction direction) { - this.field = field; - this.direction = direction; - } - - public Sort.Order getOrder() { - return new Sort.Order(direction, field); - } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/cart/controller/CartController.java b/backend/src/main/java/com/ll/nbe342team8/domain/cart/controller/CartController.java index 3d3ca33..3799316 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/cart/controller/CartController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/cart/controller/CartController.java @@ -7,14 +7,23 @@ import com.ll.nbe342team8.domain.cart.dto.CartResponseDto; import com.ll.nbe342team8.domain.cart.entity.Cart; import com.ll.nbe342team8.domain.cart.service.CartService; +import com.ll.nbe342team8.domain.jwt.AuthService; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.domain.oauth.SecurityUser; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @RestController @@ -24,54 +33,39 @@ public class CartController { private final CartService cartService; - private final BookService bookService; private final MemberService memberService; + private final BookService bookService; @Operation(summary = "장바구니 추가") - @PostMapping("/{book-id}/{member-id}") - public void addCart(@PathVariable("book-id") long bookId, - @PathVariable("member-id") long memberId, - @RequestParam("quantity") int quantity) { - - CartItemRequestDto cartItemRequestDto = new CartItemRequestDto(bookId, quantity); - CartRequestDto cartRequestDto = new CartRequestDto(List.of(cartItemRequestDto)); - - updateCartItems(memberId, cartRequestDto); - } + @PostMapping + public void addCart(@RequestBody CartRequestDto cartRequestDto, // CartRequestDto로 변경 + @AuthenticationPrincipal SecurityUser securityUser) { - @Operation(summary = "장바구니 수정") - @PutMapping("/{book-id}/{member-id}") - public void updateCartItem(@PathVariable("book-id") long bookId, - @PathVariable("member-id") long memberId, - @RequestParam("quantity") int quantity) { + Member member = securityUser.getMember(); - Member member = memberService.getMemberById(memberId); - - Cart cartItem = member.getCarts().stream() - .filter(cart -> cart.getBook().getId().equals(bookId)) - .findFirst() - .orElse(null); - - cartService.updateCartItem(cartItem, quantity); + if (cartRequestDto != null) { + cartService.updateCartItems(member, cartRequestDto); + } } @Operation(summary = "장바구니 수정 json") - @PostMapping("/{member-id}") - public void updateCartItems(@PathVariable("member-id") long memberId, - @RequestBody CartRequestDto cartRequestDto){ + @PutMapping + public void updateCartItems(@AuthenticationPrincipal SecurityUser securityUser, + @RequestBody @Valid CartRequestDto cartRequestDto) { + + Member member = securityUser.getMember(); - Member member = memberService.getMemberById(memberId); if (cartRequestDto != null) { cartService.updateCartItems(member, cartRequestDto); } } @Operation(summary = "장바구니 삭제") - @DeleteMapping("/{member-id}") - public void deleteBook(@PathVariable("member-id") long memberId, + @DeleteMapping + public void deleteBook(@AuthenticationPrincipal SecurityUser securityUser, @RequestBody CartRequestDto cartRequestDto) { - Member member = memberService.getMemberById(memberId); + Member member = securityUser.getMember(); if (cartRequestDto != null) { cartService.deleteProduct(member, cartRequestDto); @@ -79,13 +73,25 @@ public void deleteBook(@PathVariable("member-id") long memberId, } @Operation(summary = "장바구니 조회") - @GetMapping("/{member-id}") - public List getCart(@PathVariable("member-id") long memberId) { - Member member = memberService.getMemberById(memberId); + @GetMapping + public List getCart(@AuthenticationPrincipal SecurityUser securityUser) { + + Member member = securityUser.getMember(); List carts = cartService.findCartByMember(member); return carts.stream() .map(CartResponseDto::from) .collect(Collectors.toList()); } + + @PostMapping("/anonymous") + public ResponseEntity> getAnonymousCart(@RequestBody @Valid CartRequestDto cartRequestDto) { + + List cartItems = cartService.getCartItems(cartRequestDto); + List cartResponseDto = cartItems.stream() + .map(CartResponseDto::from) + .collect(Collectors.toList()); + + return ResponseEntity.ok(cartResponseDto); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartDto.java new file mode 100644 index 0000000..12b3848 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartDto.java @@ -0,0 +1,13 @@ +package com.ll.nbe342team8.domain.cart.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Builder; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +@Builder +public class CartDto { +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartItemRequestDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartItemRequestDto.java index 78b7c31..661a516 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartItemRequestDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartItemRequestDto.java @@ -1,6 +1,14 @@ package com.ll.nbe342team8.domain.cart.dto; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + public record CartItemRequestDto( + + @NotNull(message = "도서 ID는 필수입니다") Long bookId, - int quantity + + @Min(1) int quantity, + + boolean isAddToCart ) {} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartRequestDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartRequestDto.java index b4ab532..3585fbc 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartRequestDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartRequestDto.java @@ -1,7 +1,13 @@ package com.ll.nbe342team8.domain.cart.dto; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + import java.util.List; public record CartRequestDto( + + @NotEmpty(message = "장바구니 항목은 최소 1개 이상이어야 합니다") + @Valid List cartItems ) {} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartResponseDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartResponseDto.java index fa7ecc5..d3ab9a6 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartResponseDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/cart/dto/CartResponseDto.java @@ -3,7 +3,6 @@ import com.ll.nbe342team8.domain.cart.entity.Cart; public record CartResponseDto( - Long memberId, Long bookId, int quantity, String title, @@ -13,7 +12,6 @@ public record CartResponseDto( public static CartResponseDto from(Cart cart) { return new CartResponseDto( - cart.getMember().getId(), cart.getBook().getId(), cart.getQuantity(), cart.getBook().getTitle(), diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/cart/entity/Cart.java b/backend/src/main/java/com/ll/nbe342team8/domain/cart/entity/Cart.java index 01a38f1..79e0bfb 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/cart/entity/Cart.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/cart/entity/Cart.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.ll.nbe342team8.domain.book.book.entity.Book; import com.ll.nbe342team8.domain.member.member.entity.Member; -import com.ll.nbe342team8.global.jpa.entity.BaseEntity; +import com.ll.nbe342team8.global.jpa.entity.BaseTime; import jakarta.persistence.*; import lombok.*; @@ -13,7 +13,18 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class Cart extends BaseEntity { +@Table( + name = "cart", + uniqueConstraints = @UniqueConstraint( + name = "unique_member_book", + columnNames = {"member_id", "book_id"} + ) +) +public class Cart extends BaseTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT + private Long id; @JsonIgnore @ManyToOne(fetch = FetchType.LAZY) @@ -28,4 +39,24 @@ public class Cart extends BaseEntity { public void updateCart(int quantity) { this.quantity = quantity; } + + public Cart(Member member, Book book, int quantity) { + this.member = member; + this.book = book; + this.quantity = quantity; + } + + public static Cart create(Book book, int quantity) { + return new Cart(null, book, quantity); + } + + @Override + public String toString() { + return "Cart{" + + "id=" + id + + ", member=" + member + + ", book=" + book + + ", quantity=" + quantity + + '}'; + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/cart/repository/CartRepository.java b/backend/src/main/java/com/ll/nbe342team8/domain/cart/repository/CartRepository.java index e14b06e..6e831a1 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/cart/repository/CartRepository.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/cart/repository/CartRepository.java @@ -1,13 +1,19 @@ package com.ll.nbe342team8.domain.cart.repository; +import com.ll.nbe342team8.domain.book.book.entity.Book; import com.ll.nbe342team8.domain.cart.entity.Cart; import com.ll.nbe342team8.domain.member.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface CartRepository extends JpaRepository { List findAllByMember(Member member); + + Optional findByMemberAndBook(Member member, Book book); + + void deleteByMember(Member member); } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/cart/service/CartService.java b/backend/src/main/java/com/ll/nbe342team8/domain/cart/service/CartService.java index 187e4a7..c5cd602 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/cart/service/CartService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/cart/service/CartService.java @@ -4,13 +4,23 @@ import com.ll.nbe342team8.domain.book.book.service.BookService; import com.ll.nbe342team8.domain.cart.dto.CartItemRequestDto; import com.ll.nbe342team8.domain.cart.dto.CartRequestDto; +import com.ll.nbe342team8.domain.cart.dto.CartResponseDto; import com.ll.nbe342team8.domain.cart.entity.Cart; import com.ll.nbe342team8.domain.cart.repository.CartRepository; import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.global.exceptions.ServiceException; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -28,27 +38,62 @@ public void updateCartItem(Cart cart, int quantity) { cartRepository.save(cart); } + @Transactional public void updateCartItems(Member member, CartRequestDto cartRequestDto) { - for (CartItemRequestDto cartItemRequestDto : cartRequestDto.cartItems()) { - Book book = bookService.getBookById(cartItemRequestDto.bookId()); - Cart cart = findCartByBook(member, cartItemRequestDto.bookId()); + // 기존 장바구니 데이터를 맵으로 변환 (bookId 기준) + Map cartMap = member.getCarts().stream() + .collect(Collectors.toMap(cart -> cart.getBook().getId(), cart -> cart)); - if (cart != null) { - cart.updateCart(cart.getQuantity() + cartItemRequestDto.quantity()); - } else { + // 변경된 Cart 객체를 저장할 리스트 + List cartsToSave = new ArrayList<>(); + + cartRequestDto.cartItems().forEach(item -> { + Book book = bookService.getBookById(item.bookId()); + Cart cart = cartMap.get(book.getId()); + + if (cart == null) { + // 새로운 Cart 객체 생성 cart = Cart.builder() .member(member) .book(book) - .quantity(cartItemRequestDto.quantity()) + .quantity(item.quantity()) .build(); + } else { + // 기존 Cart 객체 업데이트 + int newQuantity = item.isAddToCart() ? cart.getQuantity() + item.quantity() : item.quantity(); + cart.updateCart(newQuantity); } - cartRepository.save(cart); - } + + cartsToSave.add(cart); + }); + + // saveAll로 한 번에 저장 + cartRepository.saveAll(cartsToSave); } +// public void updateCartItems(Member member, CartRequestDto cartRequestDto) { +// cartRequestDto.cartItems().forEach(item -> { +// Book book = bookService.getBookById(item.bookId()); +// Cart cart = findCartByBook(member, book.getId()); +// +// if (cart == null) { +// cart = Cart.builder() +// .member(member) +// .book(book) +// .quantity(item.quantity()) +// .build(); +// } else { +// int newQuantity = item.isAddToCart() ? cart.getQuantity() + item.quantity() : item.quantity(); +// cart.updateCart(newQuantity); +// } +// +// cartRepository.save(cart); +// }); +// } + private Cart findCartByBook(Member member, Long bookId) { for (Cart cart : member.getCarts()) { - if (cart.getBook().getId().equals(bookId)) { + if (cart.getBook().getId() == bookId) { return cart; } } @@ -62,12 +107,25 @@ public void deleteProduct(Member member, CartRequestDto cartRequestDto) { Cart cartItem = member.getCarts().stream() .filter(cart -> cart.getBook().getId().equals(cartItemRequestDto.bookId())) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("장바구니에 없음")); + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "해당하는 장바구니 정보가 없습니다.")); cartRepository.delete(cartItem); }); } + public void deleteProduct(Member member) { + cartRepository.deleteByMember(member); + } + public List findCartByMember(Member member) { return cartRepository.findAllByMember(member); } + + public List getCartItems(@Valid CartRequestDto cartRequestDto) { + return cartRequestDto.cartItems().stream() + .map(cartItemRequestDto -> { + Book book = bookService.getBookById(cartItemRequestDto.bookId()); + return Cart.create(book, cartItemRequestDto.quantity()); + }) + .toList(); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/event/controller/EventController.java b/backend/src/main/java/com/ll/nbe342team8/domain/event/controller/EventController.java index ce69198..c3d0ab7 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/event/controller/EventController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/event/controller/EventController.java @@ -1,31 +1,42 @@ -package com.ll.nbe342team8.domain.event.controller; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/event") -public class EventController { - - @GetMapping("/banners") - public List getBannerImages() throws IOException { - String location = "classpath:static/images/eventBanner/"; - PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); - Resource[] resources = resolver.getResources(location + "**"); - - return Arrays.stream(resources) - .map(resource -> "/images/eventBanner/" + resource.getFilename()) - .collect(Collectors.toList()); - } - -} - +//package com.ll.nbe342team8.domain.event.controller; +// +//import jakarta.annotation.PostConstruct; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.core.io.Resource; +//import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RestController; +// +//import java.io.IOException; +//import java.util.Arrays; +//import java.util.List; +//import java.util.stream.Collectors; +// +//@RestController +//@RequestMapping("/event") +//public class EventController { +// +// //서버 실행 시 이미지를 저장해둘 리스트 +// private List bannerImages; +// +// @PostConstruct +// public void initBannerImages() throws IOException { +// String location = "classpath:static/images/eventBanner/"; +// PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); +// Resource[] resources = resolver.getResources(location + "**"); +// +// // spring 실행 시 처음 한번만 리스트에 이미지 저장 +// // 프론트에서 요청 시 리스트 반환 +// bannerImages = Arrays.stream(resources) +// .map(resource -> "/images/eventBanner/" + resource.getFilename()) +// .collect(Collectors.toList()); +// } +// +// @GetMapping("/banners") +// public List getBannerImages() { +// return bannerImages; +// } +// +//} +// diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/jwt/AuthService.java b/backend/src/main/java/com/ll/nbe342team8/domain/jwt/AuthService.java new file mode 100644 index 0000000..a837670 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/jwt/AuthService.java @@ -0,0 +1,98 @@ +package com.ll.nbe342team8.domain.jwt; + +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.repository.MemberRepository; +import com.ll.nbe342team8.global.exceptions.ServiceException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.CookieValue; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final JwtService jwtService; + private final MemberRepository memberRepository; + + // ✅ 토큰 유효성 검사 - Authorization 헤더 제거, 쿠키 사용 + public Member validateTokenAndGetMember(@CookieValue(value = "accessToken", required = false) String token) { + if (token == null) { + throw new ServiceException(401, "로그인이 필요합니다."); + } + + if (!jwtService.validateToken(token)) { + throw new ServiceException(401, "Invalid token"); + } + + String kakaoId = jwtService.getKakaoIdFromToken(token); + return memberRepository.findByoAuthId(kakaoId) + .orElseThrow(() -> new ServiceException(404, "사용자를 찾을 수 없습니다.")); + } + + // ✅ JWT 기반 로그인 처리 - 쿠키로 `accessToken`, `refreshToken` 설정 + public ResponseEntity authenticate(@CookieValue(value = "accessToken", required = false) String token) { + Member member = validateTokenAndGetMember(token); + + String newAccessToken = jwtService.generateToken(member); + String newRefreshToken = jwtService.generateRefreshToken(member); + + // ✅ 쿠키에 `accessToken`, `refreshToken` 저장 + ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", newAccessToken) + .httpOnly(true) + .secure(false) + .sameSite("None") + .path("/") + .maxAge(60 * 60) // 1시간 + .build(); + + ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", newRefreshToken) + .httpOnly(true) + .secure(false) + .sameSite("None") + .path("/api/auth/refresh") + .maxAge(7 * 24 * 60 * 60) // 7일 + .build(); + + return ResponseEntity.ok() + .header("Set-Cookie", accessTokenCookie.toString()) + .header("Set-Cookie", refreshTokenCookie.toString()) + .body("Authentication successful"); + } + + // ✅ 사용자 정보 조회 - 쿠키 기반 인증 적용 + public Member getMemberFromToken(@CookieValue(value = "accessToken", required = false) String token) { + if (token == null) { + throw new ServiceException(401, "로그인이 필요합니다."); + } + + if (!jwtService.validateToken(token)) { + throw new ServiceException(401, "Invalid token"); + } + + String kakaoId = jwtService.getKakaoIdFromToken(token); + return memberRepository.findByoAuthId(kakaoId) + .orElseThrow(() -> new ServiceException(404, "사용자를 찾을 수 없습니다.")); + + } + + // ✅ 로그아웃 - `Set-Cookie`로 `accessToken`, `refreshToken` 삭제 + public ResponseEntity logout() { + ResponseCookie deleteAccessToken = ResponseCookie.from("accessToken", "") + .path("/") + .maxAge(0) + .secure(true) + .build(); + + ResponseCookie deleteRefreshToken = ResponseCookie.from("refreshToken", "") + .path("/") + .maxAge(0) + .secure(true) + .build(); + + return ResponseEntity.ok() + .header("Set-Cookie", deleteAccessToken.toString()) + .header("Set-Cookie", deleteRefreshToken.toString()) + .body("로그아웃 되었습니다."); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/jwt/JwtAuthenticationFilter.java b/backend/src/main/java/com/ll/nbe342team8/domain/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..de6e6fa --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,78 @@ +package com.ll.nbe342team8.domain.jwt; + + +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.domain.oauth.SecurityUser; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final MemberService memberService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + log.info("현재 요청 URI: {}", request.getRequestURI()); + String token = extractTokenFromCookies(request); + log.info("쿠키에서 추출된 토큰 존재 여부: {}", token != null); + + if (token != null && jwtService.validateToken(token)) { + String kakaoId = jwtService.getKakaoIdFromToken(token); // findByOauthId랑 기능 비교하기 + log.info("토큰에서 추출된 카카오 ID: {}", kakaoId); + + Member member = memberService.findByOauthId(kakaoId) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + log.info("회원 조회 성공: {}", member.getEmail()); + + SecurityUser securityUser = new SecurityUser(member); + Authentication authentication = new UsernamePasswordAuthenticationToken( + securityUser, null, securityUser.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + log.info("SecurityContextHolder 최종 인증 정보: {}", SecurityContextHolder.getContext().getAuthentication()); + log.info("인증 정보 설정 완료"); + } + } catch (Exception e) { + log.error("JWT 인증 처리 중 오류 발생: ", e); + } + + chain.doFilter(request, response); + } + + private String extractTokenFromCookies(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + log.info("요청에 포함된 쿠키 개수: {}", cookies != null ? cookies.length : 0); + if (cookies != null) { + for (Cookie cookie : cookies) { + log.info("쿠키 정보 - 이름: {}, 값 존재 여부: {}", + cookie.getName(), cookie.getValue() != null); + if ("accessToken".equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + return path.startsWith("/books") || path.equals("/login") || path.equals("/cart/anonymous"); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/jwt/JwtService.java b/backend/src/main/java/com/ll/nbe342team8/domain/jwt/JwtService.java new file mode 100644 index 0000000..2d00914 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/jwt/JwtService.java @@ -0,0 +1,101 @@ +package com.ll.nbe342team8.domain.jwt; + +import com.ll.nbe342team8.domain.member.member.entity.Member; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@Slf4j +public class JwtService { + // 서명 키를 한 번만 생성하여 재사용 + private final Key key; + private static final long TOKEN_VALIDITY = 60 * 60 * 1000L; // 1시간 + private static final long REFRESH_TOKEN_VALIDITY = 7 * 24 * 60 * 60 * 1000L; // 7일 + private final Set tokenBlacklist = ConcurrentHashMap.newKeySet(); + + // 생성자에서 키를 초기화하여 일관성 보장 + public JwtService(@Value("${custom.jwt.secretKey}") String secretKey) { + byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public String generateToken(Member member) { + Date now = new Date(); + Date validity = new Date(now.getTime() + TOKEN_VALIDITY); + + String token = Jwts.builder() + .setSubject(member.getOAuthId()) + .claim("id", member.getId()) + .claim("email", member.getEmail()) + .claim("name", member.getName()) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(key) + .compact(); + + // URL 안전한 형태로 인코딩 + return URLEncoder.encode(token, StandardCharsets.UTF_8); + } + + public String generateRefreshToken(Member member) { + String token = Jwts.builder() + .setSubject(member.getOAuthId()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_VALIDITY)) + .signWith(key) + .compact(); + + return URLEncoder.encode(token, StandardCharsets.UTF_8); + } + + public boolean validateToken(String token) { + try { + // URL 디코딩 후 검증 + log.debug("받은 토큰: {}", token); + String decodedToken = URLDecoder.decode(token, StandardCharsets.UTF_8); + log.debug("디코딩된 토큰: {}", decodedToken); + + if (tokenBlacklist.contains(decodedToken)) { + log.debug("블랙리스트에 있는 토큰"); + return false; + } + + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(decodedToken); + log.debug("토큰 검증 성공"); + return true; + } catch (Exception e) { + log.error("토큰 검증 실패: ", e); + log.error("상세 에러: ", e); + return false; + } + } + + public String getKakaoIdFromToken(String token) { + try { + String decodedToken = URLDecoder.decode(token, StandardCharsets.UTF_8); + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(decodedToken) + .getBody() + .getSubject(); + } catch (Exception e) { + log.error("토큰에서 카카오 ID 추출 실패: ", e); + throw e; + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/controller/DeliveryInformationController.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/controller/DeliveryInformationController.java index b9221fa..b61c324 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/controller/DeliveryInformationController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/controller/DeliveryInformationController.java @@ -7,14 +7,22 @@ import com.ll.nbe342team8.domain.member.member.dto.ResMemberMyPageDto; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.domain.oauth.SecurityUser; +import com.ll.nbe342team8.domain.qna.question.entity.Question; import com.ll.nbe342team8.global.exceptions.ServiceException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; -import java.util.Optional; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +@Tag(name = "DeliveryInformationController", description = "배송 정보 컨트롤러") @RestController @RequiredArgsConstructor public class DeliveryInformationController { @@ -24,82 +32,108 @@ public class DeliveryInformationController { // 배송 정보 등록. 5개 까지 등록 할 수 있으며 한번에 하나씩 등록한다. // 등록할 배송 정보인 DeliveryInformationDto를 매개변수로 받는다. + @Operation(summary = "배송 정보 등록(최대 5개)") @PostMapping("/my/deliveryInformation") public ResponseEntity postDeliveryInformation(@RequestBody @Valid ReqDeliveryInformationDto reqDeliveryInformationDto - ) { + ) { - //jwt 토큰에서 id를 통해 회원정보를 찾는다. - //여기선 임시로 이메일을 통해 회원정보를 찾는다. - String email="rdh0427@naver.com"; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Optional optionalMember = memberService.findByEmail(email); + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } - //이메일에 대응하는 사용자가 없는 경우 에러 발생 - if(optionalMember.isEmpty()) { throw new ServiceException(404,"사용자를 찾을 수 없습니다.");} + String oauthId=securityUser.getMember().getOAuthId(); - Member member=optionalMember.get(); + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); //배송 정보 설정은 5개 까지 허용한다. 5개 일때 배송지 추가 등록 요청이 올 경우 에러를 반환한다. - if(member.getDeliveryInformations().size() >=5) { throw new ServiceException(400,"배송지는 5개까지 설정할수있습니다."); } + if(member.getDeliveryInformations().size() >=5) { throw new ServiceException(HttpStatus.CONFLICT.value(), "배송지는 5개까지 설정할수있습니다."); } //사용자 개체의 배송 정보 리스트에 배송 정보 추가 deliveryInformationService.addDeliveryInformation(member,reqDeliveryInformationDto); //갱신된 사용자 개체를 dto로 변환해 반환한다. 프론트에선 반환 받는 memberDto로 마이페이지 갱신 ResMemberMyPageDto resMemberMyPageDto=new ResMemberMyPageDto(member); - return ResponseEntity.status(200).body(resMemberMyPageDto); + return ResponseEntity.status(HttpStatus.CREATED).body(resMemberMyPageDto); } //등록해논 배송 정보 삭제기능. 하나씩 삭제 가능하며 배송 정보의 id를 매개변수로 받는다. + @Operation(summary = "배송 정보 삭제 (한개)") @DeleteMapping("/my/deliveryInformation/{id}") public ResponseEntity deleteDeliveryInformation(@PathVariable(name = "id") Long id - ) { + ) { - //jwt 토큰에서 id를 통해 회원정보를 찾는다. - //여기선 임시로 이메일을 통해 회원정보를 찾는다. - String email="rdh0427@naver.com"; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Optional optionalMember = memberService.findByEmail(email); + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } - if(optionalMember.isEmpty()) { throw new ServiceException(404,"사용자를 찾을 수 없습니다.");} + String oauthId=securityUser.getMember().getOAuthId(); - Member member=optionalMember.get(); + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); + + // 삭제할 배송 정보를 id로 탐색 + DeliveryInformation deliveryInformation = deliveryInformationService.findById(id) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "배송정보를 찾을 수 없습니다.")); + + validateDeliveryInformationOwner(member, deliveryInformation); //배송 정보 id로 배송 정보를 찾아 삭제 - deliveryInformationService.deleteDeliveryInformation(member,id); + deliveryInformationService.deleteDeliveryInformation(deliveryInformation); - // dto로 갱신된 member 데이터를 반환 + // dto로 갱신된 memberId 데이터를 반환 ResMemberMyPageDto resMemberMyPageDto=new ResMemberMyPageDto(member); - return ResponseEntity.status(200).body(resMemberMyPageDto); + return ResponseEntity.status(HttpStatus.OK).body(resMemberMyPageDto); } //등록해논 배송 정보를 갱신한다. 하나씩 갱신 가능하며 배송 정보 id를 매개변수로 받는다. + @Operation(summary = "배송 정보 갱신 (한개)") @PutMapping("/my/deliveryInformation/{id}") public ResponseEntity putDeliveryInformation(@PathVariable(name = "id") Long id - , @RequestBody @Valid ReqDeliveryInformationDto reqDeliveryInformationDto) { + , @RequestBody @Valid ReqDeliveryInformationDto reqDeliveryInformationDto) { - //jwt 토큰에서 id를 통해 회원정보를 찾는다. - //여기선 임시로 이메일을 통해 회원정보를 찾는다. - String email="rdh0427@naver.com"; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Optional optionalMember = memberService.findByEmail(email); + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } - if(optionalMember.isEmpty()) { throw new ServiceException(404,"사용자를 찾을 수 없습니다.");} + String oauthId=securityUser.getMember().getOAuthId(); - Member member=optionalMember.get(); + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); // 갱신할 배송 정보를 id로 탐색 - Optional optionalDeliveryInformation = deliveryInformationService.findById(id); + DeliveryInformation deliveryInformation = deliveryInformationService.findById(id) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); - if(optionalDeliveryInformation.isEmpty()) { throw new ServiceException(404,"배송지를 찾을 수 없습니다.");} + validateDeliveryInformationOwner(member, deliveryInformation); + //validateExistsDuplicateQuestionInShortTime 추가 필요(매개변수 너무 많은거 해결 필요) //배송 정보와 기본 배송지 설정을 갱신한다. - deliveryInformationService.modifyDeliveryInformation(optionalDeliveryInformation.get(),reqDeliveryInformationDto,member); + deliveryInformationService.modifyDeliveryInformation(deliveryInformation,reqDeliveryInformationDto,member); ResMemberMyPageDto resMemberMyPageDto=new ResMemberMyPageDto(member); - return ResponseEntity.status(200).body(resMemberMyPageDto); + return ResponseEntity.ok(resMemberMyPageDto); + } + + //사용자 권한 확인, 관리자 계정이여도 접근 가능 + private void validateDeliveryInformationOwner(Member member, DeliveryInformation deliveryInformation) { + if (!(deliveryInformationService.isDeliveryInformationOwner(member, deliveryInformation) || checkAdmin(member))) { + throw new ServiceException(HttpStatus.FORBIDDEN.value(), "권한이 없습니다."); + } + } + + + + private boolean checkAdmin(Member member) { + return member.getMemberType() == Member.MemberType.ADMIN; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/dto/DeliveryInformationDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/dto/DeliveryInformationDto.java index 4cdea44..c60d458 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/dto/DeliveryInformationDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/dto/DeliveryInformationDto.java @@ -6,44 +6,24 @@ import lombok.NoArgsConstructor; import lombok.Setter; -@Getter -@Setter -@NoArgsConstructor -public class DeliveryInformationDto { - - @JsonProperty("id") - Long id; - - @JsonProperty("addressName") - String addressName; - - @JsonProperty("postCode") - String postCode; - - @JsonProperty("detailAddress") - String detailAddress; - - @JsonProperty("recipient") - String recipient; - - @JsonProperty("phone") - String phone; - - @JsonProperty("isDefaultAddress") - Boolean isDefaultAddress; - +public record DeliveryInformationDto( + Long id, + String addressName, + String postCode, + String detailAddress, + String recipient, + String phone, + Boolean isDefaultAddress +) { public DeliveryInformationDto(DeliveryInformation deliveryInformation) { - - this.id=deliveryInformation.getId(); - this.addressName=deliveryInformation.getAddressName(); - this.postCode=deliveryInformation.getPostCode(); - this.detailAddress=deliveryInformation.getDetailAddress(); - this.recipient=deliveryInformation.getRecipient(); - this.phone=deliveryInformation.getPhone(); - this.isDefaultAddress= deliveryInformation.getIsDefaultAddress(); - + this( + deliveryInformation.getId(), + deliveryInformation.getAddressName(), + deliveryInformation.getPostCode(), + deliveryInformation.getDetailAddress(), + deliveryInformation.getRecipient(), + deliveryInformation.getPhone(), + deliveryInformation.getIsDefaultAddress() + ); } - - - -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/dto/ReqDeliveryInformationDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/dto/ReqDeliveryInformationDto.java index 2b87e59..d56cc1e 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/dto/ReqDeliveryInformationDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/dto/ReqDeliveryInformationDto.java @@ -10,39 +10,26 @@ import lombok.NoArgsConstructor; import lombok.Setter; -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -public class ReqDeliveryInformationDto { - @JsonProperty("id") - Long id; +public record ReqDeliveryInformationDto( + Long id, - @NotBlank(message = "공백은 허용하지 않습니다.") - @JsonProperty("addressName") - String addressName; + @NotBlank(message = "공백은 허용하지 않습니다.") + String addressName, - @NotBlank(message = "공백은 허용하지 않습니다.") - @JsonProperty("postCode") - String postCode; + @NotBlank(message = "공백은 허용하지 않습니다.") + String postCode, - @NotBlank(message = "공백은 허용하지 않습니다.") - @JsonProperty("detailAddress") - String detailAddress; + @NotBlank(message = "공백은 허용하지 않습니다.") + String detailAddress, - @NotBlank(message = "공백은 허용하지 않습니다.") - @JsonProperty("recipient") - String recipient; + @NotBlank(message = "공백은 허용하지 않습니다.") + String recipient, - @NotBlank(message = "공백은 허용하지 않습니다.") - @Pattern(regexp = "^\\d{3}-\\d{4}-\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다. (010-XXXX-XXXX)") - @JsonProperty("phone") - String phone; + @NotBlank(message = "공백은 허용하지 않습니다.") + @Pattern(regexp = "^\\d{3}-\\d{4}-\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다. (010-XXXX-XXXX)") + String phone, - @NotNull - @JsonProperty("isDefaultAddress") - Boolean isDefaultAddress; - - -} + @NotNull + Boolean isDefaultAddress +) {} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/entity/DeliveryInformation.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/entity/DeliveryInformation.java index 20fd2dd..f1ac7ac 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/entity/DeliveryInformation.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/entity/DeliveryInformation.java @@ -5,9 +5,7 @@ import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.global.jpa.entity.BaseTime; import com.ll.nbe342team8.standard.util.Ut; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.*; @Entity @@ -18,39 +16,43 @@ @AllArgsConstructor public class DeliveryInformation extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT + private Long id; + private String addressName; - + private String postCode; - + private String detailAddress; - + private Boolean isDefaultAddress; - + private String recipient; - + private String phone; @ManyToOne(fetch = FetchType.LAZY) private Member member; public DeliveryInformation(ReqDeliveryInformationDto dto, Member member) { - this.addressName = Ut.XSSSanitizer.sanitize(dto.getAddressName()); - this.postCode = Ut.XSSSanitizer.sanitize(dto.getPostCode()); - this.detailAddress = Ut.XSSSanitizer.sanitize(dto.getDetailAddress()); - this.recipient = Ut.XSSSanitizer.sanitize(recipient); - this.phone = Ut.XSSSanitizer.sanitize(dto.getPhone()); - this.isDefaultAddress=dto.getIsDefaultAddress(); + this.addressName = Ut.XSSSanitizer.sanitize(dto.addressName()); + this.postCode = Ut.XSSSanitizer.sanitize(dto.postCode()); + this.detailAddress = Ut.XSSSanitizer.sanitize(dto.detailAddress()); + this.recipient = Ut.XSSSanitizer.sanitize(dto.recipient()); + this.phone = Ut.XSSSanitizer.sanitize(dto.phone()); + this.isDefaultAddress=dto.isDefaultAddress(); this.member=member; } public void updateDeliveryInfo(ReqDeliveryInformationDto dto) { - this.addressName = Ut.XSSSanitizer.sanitize(dto.getAddressName()); - this.postCode = Ut.XSSSanitizer.sanitize(dto.getPostCode()); - this.detailAddress = Ut.XSSSanitizer.sanitize(dto.getDetailAddress()); - this.recipient = Ut.XSSSanitizer.sanitize(recipient); - this.phone = Ut.XSSSanitizer.sanitize(dto.getPhone()); - this.isDefaultAddress=dto.getIsDefaultAddress(); + this.addressName = Ut.XSSSanitizer.sanitize(dto.addressName()); + this.postCode = Ut.XSSSanitizer.sanitize(dto.postCode()); + this.detailAddress = Ut.XSSSanitizer.sanitize(dto.detailAddress()); + this.recipient = Ut.XSSSanitizer.sanitize(dto.recipient()); + this.phone = Ut.XSSSanitizer.sanitize(dto.phone()); + this.isDefaultAddress=dto.isDefaultAddress(); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/service/DeliveryInformationService.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/service/DeliveryInformationService.java index 83d88fe..772100c 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/service/DeliveryInformationService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/deliveryInformation/service/DeliveryInformationService.java @@ -5,6 +5,7 @@ import com.ll.nbe342team8.domain.member.deliveryInformation.dto.DeliveryInformationDto; import com.ll.nbe342team8.domain.member.deliveryInformation.repository.DeliveryInformationRepository; import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.qna.question.entity.Question; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,16 +26,16 @@ public void addDeliveryInformation(Member member, ReqDeliveryInformationDto dto) //사용자 개체의 배송 정보 리스트에 생성한 배송 정보개체를 추가 등록한다. //더티 체킹을 이용해 사용자 개체를 갱신한다. - member.addDeliveryInformaiton(deliveryInformation); + member.addDeliveryInformation(deliveryInformation); } @Transactional - public void deleteDeliveryInformation(Member member,Long id) { + public void deleteDeliveryInformation(DeliveryInformation deliveryInformation) { //사용자개체의 배송 정보 리스트에서 id에 해당하는 deliveryInformation을 찾아 삭제 //더티 체킹을 이용해 개체 갱신 - member.deleteDeliveryInformaiton(id); + deliveryInformationRepository.delete(deliveryInformation); } @Transactional @@ -43,8 +44,8 @@ public void modifyDeliveryInformation(DeliveryInformation deliveryInformation,Re //isDefaultAddress 값을 모두 false로 만든 후 deliveryInformation을 갱신 -> 언제나 기본 배송지 1개로 유지 가능하다. //dto의 isDefaultAddress가 false인 경우 그냥 데이터 갱신한다. //더티 체킹을 이용한 개체 갱신 - if(dto.getIsDefaultAddress()) { - member.convertFalseDeliveryInformaitonsIsDefaultAddress(); + if(dto.isDefaultAddress()) { + member.convertFalseDeliveryInformationsIsDefaultAddress(); } deliveryInformation.updateDeliveryInfo(dto); } @@ -52,4 +53,10 @@ public void modifyDeliveryInformation(DeliveryInformation deliveryInformation,Re public Optional findById(Long id) { return deliveryInformationRepository.findById(id); } -} + + //수정, 삭제하려는 게시글을 사용자가 작성한지 학인 + public boolean isDeliveryInformationOwner(Member member, DeliveryInformation deliveryInformation) { + + return deliveryInformation.getMember().getId().equals(member.getId()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/controller/MemberController.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/controller/MemberController.java index a010daf..aca496c 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/controller/MemberController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/controller/MemberController.java @@ -1,71 +1,110 @@ package com.ll.nbe342team8.domain.member.member.controller; +import com.ll.nbe342team8.domain.jwt.AuthService; +import com.ll.nbe342team8.domain.member.member.dto.MemberDto; import com.ll.nbe342team8.domain.member.member.dto.PutReqMemberMyPageDto; import com.ll.nbe342team8.domain.member.member.dto.ResMemberMyPageDto; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.domain.oauth.SecurityUser; import com.ll.nbe342team8.global.exceptions.ServiceException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; import java.util.Optional; +@RequestMapping("/api/auth/me") @RestController +@Tag(name = "Member", description = "Member API") @RequiredArgsConstructor public class MemberController { + private final AuthService authService; private final MemberService memberService; + @GetMapping + public ResponseEntity getUserInfo() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null && authentication.getPrincipal() instanceof SecurityUser securityUser) { + return ResponseEntity.ok(new MemberDto(securityUser.getMember())); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + //마이페이지 데이터를 불러온다. 마이페이지는 resMemberMyPageDto 데이터를 이용해 마이페이지를 구성한다. + @Operation(summary = "사용자 정보 조회") @GetMapping("/my") public ResponseEntity getMyPage() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - //jwt 토큰에서 id를 통해 회원정보를 찾는다. - //여기선 임시로 이메일을 통해 회원정보를 찾는다. - String email="rdh0427@naver.com"; + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } - Optional optionalMember = memberService.findByEmail(email); + String oauthId=securityUser.getMember().getOAuthId(); - //사용자가 존재하지 않는 경우 에러 반환 - if(optionalMember.isEmpty()) { throw new ServiceException(404,"사용자를 찾을 수 없습니다.");} + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); //마이페이지 구성을 위한 데이터 반환 - ResMemberMyPageDto memberMyPageDto=new ResMemberMyPageDto(optionalMember.get()); + ResMemberMyPageDto memberMyPageDto=new ResMemberMyPageDto(member); + + return ResponseEntity.ok(memberMyPageDto); - return ResponseEntity.status(200).body(memberMyPageDto); + /* + @GetMapping("/my") + public ResponseEntity getMyPage(@CookieValue(value = "accessToken", required = false) String token) { + Member memberId = authService.getMemberFromToken(token); + // 명시적으로 지연 로딩 데이터 초기화 + + ResMemberMyPageDto memberMyPageDto = new ResMemberMyPageDto(memberId); + return ResponseEntity.ok(memberMyPageDto); + }*/ } //사용자 정보를 갱신하는 기능(배송 정보 제외)이며 갱신 정보는 putReqMemberMyPageDto로 받는다. + @Operation(summary = "사용자 정보 갱신") @PutMapping("/my") public ResponseEntity putMyPage(@RequestBody @Valid PutReqMemberMyPageDto putReqMemberMyPageDto - ) { + ) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - //jwt 토큰에서 id를 통해 회원정보를 찾는다. - //여기선 임시로 이메일을 통해 회원정보를 찾는다. - String email="rdh0427@naver.com"; + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { - Optional optionalMember = memberService.findByEmail(email); + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } - if(optionalMember.isEmpty()) { throw new ServiceException(404,"사용자를 찾을 수 없습니다.");} + String oauthId=securityUser.getMember().getOAuthId(); - Member member=optionalMember.get(); + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); // jwt 토큰으로 찾은 사용자 개체 갱신 - memberService.modifyMember(member,putReqMemberMyPageDto); + memberService.modifyOrJoin(member.getOAuthId(), putReqMemberMyPageDto, member.getEmail()); - ResMemberMyPageDto resMemberMyPageDto=new ResMemberMyPageDto(member); + ResMemberMyPageDto resMemberMyPageDto = new ResMemberMyPageDto(member); - return ResponseEntity.status(200).body(resMemberMyPageDto); + return ResponseEntity.ok(resMemberMyPageDto); } - - + @PostMapping("/logout") + public ResponseEntity logout() { + return authService.logout(); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/MemberDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/MemberDto.java new file mode 100644 index 0000000..3ac6f9f --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/MemberDto.java @@ -0,0 +1,45 @@ +package com.ll.nbe342team8.domain.member.member.dto; + + +import com.ll.nbe342team8.domain.member.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@NoArgsConstructor +public class MemberDto { + private Long id; + private String oAuthId; + private String name; + private String email; + private Member.MemberType memberType; + private String profileImageUrl; + + public MemberDto(Member entity) { + this.id = entity.getId(); + this.oAuthId = entity.getOAuthId(); + this.name = entity.getName(); + this.email = entity.getEmail(); + this.memberType = entity.getMemberType(); + this.profileImageUrl = entity.getProfileImageUrl(); + } + + public Map getAttributes() { + Map attributes = new HashMap<>(); + attributes.put("id", this.id); + attributes.put("oAuthId", this.oAuthId); + attributes.put("name", this.name); + attributes.put("email", this.email); + attributes.put("memberType", this.memberType); + attributes.put("profileImageUrl", this.profileImageUrl); + return attributes; + } + + public enum MemberType { + USER, + ADMIN + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/PutReqMemberMyPageDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/PutReqMemberMyPageDto.java index 070e4c7..4bd4765 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/PutReqMemberMyPageDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/PutReqMemberMyPageDto.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,21 +11,8 @@ -@Getter -@Setter -@NoArgsConstructor -public class PutReqMemberMyPageDto { - - @NotBlank(message = "공백은 허용하지 않습니다.") - @Pattern(regexp = "^[가-힣a-zA-Z0-9 ]+$", message = "이름에는 특수문자를 포함할 수 없습니다.") - @JsonProperty("name") - String name; - - - @Pattern(regexp = "^\\d{3}-\\d{4}-\\d{4}$", message = "휴대폰 번호 형식이 올바르지 않습니다. (010-XXXX-XXXX)") - @NotBlank(message = "공백은 허용하지 않습니다.") - @JsonProperty("phoneNumber") - String phoneNumber; - - -} +public record PutReqMemberMyPageDto( + @NotBlank(message = "공백은 허용하지 않습니다.") String name, + @NotNull String phoneNumber, + String profileImageUrl +) {} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/ResMemberMyPageDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/ResMemberMyPageDto.java index 0556393..b1287b5 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/ResMemberMyPageDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/dto/ResMemberMyPageDto.java @@ -13,30 +13,21 @@ import java.util.List; import java.util.stream.Collectors; -@Getter -@Setter -@NoArgsConstructor -public class ResMemberMyPageDto { - - @JsonProperty("name") - String name; - - @JsonProperty("phoneNumber") - String phoneNumber; - - List deliveryInformationDtos; - +public record ResMemberMyPageDto( + String name, + String phoneNumber, + List deliveryInformationDtos +) { public ResMemberMyPageDto(Member member) { - this.name=member.getName(); - this.phoneNumber= member.getPhoneNumber(); - List deliveryInformations=member.getDeliveryInformations(); - - this.deliveryInformationDtos = deliveryInformations.stream() - .sorted(Comparator.comparing((DeliveryInformation di) -> di.getIsDefaultAddress() != null && di.getIsDefaultAddress() ? 0 : 1) // true(0) 우선 정렬 - .thenComparing(DeliveryInformation::getAddressName, Comparator.nullsLast(Comparator.naturalOrder()))) // 나머지는 addressName 오름차순 - .map(DeliveryInformationDto::new) // DTO 변환 - .collect(Collectors.toList()); - - + this( + member.getName(), + member.getPhoneNumber(), + member.getDeliveryInformations().stream() + .sorted(Comparator + .comparing((DeliveryInformation di) -> di.getIsDefaultAddress() != null && di.getIsDefaultAddress() ? 0 : 1) // 기본 배송지를 우선 정렬 + .thenComparing(DeliveryInformation::getAddressName, Comparator.nullsLast(Comparator.naturalOrder()))) // addressName 오름차순 정렬 + .map(DeliveryInformationDto::new) // DTO 변환 + .collect(Collectors.toList()) + ); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/entity/Member.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/entity/Member.java index 23560cc..3607703 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/entity/Member.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/entity/Member.java @@ -1,72 +1,124 @@ package com.ll.nbe342team8.domain.member.member.entity; -import com.ll.nbe342team8.domain.book.review.entity.Review; +import java.util.Collection; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + import com.ll.nbe342team8.domain.cart.entity.Cart; import com.ll.nbe342team8.domain.member.deliveryInformation.entity.DeliveryInformation; import com.ll.nbe342team8.domain.member.member.dto.PutReqMemberMyPageDto; +import com.ll.nbe342team8.domain.qna.question.entity.Question; import com.ll.nbe342team8.global.jpa.entity.BaseTime; -import jakarta.persistence.*; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.List; -import java.util.stream.Collectors; - @Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor -public class Member extends BaseTime { +public class Member extends BaseTime implements UserDetails { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT + private Long id; + + private String name; // 사용자 이름 + + private String phoneNumber; // 전화번호 + + @Enumerated(EnumType.STRING) + private MemberType memberType; // 사용자 역할(사용자, 관리자) + + private String oAuthId; // 필드 이름 변경 + + private String email; // 사용자 이메일 + + private String password; + + private String username; - @Column(name = "name") - private String name; // 사용자 이름 + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + private List deliveryInformations; - @Column(name = "phone_number") - private String phoneNumber; // 전화번호 + @OneToMany(mappedBy = "member", fetch = FetchType.EAGER) + private List carts; - @Enumerated(EnumType.STRING) - @Column(name = "member_type") - private MemberType memberType; // 사용자 역할(사용자, 관리자) + @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) + private List questions; - @Column(name="oauth_id") - private Long oauthId; + private String profileImageUrl; - @Column(name = "email") - private String email; // 소셜 로그인 ID + // Enum 사용자 역할 + public enum MemberType { + USER, + ADMIN + } + + public void updateMemberInfo(PutReqMemberMyPageDto dto) { + this.name = dto.name(); + this.phoneNumber = dto.phoneNumber(); + } - // Enum 사용자 역할 - public enum MemberType { - USER, - ADMIN - } + public void addDeliveryInformation(DeliveryInformation deliveryInformation) { + this.deliveryInformations.add(deliveryInformation); + } - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - private List deliveryInformations; + public void convertFalseDeliveryInformationsIsDefaultAddress() { + deliveryInformations.forEach(info -> info.setIsDefaultAddress(false)); + } - @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) - private List carts; + public String getUsername() { + return oAuthId; + } + public String getOAuthId() { + return oAuthId; + } - public void updateMemberInfo(PutReqMemberMyPageDto dto) { - this.name = dto.getName(); - this.phoneNumber = dto.getPhoneNumber(); + @Override + public Collection getAuthorities() { + if (this.memberType == MemberType.ADMIN) { + return List.of(new SimpleGrantedAuthority("ROLE_ADMIN")); + } else { + // 일반 사용자라면 필요에 따라 ROLE_USER 또는 빈 리스트를 반환할 수 있음. + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + } - } + @Override + public boolean isAccountNonExpired() { + return true; + } - public void addDeliveryInformaiton(DeliveryInformation deliveryInformation) { - this.deliveryInformations.add(deliveryInformation); - } + @Override + public boolean isAccountNonLocked() { + return true; + } - public void convertFalseDeliveryInformaitonsIsDefaultAddress() { - deliveryInformations.forEach(info -> info.setIsDefaultAddress(false)); - } + @Override + public boolean isCredentialsNonExpired() { + return true; + } - public void deleteDeliveryInformaiton(Long id) { - deliveryInformations.removeIf(deliveryInfo -> deliveryInfo.getId().equals(id)); - } + @Override + public boolean isEnabled() { + return true; + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/repository/MemberRepository.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/repository/MemberRepository.java index 792d1e2..8c7bd25 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/repository/MemberRepository.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/repository/MemberRepository.java @@ -1,12 +1,19 @@ package com.ll.nbe342team8.domain.member.member.repository; -import com.ll.nbe342team8.domain.member.member.entity.Member; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; +import com.ll.nbe342team8.domain.member.member.entity.Member; public interface MemberRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByEmail(String email); + + Optional findByoAuthId(String oAuthId); + + // 관리자 계정 조회 (이메일ID, 회원 유형 확인) + Optional findByEmailAndMemberType(String email, Member.MemberType memberType); - Optional findByName(String name); + // 관리자 계정 개수 확인 + long countByMemberType(Member.MemberType memberType); } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/service/MemberService.java b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/service/MemberService.java index f38e434..a5214c8 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/member/member/service/MemberService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/member/member/service/MemberService.java @@ -1,22 +1,23 @@ package com.ll.nbe342team8.domain.member.member.service; -import com.ll.nbe342team8.domain.member.deliveryInformation.entity.DeliveryInformation; import com.ll.nbe342team8.domain.member.member.dto.PutReqMemberMyPageDto; -import com.ll.nbe342team8.domain.book.book.entity.Book; -import com.ll.nbe342team8.domain.book.review.entity.Review; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.member.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor -public class MemberService { +public class MemberService implements UserDetailsService { private final MemberRepository memberRepository; @@ -25,9 +26,27 @@ public Optional findByEmail(String email) { } @Transactional - public void modifyMember(Member member, PutReqMemberMyPageDto dto) { - //사용자 개체 데이터 갱신 - member.updateMemberInfo(dto); + public Member modifyOrJoin(String oAuthId, PutReqMemberMyPageDto dto, String email) { + return memberRepository.findByoAuthId(oAuthId) // 기존 회원인지 확인 (oAuthId 기준으로 검색) + .map(member -> { + // 기존 회원 정보 업데이트 + member.updateMemberInfo(dto); + member.setEmail(email); // 이메일 업데이트 추가 + return memberRepository.save(member); + }) + .orElseGet(() -> { + // 새 회원 생성 시 기본값으로 USER 타입 설정 + Member member = Member.builder() + .oAuthId(oAuthId) + .email(email) + .name(dto.name()) + .phoneNumber(dto.phoneNumber() != null ? dto.phoneNumber() : "")//전화번호가 없으면 빈 문자열("") 저장 + .memberType(Member.MemberType.USER) + .profileImageUrl(dto.profileImageUrl()) + .password("") + .build(); + return memberRepository.save(member); + }); } @@ -42,4 +61,24 @@ public Member create(Member member) { public long count(){ return memberRepository.count(); } + + public Optional findByOauthId(String oAuthId) { + return memberRepository.findByoAuthId(oAuthId); + } + + @Override + public UserDetails loadUserByUsername(String oAuthId) throws UsernameNotFoundException { + Member member = findByOauthId(oAuthId) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); + + if (member.getMemberType() != Member.MemberType.ADMIN) { + throw new UsernameNotFoundException("관리자 권한이 없습니다."); + } + + return new User( + member.getOAuthId(), + "", + List.of(new SimpleGrantedAuthority("ROLE_ADMIN")) + ); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/oauth/CustomAuthorizationRequestResolver.java b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/CustomAuthorizationRequestResolver.java new file mode 100644 index 0000000..94c5d7e --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/CustomAuthorizationRequestResolver.java @@ -0,0 +1,50 @@ +package com.ll.nbe342team8.domain.oauth; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + private final DefaultOAuth2AuthorizationRequestResolver defaultResolver; + + public CustomAuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) { + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization"); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request); + return customizeAuthorizationRequest(authorizationRequest, request); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { + OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request, clientRegistrationId); + return customizeAuthorizationRequest(authorizationRequest, request); + } + + private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request) { + if (authorizationRequest == null || request == null) { + return null; + } + + String redirectUrl = request.getParameter("redirectUrl"); + + Map additionalParameters = new HashMap<>(authorizationRequest.getAdditionalParameters()); + if (redirectUrl != null && !redirectUrl.isEmpty()) { + additionalParameters.put("state", redirectUrl); + } + + return OAuth2AuthorizationRequest.from(authorizationRequest) + .additionalParameters(additionalParameters) + .state(redirectUrl) + .build(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/oauth/CustomOAuth2UserService.java b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/CustomOAuth2UserService.java new file mode 100644 index 0000000..0195154 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/CustomOAuth2UserService.java @@ -0,0 +1,49 @@ +package com.ll.nbe342team8.domain.oauth; + + +import com.ll.nbe342team8.domain.jwt.JwtService; +import com.ll.nbe342team8.domain.member.member.dto.PutReqMemberMyPageDto; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + private final MemberService memberService; + private final JwtService jwtService; + + @Override + public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2AuthenticationException { + OAuth2User oauth2User = super.loadUser(request); + + // 안전한 타입 캐스팅과 널 체크 + Map attributes = oauth2User.getAttributes(); + Map kakaoAccount = (Map) attributes.getOrDefault("kakao_account", new HashMap<>()); + Map profile = (Map) kakaoAccount.getOrDefault("profile", new HashMap<>()); + + String oAuthId = oauth2User.getName(); // kakaoId -> oAuthId + String email = (String) kakaoAccount.getOrDefault("email", ""); + String name = (String) profile.getOrDefault("nickname", ""); // nickname -> name + String profileImageUrl = (String) profile.getOrDefault("profile_image_url", ""); + + Optional existingMember = memberService.findByOauthId(oAuthId); + String phoneNumber = (existingMember.isPresent()) ? existingMember.get().getPhoneNumber() : ""; // 기존 유저면 phoneNumber 유지, 없으면 빈 값 + + PutReqMemberMyPageDto dto = new PutReqMemberMyPageDto(name,phoneNumber, profileImageUrl); + + Member member = memberService.modifyOrJoin(oAuthId, dto, email); + String refreshToken = jwtService.generateRefreshToken(member); // generateRefreshToken에서 리프레시 토큰 값 설정하는건지 확인 + + return new SecurityUser(member); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/oauth/OAuth2SuccessHandler.java b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/OAuth2SuccessHandler.java new file mode 100644 index 0000000..8646c62 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/OAuth2SuccessHandler.java @@ -0,0 +1,62 @@ +package com.ll.nbe342team8.domain.oauth; + + +import com.ll.nbe342team8.domain.jwt.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseCookie; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@Slf4j +@RequiredArgsConstructor +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private final JwtService jwtService; + private final OAuth2AuthorizedClientService authorizedClientService; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); + + log.info("OAuth 인증 성공: 사용자 이메일 = {}", securityUser.getMember().getEmail()); + + String jwtToken = jwtService.generateToken(securityUser.getMember()); + String refreshToken = jwtService.generateRefreshToken(securityUser.getMember()); + + + log.info("JWT 토큰 생성 완료: accessToken 존재 = {}, refreshToken 존재 = {}", + jwtToken != null, refreshToken != null); + + // 쿠키 설정 + ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", jwtToken) + .path("/") + .httpOnly(true) + .secure(true) + .sameSite("None") // 크로스 사이트 요청에 대한 제한 완화 + .maxAge(60 * 60) // 1시간 + .build(); + + ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken) + .path("/") + .httpOnly(true) + .secure(true) + .sameSite("None") + .maxAge(7 * 24 * 60 * 60) // 7일 + .build(); + log.info("쿠키 설정: accessToken = {}, refreshToken = {}", + accessTokenCookie.toString(), refreshTokenCookie.toString()); + + response.addHeader("Set-Cookie", accessTokenCookie.toString()); + response.addHeader("Set-Cookie", refreshTokenCookie.toString()); + + getRedirectStrategy().sendRedirect(request, response, "http://localhost:3000/"); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/oauth/OAuthAttributes.java b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/OAuthAttributes.java new file mode 100644 index 0000000..7e926c0 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/OAuthAttributes.java @@ -0,0 +1,72 @@ +package com.ll.nbe342team8.domain.oauth; + +import com.ll.nbe342team8.domain.member.member.entity.Member; +import lombok.Builder; +import lombok.Getter; + +import java.util.Collections; +import java.util.Map; + +@Getter +public class OAuthAttributes { + private Map attributes; + private String nameAttributeKey; + private String oAuthId; // kakaoId를 oAuthId로 변경 + private String name; + private String email; // + private String profileImageUrl; + + @Builder + public OAuthAttributes(Map attributes, + String nameAttributeKey, + String oAuthId, + String name, + String email, + String profileImageUrl) { + this.attributes = attributes; + this.nameAttributeKey = nameAttributeKey; + this.oAuthId = oAuthId; + this.name = name; + this.email = email; + this.profileImageUrl = profileImageUrl; + } + + public static OAuthAttributes of(String registrationId, + String userNameAttributeName, + Map attributes) { + // kakao_account에서 필요한 정보 추출 + Map kakaoAccount = (Map) attributes.get("kakao_account"); + Map profile = (Map) kakaoAccount.get("profile"); + + return OAuthAttributes.builder() + .name((String) profile.get("nickname")) + .profileImageUrl((String) profile.get("profile_image_url")) + .email((String) kakaoAccount.get("email")) + .oAuthId(String.valueOf(attributes.get("id"))) // + .attributes(attributes) + .nameAttributeKey(userNameAttributeName) + .build(); + } + + public Member toEntity() { + return Member.builder() + .oAuthId(oAuthId) + .name(name) + .email(email) + .phoneNumber("") + .memberType(Member.MemberType.USER) + .deliveryInformations(Collections.emptyList()) + .profileImageUrl(profileImageUrl) + .build(); + } + + // OAuth2User의 attributes를 만들기 위한 메소드 추가 + public Map getAttributes() { + return Map.of( + "id", oAuthId, + "name", name, + "email", email, + "profileImageUrl", profileImageUrl + ); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/oauth/SecurityUser.java b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/SecurityUser.java new file mode 100644 index 0000000..0564b17 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/SecurityUser.java @@ -0,0 +1,44 @@ +package com.ll.nbe342team8.domain.oauth; + +import com.ll.nbe342team8.domain.member.member.entity.Member; +import lombok.Getter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Map; + + +@Getter +public class SecurityUser extends User implements OAuth2User { + private final long id; + private final String nickname; + private final String email; + private final Member member; + + public SecurityUser(Member member) { + super( + member.getUsername(), // oAuthId를 username으로 사용 + "", // 비밀번호는 빈 문자열 + member.getAuthorities() // Member에서 정의한 권한 사용 + ); + this.id = member.getId(); + this.nickname = member.getName(); + this.email = member.getEmail(); // email 필드명 주의 + this.member = member; + } + + @Override + public Map getAttributes() { + return Map.of( + "id", id, + "nickname", nickname, + "email", email, + "memberType", member.getMemberType() + ); + } + + @Override + public String getName() { + return getUsername(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/oauth/controller/AuthController.java b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/controller/AuthController.java new file mode 100644 index 0000000..e26b69d --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/oauth/controller/AuthController.java @@ -0,0 +1,61 @@ +package com.ll.nbe342team8.domain.oauth.controller; + +import com.ll.nbe342team8.domain.jwt.JwtService; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.repository.MemberRepository; +import com.ll.nbe342team8.global.exceptions.ServiceException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final JwtService jwtService; + private final MemberRepository memberRepository; + + @PostMapping("/refresh") + public ResponseEntity refreshAccessToken( + @CookieValue(value = "refreshToken", required = false) String refreshToken) { + + if (refreshToken == null) { + throw new ServiceException(401, "리프레시 토큰이 없습니다."); + } + + // 리프레시 토큰 검증 + if (!jwtService.validateToken(refreshToken)) { + throw new ServiceException(401, "유효하지 않은 리프레시 토큰입니다."); + } + + try { + String kakaoId = jwtService.getKakaoIdFromToken(refreshToken); + Member member = memberRepository.findByoAuthId(kakaoId) + .orElseThrow(() -> new ServiceException(404, "사용자를 찾을 수 없습니다.")); + + // 새로운 액세스 토큰 생성 + String newAccessToken = jwtService.generateToken(member); + + // 쿠키 설정 + ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", newAccessToken) + .path("/") + .httpOnly(true) + .secure(true) + .sameSite("None") // 개발 환경에 맞춤 + .maxAge(60 * 60) // 1시간 + .build(); + + return ResponseEntity.ok() + .header("Set-Cookie", accessTokenCookie.toString()) + .body("액세스 토큰이 갱신되었습니다."); + } catch (Exception e) { + throw new ServiceException(401, "토큰 갱신에 실패했습니다."); + } + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/controller/AdminDetailOrderController.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/controller/AdminDetailOrderController.java new file mode 100644 index 0000000..6c34ce2 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/controller/AdminDetailOrderController.java @@ -0,0 +1,53 @@ +package com.ll.nbe342team8.domain.order.detailOrder.controller; + +import com.ll.nbe342team8.domain.order.detailOrder.entity.DeliveryStatus; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ll.nbe342team8.domain.order.detailOrder.dto.AdminDetailOrderDTO; +import com.ll.nbe342team8.domain.order.detailOrder.dto.UpdateDetailOrderStatusRequest; +import com.ll.nbe342team8.domain.order.detailOrder.service.AdminDetailOrderService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/admin") +@Tag(name = "DetailOrder", description = "관리자 주문 상세 API") +@RequiredArgsConstructor +public class AdminDetailOrderController { + + private final AdminDetailOrderService adminDetailOrderService; + + @GetMapping("/orders/{orderId}/details") + @Operation(summary = "상세 주문 조회", description = "상세 주문 ID를 이용해 정보를 조회합니다.") + public Page getOrderDetails( + @PathVariable Long orderId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "book-title") String sort) { + + return adminDetailOrderService.getDetailsByOrderId(orderId, page, size, sort); + } + + @PatchMapping("/detail-orders/{detailOrderId}/status") + @Operation(summary = "상세 주문 배송 상태 수정", description = "상세 주문 ID를 이용해 배송 상태를 변경합니다.") + public ResponseEntity updateDetailStatus( + @PathVariable(name = "detailOrderId") Long detailOrderId, + @RequestBody UpdateDetailOrderStatusRequest request) { + + AdminDetailOrderDTO updatedOrder + = adminDetailOrderService.updateDetailStatus(detailOrderId, DeliveryStatus.valueOf(request.getStatus().name())); + + return ResponseEntity.ok(updatedOrder); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/controller/DetailOrderController.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/controller/DetailOrderController.java index 8475012..29531d2 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/controller/DetailOrderController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/controller/DetailOrderController.java @@ -1,26 +1,34 @@ package com.ll.nbe342team8.domain.order.detailOrder.controller; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.oauth.SecurityUser; import com.ll.nbe342team8.domain.order.detailOrder.dto.DetailOrderDto; import com.ll.nbe342team8.domain.order.detailOrder.service.DetailOrderService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; import java.util.List; @RestController +@RequiredArgsConstructor @RequestMapping("/my/orders") public class DetailOrderController { private final DetailOrderService detailOrderService; - public DetailOrderController(DetailOrderService detailOrderService){ - this.detailOrderService = detailOrderService; - } + // 주문 상세 조회 - @GetMapping("/{orderId}/details") - public ResponseEntity> getDetailOrders(@PathVariable Long orderId){ - List detailOrders = detailOrderService.getDetailOrdersByOrderId(orderId); - return ResponseEntity.ok(detailOrders); + public List getDetailOrdersByOrderIdAndMember( + @PathVariable Long orderId, + @AuthenticationPrincipal SecurityUser securityUser) { + + Member member = securityUser.getMember(); + List detailOrderDtoList = detailOrderService.getDetailOrdersByOrderIdAndMember(orderId, member); + System.out.println("실행됨"); + System.out.println("orderId = " + orderId); + System.out.println("detailOrderDtoList = " + detailOrderDtoList.toString()); + System.out.println("출력 결과 end"); + + return detailOrderDtoList; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/AdminDetailOrderDTO.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/AdminDetailOrderDTO.java new file mode 100644 index 0000000..8779717 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/AdminDetailOrderDTO.java @@ -0,0 +1,28 @@ +package com.ll.nbe342team8.domain.order.detailOrder.dto; + +import java.time.LocalDateTime; + +import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; + +public record AdminDetailOrderDTO( + Long id, // 상세 주문 번호 + Long orderId, // 주문 번호 + LocalDateTime modifyDate, // 수정날짜 + String bookTitle, // 책 제목 + int bookQuantity, // 구입수량 + String deliveryStatus // 개별 배송상태 +) { + public static AdminDetailOrderDTO fromEntity(DetailOrder detailOrder) { + if (detailOrder == null || detailOrder.getOrder() == null || detailOrder.getBook() == null) { + throw new IllegalArgumentException("주문, 책이 비어있습니다."); + } + return new AdminDetailOrderDTO( + detailOrder.getId(), + detailOrder.getOrder().getId(), + detailOrder.getModifyDate(), + detailOrder.getBook().getTitle(), + detailOrder.getBookQuantity(), + detailOrder.getDeliveryStatus().name() + ); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/DetailOrderDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/DetailOrderDto.java index db454dd..fb08764 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/DetailOrderDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/DetailOrderDto.java @@ -1,18 +1,11 @@ package com.ll.nbe342team8.domain.order.detailOrder.dto; - -import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder.DeliveryStatus; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class DetailOrderDto { - private Long orderId; - private Long bookId; - private int bookQuantity; - private DeliveryStatus deliveryStatus; - - - -} +import com.ll.nbe342team8.domain.order.detailOrder.entity.DeliveryStatus; + +public record DetailOrderDto( + Long orderId, + Long bookId, + int bookQuantity, + DeliveryStatus deliveryStatus +) { +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/UpdateDetailOrderStatusRequest.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/UpdateDetailOrderStatusRequest.java new file mode 100644 index 0000000..7a98b37 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/dto/UpdateDetailOrderStatusRequest.java @@ -0,0 +1,17 @@ +package com.ll.nbe342team8.domain.order.detailOrder.dto; + +import com.ll.nbe342team8.domain.order.detailOrder.entity.DeliveryStatus; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UpdateDetailOrderStatusRequest { + + @Schema(description = "변경할 배송 상태") + private DeliveryStatus status; +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/entity/DeliveryStatus.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/entity/DeliveryStatus.java new file mode 100644 index 0000000..efae2de --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/entity/DeliveryStatus.java @@ -0,0 +1,16 @@ +package com.ll.nbe342team8.domain.order.detailOrder.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum DeliveryStatus { + PENDING, // 대기중 + SHIPPING, // 배송중 + DELIVERED, // 배송완료 + RETURNED; // 반품 + + @JsonCreator + public static DeliveryStatus from(String value) { + return DeliveryStatus.valueOf(value.toUpperCase()); // 소문자로 요청이 와도 변환 가능 + } +} + diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/entity/DetailOrder.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/entity/DetailOrder.java index f9991f9..833dd08 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/entity/DetailOrder.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/entity/DetailOrder.java @@ -1,6 +1,7 @@ package com.ll.nbe342team8.domain.order.detailOrder.entity; import com.ll.nbe342team8.domain.book.book.entity.Book; +import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.order.order.entity.Order; import com.ll.nbe342team8.global.jpa.entity.BaseTime; import jakarta.persistence.*; @@ -8,27 +9,29 @@ @Entity @Getter -@Setter @Builder @NoArgsConstructor @AllArgsConstructor public class DetailOrder extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "order_id", nullable = false) private Order order; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "book_id", nullable = false) private Book book; - @Column(name = "book_quantity") + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + private int bookQuantity; @Enumerated(EnumType.STRING) - @Column(name = "delivery_status") private DeliveryStatus deliveryStatus; - public enum DeliveryStatus { - PENDING, SHIPPED, DELIVERED + public void setDeliveryStatus(DeliveryStatus deliveryStatus) { + this.deliveryStatus = deliveryStatus; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/repository/DetailOrderRepository.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/repository/DetailOrderRepository.java index cf9ed4c..a8ff292 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/repository/DetailOrderRepository.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/repository/DetailOrderRepository.java @@ -1,13 +1,22 @@ package com.ll.nbe342team8.domain.order.detailOrder.repository; -import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; +import com.ll.nbe342team8.domain.member.member.entity.Member; @Repository public interface DetailOrderRepository extends JpaRepository { - List findByOrderId(Long orderId); - void deleteByOrderId(Long orderId); -} + + List findByOrderId(Long orderId); + + Page findByOrderId(Long orderId, Pageable pageable); + + void deleteByOrderId(Long orderId); + +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/service/AdminDetailOrderService.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/service/AdminDetailOrderService.java new file mode 100644 index 0000000..ccb493e --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/service/AdminDetailOrderService.java @@ -0,0 +1,53 @@ +package com.ll.nbe342team8.domain.order.detailOrder.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import com.ll.nbe342team8.domain.order.detailOrder.dto.AdminDetailOrderDTO; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DeliveryStatus; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; +import com.ll.nbe342team8.domain.order.detailOrder.repository.DetailOrderRepository; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AdminDetailOrderService { + + private final DetailOrderRepository detailOrderRepository; + + // 상세 주문 내역 조회 + public Page getDetailsByOrderId(Long orderId, int page, int size, String sort) { + Sort sorting; + + if ("book-title".equals(sort)) { + sorting = Sort.by(Sort.Direction.ASC, "book.title"); // 책 제목 오름차순 + } else if ("book-title,desc".equals(sort)) { + sorting = Sort.by(Sort.Direction.DESC, "book.title"); // 책 제목 내림차순 + } else { + sorting = Sort.by(Sort.Direction.ASC, "book.title"); // 기본 정렬 + } + + Pageable pageable = PageRequest.of(page, size, sorting); + + return detailOrderRepository.findByOrderId(orderId, pageable) + .map(AdminDetailOrderDTO::fromEntity); + } + + // 상세 주문 배송 상태 수정 + @Transactional + public AdminDetailOrderDTO updateDetailStatus(Long detailOrderId, DeliveryStatus status) { + DetailOrder detailOrder = detailOrderRepository.findById(detailOrderId) + .orElseThrow(() -> new EntityNotFoundException("해당 상세 주문을 찾을 수 없습니다.")); + + detailOrder.setDeliveryStatus(status); // 배송 상태 변경 + detailOrderRepository.save(detailOrder); // 변경사항 저장 + + return AdminDetailOrderDTO.fromEntity(detailOrder); // 변경된 값 반환 + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/service/DetailOrderService.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/service/DetailOrderService.java index bfc4fdc..5d4c8c5 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/service/DetailOrderService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/detailOrder/service/DetailOrderService.java @@ -1,5 +1,7 @@ package com.ll.nbe342team8.domain.order.detailOrder.service; +import com.ll.nbe342team8.domain.jwt.AuthService; +import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.order.detailOrder.dto.DetailOrderDto; import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; import com.ll.nbe342team8.domain.order.detailOrder.repository.DetailOrderRepository; @@ -11,13 +13,19 @@ @Service public class DetailOrderService { private final DetailOrderRepository detailOrderRepository; + private final AuthService authService; - public DetailOrderService(DetailOrderRepository detailOrderRepository){ + public DetailOrderService(DetailOrderRepository detailOrderRepository, AuthService authService) { this.detailOrderRepository = detailOrderRepository; + this.authService = authService; } - public List getDetailOrdersByOrderId(Long orderId) { + // 주문상세조회 + public List getDetailOrdersByOrderIdAndMember(Long orderId, Member member) { + // 레포지토리에서 orderId와 member로 주문 상세 조회 List detailOrders = detailOrderRepository.findByOrderId(orderId); + System.out.println("detailOrders = " + detailOrders.toString()); + // 주문 상세 정보를 DetailOrderDto로 변환하여 반환 return detailOrders.stream() .map(detailOrder -> new DetailOrderDto( detailOrder.getOrder().getId(), @@ -25,8 +33,5 @@ public List getDetailOrdersByOrderId(Long orderId) { detailOrder.getBookQuantity(), detailOrder.getDeliveryStatus())) .collect(Collectors.toList()); - - } } - - +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/controller/AdminOrderController.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/controller/AdminOrderController.java new file mode 100644 index 0000000..81a8868 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/controller/AdminOrderController.java @@ -0,0 +1,32 @@ +package com.ll.nbe342team8.domain.order.order.controller; + +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ll.nbe342team8.domain.order.order.dto.AdminOrderDTO; +import com.ll.nbe342team8.domain.order.order.service.AdminOrderService; +import com.ll.nbe342team8.domain.order.type.SortType; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/admin/orders") +@Tag(name = "Order", description = "관리자 주문 API") +@RequiredArgsConstructor +public class AdminOrderController { + + private final AdminOrderService adminOrderService; + + @GetMapping + @Operation(summary = "전체 회원 주문 조회") + public Page getAllOrders(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(defaultValue = "ORDER_DATE") SortType sortType) { + return adminOrderService.getAllOrders(page, pageSize, sortType); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/controller/OrderController.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/controller/OrderController.java index 0da3a32..e516fe0 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/controller/OrderController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/controller/OrderController.java @@ -1,33 +1,78 @@ package com.ll.nbe342team8.domain.order.order.controller; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.oauth.SecurityUser; +import com.ll.nbe342team8.domain.jwt.AuthService; import com.ll.nbe342team8.domain.order.order.dto.OrderDTO; +import com.ll.nbe342team8.domain.order.order.dto.OrderRequestDto; +import com.ll.nbe342team8.domain.order.order.dto.OrderResponseDto; +import com.ll.nbe342team8.domain.order.order.dto.PaymentResponseDto; +import com.ll.nbe342team8.domain.order.order.entity.Order; import com.ll.nbe342team8.domain.order.order.service.OrderService; +import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; - import java.util.List; @RestController @RequestMapping("/my/orders") +@CrossOrigin(origins = "http://localhost:3000") public class OrderController { private final OrderService orderService; + private final AuthService authService; - public OrderController(OrderService orderService) { + public OrderController(OrderService orderService, AuthService authService) { this.orderService = orderService; + this.authService = authService; } - //주문조회 + // 주문조회 @GetMapping - public ResponseEntity> getOrders(@RequestParam Long memberId) { // 추후 변경 - List orders = orderService.getOrdersByMemberId(memberId); - return ResponseEntity.ok(orders); + public Page getOrdersByMember(@AuthenticationPrincipal SecurityUser securityUser, Pageable pageable) { + Member member = securityUser.getMember(); // 인증된 사용자의 Member 객체를 가져옴 + return orderService.getOrdersByMember(member, pageable); } - //주문삭제 + // 주문삭제 @DeleteMapping("/{orderId}") - public ResponseEntity deleteOrder(@PathVariable Long orderId) { - orderService.deleteOrder(orderId); + public ResponseEntity deleteOrder(@PathVariable Long orderId, + @CookieValue(value = "accessToken", required = false) String token) { + Member member = authService.validateTokenAndGetMember(token); + orderService.deleteOrder(orderId, member); return ResponseEntity.ok("주문 삭제 완료"); } -} + // 주문등록 (일반 주문: 장바구니 기반) + @PostMapping("/create") + public ResponseEntity createOrder(@RequestBody @Valid OrderRequestDto orderRequestDto, + @AuthenticationPrincipal SecurityUser securityUser) { + System.out.println("orderRequestDto = " + orderRequestDto); + Member member = securityUser.getMember(); + Order order = orderService.createOrder(member, orderRequestDto); + return ResponseEntity.ok(OrderResponseDto.from(order)); + } + + // 주문등록 (단일 상품 주문) + @PostMapping("/create/fast") + public ResponseEntity createFastOrder( + @RequestBody @Valid OrderRequestDto orderRequestDto, + @RequestParam("bookId") Long bookId, + @RequestParam("quantity") int quantity, + @AuthenticationPrincipal SecurityUser securityUser) { + + System.out.println("orderRequestDto = " + orderRequestDto + ", bookId = " + bookId + ", quantity = " + quantity); + Member member = securityUser.getMember(); + Order order = orderService.createFastOrder(member, orderRequestDto, bookId, quantity); + return ResponseEntity.ok(OrderResponseDto.from(order)); + } + + @GetMapping("/payment") + public ResponseEntity payment(@AuthenticationPrincipal SecurityUser securityUser) { + Member member = securityUser.getMember(); + PaymentResponseDto paymentResponseDto = orderService.createPaymentInfo(member); + return ResponseEntity.ok(paymentResponseDto); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/AdminOrderDTO.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/AdminOrderDTO.java new file mode 100644 index 0000000..bd58cea --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/AdminOrderDTO.java @@ -0,0 +1,25 @@ +package com.ll.nbe342team8.domain.order.order.dto; + +import java.time.LocalDateTime; +import java.util.List; + +import com.ll.nbe342team8.domain.order.detailOrder.dto.AdminDetailOrderDTO; +import com.ll.nbe342team8.domain.order.order.entity.Order; + +public record AdminOrderDTO( + Long orderId, // 주문 번호 + LocalDateTime createdDate, // 주문 일시 + long totalPrice, // 총 주문 금액 + String status, // 주문 상태 + List detailOrders // 상세 주문 내역 +) { + public static AdminOrderDTO fromEntity(Order order, List detailOrders) { + return new AdminOrderDTO( + order.getId(), + order.getCreateDate(), + order.getTotalPrice(), + order.getOrderStatus().name(), + detailOrders + ); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderDTO.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderDTO.java index 3225cfb..efcfc61 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderDTO.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderDTO.java @@ -4,10 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; -@Getter -@AllArgsConstructor -public class OrderDTO { - private Long memberId; - private String orderStatus; - private long totalPrice; +import java.time.LocalDateTime; + +public record OrderDTO(Long orderId, String orderStatus, long totalPrice, LocalDateTime createDate) { } \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderRequestDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderRequestDto.java new file mode 100644 index 0000000..c7a3f7c --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderRequestDto.java @@ -0,0 +1,14 @@ +package com.ll.nbe342team8.domain.order.order.dto; + +import jakarta.validation.constraints.Pattern; + +public record OrderRequestDto( + String postCode, // 우편번호 + String fullAddress, // 주소(도로명 + 상세주소) + String recipient, // 수령인 + + @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", message = "10 ~ 11 자리의 숫자만 입력 가능합니다.") + String phone, // 전화번호 + String paymentMethod // 결제수단 +) { +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderResponseDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderResponseDto.java new file mode 100644 index 0000000..dfbfe89 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/OrderResponseDto.java @@ -0,0 +1,13 @@ +package com.ll.nbe342team8.domain.order.order.dto; + +import com.ll.nbe342team8.domain.order.order.entity.Order; + +public record OrderResponseDto( + Long orderId +) { + public static OrderResponseDto from(Order order) { + return new OrderResponseDto( + order.getId() + ); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/PaymentResponseDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/PaymentResponseDto.java new file mode 100644 index 0000000..10cbcb4 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/dto/PaymentResponseDto.java @@ -0,0 +1,14 @@ +package com.ll.nbe342team8.domain.order.order.dto; + +import com.ll.nbe342team8.domain.cart.dto.CartResponseDto; +import com.ll.nbe342team8.domain.cart.entity.Cart; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public record PaymentResponseDto ( + List cartList, + Long priceStandard, + Long pricesSales +){ +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/entity/Order.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/entity/Order.java index ae850ac..96cab9b 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/entity/Order.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/entity/Order.java @@ -1,34 +1,62 @@ - package com.ll.nbe342team8.domain.order.order.entity; +import java.util.ArrayList; +import java.util.List; + import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; import com.ll.nbe342team8.global.jpa.entity.BaseTime; + import jakarta.persistence.*; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Getter -@Setter @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "orders") public class Order extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private Member member; + + @Enumerated(EnumType.STRING) + private OrderStatus orderStatus; + + private long totalPrice; + + private String fullAddress; + + private String postCode; + + private String recipient; + + private String phone; - @ManyToOne - @JoinColumn(name = "member_id") - private Member member; + private String paymentMethod; - @Enumerated(EnumType.STRING) - @Column(name = "order_status") - private OrderStatus orderStatus; + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List detailOrders = new ArrayList<>(); - @Column(name = "total_price") - private long totalPrice; + public enum OrderStatus { + ORDERED, + CANCELLED, + COMPLETE + } - public enum OrderStatus{ - ORDERED, - DELIVERY, - COMPLETE - } + public Order(Member member, OrderStatus orderStatus, long totalPrice) { + this.member = member; + this.orderStatus = orderStatus; + this.totalPrice = totalPrice; + this.detailOrders = new ArrayList<>(); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/exception/GlobalRestExceptionHandler.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/exception/GlobalRestExceptionHandler.java deleted file mode 100644 index cfcfe39..0000000 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/exception/GlobalRestExceptionHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ll.nbe342team8.domain.order.order.exception; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.http.HttpStatus; - -@RestControllerAdvice -public class GlobalRestExceptionHandler { - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()); - } - - @ExceptionHandler(IllegalStateException.class) - public ResponseEntity handleIllegalStateException(IllegalStateException ex) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); - } - - -} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/repository/OrderRepository.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/repository/OrderRepository.java index d1c6c25..ddcb0b7 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/repository/OrderRepository.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/repository/OrderRepository.java @@ -2,14 +2,19 @@ import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.order.order.entity.Order; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; + import java.util.List; +import java.util.Optional; @Repository public interface OrderRepository extends JpaRepository { - List findByMember(Member member); - List findByMemberId(Long memberId);//일단 추가 테스트위헤 + Page findByMember(Member member, Pageable pageable); + Optional findByIdAndMember(Long orderId, Member member); } + diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/service/AdminOrderService.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/service/AdminOrderService.java new file mode 100644 index 0000000..f836423 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/service/AdminOrderService.java @@ -0,0 +1,33 @@ +package com.ll.nbe342team8.domain.order.order.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import com.ll.nbe342team8.domain.order.detailOrder.dto.AdminDetailOrderDTO; +import com.ll.nbe342team8.domain.order.order.dto.AdminOrderDTO; +import com.ll.nbe342team8.domain.order.order.entity.Order; +import com.ll.nbe342team8.domain.order.order.repository.OrderRepository; +import com.ll.nbe342team8.domain.order.type.SortType; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AdminOrderService { + + private final OrderRepository orderRepository; + + public Page getAllOrders(int page, int pageSize, SortType sortType) { + Pageable pageable = PageRequest.of(page, pageSize, Sort.by(sortType.getOrder())); + + Page orders = orderRepository.findAll(pageable); + + return orders.map(order -> AdminOrderDTO.fromEntity(order, order.getDetailOrders() + .stream() + .map(AdminDetailOrderDTO::fromEntity) + .toList())); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/service/OrderService.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/service/OrderService.java index 4fcdb05..e1029a7 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/order/order/service/OrderService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/order/service/OrderService.java @@ -1,15 +1,26 @@ package com.ll.nbe342team8.domain.order.order.service; +import com.ll.nbe342team8.domain.book.book.entity.Book; +import com.ll.nbe342team8.domain.book.book.service.BookService; +import com.ll.nbe342team8.domain.cart.dto.CartResponseDto; +import com.ll.nbe342team8.domain.cart.entity.Cart; +import com.ll.nbe342team8.domain.cart.service.CartService; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.member.member.repository.MemberRepository; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DeliveryStatus; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; import com.ll.nbe342team8.domain.order.detailOrder.repository.DetailOrderRepository; import com.ll.nbe342team8.domain.order.order.dto.OrderDTO; +import com.ll.nbe342team8.domain.order.order.dto.OrderRequestDto; +import com.ll.nbe342team8.domain.order.order.dto.PaymentResponseDto; import com.ll.nbe342team8.domain.order.order.entity.Order; import com.ll.nbe342team8.domain.order.order.entity.Order.OrderStatus; import com.ll.nbe342team8.domain.order.order.repository.OrderRepository; -import jakarta.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; @@ -19,44 +30,157 @@ public class OrderService { private final OrderRepository orderRepository; private final DetailOrderRepository detailOrderRepository; private final MemberRepository memberRepository; + private final CartService cartService; + private final BookService bookService; // Book 정보를 가져오기 위한 서비스 + @Autowired - public OrderService(OrderRepository orderRepository, DetailOrderRepository detailOrderRepository, MemberRepository memberRepository) { + public OrderService(OrderRepository orderRepository, + DetailOrderRepository detailOrderRepository, + MemberRepository memberRepository, + CartService cartService, + BookService bookService) { this.orderRepository = orderRepository; this.detailOrderRepository = detailOrderRepository; this.memberRepository = memberRepository; + this.cartService = cartService; + this.bookService = bookService; } - public List getOrdersByMemberId(Long memberId) { - // 회원이 존재하는지 먼저 체크 - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new IllegalArgumentException("회원이 존재하지 않습니다.")); - - // 주문 조회 - List orders = orderRepository.findByMemberId(memberId); - if (orders.isEmpty()) { + @Transactional(readOnly = true) + public Page getOrdersByMember(Member member, Pageable pageable) { + Page ordersPage = orderRepository.findByMember(member, pageable); + if (ordersPage.isEmpty()) { throw new IllegalArgumentException("주문이 존재하지 않습니다."); } - // DTO로 변환하여 반환 - return orders.stream() - .map(order -> new OrderDTO(order.getMember().getId(), - order.getOrderStatus().name(), - order.getTotalPrice())) - .collect(Collectors.toList()); + return ordersPage.map(order -> new OrderDTO( + order.getId(), + order.getOrderStatus().name(), + order.getTotalPrice(), + order.getCreateDate())); } @Transactional - public void deleteOrder(Long orderId) { - Order order = orderRepository.findById(orderId) - .orElseThrow(() -> new IllegalArgumentException("해당 주문이 존재하지 않습니다.")); - + public void deleteOrder(Long orderId, Member member) { + Order order = orderRepository.findByIdAndMember(orderId, member) + .orElseThrow(() -> new IllegalArgumentException("해당 주문이 존재하지 않거나 권한이 없습니다.")); if (order.getOrderStatus() != OrderStatus.COMPLETE) { throw new IllegalStateException("주문이 완료되지 않아 삭제할 수 없습니다."); } - detailOrderRepository.deleteByOrderId(orderId); orderRepository.delete(order); } + @Transactional + public Order createOrder(Member member, OrderRequestDto orderRequestDTO) { + List cartList = cartService.findCartByMember(member); + + Order order = Order.builder() + .member(member) + .orderStatus(OrderStatus.ORDERED) + .fullAddress(orderRequestDTO.fullAddress()) + .postCode(orderRequestDTO.postCode()) + .phone(orderRequestDTO.phone()) + .recipient(orderRequestDTO.recipient()) + .paymentMethod(orderRequestDTO.paymentMethod()) + .totalPrice(calculateTotalPriceSales(cartList)) + .build(); + + orderRepository.save(order); + + List detailOrders = cartList.stream() + .map(cart -> DetailOrder.builder() + .order(order) + .deliveryStatus(DeliveryStatus.PENDING) + .book(cart.getBook()) + .bookQuantity(cart.getQuantity()) + .build()) + .collect(Collectors.toList()); + + detailOrderRepository.saveAll(detailOrders); + + cartService.deleteProduct(member); // 주문 완료 후 장바구니 비우기 + + return order; + } + + @Transactional + public Order createFastOrder(Member member, OrderRequestDto orderRequestDTO) { + List cartList = cartService.findCartByMember(member); + Order order = Order.builder() + .member(member) + .orderStatus(OrderStatus.ORDERED) + .fullAddress(orderRequestDTO.fullAddress()) + .postCode(orderRequestDTO.postCode()) + .phone(orderRequestDTO.phone()) + .recipient(orderRequestDTO.recipient()) + .paymentMethod(orderRequestDTO.paymentMethod()) + .totalPrice(calculateTotalPriceSales(cartList)) + .build(); + orderRepository.save(order); + + List detailOrders = cartList.stream() + .map(cart -> DetailOrder.builder() + .order(order) + .deliveryStatus(DeliveryStatus.PENDING) + .book(cart.getBook()) + .bookQuantity(cart.getQuantity()) + .build()) + .collect(Collectors.toList()); + detailOrderRepository.saveAll(detailOrders); + + cartService.deleteProduct(member); // 주문 완료 후 장바구니 비우기 + return order; + } + + @Transactional + public Order createFastOrder(Member member, OrderRequestDto orderRequestDTO, Long bookId, int quantity) { + Book book = bookService.getBookById(bookId); + long totalPrice = (long) book.getPricesSales() * quantity; + + Order order = Order.builder() + .member(member) + .orderStatus(OrderStatus.ORDERED) + .fullAddress(orderRequestDTO.fullAddress()) + .postCode(orderRequestDTO.postCode()) + .phone(orderRequestDTO.phone()) + .recipient(orderRequestDTO.recipient()) + .paymentMethod(orderRequestDTO.paymentMethod()) + .totalPrice(totalPrice) + .build(); + orderRepository.save(order); + + DetailOrder detailOrder = DetailOrder.builder() + .order(order) + .deliveryStatus(DeliveryStatus.PENDING) + .book(book) + .bookQuantity(quantity) + .build(); + detailOrderRepository.save(detailOrder); + + return order; + } + + private Long calculateTotalPriceSales(List cartList) { + return cartList.stream() + .mapToLong(cart -> (long) cart.getBook().getPricesSales() * cart.getQuantity()) + .sum(); + } + + private Long calculateTotalPriceStandard(List cartList) { + return cartList.stream() + .mapToLong(cart -> (long) cart.getBook().getPriceStandard() * cart.getQuantity()) + .sum(); + } + + public PaymentResponseDto createPaymentInfo(Member member) { + List cartList = cartService.findCartByMember(member); + List cartResponseDtoList = cartList.stream() + .map(CartResponseDto::from) + .collect(Collectors.toList()); + Long totalPriceSales = calculateTotalPriceSales(cartList); + Long totalPriceStandard = calculateTotalPriceStandard(cartList); + return new PaymentResponseDto(cartResponseDtoList, totalPriceStandard, totalPriceSales); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/order/type/SortType.java b/backend/src/main/java/com/ll/nbe342team8/domain/order/type/SortType.java new file mode 100644 index 0000000..9ff3151 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/order/type/SortType.java @@ -0,0 +1,22 @@ +package com.ll.nbe342team8.domain.order.type; + +import org.springframework.data.domain.Sort; + +public enum SortType { + // 주문 정렬 기준 (관리자) + ORDER_DATE("createDate", Sort.Direction.DESC), // 주문일자 최신순 + TOTAL_PRICE("totalPrice", Sort.Direction.DESC), // 주문 총액 높은순 + STATUS("orderStatus", Sort.Direction.ASC); // 주문 상태별 정렬 + + private final String field; + private final Sort.Direction direction; + + SortType(String field, Sort.Direction direction) { + this.field = field; + this.direction = direction; + } + + public Sort.Order getOrder() { + return new Sort.Order(direction, field); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/controller/AnswerController.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/controller/AnswerController.java index 84a685b..809c685 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/controller/AnswerController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/controller/AnswerController.java @@ -1,7 +1,180 @@ package com.ll.nbe342team8.domain.qna.answer.controller; +import java.time.Duration; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.domain.oauth.SecurityUser; +import com.ll.nbe342team8.domain.qna.answer.dto.AnswerDto; +import com.ll.nbe342team8.domain.qna.answer.dto.GetResAnswersDto; +import com.ll.nbe342team8.domain.qna.answer.dto.ReqAnswerDto; +import com.ll.nbe342team8.domain.qna.answer.entity.Answer; +import com.ll.nbe342team8.domain.qna.answer.service.AnswerService; +import com.ll.nbe342team8.domain.qna.question.entity.Question; +import com.ll.nbe342team8.domain.qna.question.service.QuestionService; +import com.ll.nbe342team8.global.exceptions.ServiceException; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + @RestController +@RequiredArgsConstructor +@RequestMapping("/admin/dashboard") +@Tag(name = "Answer Controller", description = "관리자 전용 QnA 답변 API") public class AnswerController { + + private final MemberService memberService; + private final QuestionService questionService; + private final AnswerService answerService; + + @Operation(summary = "사용자가 작성한 QnA 질문에 대한 답변 조회") + @GetMapping("/question/{questionId}/answer") + public ResponseEntity getAnswers( + @PathVariable Long questionId, + @AuthenticationPrincipal SecurityUser securityUser + ) { + Member member = getAuthenticatedMember(securityUser); + Question question = getQuestionById(questionId); + + // 질문 작성자 또는 관리자인지 확인 + validateQuestionOwner(member, question); + + List answers = answerService.findByQuestion(question); + return ResponseEntity.ok(new GetResAnswersDto(answers)); + } + + @Operation(summary = "사용자가 작성한 QnA 질문의 상세 답변 조회 (관리자 전용)") + @GetMapping("/questions/{questionId}/answers/{answerId}") + public ResponseEntity getAnswer( + @PathVariable Long questionId, + @PathVariable Long answerId, + @AuthenticationPrincipal SecurityUser securityUser + ) { + Member admin = getAuthenticatedMember(securityUser); + validateAdmin(admin); + + Question question = getQuestionById(questionId); + Answer answer = getAnswerById(answerId); + + return ResponseEntity.ok(new AnswerDto(answer)); + } + + @Operation(summary = "질문에 답변 등록 (관리자 전용)") + @PostMapping("/questions/{questionId}/answers") + public ResponseEntity postAnswer( + @PathVariable Long questionId, + @RequestBody @Valid ReqAnswerDto reqAnswerDto, + @AuthenticationPrincipal SecurityUser securityUser + ) { + Member admin = getAuthenticatedMember(securityUser); + validateAdmin(admin); + + Question question = getQuestionById(questionId); + validateExistsDuplicateAnswerInShortTime(question, admin, reqAnswerDto.content(), Duration.ofSeconds(5)); + + answerService.createAnswer(question, admin, reqAnswerDto); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @Operation(summary = "질문에 대한 답변 수정 (관리자 전용)") + @PutMapping("/questions/{questionId}/answers/{answerId}") + public ResponseEntity modifyAnswer( + @PathVariable Long questionId, + @PathVariable Long answerId, + @RequestBody @Valid ReqAnswerDto reqAnswerDto, + @AuthenticationPrincipal SecurityUser securityUser + ) { + Member admin = getAuthenticatedMember(securityUser); + validateAdmin(admin); + + Question question = getQuestionById(questionId); + Answer answer = getAnswerById(answerId); + + answerService.modifyAnswer(answer, reqAnswerDto); + return ResponseEntity.ok().build(); + } + + @Operation(summary = "질문에 대한 답변 삭제 (관리자 전용)") + @DeleteMapping("/questions/{questionId}/answers/{answerId}") + public ResponseEntity deleteAnswer( + @PathVariable Long questionId, + @PathVariable Long answerId, + @AuthenticationPrincipal SecurityUser securityUser + ) { + Member admin = getAuthenticatedMember(securityUser); + validateAdmin(admin); + + Question question = getQuestionById(questionId); + Answer answer = getAnswerById(answerId); + + answerService.deleteAnswer(answer); + return ResponseEntity.noContent().build(); + } + + /** + * 🔥 공통 메서드들 (중복 코드 제거) + */ + + // 로그인된 사용자 정보 가져오기 + private Member getAuthenticatedMember(SecurityUser securityUser) { + if (securityUser == null) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(), "로그인을 해야 합니다."); + } + + return memberService.findByOauthId(securityUser.getMember().getOAuthId()) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); + } + + // 질문 ID로 질문 가져오기 + private Question getQuestionById(Long questionId) { + return questionService.findById(questionId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "질문을 찾을 수 없습니다.")); + } + + // 답변 ID로 답변 가져오기 + private Answer getAnswerById(Long answerId) { + return answerService.findById(answerId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "답변을 찾을 수 없습니다.")); + } + + // 질문 작성자인지 확인 + private void validateQuestionOwner(Member member, Question question) { + if (!(questionService.isQuestionOwner(member, question) || checkAdmin(member))) { + throw new ServiceException(HttpStatus.FORBIDDEN.value(), "권한이 없습니다."); + } + } + + // 관리자 계정인지 확인 + private void validateAdmin(Member member) { + if (!checkAdmin(member)) { + throw new ServiceException(HttpStatus.FORBIDDEN.value(), "권한이 없습니다."); + } + } + + // 짧은 시간 내 중복 답변 등록 방지 + private void validateExistsDuplicateAnswerInShortTime(Question question, Member member, String content, Duration duration) { + if (answerService.existsDuplicateAnswerInShortTime(question, member, content, duration)) { + throw new ServiceException(HttpStatus.TOO_MANY_REQUESTS.value(), "너무 빠르게 동일한 답변을 등록할 수 없습니다."); + } + } + + // 관리자 여부 체크 + private boolean checkAdmin(Member member) { + return member.getMemberType() == Member.MemberType.ADMIN; + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/AnswerDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/AnswerDto.java index f32e0f6..0653636 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/AnswerDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/AnswerDto.java @@ -1,13 +1,27 @@ package com.ll.nbe342team8.domain.qna.answer.dto; +import com.ll.nbe342team8.domain.qna.answer.entity.Answer; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.Setter; -@Getter -@Setter -@Builder -@AllArgsConstructor -public class AnswerDto { +import java.time.LocalDateTime; + +public record AnswerDto( + Long id, + LocalDateTime createDate, + LocalDateTime modifyDate, + @NotNull String content +) { + public AnswerDto(Answer answer) { + this( + answer.getId(), + answer.getCreateDate(), + answer.getModifyDate(), + answer.getContent() + ); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/GetResAnswersDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/GetResAnswersDto.java new file mode 100644 index 0000000..b759a7a --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/GetResAnswersDto.java @@ -0,0 +1,17 @@ +package com.ll.nbe342team8.domain.qna.answer.dto; + +import com.ll.nbe342team8.domain.qna.answer.entity.Answer; +import java.util.List; +import java.util.stream.Collectors; + +public class GetResAnswersDto { + + List answers; + + public GetResAnswersDto(List answers) { + + this.answers= answers.stream() + .map(AnswerDto::new) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/ReqAnswerDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/ReqAnswerDto.java new file mode 100644 index 0000000..0e6964a --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/dto/ReqAnswerDto.java @@ -0,0 +1,8 @@ +package com.ll.nbe342team8.domain.qna.answer.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +public record ReqAnswerDto(@NotNull String content) {} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/entity/Answer.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/entity/Answer.java index 04752e1..88b6bb9 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/entity/Answer.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/entity/Answer.java @@ -1,7 +1,10 @@ package com.ll.nbe342team8.domain.qna.answer.entity; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.qna.answer.dto.ReqAnswerDto; import com.ll.nbe342team8.domain.qna.question.entity.Question; import com.ll.nbe342team8.global.jpa.entity.BaseTime; +import com.ll.nbe342team8.standard.util.Ut; import jakarta.persistence.*; import lombok.*; @@ -12,13 +15,25 @@ @AllArgsConstructor @Builder public class Answer extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT + private Long id; - - @Column(columnDefinition = "TEXT") + @Column(columnDefinition = "TEXT", nullable = false) private String content; @ManyToOne(fetch = FetchType.LAZY) private Question question; -} + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + public void updateAnswerInfo(ReqAnswerDto dto) { + this.content=Ut.XSSSanitizer.sanitize(dto.content()); + } + public void setQuestion(Question question) { + this.question = question; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/repository/AnswerRepository.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/repository/AnswerRepository.java index aa24f30..bbedfc4 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/repository/AnswerRepository.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/repository/AnswerRepository.java @@ -1,9 +1,20 @@ package com.ll.nbe342team8.domain.qna.answer.repository; -import com.ll.nbe342team8.domain.qna.answer.entity.Answer; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.qna.answer.entity.Answer; +import com.ll.nbe342team8.domain.qna.question.entity.Question; + @Repository public interface AnswerRepository extends JpaRepository { + boolean existsByQuestionAndMemberAndContentAndCreateDateAfter(Question question, Member member, String content, LocalDateTime cutoffTime); + + List findByQuestionOrderByCreateDateDesc(Question question); + + List findByQuestionId(Long questionId); } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/service/AnswerService.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/service/AnswerService.java index 3980a95..42dc07f 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/service/AnswerService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/answer/service/AnswerService.java @@ -1,7 +1,70 @@ package com.ll.nbe342team8.domain.qna.answer.service; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.qna.answer.dto.ReqAnswerDto; +import com.ll.nbe342team8.domain.qna.answer.entity.Answer; +import com.ll.nbe342team8.domain.qna.answer.repository.AnswerRepository; +import com.ll.nbe342team8.domain.qna.question.dto.ReqQuestionDto; +import com.ll.nbe342team8.domain.qna.question.entity.Question; +import com.ll.nbe342team8.standard.util.Ut; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor @Service public class AnswerService { -} + + private final AnswerRepository answerRepository; + + + @Transactional + public void createAnswer(Question question, Member member, ReqAnswerDto dto) { + + //이스케이프 처리후 저장 + String sanitizedContent = Ut.XSSSanitizer.sanitize(dto.content()); + + Answer answer = Answer.builder() + .content(sanitizedContent) + .member(member) + .question(question) + .build(); + + question.addAnswer(answer); + answerRepository.save(answer); + } + + @Transactional + public void modifyAnswer(Answer answer ,ReqAnswerDto dto) { + //이스케이프 처리한 데이터를 question 개체에 갱신 + answer.updateAnswerInfo(dto); + } + + @Transactional + public void deleteAnswer(Answer answer) { + Question question = answer.getQuestion(); + question.removeAnswer(answer); + answerRepository.delete(answer); + } + + public Optional findById(Long id) { + return answerRepository.findById(id); + } + + + public List findByQuestion(Question question) { + return answerRepository.findByQuestionOrderByCreateDateDesc(question); + } + + //네트워크 지연, 스팸 봇, 답변 등록 버튼 연타로 생성되는 중복 답변 방지 + public boolean existsDuplicateAnswerInShortTime(Question question, Member member, String content, Duration duration) { + String sanitizedContent = Ut.XSSSanitizer.sanitize(content); + LocalDateTime cutoffTime = LocalDateTime.now().minus(duration); + return answerRepository.existsByQuestionAndMemberAndContentAndCreateDateAfter(question, member, sanitizedContent, cutoffTime); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/controller/QuestionController.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/controller/QuestionController.java index 8453ecf..e73ee26 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/controller/QuestionController.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/controller/QuestionController.java @@ -3,18 +3,27 @@ import com.ll.nbe342team8.domain.member.member.dto.ResMemberMyPageDto; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.domain.oauth.SecurityUser; +import com.ll.nbe342team8.domain.qna.question.dto.QuestionDto; import com.ll.nbe342team8.domain.qna.question.dto.ReqQuestionDto; +import com.ll.nbe342team8.domain.qna.question.entity.Question; import com.ll.nbe342team8.domain.qna.question.service.QuestionService; import com.ll.nbe342team8.global.exceptions.ServiceException; +import com.ll.nbe342team8.standard.PageDto.PageDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; -import java.util.Optional; +import java.time.Duration; +import java.util.Map; +@Tag(name = "QuestionController", description = " qna 질문 컨트롤러") @RequiredArgsConstructor @RestController public class QuestionController { @@ -22,25 +31,145 @@ public class QuestionController { private final MemberService memberService; private final QuestionService questionService; + @Operation(summary = "사용자가 작성한 qna 질문 목록 조회") + @GetMapping("/my/question") + public ResponseEntity getQuesitons(@RequestParam(name = "page", defaultValue = "0") int page + ) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } + + String oauthId=securityUser.getMember().getOAuthId(); + + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); + + PageDto pageDto = new PageDto<>(); + + pageDto = questionService.getPage(member, page); + + return ResponseEntity.ok(pageDto); + } + + @Operation(summary = "사용자의 특정 qna 질문 조회") + @GetMapping("/my/question/{id}") + public ResponseEntity getQuestion(@PathVariable(name = "id") Long id) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } + + String oauthId=securityUser.getMember().getOAuthId(); + + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); + + Question question = questionService.findById(id) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "질문을 찾을 수 없습니다.")); + + validateQuestionOwner( member,question); + + QuestionDto questionDto=new QuestionDto(question); + + return ResponseEntity.ok(questionDto); + } + + @Operation(summary = "사용자가 qna 질문 등록") @PostMapping("/my/question") public ResponseEntity postQuesiton(@RequestBody @Valid ReqQuestionDto reqQuestionDto - ) { + ) { - //jwt 토큰에서 id를 통해 회원정보를 찾는다. - //여기선 임시로 이메일을 통해 회원정보를 찾는다. - String email="rdh0427@naver.com"; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Optional optionalMember = memberService.findByEmail(email); + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } - //이메일에 대응하는 사용자가 없는 경우 에러 발생 - if(optionalMember.isEmpty()) { throw new ServiceException(404,"사용자를 찾을 수 없습니다.");} + String oauthId=securityUser.getMember().getOAuthId(); - Member member=optionalMember.get(); + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); + validateExistsDuplicateQuestionInShortTime(member, reqQuestionDto.title(),reqQuestionDto.content(),Duration.ofSeconds(5)); questionService.createQuestion(member,reqQuestionDto); - //갱신된 사용자 개체를 dto로 변환해 반환한다. 프론트에선 반환 받는 memberDto로 마이페이지 갱신 - ResMemberMyPageDto resMemberMyPageDto=new ResMemberMyPageDto(member); - return ResponseEntity.status(200).body(resMemberMyPageDto); + return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "질문 등록 성공.")); + } + + @Operation(summary = "사용자의 특정 qna 질문 수정") + @PutMapping("/my/question/{id}") + public ResponseEntity modifyQuesiton(@PathVariable(name = "id") Long id + ,@RequestBody @Valid ReqQuestionDto reqQuestionDto) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } + + String oauthId=securityUser.getMember().getOAuthId(); + + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); + + Question question = questionService.findById(id) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "질문을 찾을 수 없습니다.")); + + validateQuestionOwner(member, question); + + questionService.modifyQuestion(question,reqQuestionDto); + + return ResponseEntity.ok(Map.of("message", "질문 수정 성공.")); + } + + @Operation(summary = "사용자의 특정 qna 질문 삭제") + @DeleteMapping("/my/question/{id}") + public ResponseEntity removeQuesiton(@PathVariable(name = "id") Long id + ) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !(authentication.getPrincipal() instanceof SecurityUser securityUser)) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(),"로그인을 해야합니다."); + } + + String oauthId=securityUser.getMember().getOAuthId(); + + Member member = memberService.findByOauthId(oauthId) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "사용자를 찾을 수 없습니다.")); + + Question question = questionService.findById(id) + .orElseThrow(() -> new ServiceException(HttpStatus.NOT_FOUND.value(), "질문을 찾을 수 없습니다.")); + + validateQuestionOwner(member, question); + + questionService.deleteQuestion(question); + + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(Map.of("message", "질문 삭제 성공.")); + } + + + + //사용자 권한 확인, 관리자 계정이여도 접근 가능 + private void validateQuestionOwner(Member member, Question question) { + if (!(questionService.isQuestionOwner(member, question) || checkAdmin(member))) { + throw new ServiceException(HttpStatus.UNAUTHORIZED.value(), "권한이 없습니다."); + } + } + + + private void validateExistsDuplicateQuestionInShortTime(Member member,String title, String content, Duration duration) { + if (questionService.existsDuplicateQuestionInShortTime(member,title, content, duration)) { + throw new ServiceException(HttpStatus.TOO_MANY_REQUESTS.value(), "너무 빠르게 동일한 답변을 등록할 수 없습니다."); + } + } + + private boolean checkAdmin(Member member) { + return member.getMemberType() == Member.MemberType.ADMIN; } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/PostResQuestionDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/PostResQuestionDto.java index 42edffc..624c9db 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/PostResQuestionDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/PostResQuestionDto.java @@ -1,25 +1,13 @@ package com.ll.nbe342team8.domain.qna.question.dto; -import com.fasterxml.jackson.annotation.JsonProperty; import com.ll.nbe342team8.domain.member.member.entity.Member; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -public class PostResQuestionDto { - - @NotBlank - @JsonProperty("title") - private String title; - - @NotNull - private String content; +import java.util.List; +import java.util.stream.Collectors; +public record PostResQuestionDto(List questions) { public PostResQuestionDto(Member member) { + this(List.copyOf(member.getQuestions().stream() + .map(QuestionDto::new) + .collect(Collectors.toList()))); // 🔥 불변 리스트 적용 } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/QuestionDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/QuestionDto.java index a9fa51e..e6e95ff 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/QuestionDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/QuestionDto.java @@ -1,22 +1,38 @@ package com.ll.nbe342team8.domain.qna.question.dto; -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; +import com.ll.nbe342team8.domain.qna.answer.dto.AnswerDto; +import com.ll.nbe342team8.domain.qna.question.entity.Question; +import jakarta.validation.constraints.NotNull; -@Getter -@Setter -@AllArgsConstructor -public class QuestionDto { - @NotBlank - @JsonProperty("title") - String title; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; - @JsonProperty("content") - String content; -} +public record QuestionDto( + + @NotNull Long id, + LocalDateTime createDate, + LocalDateTime modifyDate, + String title, + String content, + Boolean isAnswer, + List answers +) { + public QuestionDto(Question question) { + + this( + question.getId(), + question.getCreateDate(), + question.getModifyDate(), + question.getTitle(), + question.getContent(), + question.getAnswers() != null && !question.getAnswers().isEmpty(), + question.getAnswers() != null + ? question.getAnswers().stream() + .map(AnswerDto::new) // ✅ List → List 변환 + .collect(Collectors.toList()) + : List.of() // null 방지 (빈 리스트 반환) + ); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/ReqQuestionDto.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/ReqQuestionDto.java index 72d67d0..6954c26 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/ReqQuestionDto.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/dto/ReqQuestionDto.java @@ -1,24 +1,10 @@ package com.ll.nbe342team8.domain.qna.question.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -public class ReqQuestionDto { - - @NotBlank - @JsonProperty("title") - private String title; - - @NotNull - private String content; -} +public record ReqQuestionDto( + @NotBlank String title, + @NotNull String content +) {} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/entity/Question.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/entity/Question.java index a5984ae..e2c7c96 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/entity/Question.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/entity/Question.java @@ -1,11 +1,22 @@ package com.ll.nbe342team8.domain.qna.question.entity; +import java.util.List; + import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.qna.answer.entity.Answer; +import com.ll.nbe342team8.domain.qna.question.dto.ReqQuestionDto; import com.ll.nbe342team8.global.jpa.entity.BaseTime; +import com.ll.nbe342team8.standard.util.Ut; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -18,16 +29,38 @@ @Builder public class Question extends BaseTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT + private Long id; + + @Column(nullable = true) // 질문 제목 + private String title; + + @Column(nullable = true) // 질문 내용 + private String content; - @Column(nullable = true) // 질문 제목 - private String title; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) // 회원이 반드시 존재해야 한다. + private Member member; - @Column(nullable = true) // 질문 내용 - private String content; - @ManyToOne(fetch = FetchType.LAZY) - private Member member; + @OneToMany(mappedBy = "question", fetch = FetchType.LAZY) + private List answers; + public void updateQuestionInfo(ReqQuestionDto dto) { + this.title= Ut.XSSSanitizer.sanitize(dto.title()); + this.content=Ut.XSSSanitizer.sanitize(dto.content()); + } + public void addAnswer(Answer answer) { + this.answers.add(answer); + if (answer.getQuestion() != this) { + answer.setQuestion(this); + } + } + public void removeAnswer(Answer answer) { + this.answers.remove(answer); + answer.setQuestion(null); + } } diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/repository/QuestionRepository.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/repository/QuestionRepository.java index 6955097..61638db 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/repository/QuestionRepository.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/repository/QuestionRepository.java @@ -1,9 +1,27 @@ package com.ll.nbe342team8.domain.qna.question.repository; +import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.qna.question.entity.Question; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; + @Repository public interface QuestionRepository extends JpaRepository { -} + Page findByTitleContainingOrContentContaining(String title, String content, Pageable pageable); + + Page findByAnswersIsNotEmptyAndTitleContainingOrContentContaining( + String title, String content, Pageable pageable); + + Page findByAnswersIsEmptyAndTitleContainingOrContentContaining( + String title, String content, Pageable pageable); + + Page findByAnswersIsNotEmpty(Pageable pageable); + Page findByAnswersIsEmpty(Pageable pageable); + + Page findByMember(Pageable pageable, Member member); + boolean existsByMemberAndTitleAndContentAndCreateDateAfter( Member member, String title,String content, LocalDateTime cutoffTime); +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/service/QuestionService.java b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/service/QuestionService.java index 245ab8c..aad8010 100644 --- a/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/service/QuestionService.java +++ b/backend/src/main/java/com/ll/nbe342team8/domain/qna/question/service/QuestionService.java @@ -1,14 +1,24 @@ package com.ll.nbe342team8.domain.qna.question.service; import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.qna.question.dto.QuestionDto; import com.ll.nbe342team8.domain.qna.question.dto.ReqQuestionDto; import com.ll.nbe342team8.domain.qna.question.entity.Question; import com.ll.nbe342team8.domain.qna.question.repository.QuestionRepository; +import com.ll.nbe342team8.standard.PageDto.PageDto; import com.ll.nbe342team8.standard.util.Ut; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Optional; + @RequiredArgsConstructor @Service public class QuestionService { @@ -18,8 +28,8 @@ public class QuestionService { @Transactional public void createQuestion(Member member, ReqQuestionDto dto) { //이스케이프 처리 - String sanitizedTitle = Ut.XSSSanitizer.sanitize(dto.getTitle()); - String sanitizedContent = Ut.XSSSanitizer.sanitize(dto.getContent()); + String sanitizedTitle = Ut.XSSSanitizer.sanitize(dto.title()); + String sanitizedContent = Ut.XSSSanitizer.sanitize(dto.content()); Question question = Question.builder() .title(sanitizedTitle) .content(sanitizedContent) @@ -28,4 +38,46 @@ public void createQuestion(Member member, ReqQuestionDto dto) { questionRepository.save(question); } -} + + public PageDto getPage(Member member, int page) { + + Pageable pageable = PageRequest.of(page, 10, Sort.by("createDate").descending()); + Page paging = this.questionRepository.findByMember(pageable, member); + + Page pagingOrderDto = paging.map(QuestionDto::new); + PageDto pageDto = new PageDto<>(pagingOrderDto); + return pageDto; + + } + + @Transactional + public void deleteQuestion(Question question) { + + questionRepository.delete(question); + } + + @Transactional + public void modifyQuestion(Question question,ReqQuestionDto dto) { + //이스케이프 처리한 데이터를 question 개체에 갱신 + question.updateQuestionInfo(dto); + } + + public Optional findById(Long id) { + + return questionRepository.findById(id); + } + + //수정, 삭제하려는 게시글을 사용자가 작성한지 학인 + public boolean isQuestionOwner(Member member,Question question) { + + return question.getMember().getId().equals(member.getId()); + } + + //네트워크 지연, 스팸 봇, 답변 등록 버튼 연타로 생성되는 중복 답변 방지 + public boolean existsDuplicateQuestionInShortTime(Member member,String title, String content, Duration duration) { + String sanitizedContent = Ut.XSSSanitizer.sanitize(content); + String sanitizedTitle = Ut.XSSSanitizer.sanitize(title); + LocalDateTime cutoffTime = LocalDateTime.now().minus(duration); + return questionRepository.existsByMemberAndTitleAndContentAndCreateDateAfter( member, sanitizedTitle, sanitizedContent, cutoffTime); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/AdminUserInitializer.java b/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/AdminUserInitializer.java new file mode 100644 index 0000000..c8d8861 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/AdminUserInitializer.java @@ -0,0 +1,43 @@ +package com.ll.nbe342team8.global.baseInit.data; + +import java.util.UUID; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.repository.MemberRepository; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AdminUserInitializer { + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + + @PostConstruct + public void init() { + String adminEmail = "admin@thebook.co.kr"; + String adminPassword = "1111"; + + if (memberRepository.countByMemberType(Member.MemberType.ADMIN) == 0) { + String generatedOAuthId = "admin-" + UUID.randomUUID(); // 랜덤 oAuthId 생성 + + Member admin = Member.builder() + .name("Admin") + .email(adminEmail) + .password(passwordEncoder.encode(adminPassword)) + .memberType(Member.MemberType.ADMIN) + .oAuthId(generatedOAuthId) // oAuthId 자동 생성 + .build(); + memberRepository.save(admin); + System.out.println("기본 관리자 이메일: " + adminEmail); + System.out.println("기본 관리자 비밀번호: " + adminPassword); + System.out.println("기본 관리자 oAuthId 할당: " + generatedOAuthId); + System.out.println("기본 관리자 계정 생성 완료"); + + } + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/DataInitializer.java b/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/DataInitializer.java index f5da8a9..2df0b10 100644 --- a/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/DataInitializer.java +++ b/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/DataInitializer.java @@ -1,11 +1,14 @@ package com.ll.nbe342team8.global.baseInit.data; +import com.ll.nbe342team8.domain.book.book.dto.ExternalBookDto; import com.ll.nbe342team8.domain.book.book.entity.Book; import com.ll.nbe342team8.domain.book.book.repository.BookRepository; +import com.ll.nbe342team8.domain.book.book.service.ExternalBookApiService; import com.ll.nbe342team8.domain.book.category.entity.Category; import com.ll.nbe342team8.domain.book.category.repository.CategoryRepository; import com.ll.nbe342team8.domain.member.member.entity.Member; import com.ll.nbe342team8.domain.member.member.repository.MemberRepository; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DeliveryStatus; import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; import com.ll.nbe342team8.domain.order.detailOrder.repository.DetailOrderRepository; import com.ll.nbe342team8.domain.order.order.entity.Order; @@ -18,7 +21,7 @@ import java.util.ArrayList; @RequiredArgsConstructor -//@Component +@Component public class DataInitializer { private final CategoryRepository categoryRepository; @@ -26,11 +29,11 @@ public class DataInitializer { private final BookRepository bookRepository; private final OrderRepository orderRepository; private final DetailOrderRepository detailOrderRepository; + private final ExternalBookApiService externalBookApiService; - - @PostConstruct +// @PostConstruct public void init() { - // 카테고리 데이터 초기화 + // Initialize categories if (categoryRepository.count() == 0) { Category category1 = Category.builder() .categoryId(1) @@ -40,11 +43,11 @@ public void init() { .depth2("재테크/금융") .depth3("재테크") .depth4("부자되는법") - .depth5(null) // 값이 없는 경우 null + .depth5(null) .category("국내도서 > 경제/경영 > 재테크/금융 > 재테크 > 부자되는법") - .books(new ArrayList<>()) // 초기에는 빈 리스트 + .books(new ArrayList<>()) .build(); - Category category2= Category.builder() + Category category2 = Category.builder() .categoryId(2) .categoryName("경제/경영") .mall("국내도서") @@ -52,117 +55,150 @@ public void init() { .depth2("재테크/금융") .depth3("재테크") .depth4("거지 되는법") - .depth5(null) // 값이 없는 경우 null + .depth5(null) .category("국내도서 > 경제/경영 > 재테크/금융 > 재테크 > 부자되는법") - .books(new ArrayList<>()) // 초기에는 빈 리스트 + .books(new ArrayList<>()) .build(); categoryRepository.save(category1); categoryRepository.save(category2); } - // 회원 데이터 초기화 + // Initialize members if (memberRepository.count() == 0) { Member member1 = Member.builder() .name("user1") .phoneNumber("010-1234-5678") - .memberType(Member.MemberType.USER) // 사용자 역할 - .oauthId(123456789L) // 예제 OAuth ID - .email("chulsoo@example.com") // 소셜 로그인 ID - .deliveryInformations(new ArrayList<>()) // 초기에는 빈 리스트 + .memberType(Member.MemberType.USER) + .oAuthId("123456789L") + .email("chulsoo@example.com") + .password("securePassword1") + .deliveryInformations(new ArrayList<>()) .build(); Member member2 = Member.builder() .name("user2") .phoneNumber("010-1234-5678") - .memberType(Member.MemberType.USER) // 사용자 역할 - .oauthId(123456789L) // 예제 OAuth ID - .email("chulsoo11@example.com") // 소셜 로그인 ID - .deliveryInformations(new ArrayList<>()) // 초기에는 빈 리스트 + .memberType(Member.MemberType.USER) + .oAuthId("123456789L") + .email("chulsoo11@example.com") + .password("securePassword2") + .deliveryInformations(new ArrayList<>()) .build(); Member member3 = Member.builder() .name("user3") .phoneNumber("010-1234-5678") - .memberType(Member.MemberType.USER) // 사용자 역할 - .oauthId(123456789L) // 예제 OAuth ID - .email("chulsoo3@example.com") // 소셜 로그인 ID - .deliveryInformations(new ArrayList<>()) // 초기에는 빈 리스트 + .memberType(Member.MemberType.USER) + .oAuthId("123456789L") + .email("chulsoo3@example.com") + .password("securePassword3") + .deliveryInformations(new ArrayList<>()) .build(); memberRepository.save(member1); memberRepository.save(member2); memberRepository.save(member3); } - // 상품 데이터 초기화 + // Initialize books if (bookRepository.count() == 0) { - Category category1 = categoryRepository.findById(1L).orElseThrow(); // 카테고리 데이터 가져오기 + Category category1 = categoryRepository.findById(1L).orElseThrow(); Category category2 = categoryRepository.findById(2L).orElseThrow(); - Book book1 = Book.builder() - .title("부자 되는 법") // 책 제목 - .author("홍길동") // 저자 - .isbn("978-89-1234-567-8") // ISBN - .isbn13("9788912345678") // ISBN13 - .pubDate(LocalDate.of(2023, 10, 15)) // 출판일 - .priceStandard(25000) // 정가 - .pricesSales(22000) // 판매가 - .stock(100) // 재고 - .status(1) // 판매 상태 (1: 판매 중, 0: 품절 등) - .rating(4.5f) // 평점 - .toc("1장: 시작하기\n2장: 재테크 기본기\n3장: 투자 전략") // 목차 - .coverImage("https://example.com/cover.jpg") // 커버 이미지 URL - .description("이 책은 부자가 되는 법을 알려주는 최고의 가이드입니다.") // 상세 설명 - .descriptionImage("https://example.com/description.jpg") // 상세 이미지 URL - .salesPoint(5000) // 판매 포인트 - .reviewCount(120) // 리뷰 개수 - .publisher("성공출판사") // 출판사 - .categoryId(category1) // 앞서 생성한 Category 객체를 사용 - .review(new ArrayList<>()) // 초기에는 빈 리스트 + .title("부자 되는 법") + .author("홍길동") + .isbn("978-89-1234-567-8") + .isbn13("9788912345678") + .pubDate(LocalDate.of(2023, 10, 15)) + .priceStandard(25000) + .pricesSales(22000) + .stock(100) + .status(1) + .rating(4.5) + .toc("1장: 시작하기\n2장: 재테크 기본기\n3장: 투자 전략") + .coverImage("https://example.com/cover.jpg") + .description("이 책은 부자가 되는 법을 알려주는 최고의 가이드입니다.") + .descriptionImage("https://example.com/description.jpg") + .salesPoint(5000L) + .reviewCount(120L) + .publisher("성공출판사") + .categoryId(category1) + .review(new ArrayList<>()) .build(); Book book2 = Book.builder() - .title("거지 되는 법") // 책 제목 - .author("홍길동") // 저자 - .isbn("978-89-1234-567-8") // ISBN - .isbn13("9788912345678") // ISBN13 - .pubDate(LocalDate.of(2023, 10, 15)) // 출판일 - .priceStandard(25000) // 정가 - .pricesSales(22000) // 판매가 - .stock(100) // 재고 - .status(1) // 판매 상태 (1: 판매 중, 0: 품절 등) - .rating(4.5f) // 평점 - .toc("1장: 시작하기\n2장: 재테크 기본기\n3장: 투자 전략") // 목차 - .coverImage("https://example.com/cover.jpg") // 커버 이미지 URL - .description("이 책은 부자가 되는 법을 알려주는 최고의 가이드입니다.") // 상세 설명 - .descriptionImage("https://example.com/description.jpg") // 상세 이미지 URL - .salesPoint(5000) // 판매 포인트 - .reviewCount(120) // 리뷰 개수 - .publisher("성공출판사") // 출판사 - .categoryId(category2) // 앞서 생성한 Category 객체를 사용 - .review(new ArrayList<>()) // 초기에는 빈 리스트 + .title("거지 되는 법") + .author("홍길동") + .isbn("978-89-1234-567-8") + .isbn13("9788912345678") + .pubDate(LocalDate.of(2023, 10, 15)) + .priceStandard(25000) + .pricesSales(22000) + .stock(100) + .status(1) + .rating(4.5) + .toc("1장: 시작하기\n2장: 재테크 기본기\n3장: 투자 전략") + .coverImage("https://example.com/cover.jpg") + .description("이 책은 부자가 되는 법을 알려주는 최고의 가이드입니다.") + .descriptionImage("https://example.com/description.jpg") + .salesPoint(5000L) + .reviewCount(120L) + .publisher("성공출판사") + .categoryId(category2) + .review(new ArrayList<>()) .build(); bookRepository.save(book1); bookRepository.save(book2); } - // 주문 데이터 초기화 + // Initialize orders (주문 생성) if (orderRepository.count() == 0) { - Member member1 = memberRepository.findById(1L).orElseThrow(); // 이미 초기화된 회원을 가져옴 + Member member1 = memberRepository.findById(1L).orElseThrow(); Member member2 = memberRepository.findById(2L).orElseThrow(); Member member3 = memberRepository.findById(3L).orElseThrow(); - Order order1 = new Order(member1, Order.OrderStatus.COMPLETE, 2500); - Order order2 = new Order(member2, Order.OrderStatus.ORDERED, 4500); - Order order3 = new Order(member3, Order.OrderStatus.ORDERED, 3500); + Order order1 = Order.builder() + .member(member1) + .orderStatus(Order.OrderStatus.COMPLETE) + .totalPrice(25000) + .build(); + Order order2 = Order.builder() + .member(member2) + .orderStatus(Order.OrderStatus.ORDERED) + .totalPrice(45000) + .build(); + Order order3 = Order.builder() + .member(member3) + .orderStatus(Order.OrderStatus.ORDERED) + .totalPrice(35000) + .build(); + orderRepository.save(order1); orderRepository.save(order2); orderRepository.save(order3); - // 주문 세부 사항 (DetailOrder) - Book book1 = bookRepository.findById(1L).orElseThrow(); // 이미 초기화된 상품을 가져옴 + // Initialize detail orders (상세 주문 생성) + Book book1 = bookRepository.findById(1L).orElseThrow(); Book book2 = bookRepository.findById(2L).orElseThrow(); - DetailOrder detailOrder1 = new DetailOrder(order1, book1, 2, DetailOrder.DeliveryStatus.PENDING); - DetailOrder detailOrder2 = new DetailOrder(order2, book2, 3, DetailOrder.DeliveryStatus.PENDING); - DetailOrder detailOrder3 = new DetailOrder(order3, book1, 1, DetailOrder.DeliveryStatus.PENDING); + DetailOrder detailOrder1 = DetailOrder.builder() + .order(order1) + .book(book1) + .bookQuantity(2) + .deliveryStatus(DeliveryStatus.PENDING) + .build(); + + DetailOrder detailOrder2 = DetailOrder.builder() + .order(order2) + .book(book2) + .bookQuantity(3) + .deliveryStatus(DeliveryStatus.PENDING) + .build(); + + DetailOrder detailOrder3 = DetailOrder.builder() + .order(order3) + .book(book1) + .bookQuantity(1) + .deliveryStatus(DeliveryStatus.PENDING) + .build(); + detailOrderRepository.save(detailOrder1); detailOrderRepository.save(detailOrder2); detailOrderRepository.save(detailOrder3); diff --git a/backend/src/main/java/com/ll/nbe342team8/global/config/SwaggerConfig.java b/backend/src/main/java/com/ll/nbe342team8/global/config/SwaggerConfig.java deleted file mode 100644 index 3418bfd..0000000 --- a/backend/src/main/java/com/ll/nbe342team8/global/config/SwaggerConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ll.nbe342team8.global.config; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import org.springdoc.core.models.GroupedOpenApi; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@OpenAPIDefinition(info = @Info(title = "API 서버", version = "v1")) -public class SwaggerConfig { - @Bean - public GroupedOpenApi booksAPI() { - return GroupedOpenApi.builder() - .group("book") - .pathsToMatch("/books/**") - .build(); - } - - @Bean - public GroupedOpenApi reviewsAPI() { - return GroupedOpenApi.builder() - .group("review") - .pathsToMatch("/reviews/**") - .build(); - } - - @Bean - public GroupedOpenApi cartsAPI() { - return GroupedOpenApi.builder() - .group("cart") - .pathsToMatch("/cart/**") - .build(); - } -} diff --git a/backend/src/main/java/com/ll/nbe342team8/global/config/WebConfig.java b/backend/src/main/java/com/ll/nbe342team8/global/config/WebConfig.java index 9dd5b3b..fbaa861 100644 --- a/backend/src/main/java/com/ll/nbe342team8/global/config/WebConfig.java +++ b/backend/src/main/java/com/ll/nbe342team8/global/config/WebConfig.java @@ -8,10 +8,14 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") // 모든 요청에 대해 - .allowedOrigins("http://localhost:3000") // 허용할 출처 - .allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드 - .allowedHeaders("*") // 모든 헤더를 허용 - .allowCredentials(true); // 인증 정보 포함 허용 + registry.addMapping("/**") // 모든 경로에 대해 CORS 설정 적용 + .allowedOriginPatterns("*") // 모든 출처 허용 (개발 환경용) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") // 허용할 HTTP 메서드 + .allowedHeaders("Authorization", "Content-Type", "X-Requested-With", + "Accept", "Origin", "Access-Control-Request-Method", + "Access-Control-Request-Headers", "*") // 허용할 헤더 + .exposedHeaders("Authorization", "RefreshToken", "*") // 브라우저에 노출할 헤더 + .allowCredentials(true) // 쿠키 등 자격 증명 포함 허용 + .maxAge(3600); // CORS preflight 요청 결과를 캐시하는 시간 } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/global/exceptions/ErrorCode.java b/backend/src/main/java/com/ll/nbe342team8/global/exceptions/ErrorCode.java new file mode 100644 index 0000000..77adb0d --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/global/exceptions/ErrorCode.java @@ -0,0 +1,28 @@ +package com.ll.nbe342team8.global.exceptions; + +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public enum ErrorCode { + // ✅ 공통 에러 코드 + INTERNAL_SERVER_ERROR("서버 내부 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + BAD_REQUEST("잘못된 요청입니다.", HttpStatus.BAD_REQUEST), + + // ✅ 관리자 관련 에러 코드 + ADMIN_NOT_FOUND("관리자 계정을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + INVALID_PASSWORD("비밀번호가 올바르지 않습니다.", HttpStatus.UNAUTHORIZED), + LOGIN_ATTEMPT_EXCEEDED("5회 이상 로그인 실패. 30분 후 다시 시도하세요.", HttpStatus.FORBIDDEN), + ADMIN_ACCESS_DENIED("관리자 권한이 없습니다.", HttpStatus.FORBIDDEN), + UNAUTHORIZED_ACCESS("관리자 인증 실패: 접근 권한이 없습니다.", HttpStatus.FORBIDDEN), + ADMIN_CREATION_FAILED("관리자 계정을 생성할 수 없습니다.", HttpStatus.INTERNAL_SERVER_ERROR); + + private final String message; + private final HttpStatus status; + + ErrorCode(String message, HttpStatus status) { + this.message = message; + this.status = status; + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/global/exceptions/ErrorResponse.java b/backend/src/main/java/com/ll/nbe342team8/global/exceptions/ErrorResponse.java new file mode 100644 index 0000000..e2dce38 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/global/exceptions/ErrorResponse.java @@ -0,0 +1,11 @@ +package com.ll.nbe342team8.global.exceptions; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ErrorResponse { + private String message; + private int status; +} diff --git a/backend/src/main/java/com/ll/nbe342team8/global/globalExceptionHandler/GlobalExceptionHandler.java b/backend/src/main/java/com/ll/nbe342team8/global/globalExceptionHandler/GlobalExceptionHandler.java index d2a16aa..1b01205 100644 --- a/backend/src/main/java/com/ll/nbe342team8/global/globalExceptionHandler/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/ll/nbe342team8/global/globalExceptionHandler/GlobalExceptionHandler.java @@ -17,6 +17,7 @@ @RequiredArgsConstructor public class GlobalExceptionHandler { + // Todo: 빈 리스트에서 getFirst 를 호출해도 NoSuchElementException이 발생함. 예외를 이렇게 받는건 신뢰할 수 없는 결과를 낼 것으로 예상됨. @ExceptionHandler(NoSuchElementException.class) public ResponseEntity handle(NoSuchElementException ex) { @@ -34,7 +35,6 @@ public ResponseEntity handle(MethodArgumentNotValidException ex) { .body(Map.of("message", "유효성 검사 에러.")); } - @ExceptionHandler(ServiceException.class) public ResponseEntity handle(ServiceException ex) { diff --git a/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/BaseInitData.java b/backend/src/main/java/com/ll/nbe342team8/global/initData/BaseInitData.java similarity index 93% rename from backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/BaseInitData.java rename to backend/src/main/java/com/ll/nbe342team8/global/initData/BaseInitData.java index 47a33ab..b89acb8 100644 --- a/backend/src/main/java/com/ll/nbe342team8/global/baseInit/data/BaseInitData.java +++ b/backend/src/main/java/com/ll/nbe342team8/global/initData/BaseInitData.java @@ -1,4 +1,4 @@ -package com.ll.nbe342team8.global.baseInit.data; +package com.ll.nbe342team8.global.initData; import com.ll.nbe342team8.domain.book.book.entity.Book; import com.ll.nbe342team8.domain.book.book.service.BookService; @@ -22,7 +22,7 @@ import java.util.List; import java.util.Random; -//@Configuration +@Configuration @RequiredArgsConstructor public class BaseInitData { @@ -51,6 +51,7 @@ public void makeSampleMembers() throws IOException { for (int i = 1; i <= 10; i++) { Member member = Member.builder() .name("test" + i) + .password("") .phoneNumber("01012345678") .memberType(Member.MemberType.USER) .build(); @@ -106,17 +107,21 @@ public void makeSampleBooks() throws IOException { categoryRepository.save(category); Book book = Book.builder() - .title("제목") - .author("작가") - .priceStandard(10000) + .title("제목" + i) + .author("작가" + i) + .priceStandard(10000 + i) .pricesSales(9000) .stock(100) .coverImage(coverUrls.get(i-1)) // .coverImage("img src") .pubDate(date.plusDays(i)) .publisher("출판사") + .salesPoint(50L + i) + .rating(0.0) + .reviewCount(0L) .categoryId(category) .isbn13("isbn13") + .status(1) .build(); bookService.create(book); @@ -136,7 +141,7 @@ public void makeSampleReviews() throws IOException { Member member = memberService.getMemberById((long) i); for (int j = 1; j <= 10; j++) { - float rating = (float) (random.nextInt(11) * 0.5); + Double rating = random.nextInt(11) * 0.5; Review review = Review.builder() .book(book) .member(member) diff --git a/backend/src/main/java/com/ll/nbe342team8/global/initData/DataInitializer.java b/backend/src/main/java/com/ll/nbe342team8/global/initData/DataInitializer.java new file mode 100644 index 0000000..318d9c2 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/global/initData/DataInitializer.java @@ -0,0 +1,189 @@ +package com.ll.nbe342team8.global.initData; + +import com.ll.nbe342team8.domain.book.book.entity.Book; +import com.ll.nbe342team8.domain.book.book.repository.BookRepository; +import com.ll.nbe342team8.domain.book.category.entity.Category; +import com.ll.nbe342team8.domain.book.category.repository.CategoryRepository; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.repository.MemberRepository; +import com.ll.nbe342team8.domain.order.detailOrder.entity.DetailOrder; +import com.ll.nbe342team8.domain.order.detailOrder.repository.DetailOrderRepository; +import com.ll.nbe342team8.domain.order.order.entity.Order; +import com.ll.nbe342team8.domain.order.order.repository.OrderRepository; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.ArrayList; + +//@RequiredArgsConstructor +//@Component +//public class DataInitializer { +// +// private final CategoryRepository categoryRepository; +// private final MemberRepository memberRepository; +// private final BookRepository bookRepository; +// private final OrderRepository orderRepository; +// private final DetailOrderRepository detailOrderRepository; +// +// @PostConstruct +// public void init() { +// // 카테고리 데이터 초기화 +// if (categoryRepository.count() == 0) { +// Category category1 = Category.builder() +// .categoryId(1) +// .categoryName("경제/경영") +// .mall("국내도서") +// .depth1("경제/경영") +// .depth2("재테크/금융") +// .depth3("재테크") +// .depth4("부자되는법") +// .depth5(null) // 값이 없는 경우 null +// .category("국내도서 > 경제/경영 > 재테크/금융 > 재테크 > 부자되는법") +// .books(new ArrayList<>()) // 초기에는 빈 리스트 +// .build(); +// Category category2= Category.builder() +// .categoryId(2) +// .categoryName("경제/경영") +// .mall("국내도서") +// .depth1("경제/경영") +// .depth2("재테크/금융") +// .depth3("재테크") +// .depth4("거지 되는법") +// .depth5(null) // 값이 없는 경우 null +// .category("국내도서 > 경제/경영 > 재테크/금융 > 재테크 > 부자되는법") +// .books(new ArrayList<>()) // 초기에는 빈 리스트 +// .build(); +// categoryRepository.save(category1); +// categoryRepository.save(category2); +// } +// +// // 회원 데이터 초기화 +// if (memberRepository.count() == 0) { +// Member member1 = Member.builder() +// .name("user1") +// .phoneNumber("010-1234-5678") +// .memberType(Member.MemberType.USER) // 사용자 역할 +// .oAuthId("123456789") // 예제 OAuth ID +// .email("chulsoo@example.com") // 소셜 로그인 ID +// .deliveryInformations(new ArrayList<>()) // 초기에는 빈 리스트 +// .build(); +// Member member2 = Member.builder() +// .name("user2") +// .phoneNumber("010-1234-5678") +// .memberType(Member.MemberType.USER) // 사용자 역할 +// .oAuthId("123456789") // 예제 OAuth ID +// .email("chulsoo11@example.com") // 소셜 로그인 ID +// .deliveryInformations(new ArrayList<>()) // 초기에는 빈 리스트 +// .build(); +// Member member3 = Member.builder() +// .name("user3") +// .phoneNumber("010-1234-5678") +// .memberType(Member.MemberType.USER) // 사용자 역할 +// .oAuthId("123456789") // 예제 OAuth ID +// .email("chulsoo3@example.com") // 소셜 로그인 ID +// .deliveryInformations(new ArrayList<>()) // 초기에는 빈 리스트 +// .build(); +// memberRepository.save(member1); +// memberRepository.save(member2); +// memberRepository.save(member3); +// } +// +// // 상품 데이터 초기화 +// if (bookRepository.count() == 0) { +// Category category1 = categoryRepository.findById(1L).orElseThrow(); // 카테고리 데이터 가져오기 +// Category category2 = categoryRepository.findById(2L).orElseThrow(); +// +// Book book1 = Book.builder() +// .title("부자 되는 법") // 책 제목 +// .memberId("홍길동") // 저자 +// .isbn("978-89-1234-567-8") // ISBN +// .isbn13("9788912345678") // ISBN13 +// .pubDate(LocalDate.of(2023, 10, 15)) // 출판일 +// .priceStandard(25000) // 정가 +// .pricesSales(22000) // 판매가 +// .stock(100) // 재고 +// .status(1) // 판매 상태 (1: 판매 중, 0: 품절 등) +// .rating(4.5) // 평점 +// .toc("1장: 시작하기\n2장: 재테크 기본기\n3장: 투자 전략") // 목차 +// .coverImage("https://example.com/cover.jpg") // 커버 이미지 URL +// .description("이 책은 부자가 되는 법을 알려주는 최고의 가이드입니다.") // 상세 설명 +// .descriptionImage("https://example.com/description.jpg") // 상세 이미지 URL +// .salesPoint(5000) // 판매 포인트 +// .reviewCount(120) // 리뷰 개수 +// .publisher("성공출판사") // 출판사 +// .categoryId(category1) // 앞서 생성한 Category 객체를 사용 +// .review(new ArrayList<>()) // 초기에는 빈 리스트 +// .build(); +// Book book2 = Book.builder() +// .title("거지 되는 법") // 책 제목 +// .memberId("홍길동") // 저자 +// .isbn("978-89-1234-567-8") // ISBN +// .isbn13("9788912345678") // ISBN13 +// .pubDate(LocalDate.of(2023, 10, 15)) // 출판일 +// .priceStandard(25000) // 정가 +// .pricesSales(22000) // 판매가 +// .stock(100) // 재고 +// .status(1) // 판매 상태 (1: 판매 중, 0: 품절 등) +// .rating(4.5f) // 평점 +// .toc("1장: 시작하기\n2장: 재테크 기본기\n3장: 투자 전략") // 목차 +// .coverImage("https://example.com/cover.jpg") // 커버 이미지 URL +// .description("이 책은 부자가 되는 법을 알려주는 최고의 가이드입니다.") // 상세 설명 +// .descriptionImage("https://example.com/description.jpg") // 상세 이미지 URL +// .salesPoint(5000) // 판매 포인트 +// .reviewCount(120) // 리뷰 개수 +// .publisher("성공출판사") // 출판사 +// .categoryId(category2) // 앞서 생성한 Category 객체를 사용 +// .review(new ArrayList<>()) // 초기에는 빈 리스트 +// .build(); +// +// bookRepository.save(book1); +// bookRepository.save(book2); +// } +// +// // 주문 데이터 초기화 +// if (orderRepository.count() == 0) { +// Member member1 = memberRepository.findById(1L).orElseThrow(); // 이미 초기화된 회원을 가져옴 +// Member member2 = memberRepository.findById(2L).orElseThrow(); +// Member member3 = memberRepository.findById(3L).orElseThrow(); +// +// Order order1 = new Order(member1, Order.OrderStatus.COMPLETE, 2500); +// Order order2 = new Order(member2, Order.OrderStatus.ORDERED, 4500); +// Order order3 = new Order(member3, Order.OrderStatus.ORDERED, 3500); +// orderRepository.save(order1); +// orderRepository.save(order2); +// orderRepository.save(order3); +// +// // 주문 세부 사항 (DetailOrder) +// Book book1 = bookRepository.findById(1L).orElseThrow(); // 이미 초기화된 상품을 가져옴 +// Book book2 = bookRepository.findById(2L).orElseThrow(); +// Book book3 = bookRepository.findById(3L).orElseThrow(); +// +// DetailOrder detailOrder1 = DetailOrder.builder() +// .order(order1) +// .book(book1) +// .bookQuantity(2) +// .deliveryStatus(DetailOrder.DeliveryStatus.PENDING) +// .build(); +// +// DetailOrder detailOrder2 = DetailOrder.builder() +// .order(order2) +// .book(book2) +// .bookQuantity(2) +// .deliveryStatus(DetailOrder.DeliveryStatus.PENDING) +// .build(); +// +// DetailOrder detailOrder3 = DetailOrder.builder() +// .order(order3) +// .book(book3) +// .bookQuantity(3) +// .deliveryStatus(DetailOrder.DeliveryStatus.PENDING) +// .build(); +// +// detailOrderRepository.save(detailOrder1); +// detailOrderRepository.save(detailOrder2); +// detailOrderRepository.save(detailOrder3); +// } +// } +//} \ No newline at end of file diff --git a/backend/src/main/java/com/ll/nbe342team8/global/initData/DevInitData.java b/backend/src/main/java/com/ll/nbe342team8/global/initData/DevInitData.java new file mode 100644 index 0000000..c3d2164 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/global/initData/DevInitData.java @@ -0,0 +1,31 @@ +package com.ll.nbe342team8.global.initData; + +import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.standard.util.Ut; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Profile; + +@Profile("dev") +@Configuration +@RequiredArgsConstructor +public class DevInitData { + + @Autowired + @Lazy + private DevInitData self; + + @Bean + public ApplicationRunner devInitDataApplicationRunner() { + return args -> { + Ut.file.downloadByHttp("http://localhost:8080/v3/api-docs", "."); + + String cmd = "yes | npx --package typescript --package openapi-typescript --package punycode openapi-typescript api-docs.json -o ./frontend/app/backend/api/schema.d.ts"; + Ut.cmd.runAsync(cmd); + }; + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/global/jpa/entity/BaseEntity.java b/backend/src/main/java/com/ll/nbe342team8/global/jpa/entity/BaseEntity.java deleted file mode 100644 index 6716a79..0000000 --- a/backend/src/main/java/com/ll/nbe342team8/global/jpa/entity/BaseEntity.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ll.nbe342team8.global.jpa.entity; - -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.MappedSuperclass; -import lombok.AccessLevel; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; - -import static jakarta.persistence.GenerationType.IDENTITY; - -@Getter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -@MappedSuperclass -public class BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT - @Setter(AccessLevel.PROTECTED) - @EqualsAndHashCode.Include - private Long id; -} diff --git a/backend/src/main/java/com/ll/nbe342team8/global/jpa/entity/BaseTime.java b/backend/src/main/java/com/ll/nbe342team8/global/jpa/entity/BaseTime.java index cee842e..02b354a 100644 --- a/backend/src/main/java/com/ll/nbe342team8/global/jpa/entity/BaseTime.java +++ b/backend/src/main/java/com/ll/nbe342team8/global/jpa/entity/BaseTime.java @@ -14,13 +14,11 @@ @Getter @EntityListeners(AuditingEntityListener.class) @MappedSuperclass -public class BaseTime extends BaseEntity { +public class BaseTime { @CreatedDate - @Setter(AccessLevel.PRIVATE) private LocalDateTime createDate; @LastModifiedDate - @Setter(AccessLevel.PRIVATE) private LocalDateTime modifyDate; } diff --git a/backend/src/main/java/com/ll/nbe342team8/global/security/SecurityConfig.java b/backend/src/main/java/com/ll/nbe342team8/global/security/SecurityConfig.java index 5bc8fdc..104d178 100644 --- a/backend/src/main/java/com/ll/nbe342team8/global/security/SecurityConfig.java +++ b/backend/src/main/java/com/ll/nbe342team8/global/security/SecurityConfig.java @@ -1,50 +1,142 @@ package com.ll.nbe342team8.global.security; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Collections; -import java.util.List; +import com.ll.nbe342team8.domain.jwt.JwtAuthenticationFilter; +import com.ll.nbe342team8.domain.jwt.JwtService; +import com.ll.nbe342team8.domain.member.member.entity.Member; +import com.ll.nbe342team8.domain.member.member.service.MemberService; +import com.ll.nbe342team8.domain.oauth.CustomOAuth2UserService; +import com.ll.nbe342team8.domain.oauth.OAuth2SuccessHandler; +import com.ll.nbe342team8.domain.oauth.SecurityUser; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; @Configuration @EnableWebSecurity +@RequiredArgsConstructor @EnableMethodSecurity(prePostEnabled = true) // @PreAuthorize 사용 public class SecurityConfig { + private final JwtService jwtService; + private final MemberService memberService; + private final OAuth2AuthorizedClientService authorizedClientService; + private final CustomOAuth2UserService customOAuth2UserService; + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // 관리자 로그인 페이지는 모든 사용자에게 허용 + .requestMatchers("/admin/login").permitAll() + // 그 외 관리자 페이지는 관리자 권한이 있는 사용자에게만 허용 + .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN") + .requestMatchers("/api/public/**", "/oauth2/**", "/api/auth/**", "/refresh", "/api/auth/refresh", "/swagger-ui/**", "/v3/api-docs/**", "/api/auth/me", "/api/auth/me/**").permitAll() + .requestMatchers("/my/orders").permitAll() + .requestMatchers("/books/**", "/event/**", "/images/**", "/cart/**").permitAll() // 카트, 메인페이지 추가 + .requestMatchers(HttpMethod.GET, "/reviews/**", "/cart").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(new JwtAuthenticationFilter(jwtService, memberService), + UsernamePasswordAuthenticationFilter.class) + .headers((headers) -> headers + .addHeaderWriter(new XFrameOptionsHeaderWriter( + XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))) + .oauth2Login(oauth2 -> oauth2 + .authorizationEndpoint(authorization -> authorization + .baseUri("/oauth2/authorization") + .authorizationRequestRepository(new HttpSessionOAuth2AuthorizationRequestRepository()) + ) + .redirectionEndpoint(redirection -> redirection + .baseUri("/login/oauth2/code/*") + ) + .successHandler(oAuth2SuccessHandler()) + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) + ) + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("인증이 필요합니다."); + }) + ); + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.setAllowedOrigins(List.of("http://localhost:3000")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setExposedHeaders(Arrays.asList("Authorization", "RefreshToken")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + + @Bean + public OAuth2SuccessHandler oAuth2SuccessHandler() { + return new OAuth2SuccessHandler(jwtService, authorizedClientService); + } + + @Bean + public AuthenticationManager authenticationManager(HttpSecurity http, MemberService memberService) throws Exception { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setUserDetailsService(identifier -> { + // 이메일을 입력하면, oAuthId로 변환하여 인증 + Optional optionalMember; + if (identifier.contains("@")) { + optionalMember = memberService.findByEmail(identifier); + } else { + optionalMember = memberService.findByOauthId(identifier); + } + + if (optionalMember.isEmpty()) { + throw new UsernameNotFoundException("사용자를 찾을 수 없습니다."); + } + + return new SecurityUser(optionalMember.get()); + }); + + authenticationProvider.setPasswordEncoder(passwordEncoder()); + + return new ProviderManager(authenticationProvider); + } - @Bean - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(auth -> auth - .requestMatchers("/admin/**").hasRole("ADMIN") - .anyRequest().permitAll() - ) - .csrf((csrf) -> csrf - .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**"))) // H2 콘솔 예외처리 - .csrf(AbstractHttpConfigurer::disable) - .headers((headers) -> headers - .addHeaderWriter(new XFrameOptionsHeaderWriter( - XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))) - .logout(logout -> logout - .logoutUrl("/logout") - .logoutSuccessUrl("/") // 로그아웃 성공 후 리다이렉트 - ) - ; - - return http.build(); - } -} \ No newline at end of file + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); // BCrypt 알고리즘 사용 + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/global/springDoc/SwaggerConfig.java b/backend/src/main/java/com/ll/nbe342team8/global/springDoc/SwaggerConfig.java new file mode 100644 index 0000000..29e5953 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/global/springDoc/SwaggerConfig.java @@ -0,0 +1,61 @@ +package com.ll.nbe342team8.global.springDoc; + +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; + +@Configuration +@OpenAPIDefinition(info = @Info(title = "API 서버", version = "v1")) +public class SwaggerConfig { + @Bean + public GroupedOpenApi booksAPI() { + return GroupedOpenApi.builder() + .group("book") + .pathsToMatch("/books/**") + .build(); + } + + @Bean + public GroupedOpenApi reviewsAPI() { + return GroupedOpenApi.builder() + .group("review") + .pathsToMatch("/reviews/**") + .build(); + } + + @Bean + public GroupedOpenApi cartsAPI() { + return GroupedOpenApi.builder() + .group("cart") + .pathsToMatch("/cart/**") + .build(); + } + + @Bean + public GroupedOpenApi ordersAPI() { + return GroupedOpenApi.builder() + .group("order") + .pathsToMatch("/my/orders/**") + .build(); + } + + @Bean + public GroupedOpenApi adminAPI() { + return GroupedOpenApi.builder() + .group("admin") + .pathsToMatch("/admin/**") + .build(); + } + + @Bean + public GroupedOpenApi memberAPI() { + return GroupedOpenApi.builder() + .group("member") + .pathsToMatch("/api/auth/me/**") + .build(); + } +} + diff --git a/backend/src/main/java/com/ll/nbe342team8/global/types/Sortable.java b/backend/src/main/java/com/ll/nbe342team8/global/types/Sortable.java new file mode 100644 index 0000000..6355b70 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/global/types/Sortable.java @@ -0,0 +1,12 @@ +package com.ll.nbe342team8.global.types; + +import org.springframework.data.domain.Sort; + +public interface Sortable { + String getField(); + Sort.Direction getDirection(); + + default Sort.Order getOrder() { + return new Sort.Order(getDirection(), getField()); + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/global/util/BookSpecifications.java b/backend/src/main/java/com/ll/nbe342team8/global/util/BookSpecifications.java new file mode 100644 index 0000000..241ab92 --- /dev/null +++ b/backend/src/main/java/com/ll/nbe342team8/global/util/BookSpecifications.java @@ -0,0 +1,39 @@ +package com.ll.nbe342team8.global.util; + +import org.springframework.data.jpa.domain.Specification; + +import com.ll.nbe342team8.domain.book.book.entity.Book; + +public class BookSpecifications { + + private BookSpecifications() { + throw new UnsupportedOperationException("이 클래스는 유틸리티 클래스이며 인스턴스를 생성할 수 없습니다."); + } + + public static Specification hasTitle(String title) { + return (root, query, criteriaBuilder) -> { + if (title == null || title.isEmpty()) { + return null; + } + return criteriaBuilder.like(root.get("title"), "%" + title + "%"); + }; + } + + public static Specification hasAuthor(String author) { + return (root, query, criteriaBuilder) -> { + if (author == null || author.isEmpty()) { + return null; + } + return criteriaBuilder.like(root.get("author"), "%" + author + "%"); + }; + } + + public static Specification hasIsbn(String isbn13) { + return (root, query, criteriaBuilder) -> { + if (isbn13 == null || isbn13.isEmpty()) { + return null; + } + return criteriaBuilder.equal(root.get("isbn13"), isbn13); + }; + } +} diff --git a/backend/src/main/java/com/ll/nbe342team8/standard/util/Ut.java b/backend/src/main/java/com/ll/nbe342team8/standard/util/Ut.java index 49d454c..c01bd9d 100644 --- a/backend/src/main/java/com/ll/nbe342team8/standard/util/Ut.java +++ b/backend/src/main/java/com/ll/nbe342team8/standard/util/Ut.java @@ -4,6 +4,17 @@ import lombok.SneakyThrows; import org.apache.commons.text.StringEscapeUtils; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; + public class Ut { @@ -27,4 +38,130 @@ public static String sanitize(String input) { return StringEscapeUtils.escapeHtml4(input); } } + + public static class file { + + public static void downloadByHttp(String url, String dirPath) { + try { + HttpClient client = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + // 먼저 헤더만 가져오기 위한 HEAD 요청 + HttpResponse headResponse = client.send( + HttpRequest.newBuilder(URI.create(url)) + .method("HEAD", HttpRequest.BodyPublishers.noBody()) + .build(), + HttpResponse.BodyHandlers.discarding() + ); + + // 실제 파일 다운로드 + HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofFile( + createTargetPath(url, dirPath, headResponse) + )); + } catch (IOException | InterruptedException e) { + throw new RuntimeException("다운로드 중 오류 발생: " + e.getMessage(), e); + } + } + + private static Path createTargetPath(String url, String dirPath, HttpResponse response) { + // 디렉토리가 없으면 생성 + Path directory = Path.of(dirPath); + if (!Files.exists(directory)) { + try { + Files.createDirectories(directory); + } catch (IOException e) { + throw new RuntimeException("디렉토리 생성 실패: " + e.getMessage(), e); + } + } + + // 파일명 생성 + String filename = getFilenameFromUrl(url); + String extension = getExtensionFromResponse(response); + + return directory.resolve(filename + extension); + } + + private static String getFilenameFromUrl(String url) { + try { + String path = new URI(url).getPath(); + String filename = Path.of(path).getFileName().toString(); + // 확장자 제거 + return filename.contains(".") + ? filename.substring(0, filename.lastIndexOf('.')) + : filename; + } catch (URISyntaxException e) { + // URL에서 파일명을 추출할 수 없는 경우 타임스탬프 사용 + return "download_" + System.currentTimeMillis(); + } + } + + private static String getExtensionFromResponse(HttpResponse response) { + return response.headers() + .firstValue("Content-Type") + .map(contentType -> { + // MIME 타입에 따른 확장자 매핑 + return switch (contentType.split(";")[0].trim().toLowerCase()) { + case "application/json" -> ".json"; + case "text/plain" -> ".txt"; + case "text/html" -> ".html"; + case "image/jpeg" -> ".jpg"; + case "image/png" -> ".png"; + case "application/pdf" -> ".pdf"; + case "application/xml" -> ".xml"; + case "application/zip" -> ".zip"; + default -> ""; + }; + }) + .orElse(""); + } + } + + public class cmd { + public static void runAsync(String cmd) { + new Thread(() -> { + run(cmd); + }).start(); + } + + // public static void run(String cmd) { +// try { +// ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", cmd); +// Process process = processBuilder.start(); +// process.waitFor(1, TimeUnit.MINUTES); +// System.out.println("정상@@@".repeat(200)); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + public static void run(String cmd) { + try { + ProcessBuilder pb = new ProcessBuilder("bash", "-c", cmd); + pb.redirectErrorStream(true); // 에러 출력 리다이렉트 + + Process process = pb.start(); + + // 출력 내용 읽기 + BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()) + ); + String line; + while ((line = reader.readLine()) != null) { + System.out.println("[CMD] " + line); // 로그 추가 + } + + int exitCode = process.waitFor(); + System.out.println("Exit Code: " + exitCode); + + } catch (Exception e) { + e.printStackTrace(); + } + } + } } diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/main/resources/application-mysql.yml b/backend/src/main/resources/application-mysql.yml index 2aa8371..5d60c01 100644 --- a/backend/src/main/resources/application-mysql.yml +++ b/backend/src/main/resources/application-mysql.yml @@ -2,7 +2,7 @@ spring: datasource: url: jdbc:mysql://localhost:3306/test_db username: root - password: + password: ${MYSQLPASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver output: @@ -16,4 +16,7 @@ spring: format_sql: true use_sql_comments: true hibernate: - ddl-auto: update \ No newline at end of file + ddl-auto: update + custom: + jwt: + secret: "w92mLzTnRJ6P1qfzX7KlmFdyvRk3NhGq" \ No newline at end of file diff --git a/backend/src/main/resources/application-secret.yml b/backend/src/main/resources/application-secret.yml new file mode 100644 index 0000000..8dd19e2 --- /dev/null +++ b/backend/src/main/resources/application-secret.yml @@ -0,0 +1,11 @@ +# 파일 이름에서 .default 지운 뒤 "NEED_TO_INPUT" 부분에 key를 입력해서 사용. +spring: + security: + oauth2: + client: + registration: + kakao: + clientId: ${KAKAO_CLIENT_ID} +custom: + jwt: + secretKey: ${JWT_SECRET_KEY} \ No newline at end of file diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml new file mode 100644 index 0000000..5d6ba19 --- /dev/null +++ b/backend/src/main/resources/application-test.yml @@ -0,0 +1,3 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/the_book_test \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 36d65fc..942a69c 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,10 +1,76 @@ -aladin: - ttbkey: +server: + port: 8080 spring: + profiles: - active: mysql + active: dev + include: secret + + datasource: + url: jdbc:mysql://localhost:3306/the_book + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: +# format_sql: true +# use_sql_comments: true + default_batch_fetch_size: 100 + highlight-sql: true + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + show-sql: true + output: + ansi: + enabled: always #색깔 + security: + oauth2: + client: + registration: + kakao: + clientId: ${KAKAO_CLIENT_ID} + scope: + - profile_nickname + - profile_image + - account_email + client-name: Kakao + authorization-grant-type: authorization_code + redirect-uri: "${custom.site.backUrl}/{action}/oauth2/code/{registrationId}" + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + +springdoc: + default-produces-media-type: application/json;charset=UTF-8 + +aladin: + ttbkey: ${TTBKEY} +custom: + jwt: + secretKey: ${SECRETKEY} + dev: + cookieDomain: localhost + frontUrl: "http://${custom.dev.cookieDomain}:3000" + backUrl: "http://${custom.dev.cookieDomain}:${server.port}" + prod: + cookieDomain: book.oa.gg + frontUrl: "https://www.${custom.prod.cookieDomain}" + backUrl: "https://api.${custom.prod.cookieDomain}" + site: + cookieDomain: "${custom.dev.cookieDomain}" + frontUrl: "${custom.dev.frontUrl}" + backUrl: "${custom.dev.backUrl}" + name: Book + accessToken: + expirationSeconds: "#{60 * 20}" \ No newline at end of file diff --git a/backend/src/main/resources/static/images/eventBanner/event1.jpeg b/backend/src/main/resources/static/images/eventBanner/event1.jpeg deleted file mode 100644 index 8d0a251..0000000 Binary files a/backend/src/main/resources/static/images/eventBanner/event1.jpeg and /dev/null differ diff --git a/backend/src/main/resources/static/images/eventBanner/event2.jpeg b/backend/src/main/resources/static/images/eventBanner/event2.jpeg deleted file mode 100644 index e7769f2..0000000 Binary files a/backend/src/main/resources/static/images/eventBanner/event2.jpeg and /dev/null differ diff --git a/backend/src/test/java/com/ll/nbe342team8/Book/Book/BookControllerTest.java b/backend/src/test/java/com/ll/nbe342team8/Book/Book/BookControllerTest.java new file mode 100644 index 0000000..5ed54df --- /dev/null +++ b/backend/src/test/java/com/ll/nbe342team8/Book/Book/BookControllerTest.java @@ -0,0 +1,4 @@ +package com.ll.nbe342team8.Book.Book; + +public class BookControllerTest { +} diff --git a/backend/src/test/java/com/ll/nbe342team8/Book/Cart/CartControllerTest.java b/backend/src/test/java/com/ll/nbe342team8/Book/Cart/CartControllerTest.java new file mode 100644 index 0000000..e37a152 --- /dev/null +++ b/backend/src/test/java/com/ll/nbe342team8/Book/Cart/CartControllerTest.java @@ -0,0 +1,66 @@ +package com.ll.nbe342team8.Book.Cart; + +import com.ll.nbe342team8.domain.cart.dto.CartItemRequestDto; +import com.ll.nbe342team8.domain.cart.dto.CartRequestDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.*; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +public class CartControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + // 데이터가 있을 경우에 테스트 시도 + @Test + @DisplayName("장바구니 조회 테스트") + public void testGetCart() { + // URL: GET /cart/1 + // URL: GET /cart/memberId-id + ResponseEntity response = restTemplate.getForEntity("/cart/1", String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + @DisplayName("장바구니 추가 테스트") + public void testAddCart() { + // URL: POST /cart/1/1?quantity=2 + // URL: POST /cart/book-id/memberId-id?quantity=? + ResponseEntity response = restTemplate.postForEntity("/cart/1/1?quantity=2", null, Void.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + @DisplayName("장바구니 수정 테스트") + public void testUpdateCartItem() { + // URL: PUT /cart/1/1?quantity=3 + // URL: PUT /cart/book-id/memberId-id?quantity=? + HttpHeaders headers = new HttpHeaders(); + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange("/cart/1/1?quantity=3", HttpMethod.PUT, entity, Void.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + @DisplayName("장바구니 삭제 테스트") + public void testDeleteCartItem() throws Exception { + // URL: DELETE /cart/1 JSON body + // URL: DELETE /cart/memberId-id JSON body + CartItemRequestDto itemRequest = new CartItemRequestDto(1L, 1); + CartRequestDto requestDto = new CartRequestDto(Arrays.asList(itemRequest)); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(requestDto, headers); + ResponseEntity response = restTemplate.exchange("/cart/1", HttpMethod.DELETE, entity, Void.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } +} diff --git a/backend/src/test/java/com/ll/nbe342team8/member/member/MemberControllerTest.java b/backend/src/test/java/com/ll/nbe342team8/member/member/MemberControllerTest.java deleted file mode 100644 index f2600a0..0000000 --- a/backend/src/test/java/com/ll/nbe342team8/member/member/MemberControllerTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.ll.nbe342team8.member.member; - -import com.ll.nbe342team8.domain.member.deliveryInformation.service.DeliveryInformationService; -import com.ll.nbe342team8.domain.member.member.controller.MemberController; -import com.ll.nbe342team8.domain.member.member.service.MemberService; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.transaction.annotation.Transactional; - - -import java.nio.charset.StandardCharsets; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; - -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -public class MemberControllerTest { - - @Autowired - private MockMvc mockMvc; - @Autowired - private MemberService memberService; - - @Autowired - private DeliveryInformationService deliveryInformationService; - - @Test - @DisplayName("사용자 페이지 불러오기") - void getMyPageTest1() throws Exception{ - - String email="rdh0427@naver.com"; - - ResultActions resultActions = mockMvc - .perform( - get("/my") - - .contentType( - new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8)) - ) - .andDo(print()); - - resultActions - .andExpect(handler().handlerType(MemberController.class)) - .andExpect(handler().methodName("getMyPage")) - .andExpect(status().isOk()); - //.andExpect(jsonPath("$.resultCode").value("201-1")) - //.andExpect(jsonPath("$.msg").value("원두가 추가되었습니다.")); - } -} diff --git a/db_dev.mv.db b/db_dev.mv.db new file mode 100644 index 0000000..edebfe9 Binary files /dev/null and b/db_dev.mv.db differ diff --git a/db_dev.trace.db b/db_dev.trace.db new file mode 100644 index 0000000..116e696 --- /dev/null +++ b/db_dev.trace.db @@ -0,0 +1,621 @@ +2025-02-06 09:33:52.815845+09:00 jdbc[13]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "김한민1" not found; SQL statement: +update book set title ="김한민1" where id=1 [42122-232] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.command.dml.SetClauseList$SetSimple.mapAndOptimize(SetClauseList.java:409) + at org.h2.command.dml.SetClauseList.mapAndOptimize(SetClauseList.java:219) + at org.h2.command.dml.Update.doPrepare(Update.java:130) + at org.h2.command.dml.DataChangeStatement.prepare(DataChangeStatement.java:37) + at org.h2.command.Parser.prepareCommand(Parser.java:489) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:645) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:561) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1164) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:245) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:231) + at org.h2.server.web.WebApp.getResult(WebApp.java:1344) + at org.h2.server.web.WebApp.query(WebApp.java:1142) + at org.h2.server.web.WebApp.query(WebApp.java:1118) + at org.h2.server.web.WebApp.process(WebApp.java:244) + at org.h2.server.web.WebApp.processRequest(WebApp.java:176) + at org.h2.server.web.JakartaWebServlet.doGet(JakartaWebServlet.java:129) + at org.h2.server.web.JakartaWebServlet.doPost(JakartaWebServlet.java:166) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) + at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365) + at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:101) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) + at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) + at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:243) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) + at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:238) + at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362) + at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) + at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) + at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) + at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) + at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) + at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) + at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) + at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) + at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) + at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) + at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) + at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) + at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) + at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) + at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) + at java.base/java.lang.Thread.run(Thread.java:1583) +2025-02-06 09:33:59.738765+09:00 jdbc[13]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "update book set title =[*]"" where id=1;"; SQL statement: +update book set title =" where id=1; [42000-232] +2025-02-06 09:34:05.762529+09:00 jdbc[13]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "title" not found; SQL statement: +update book set title ="title" where id=1 [42122-232] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.command.dml.SetClauseList$SetSimple.mapAndOptimize(SetClauseList.java:409) + at org.h2.command.dml.SetClauseList.mapAndOptimize(SetClauseList.java:219) + at org.h2.command.dml.Update.doPrepare(Update.java:130) + at org.h2.command.dml.DataChangeStatement.prepare(DataChangeStatement.java:37) + at org.h2.command.Parser.prepareCommand(Parser.java:489) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:645) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:561) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1164) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:245) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:231) + at org.h2.server.web.WebApp.getResult(WebApp.java:1344) + at org.h2.server.web.WebApp.query(WebApp.java:1142) + at org.h2.server.web.WebApp.query(WebApp.java:1118) + at org.h2.server.web.WebApp.process(WebApp.java:244) + at org.h2.server.web.WebApp.processRequest(WebApp.java:176) + at org.h2.server.web.JakartaWebServlet.doGet(JakartaWebServlet.java:129) + at org.h2.server.web.JakartaWebServlet.doPost(JakartaWebServlet.java:166) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) + at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365) + at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:101) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) + at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) + at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:243) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) + at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:238) + at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362) + at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) + at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) + at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) + at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) + at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) + at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) + at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) + at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) + at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) + at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) + at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) + at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) + at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) + at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) + at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) + at java.base/java.lang.Thread.run(Thread.java:1583) +2025-02-06 09:34:54.712237+09:00 jdbc[13]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "제목" not found; SQL statement: +update book set title="제목" where id=1 [42122-232] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.command.dml.SetClauseList$SetSimple.mapAndOptimize(SetClauseList.java:409) + at org.h2.command.dml.SetClauseList.mapAndOptimize(SetClauseList.java:219) + at org.h2.command.dml.Update.doPrepare(Update.java:130) + at org.h2.command.dml.DataChangeStatement.prepare(DataChangeStatement.java:37) + at org.h2.command.Parser.prepareCommand(Parser.java:489) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:645) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:561) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1164) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:245) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:231) + at org.h2.server.web.WebApp.getResult(WebApp.java:1344) + at org.h2.server.web.WebApp.query(WebApp.java:1142) + at org.h2.server.web.WebApp.query(WebApp.java:1118) + at org.h2.server.web.WebApp.process(WebApp.java:244) + at org.h2.server.web.WebApp.processRequest(WebApp.java:176) + at org.h2.server.web.JakartaWebServlet.doGet(JakartaWebServlet.java:129) + at org.h2.server.web.JakartaWebServlet.doPost(JakartaWebServlet.java:166) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) + at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365) + at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:101) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) + at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) + at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:243) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) + at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:238) + at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362) + at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) + at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) + at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) + at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) + at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) + at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) + at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) + at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) + at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) + at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) + at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) + at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) + at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) + at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) + at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) + at java.base/java.lang.Thread.run(Thread.java:1583) +2025-02-06 09:35:02.073626+09:00 jdbc[13]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "김한민" not found; SQL statement: +update book set title="김한민" where id=1 [42122-232] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.command.dml.SetClauseList$SetSimple.mapAndOptimize(SetClauseList.java:409) + at org.h2.command.dml.SetClauseList.mapAndOptimize(SetClauseList.java:219) + at org.h2.command.dml.Update.doPrepare(Update.java:130) + at org.h2.command.dml.DataChangeStatement.prepare(DataChangeStatement.java:37) + at org.h2.command.Parser.prepareCommand(Parser.java:489) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:645) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:561) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1164) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:245) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:231) + at org.h2.server.web.WebApp.getResult(WebApp.java:1344) + at org.h2.server.web.WebApp.query(WebApp.java:1142) + at org.h2.server.web.WebApp.query(WebApp.java:1118) + at org.h2.server.web.WebApp.process(WebApp.java:244) + at org.h2.server.web.WebApp.processRequest(WebApp.java:176) + at org.h2.server.web.JakartaWebServlet.doGet(JakartaWebServlet.java:129) + at org.h2.server.web.JakartaWebServlet.doPost(JakartaWebServlet.java:166) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) + at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365) + at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:101) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) + at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) + at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:243) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) + at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:238) + at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362) + at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) + at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) + at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) + at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) + at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) + at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) + at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) + at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) + at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) + at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) + at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) + at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) + at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) + at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) + at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) + at java.base/java.lang.Thread.run(Thread.java:1583) +2025-02-06 09:35:39.307139+09:00 jdbc[13]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "김한민" not found; SQL statement: +update book set TITLE="김한민" where id=1 [42122-232] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.command.dml.SetClauseList$SetSimple.mapAndOptimize(SetClauseList.java:409) + at org.h2.command.dml.SetClauseList.mapAndOptimize(SetClauseList.java:219) + at org.h2.command.dml.Update.doPrepare(Update.java:130) + at org.h2.command.dml.DataChangeStatement.prepare(DataChangeStatement.java:37) + at org.h2.command.Parser.prepareCommand(Parser.java:489) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:645) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:561) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1164) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:245) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:231) + at org.h2.server.web.WebApp.getResult(WebApp.java:1344) + at org.h2.server.web.WebApp.query(WebApp.java:1142) + at org.h2.server.web.WebApp.query(WebApp.java:1118) + at org.h2.server.web.WebApp.process(WebApp.java:244) + at org.h2.server.web.WebApp.processRequest(WebApp.java:176) + at org.h2.server.web.JakartaWebServlet.doGet(JakartaWebServlet.java:129) + at org.h2.server.web.JakartaWebServlet.doPost(JakartaWebServlet.java:166) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) + at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365) + at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:101) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) + at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) + at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:243) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) + at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:238) + at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362) + at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) + at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) + at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) + at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) + at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) + at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) + at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) + at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) + at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) + at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) + at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) + at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) + at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) + at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) + at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) + at java.base/java.lang.Thread.run(Thread.java:1583) +2025-02-06 09:35:43.166890+09:00 jdbc[13]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "김한민" not found; SQL statement: +update book set TITLE="김한민" where ID=1 [42122-232] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.command.dml.SetClauseList$SetSimple.mapAndOptimize(SetClauseList.java:409) + at org.h2.command.dml.SetClauseList.mapAndOptimize(SetClauseList.java:219) + at org.h2.command.dml.Update.doPrepare(Update.java:130) + at org.h2.command.dml.DataChangeStatement.prepare(DataChangeStatement.java:37) + at org.h2.command.Parser.prepareCommand(Parser.java:489) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:645) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:561) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1164) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:245) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:231) + at org.h2.server.web.WebApp.getResult(WebApp.java:1344) + at org.h2.server.web.WebApp.query(WebApp.java:1142) + at org.h2.server.web.WebApp.query(WebApp.java:1118) + at org.h2.server.web.WebApp.process(WebApp.java:244) + at org.h2.server.web.WebApp.processRequest(WebApp.java:176) + at org.h2.server.web.JakartaWebServlet.doGet(JakartaWebServlet.java:129) + at org.h2.server.web.JakartaWebServlet.doPost(JakartaWebServlet.java:166) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) + at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) + at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365) + at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:101) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) + at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) + at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) + at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) + at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) + at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) + at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:243) + at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) + at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) + at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:238) + at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362) + at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) + at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) + at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) + at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) + at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) + at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) + at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) + at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) + at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) + at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) + at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) + at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) + at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) + at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) + at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) + at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) + at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) + at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) + at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) + at java.base/java.lang.Thread.run(Thread.java:1583) diff --git a/frontend/app/admin/dashboard/books/list/page.tsx b/frontend/app/admin/dashboard/books/list/page.tsx new file mode 100644 index 0000000..0e3d00e --- /dev/null +++ b/frontend/app/admin/dashboard/books/list/page.tsx @@ -0,0 +1,409 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import Table from '@/app/components/admin/Table'; +import Pagination from '@/app/components/admin/Pagination'; +import { useRouter } from 'next/navigation'; + +const BookListPage = () => { + const [books, setBooks] = useState([]); + const [selectedBooks, setSelectedBooks] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [loading, setLoading] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [bookDetail, setBookDetail] = useState(null); + const [isEditable, setIsEditable] = useState(false); // 서버에서 받은 수정 권한 여부 + const [editMode, setEditMode] = useState(false); // 상세/편집 모드 구분 + const [editedBook, setEditedBook] = useState(null); // 편집 중인 도서 정보 + const router = useRouter(); + + // 도서 목록 조회 + useEffect(() => { + fetchBooks(); + }, [currentPage]); + + const fetchBooks = async () => { + setLoading(true); + try { + const response = await axios.get( + `http://localhost:8080/admin/books?page=${currentPage - 1}&pageSize=10`, + { withCredentials: true }, + ); + setBooks(response.data.content); + setTotalPages(response.data.totalPages); + } catch (error) { + console.error('❌ 도서 조회 실패:', error.response?.data); + } finally { + setLoading(false); + } + }; + + // 도서 상세 조회 및 모달 열기 (상세 보기 모드) + const handleShowDetail = async (bookId: number) => { + try { + const response = await axios.get(`http://localhost:8080/admin/books/${bookId}`, { + withCredentials: true, + }); + console.log('✅ 도서 상세 조회 성공:', response.data); + setBookDetail(response.data); + setIsEditable(true); // 수정 권한 여부 (서버에서 판단) + setEditMode(false); // 상세 보기 모드 + setIsModalOpen(true); + } catch (error) { + console.error('❌ 도서 상세 조회 실패:', error.response?.data); + } + }; + + // 상세 모달에서 수정 버튼 클릭 시 편집 모드로 전환 + const handleEnterEditMode = () => { + if (!isEditable) { + alert('수정 권한이 없습니다.'); + return; + } + if (confirm('정말 수정하시겠습니까?')) { + setEditMode(true); + setEditedBook({ ...bookDetail }); + } + }; + + // 편집 모드에서 입력값 변경 처리 + const handleInputChange = (field, value) => { + setEditedBook((prev) => ({ + ...prev, + [field]: value, + })); + }; + + // 수정 내용 저장 (API 호출) + const handleSaveBook = async () => { + try { + const response = await axios.patch( + `http://localhost:8080/admin/books/${bookDetail.id}`, + editedBook, + { withCredentials: true }, + ); + console.log('✅ 도서 수정 성공:', response.data); + setBookDetail(response.data); + setEditMode(false); + alert('도서 정보가 업데이트되었습니다.'); + } catch (error) { + console.error('❌ 도서 수정 실패:', error.response?.data); + alert('수정에 실패하였습니다.'); + } + }; + + // 삭제 + const handleDeleteBook = async (bookId: number) => { + if (!confirm('정말 이 도서를 삭제하시겠습니까?')) { + return; + } + + try { + const response = await axios.delete(`http://localhost:8080/admin/books/${bookId}`, { + withCredentials: true, + }); + + alert(response.data); // "도서가 성공적으로 삭제되었습니다." + fetchBooks(); // 삭제 후 목록 새로고침 + } catch (error) { + console.error('❌ 도서 삭제 실패:', error.response?.data); + alert('도서 삭제에 실패했습니다.'); + } + }; + + // 편집 모드 취소 + const handleCancelEdit = () => { + setEditMode(false); + setEditedBook(null); + }; + + return ( +
+

도서 목록

+ + {/* 검색 및 버튼 */} +
+ setSearchQuery(e.target.value)} + /> +
+ + +
+
+ + {/* 도서 목록 테이블 */} + {loading ? ( +
도서 목록을 불러오는 중...
+ ) : ( +
+ ( + + ), + }, + ]} + data={books.filter( + (book) => book.title.includes(searchQuery) || book.author.includes(searchQuery), + )} + onSelect={(id) => + setSelectedBooks((prev) => + prev.includes(id) ? prev.filter((bookId) => bookId !== id) : [...prev, id], + ) + } + /> + + + )} + + {/* 모달: 상세보기 및 편집 모드 */} + {isModalOpen && bookDetail && ( +
+
+ {/* 모달 헤더 */} +
+

+ {editMode ? ( + handleInputChange('title', e.target.value)} + className="border-b focus:outline-none text-2xl font-bold max-w-full w-full" + /> + ) : ( + bookDetail.title + )} +

+
+ {editMode ? ( + <> + + + + ) : ( + <> + {isEditable && ( + + )} + + + )} +
+
+ + {/* 모달 콘텐츠 */} +
+ {/* 왼쪽: 도서 표지 */} + {bookDetail.coverImage && ( +
+ 도서 표지 +
+ )} + {/* 오른쪽: 도서 상세 정보 */} +
+ {/* 저자 */} +
+ + {editMode ? ( + handleInputChange('author', e.target.value)} + className="border rounded p-2" + /> + ) : ( +

{bookDetail.author}

+ )} +
+ {/* 출판사 */} +
+ + {editMode ? ( + handleInputChange('publisher', e.target.value)} + className="border rounded p-2" + /> + ) : ( +

{bookDetail.publisher}

+ )} +
+ {/* 출판일 (읽기 전용) */} +
+ +

{bookDetail.pubDate}

+
+ {/* 카테고리 */} +
+ + {editMode ? ( + handleInputChange('category', e.target.value)} + className="border rounded p-2" + /> + ) : ( +

{bookDetail.category}

+ )} +
+ {/* ISBN (읽기 전용) */} +
+ +

{bookDetail.isbn}

+
+ {/* 정가 (읽기 전용) */} +
+ +

{bookDetail.priceStandard} 원

+
+ {/* 할인 가격 */} +
+ + {editMode ? ( + handleInputChange('pricesSales', e.target.value)} + className="border rounded p-2" + /> + ) : ( +

{bookDetail.pricesSales} 원

+ )} +
+ {/* 재고 */} +
+ + {editMode ? ( + handleInputChange('stock', e.target.value)} + className="border rounded p-2" + /> + ) : ( +

{bookDetail.stock}

+ )} +
+ {/* 판매 상태 */} +
+ + {editMode ? ( + + ) : ( +

{bookDetail.status === 1 ? '판매중' : '판매 중지'}

+ )} +
+ {/* 평점 (읽기 전용) */} +
+ +

{bookDetail.rating} / 5

+
+
+
+ + {/* 목차 및 설명 영역 */} +
+
+

📖 목차

+ {editMode ? ( +